Compare commits

...

2 Commits

Author SHA1 Message Date
Kilian Schuettler
f6fa5d7269 broken stream data 2025-02-02 02:49:56 +01:00
Kilian Schuettler
dcd1bd6d3f All views be rendering big stuff 2025-02-02 00:06:06 +01:00
16 changed files with 649 additions and 558 deletions

View File

@ -7,8 +7,8 @@ use crate::pdf::object::Resolve;
use lazy_static::lazy_static;
use pdf::file::{File, FileOptions, NoLog, ObjectCache, StreamCache};
use pdf::object::{Object, ObjectWrite, PlainRef, Stream};
use pdf::primitive::Primitive;
use pdf::object::{Object, ObjectWrite, PlainRef, Stream, Trace};
use pdf::primitive::{Dictionary, Primitive};
use pdf::xref::XRef;
use regex::Regex;
use serde::{Deserialize, Serialize};
@ -62,6 +62,19 @@ pub struct PrimitiveModel {
pub expanded: bool,
}
#[derive(Serialize, Debug, Clone)]
pub struct PrimitiveTreeView {
pub depth: usize,
pub key: String,
pub ptype: String,
pub sub_type: String,
pub value: String,
pub container: bool,
pub expanded: bool,
pub path: Vec<PathTrace>,
pub active: bool,
}
#[derive(Serialize, Debug, Clone)]
pub struct PathTrace {
pub key: String,
@ -83,6 +96,7 @@ pub struct PageModel {
pub struct TreeViewRequest {
key: String,
children: Vec<TreeViewRequest>,
expand: bool,
}
impl TreeViewRequest {
@ -209,7 +223,11 @@ fn get_contents(
}
#[tauri::command]
fn get_stream_data(id: &str, path: &str, session: State<Mutex<Session>>) -> Result<String, String> {
fn get_stream_data(
id: &str,
path: &str,
session: State<Mutex<Session>>,
) -> Result<Vec<u8>, String> {
let session_guard = session
.lock()
.map_err(|_| "Failed to lock the session mutex.".to_string())?;
@ -217,7 +235,7 @@ fn get_stream_data(id: &str, path: &str, session: State<Mutex<Session>>) -> Resu
get_stream_data_by_path_with_file(path, &file.cos_file)
}
fn get_stream_data_by_path_with_file(path: &str, file: &CosFile) -> Result<String, String> {
fn get_stream_data_by_path_with_file(path: &str, file: &CosFile) -> Result<Vec<u8>, String> {
let mut steps = Step::parse(path);
if steps
.pop_back()
@ -233,7 +251,7 @@ fn get_stream_data_by_path_with_file(path: &str, file: &CosFile) -> Result<Strin
};
let resolver = file.resolver();
let data = t!(t!(Stream::<Primitive>::from_stream(stream, &resolver)).data(&resolver));
Ok(String::from_utf8_lossy(&data).into_owned())
Ok(data.to_vec())
}
#[tauri::command]
@ -304,32 +322,45 @@ fn resolve_parent(step: Step, file: &CosFile) -> Result<(Primitive, PathTrace),
#[tauri::command]
fn get_prim_tree_by_path(
id: &str,
path: TreeViewRequest,
paths: Vec<TreeViewRequest>,
session: State<Mutex<Session>>,
) -> Result<PrimitiveModel, String> {
) -> Result<Vec<PrimitiveTreeView>, String> {
let session_guard = session
.lock()
.map_err(|_| "Failed to lock the session mutex.".to_string())?;
let file = get_file_from_state(id, &session_guard)?;
get_prim_tree_by_path_with_file(path, &file.cos_file)
let results = paths
.into_iter()
.map(|path| get_prim_tree_by_path_with_file(path, &file.cos_file))
.collect::<Result<Vec<_>, String>>()?
.into_iter()
.flatten()
.collect();
Ok(results)
}
fn get_prim_tree_by_path_with_file(
node: TreeViewRequest,
file: &CosFile,
) -> Result<PrimitiveModel, String> {
) -> Result<Vec<PrimitiveTreeView>, String> {
let step = node.step()?;
let (parent, trace) = resolve_parent(step.clone(), file)?;
let path = vec![trace];
let mut parent_model = PrimitiveModel::from_primitive_with_children(&parent, path);
let trace = vec![trace];
let mut parent_model: PrimitiveModel;
if node.expand {
parent_model = PrimitiveModel::from_primitive_with_children(&parent, trace);
for child in node.children.iter() {
expand(child, &mut parent_model, &parent, file)?;
for child in node.children.iter() {
expand(child, &mut parent_model, &parent, file)?;
}
} else {
parent_model = PrimitiveModel::from_primitive(step.get_key(), &parent, trace);
}
Ok(parent_model)
Ok(PrimitiveTreeView::flatten(0, parent_model))
}
fn expand(
@ -338,20 +369,21 @@ fn expand(
parent: &Primitive,
file: &CosFile,
) -> Result<(), String> {
if !node.expand {
return Ok(());
}
let step = node.step()?;
let prim = resolve_step(parent, &step)?;
if let Primitive::Reference(x_ref) = prim {
let jump = resolve_xref(x_ref.id, file)?;
// parent_model.ptype = format!("{}-Reference", jump.get_debug_name());
let mut jump_trace = parent_model.trace.clone();
jump_trace.push(PathTrace::new(step.get_key(), x_ref.id.to_string()));
let mut to_expand = parent_model.get_child(step.get_key()).unwrap();
to_expand.add_children(
&jump,
append_path_with_jump(step.get_key(), x_ref.id.to_string(), &to_expand.trace),
);
to_expand.add_children(&jump, &jump_trace);
expand_children(node, file, &jump, &mut to_expand)?;
} else {
let mut to_expand = parent_model.get_child(step.get_key()).unwrap();
to_expand.add_children(prim, append_path(step.get_key(), &to_expand.trace));
to_expand.add_children(prim, &to_expand.trace.clone());
expand_children(node, file, prim, &mut to_expand)?;
}
Ok(())
@ -426,6 +458,9 @@ fn retrieve_trailer(file: &CosFile) -> Primitive {
}
fn retrieve_page(page_num: u32, file: &CosFile) -> Result<(Primitive, PathTrace), String> {
if page_num <= 0 {
return Err("Page 0 does not exist, use 1-based index!".to_string());
}
let page_rc = t!(file.get_page(page_num - 1));
let p_ref = page_rc.get_ref().get_inner();
Ok((
@ -516,12 +551,6 @@ fn get_file_from_state<'a>(
.ok_or_else(|| format!("File with id {} does not exist!", id))
}
fn append_path_with_jump(key: String, last_jump: String, path: &Vec<PathTrace>) -> Vec<PathTrace> {
let mut new_path = path.clone();
new_path.push(PathTrace::new(key, last_jump));
new_path
}
fn append_path(key: String, path: &Vec<PathTrace>) -> Vec<PathTrace> {
let mut new_path = path.clone();
let last_jump = new_path.last().unwrap().last_jump.clone();
@ -538,10 +567,10 @@ impl PrimitiveModel {
Primitive::Boolean(b) => b.to_string(),
Primitive::String(s) => s.to_string_lossy(),
Primitive::Stream(_) => "-".to_string(),
Primitive::Dictionary(_) => "-".to_string(),
Primitive::Dictionary(dict) => PrimitiveModel::format_dict_content(dict),
Primitive::Array(arr) => PrimitiveModel::format_arr_content(arr),
Primitive::Reference(pref) => {
format!("Obj Number: {} Gen Number: {}", pref.id, pref.gen)
format!("Obj Nr: {} Gen Nr: {}", pref.id, pref.gen)
}
Primitive::Name(name) => name.clone().as_str().to_string(),
@ -567,6 +596,26 @@ impl PrimitiveModel {
}
}
fn format_dict_content(dict: &Dictionary) -> String {
let mut result = String::from("{");
let mut count = 0;
for (key, value) in dict.iter() {
result.push_str(&format!(
"{}: {}",
key,
PrimitiveModel::format_prim_short(value)
));
count += 1;
if count < 4 {
result.push_str(", ");
} else {
result.push_str(",...");
break;
}
}
result.push_str("}");
result
}
fn format_arr_content(arr: &Vec<Primitive>) -> String {
if arr.len() == 0 {
return "[]".to_string();
@ -575,14 +624,7 @@ impl PrimitiveModel {
let contents = if arr.len() > 4 { &arr[0..4] } else { &arr[..] };
for i in 0..contents.len() {
let prim = contents.get(i).unwrap();
result.push_str(&match prim {
Primitive::Integer(i) => format!("{}", i),
Primitive::Number(n) => format!("{}", n),
Primitive::Boolean(b) => format!("{}", b),
Primitive::String(s) => s.to_string().unwrap_or(String::from("-")),
Primitive::Name(n) => n.as_str().to_string(),
_ => prim.get_debug_name().to_string(),
});
result.push_str(&PrimitiveModel::format_prim_short(prim));
if i != contents.len() - 1 {
result.push_str(", ");
}
@ -595,17 +637,31 @@ impl PrimitiveModel {
result
}
fn from_primitive_with_children(primitive: &Primitive, path: Vec<PathTrace>) -> PrimitiveModel {
fn format_prim_short(prim: &Primitive) -> String {
match prim {
Primitive::Integer(i) => format!("{}", i),
Primitive::Number(n) => format!("{}", n),
Primitive::Boolean(b) => format!("{}", b),
Primitive::String(s) => s.to_string().unwrap_or(String::from("-")),
Primitive::Name(n) => n.as_str().to_string(),
_ => prim.get_debug_name().to_string(),
}
}
fn from_primitive_with_children(
primitive: &Primitive,
trace: Vec<PathTrace>,
) -> PrimitiveModel {
let mut model = PrimitiveModel::from_primitive(
path.last().unwrap().key.clone(),
trace.last().unwrap().key.clone(),
primitive,
path.clone(),
trace.clone(),
);
model.add_children(primitive, path);
model.add_children(primitive, &trace);
model
}
fn add_children(&mut self, primitive: &Primitive, path: Vec<PathTrace>) {
fn add_children(&mut self, primitive: &Primitive, path: &Vec<PathTrace>) {
self.expanded = true;
match primitive {
Primitive::Dictionary(dict) => dict.iter().for_each(|(name, value)| {
@ -619,6 +675,13 @@ impl PrimitiveModel {
self.add_child(i.to_string(), obj, append_path(i.to_string(), &path));
}),
Primitive::Stream(stream) => {
stream.info.iter().for_each(|(name, value)| {
self.add_child(
name.clone().as_str().to_string(),
value,
append_path(name.clone().as_str().to_string(), &path),
);
});
self.children.push(PrimitiveModel {
key: "Data".to_string(),
ptype: "Stream Data".to_string(),
@ -628,13 +691,6 @@ impl PrimitiveModel {
trace: append_path("Data".to_string(), &path),
expanded: false,
});
stream.info.iter().for_each(|(name, value)| {
self.add_child(
name.clone().as_str().to_string(),
value,
append_path(name.clone().as_str().to_string(), &path),
);
})
}
_ => (),
};
@ -654,6 +710,42 @@ impl PrimitiveModel {
fn get_child(&mut self, key: String) -> Option<&mut PrimitiveModel> {
self.children.iter_mut().find(|child| child.key == key)
}
fn is_container(&self) -> bool {
self.ptype == "Dictionary"
|| self.ptype == "Array"
|| self.ptype == "Stream"
|| self.ptype == "Reference"
}
fn drain_children(&mut self) -> Vec<PrimitiveModel> {
self.children.drain(..).collect()
}
}
impl PrimitiveTreeView {
fn from_primitive(depth: usize, primitive: PrimitiveModel) -> PrimitiveTreeView {
let is_container = primitive.is_container();
PrimitiveTreeView {
depth: depth,
key: primitive.key,
ptype: primitive.ptype,
sub_type: primitive.sub_type,
value: primitive.value,
container: is_container,
expanded: primitive.expanded,
path: primitive.trace,
active: true,
}
}
fn flatten(depth: usize, mut primitive: PrimitiveModel) -> Vec<PrimitiveTreeView> {
let mut views: Vec<PrimitiveTreeView> = Vec::new();
let children = primitive.drain_children();
views.push(PrimitiveTreeView::from_primitive(depth, primitive));
children.into_iter().for_each(|child| {
views.extend(PrimitiveTreeView::flatten(depth + 1, child.clone()));
});
views
}
}
#[tauri::command]
fn get_xref_table(id: &str, session: State<Mutex<Session>>) -> Result<XRefTableModel, String> {
@ -686,7 +778,7 @@ fn get_xref_table_model_with_file(file: &CosFile) -> Result<XRefTableModel, Stri
}
XRef::Stream { stream_id, index } => XRefEntryModel {
obj_num: i as u64,
gen_num: *stream_id as u64,
gen_num: 0,
obj_type: "Stream".into(),
offset: *index as u64,
},

View File

@ -66,27 +66,46 @@ mod tests {
children: vec![TreeViewRequest {
key: "1".to_string(),
children: vec![],
expand: true,
}],
expand: true,
});
path.push(TreeViewRequest {
key: "Info".to_string(),
children: vec![],
expand: true,
});
path.push(TreeViewRequest {
key: "Root".to_string(),
children: vec![TreeViewRequest {
key: "Pages".to_string(),
children: vec![],
expand: true,
}],
expand: true,
});
let root = TreeViewRequest {
key: "/".to_string(),
key: "Trailer".to_string(),
children: path,
expand: true,
};
let message = format!("Retrieval of {:?}", root);
let prim = timed!(get_prim_tree_by_path_with_file(root, &file), message);
print_node(prim.unwrap(), 0);
let prim = timed!(get_prim_tree_by_path_with_file(root, &file), message).unwrap();
for ele in prim {
println!(
"{}{} | {} | {} | {:?}",
" ".repeat(ele.depth),
ele.key,
ele.ptype,
ele.value,
ele.path
.iter()
.map(|p| p.key.to_string())
.collect::<Vec<String>>()
.join("/")
);
}
}
#[test]
fn test_read_by_path() {
@ -94,7 +113,7 @@ mod tests {
FileOptions::cached().open(FILE_PATH).unwrap(),
"Loading file"
);
let path = "/Root/Pages";
let path = "/Trailer/Root/Pages";
let message = format!("Retrieval of {:?}", path);
let prim = timed!(get_prim_model_by_path_with_file(path, &file), message);
@ -122,9 +141,8 @@ mod tests {
"writing trailer"
);
let trail_model = PrimitiveModel::from_primitive_with_children(
"Trailer".to_string(),
&trail,
vec![PathTrace::new("/".to_string(), "/".to_string())],
vec![PathTrace::new("Trailer".to_string(), "Trailer".to_string())],
);
print_node(trail_model, 5);
println!("{:?}", file.trailer.info_dict);

View File

@ -34,21 +34,31 @@
loadContents(path, id);
});
$inspect(contents);
function loadContents(path: string | undefined, id: string) {
console.log("Loading contents for", path, id);
if (!path || !id) return;
invoke<ContentModel>("get_contents", { id, path })
.then((result) => {
console.log("Contents loaded", result);
contents = result;
if (contents && editor) {
const text = contents.parts
.map((part) => part.join("\n"))
.join(
"\n\n%-------------------% EOF %-------------------%\n\n",
);
let text = "";
if (contents.parts.length > 1) {
let i = 0;
for (let part of contents.parts) {
text +=
"%----------------% Contents[" +
i +
"] %--------------%\n\n";
for (let line of part) {
text += " " + line + "\n";
}
text +=
"\n%-------------------% EOF %-------------------%\n\n";
i++;
}
} else {
text = contents.parts[0].join("\n");
}
editor.setValue(text);
}
})

View File

@ -4,13 +4,9 @@
import PrimitiveView from "./PrimitiveView.svelte";
import TreeView from "./TreeView.svelte";
import type FileViewState from "../models/FileViewState.svelte";
import { createEventDispatcher, onMount } from "svelte";
import PageList from "./PageList.svelte";
import ContentsView from "./ContentsView.svelte";
import TreeViewRequest from "../models/TreeViewRequest.svelte";
import TreeViewState from "../models/TreeViewState.svelte";
import type { PathSelectedEvent } from "../events/PathSelectedEvent";
TreeViewRequest;
import { onMount } from "svelte";
let {
treeShowing,
xrefTableShowing,
@ -68,19 +64,7 @@
minSize={treeShowing || pagesShowing ? 2 : 0}
maxSize={treeShowing || pagesShowing ? 100 : 0}
>
{#if pagesShowing}
<PageList {fState} h={height}></PageList>
{:else if treeShowing}
<div class="overflow-auto" style="height: {height}px">
<TreeView
{pathSelectedHandler}
file_id={fState.file.id}
root={TreeViewRequest.TRAILER}
active={true}
details={false}
></TreeView>
</div>
{/if}
<TreeView {fState} {height}></TreeView>
</Pane>
<Pane minSize={1}>

View File

@ -1,70 +0,0 @@
<script lang="ts">
import type FileViewState from "../models/FileViewState.svelte";
import TreeView from "./TreeView.svelte";
import TreeViewRequest from "../models/TreeViewRequest.svelte";
import type { PathSelectedEvent } from "../events/PathSelectedEvent";
let { fState, h }: { fState: FileViewState; h: number } = $props();
let selected_page_num: number | undefined = $state(undefined);
let pages = $derived(fState.file.pages);
function pathSelectedHandler(event: PathSelectedEvent) {
let path = event.detail.path;
if (
path.length > 0 &&
path[0].startsWith("Page") &&
!isNaN(+path[0].replace("Page", ""))
) {
selected_page_num = +path[0].replace("Page", "");
}
fState.selectPath(path);
}
</script>
<div class="overflow-x-auto">
<div class="overflow-y-auto" style="height: {h}px">
{#each pages as page}
<TreeView
file_id={fState.file.id}
root={new TreeViewRequest("Page" + page.page_num, [])}
{pathSelectedHandler}
active={selected_page_num === page.page_num}
details={true}
></TreeView>
{/each}
</div>
</div>
<style lang="postcss">
.key-field {
display: flex;
flex-direction: row;
align-items: center;
gap: 3px;
}
.selected {
@apply bg-forge-acc;
}
.t-header {
@apply uppercase text-xs text-forge-text p-5 bg-forge-sec border-r;
cursor: default;
}
.t-data {
@apply border border-forge-sec text-xs text-forge-text text-left;
text-align: left;
cursor: default;
}
.ref-cell {
@apply min-w-[100px] w-[100px] min-h-[20px] h-[20px] p-1 m-0;
user-select: none;
}
.page-cell {
@apply min-w-[150px] w-[150px] min-h-[20px] h-[20px] p-1 m-0;
user-select: none;
}
</style>

View File

@ -1,35 +1,35 @@
<script lang="ts">
import {
FileOutline,
FolderArrowRightOutline,
FolderOutline,
CodeOutline,
} from "flowbite-svelte-icons";
import {FileOutline, FolderArrowRightOutline, FolderOutline, CodeOutline} from "flowbite-svelte-icons";
let {ptype}: {ptype: string} = $props()
let { ptype }: { ptype: string } = $props();
</script>
{#if ptype === "Dictionary"}
<FolderOutline class="stroke-blue-300 text-blue-300 primitive-icon"/>
{:else if ptype === "Array"}
<FolderOutline class=" text-orange-300 primitive-icon "/>
{:else if ptype === "Reference"}
<FolderArrowRightOutline class=" text-purple-300 primitive-icon"/>
{:else if ptype === "Integer"}
<FileOutline class="text-pink-300 primitive-icon"/>
{:else if ptype === "Number"}
<FileOutline class="text-lime-300 primitive-icon"/>
{:else if ptype === "Boolean"}
<FileOutline class="text-fuchsia-300 primitive-icon"/>
{:else if ptype === "String"}
<FileOutline class="text-green-300 primitive-icon"/>
{:else if ptype === "Name"}
<FileOutline class="text-red-400 primitive-icon"/>
{:else if ptype === "Stream Data"}
<CodeOutline class="text-purple-400 primitive_icon"/>
{:else}
<FileOutline/>
{/if}
{#if ptype === "Dictionary"}
<FolderOutline class="stroke-blue-300 text-blue-300 primitive-icon" />
{:else if ptype === "Array"}
<FolderOutline class=" text-orange-300 primitive-icon " />
{:else if ptype === "Reference"}
<FolderArrowRightOutline class=" text-purple-300 primitive-icon" />
{:else if ptype === "Integer"}
<FileOutline class="text-pink-300 primitive-icon" />
{:else if ptype === "Number"}
<FileOutline class="text-lime-300 primitive-icon" />
{:else if ptype === "Boolean"}
<FileOutline class="text-fuchsia-300 primitive-icon" />
{:else if ptype === "String"}
<FileOutline class="text-green-300 primitive-icon" />
{:else if ptype === "Name"}
<FileOutline class="text-red-400 primitive-icon" />
{:else if ptype === "Stream Data"}
<CodeOutline class="text-purple-400 primitive_icon" />
{:else}
<FileOutline />
{/if}
<style lang="postcss">
.primitive_icon {
margin: auto;
}
</style>
</style>

View File

@ -5,28 +5,50 @@
import PrimitiveIcon from "./PrimitiveIcon.svelte";
import StreamEditor from "./StreamEditor.svelte";
const cellH = 29;
const headerOffset = 24;
let { fState, height }: { fState: FileViewState; height: number } =
$props();
let bodyHeight = $derived(height - 24);
let fillerHeight: number = $state(0);
let firstEntry = $state(0);
let lastEntry = $state(100);
let scrollY = $state(0);
let prim = $derived(fState.prim);
let showContents = $state(false);
let tableHeight = $state(0);
let entriesToDisplay: Primitive[] = $derived(
prim ? prim.children.slice(firstEntry, lastEntry) : [],
);
let tableHeight = $derived(prim ? prim.children.length * cellH : 0);
let bodyHeight = $derived(height - headerOffset);
let editorHeight = $derived(Math.max(800, bodyHeight - tableHeight));
$inspect(fState.highlightedPrim);
let imageUrl = "";
// Example: Simulating a binary Uint8Array (normally fetched from an API)
let binaryData = new Uint8Array([fState.stream_data]);
if (binaryData.length > 0) {
const blob = new Blob([binaryData], { type: "image/png" });
imageUrl = URL.createObjectURL(blob);
}
function handlePrimSelect(prim: Primitive) {
if (prim.isContainer()) {
if (!prim) {
fState.prim = prim;
return;
}
const _path: string[] = fState.copyPath();
_path.push(prim?.key);
fState.selectPath(_path);
}
return;
const _path: string[] = fState.copyPath();
_path.push(prim?.key);
fState.selectPath(_path);
}
function handleScroll(event: Event & { currentTarget: HTMLElement }) {
scrollY = event.currentTarget.scrollTop;
firstEntry = Math.floor(scrollY / cellH);
lastEntry = Math.ceil((scrollY + bodyHeight) / cellH);
fillerHeight = firstEntry * cellH;
}
</script>
{#if fState.stream_data}
<img src={fState.stream_data} />
{/if}
{#if prim && prim.children && prim.children.length > 0}
<div class="overflow-x-auto">
<div class="w-[851px]">
@ -41,35 +63,47 @@
</tr>
</thead>
</table>
<div class="overflow-y-auto" style="height: {bodyHeight}px">
<table bind:clientHeight={tableHeight}>
<tbody>
{#each prim.children as entry}
<tr
class:selected={entry.key ===
fState.highlightedPrim?.key}
class="hover:bg-forge-sec"
onclick={() => (fState.highlightedPrim = entry)}
ondblclick={() => handlePrimSelect(entry)}
>
<td class="page-cell t-data">
<div class="key-field">
<PrimitiveIcon ptype={entry.ptype} />
<p class="text-left">
{entry.key}
</p>
</div>
</td>
<td class="ref-cell t-data">{entry.ptype}</td>
<td class="cell t-data">{entry.value}</td>
</tr>
{/each}
</tbody>
</table>
{#if fState.prim?.ptype === "Stream"}
<div
onscroll={handleScroll}
class="overflow-y-auto"
style="height: {bodyHeight}px"
>
<div class="container" style="height: {tableHeight}px">
<table>
<tbody>
<tr class="filler" style="height: {fillerHeight}px"
></tr>
{#each entriesToDisplay as entry}
<tr
class:selected={entry.key ===
fState.highlighted_prim}
class="row"
onclick={() =>
(fState.highlighted_prim = entry.key)}
ondblclick={() => handlePrimSelect(entry)}
>
<td class="page-cell t-data">
<div class="key-field">
<PrimitiveIcon
ptype={entry.ptype}
/>
<p class="text-left">
{entry.key}
</p>
</div>
</td>
<td class="ref-cell t-data"
>{entry.ptype}</td
>
<td class="cell t-data">{entry.value}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{#if fState.stream_data}
<StreamEditor
fileId={fState.file.id}
path={fState.getMergedPath()}
stream_data={fState.stream_data}
height={editorHeight}
></StreamEditor>
{/if}
@ -86,20 +120,11 @@
gap: 3px;
}
.path-bar {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 3px;
}
.path-view {
@apply bg-forge-dark border border-forge-bound text-sm font-extralight mb-2 mt-2 rounded-sm;
flex: 1;
display: flex;
flex-direction: row;
user-select: auto;
.row {
@apply hover:bg-forge-sec;
height: 29px;
min-height: 29px;
max-height: 29px;
}
.selected {
@ -118,17 +143,17 @@
}
.cell {
@apply min-w-[600px] w-[600px] min-h-[15px] h-[15px] p-1 m-0;
@apply min-w-[600px] w-[600px] p-1 m-0;
user-select: none;
}
.ref-cell {
@apply min-w-[100px] w-[100px] min-h-[15px] h-[15px] p-1 m-0;
@apply min-w-[100px] w-[100px] p-1 m-0;
user-select: none;
}
.page-cell {
@apply min-w-[150px] w-[150px] min-h-[15px] h-[15px] p-1 m-0;
@apply min-w-[150px] w-[150px] p-1 m-0;
user-select: none;
}
</style>

View File

@ -4,11 +4,8 @@
import { onMount } from "svelte";
import * as monaco from "monaco-editor";
let {
fileId,
path,
height,
}: { fileId: string; path: string; height: number } = $props();
let { stream_data, height }: { stream_data: string; height: number } =
$props();
let contents: string | undefined = $state(undefined);
let editorContainer: HTMLElement;
@ -30,23 +27,13 @@
});
$effect(() => {
loadContents(path, fileId);
loadContents(stream_data);
});
$inspect(contents);
function loadContents(path: string | undefined, id: string) {
console.log("Loading contents for", path, id);
if (!path || !id) return;
path = path + "/Data";
invoke<string>("get_stream_data", { id, path })
.then((result) => {
console.log("Contents loaded", result);
contents = result;
if (contents && editor) {
editor.setValue(contents);
}
})
.catch((err) => console.error(err));
function loadContents(stream_data: string | undefined) {
if (!stream_data || !editor) return;
editor.setValue(stream_data);
}
</script>

View File

@ -1,167 +0,0 @@
<script lang="ts">
import type Primitive from "../models/Primitive.svelte";
import PrimitiveIcon from "./PrimitiveIcon.svelte";
import { CaretRightOutline, CaretDownOutline } from "flowbite-svelte-icons";
import type TreeViewState from "../models/TreeViewState.svelte";
import { PathSelectedEvent } from "../events/PathSelectedEvent";
import { createEventDispatcher } from "svelte";
let {
file_id,
parent_view,
parent_path,
treeState,
pathSelectedHandler = $bindable(),
details,
}: {
file_id: string;
parent_view: Primitive | undefined;
parent_path: string[];
treeState: TreeViewState | undefined;
pathSelectedHandler: any;
details: boolean;
} = $props();
function copyPathAndAppend(key: string): string[] {
const _path = copyPath();
_path.push(key);
return _path;
}
function copyPath(): string[] {
const _path: string[] = [];
for (let item of parent_path) {
_path.push(item);
}
return _path;
}
function selectItem(child: Primitive) {
let _path = copyPathAndAppend(child.key);
if (child.expanded) {
treeState?.collapseTree(_path);
} else {
treeState?.expandTree(_path);
}
const event = new PathSelectedEvent(file_id, _path);
pathSelectedHandler(event);
}
</script>
{#if parent_view}
{#each parent_view.children as child}
<button
class="row hover:bg-forge-sec w-full"
onclick={() => selectItem(child)}
>
<div class="item" style="margin-left: {parent_path.length * 1}em">
{#if child.isContainer()}
<div>
<span class="caret"
>{#if child.expanded}<CaretDownOutline
/>{:else}<CaretRightOutline />{/if}</span
>
</div>
{:else}
<span class="no-caret"></span>
{/if}
<div class="item">
<PrimitiveIcon ptype={child.ptype} />
<div class="row">
<p>
{child.key}
</p>
{#if details}
<p class="hint">
{" | " +
child.ptype +
" | " +
child.sub_type +
" | " +
child.value}
</p>
{:else}
<p class="ml-1 text-forge-sec small">
{child.sub_type}
</p>
{/if}
</div>
</div>
</div>
</button>
{#if child.children.length > 0}
<svelte:self
{file_id}
parent_view={child}
parent_path={copyPathAndAppend(child.key)}
{treeState}
bind:pathSelectedHandler
{details}
></svelte:self>
{/if}
{/each}
{/if}
<style lang="postcss">
.hint {
@apply ml-1 text-forge-text_hint text-xs whitespace-nowrap font-extralight;
}
.select-button {
@apply hover:bg-forge-sec pr-3;
cursor: pointer;
}
.item {
@apply text-sm rounded;
text-align: center;
display: flex;
flex-direction: row;
user-select: none;
}
.small {
display: flex;
justify-content: center;
align-items: center;
}
.row {
@apply ml-1;
text-align: center;
display: flex;
flex-direction: row;
user-select: none;
}
/* Remove default bullets */
ul,
#myUL {
list-style-type: none;
}
ul {
@apply pl-5;
}
.no-caret {
@apply pl-5;
user-select: none;
}
.caret {
@apply text-forge-sec;
cursor: pointer;
user-select: none;
}
.caret-active {
transform: rotate(90deg);
}
/* Show the nested list when the user clicks on the caret/arrow (with JavaScript) */
.active {
display: block;
}
</style>

View File

@ -1,96 +1,201 @@
<script lang="ts">
import TreeNode from "./TreeNode.svelte";
import PrimitiveIcon from "./PrimitiveIcon.svelte";
import TreeViewState from "../models/TreeViewState.svelte";
import type TreeViewRequest from "../models/TreeViewRequest.svelte";
import TreeViewRequest from "../models/TreeViewRequest.svelte";
import { PathSelectedEvent } from "../events/PathSelectedEvent";
import { CaretDownOutline, CaretRightOutline } from "flowbite-svelte-icons";
import type { PrimitiveView } from "../models/PrimitiveView";
import { onMount } from "svelte";
import type FileViewState from "../models/FileViewState.svelte";
const rowHeight = 24;
let {
file_id,
root,
pathSelectedHandler,
active,
details,
fState,
height,
}: {
file_id: string;
root: TreeViewRequest;
pathSelectedHandler: any;
active: boolean;
details: boolean;
fState: FileViewState;
height: number;
} = $props();
let h = $state(100);
let states: TreeViewState[] = $state([]);
let treeState: TreeViewState | undefined = $derived(
states.find((state) => state.file_id === file_id),
);
const file_id = $derived(fState.file.id);
if (active) {
loadTreeView();
}
let fillerHeight: number = $state(0);
let states: TreeViewState[] = [];
let firstEntry = $state(0);
let lastEntry = $state(100);
let scrollY = $state(0);
let entryCount = $state(0);
function loadTreeView() {
if (!treeState) {
let newState = new TreeViewState(file_id, root);
newState.loadTreeView();
let treeState: TreeViewState | undefined = $state(undefined);
let totalHeight: number = $state(100);
let entries: PrimitiveView[] | undefined = $state([]);
$effect(() => {
treeState = states.find((state) => state.file_id === file_id);
updateTreeView();
});
async function updateTreeView() {
if (!treeState || treeState.file_id !== file_id) {
let newState = new TreeViewState(file_id, fState);
await newState.loadTreeView();
states.push(newState);
treeState = newState;
totalHeight = treeState.getEntryCount() * rowHeight;
}
firstEntry = Math.floor(scrollY / rowHeight);
lastEntry = Math.ceil((scrollY + height) / rowHeight);
entryCount = treeState?.getEntryCount();
totalHeight = Math.max(entryCount * rowHeight, 0);
fillerHeight = firstEntry * rowHeight;
entries = treeState.getEntries(firstEntry, lastEntry);
}
function select() {
loadTreeView();
pathSelectedHandler(new PathSelectedEvent(file_id, [root.key]));
function handleSelect(prim: PrimitiveView) {
if (prim.expanded && prim.container) {
treeState
?.collapseTree(prim.path.map((path) => path.key))
.then(() => {
updateTreeView();
});
return;
} else if (prim.container) {
treeState
?.expandTree(prim.path.map((path) => path.key))
.then(() => {
updateTreeView();
});
}
fState.selectPathHandler(
new PathSelectedEvent(
file_id,
prim.path.map((path) => path.key),
),
);
updateTreeView();
}
function formatDisplayKey(key: string) {
if (key.startsWith("Page") && !key.startsWith("Pages")) {
return key.replace("Page", "Page ");
}
return key;
}
function handleScroll(event: Event & { currentTarget: HTMLElement }) {
scrollY = event.currentTarget.scrollTop;
updateTreeView();
}
</script>
<ul id="myUL">
{#if root}
<li>
<button
class="item hover:bg-forge-sec w-full"
onmouseenter={() => loadTreeView()}
onclick={() => select()}
>
{#if active && treeState?.view}
<span class="caret"><CaretDownOutline /></span>
{:else}
<span class="caret"><CaretRightOutline /></span>
<div onscroll={handleScroll} class="overflow-auto" style="height: {height}px">
{#if entries}
<div style="height: {totalHeight}px; width: 100%">
<div
class="filler"
style="height: {fillerHeight}px; width: 100%"
></div>
{#each entries as entry}
{#if entry.active}
{#if entry.depth == 0}
<div
style="height: 1px; width: 100%;"
class="bg-forge-bound"
></div>
{/if}
<button
class="row text-sm hover:bg-forge-sec w-full group whitespace-nowrap"
style="height: {rowHeight}px;"
onclick={() => handleSelect(entry)}
>
<div style="margin-left: {entry.depth * 1.25}em">
{#if entry.container}
<div>
<span
class="caret group-hover:text-forge-text_hint"
>{#if entry.expanded}<CaretDownOutline
/>{:else}<CaretRightOutline
/>{/if}</span
>
</div>
{:else}
<span class="no-caret"></span>
{/if}
</div>
<div>
<PrimitiveIcon ptype={entry.ptype} />
</div>
<div class="pl-1 prim_name whitespace-nowrap">
{formatDisplayKey(entry.key)}
<div
class="details group-hover:text-forge-text_hint"
>
{" | " +
entry.value +
" | " +
entry.sub_type +
" | " +
entry.ptype}
</div>
</div>
</button>
{/if}
<div class="item">
<PrimitiveIcon ptype={"Dictionary"} />
{root.key}
</div>
</button>
{#if active && treeState?.view}
<TreeNode
{file_id}
bind:pathSelectedHandler
parent_view={treeState.view}
parent_path={treeState.view.getPath()}
{treeState}
{details}
></TreeNode>
{/if}
</li>
{/each}
</div>
{/if}
</ul>
</div>
<style lang="postcss">
.prim_name {
display: flex;
flex-direction: row;
position: relative;
bottom: 0;
width: 100%;
text-align: center;
}
.item {
@apply text-sm rounded-sm;
text-align: center;
display: flex;
flex-direction: row;
}
ul,
#myUL {
list-style-type: none;
}
/* Style the caret/arrow */
.caret {
@apply text-forge-sec;
cursor: pointer;
user-select: none; /* Prevent text selection */
user-select: none;
}
.details {
@apply ml-1 text-forge-prim whitespace-nowrap font-extralight;
padding-top: 2px;
}
.item {
@apply text-sm rounded;
text-align: center;
display: flex;
flex-direction: row;
user-select: none;
}
.small {
display: flex;
justify-content: center;
align-items: center;
}
.row {
text-align: center;
display: flex;
flex-direction: row;
user-select: none;
}
.no-caret {
@apply pl-5;
user-select: none;
}
</style>

View File

@ -84,10 +84,10 @@
<td class="cell t-header border-forge-sec">Offset</td>
</tr>
<tr
class={selectedPath === "/"
class={selectedPath === "Trailer"
? "bg-forge-acc"
: "hover:bg-forge-sec"}
ondblclick={() => fState.selectXref(undefined)}
onclick={() => fState.selectXref(undefined)}
>
<td class="cell t-data">Trailer</td>
<td class="cell t-data">65535</td>
@ -112,7 +112,7 @@
class={selectedPath === entry.obj_num
? "bg-forge-acc"
: "hover:bg-forge-sec"}
ondblclick={() => fState.selectXref(entry)}
onclick={() => fState.selectXref(entry)}
>
<td class="cell t-data">{entry.obj_num}</td>
<td class="cell t-data">{entry.gen_num}</td>
@ -149,15 +149,4 @@
.scrollContainer {
overflow-y: auto;
}
.xref-modal {
position: fixed;
right: 0;
top: 0;
bottom: 0;
width: 281px;
background: var(--background-color);
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
z-index: 1000;
overflow-y: auto;
}
</style>

View File

@ -12,8 +12,9 @@ export default class FileViewState {
public path: string[] = $state(["Trailer"]);
public file: PdfFile;
public prim: Primitive | undefined = $state();
public highlightedPrim: Primitive | undefined = $state();
public highlighted_prim: string | undefined = $state();
public xref_entries: XRefEntry[] = $state([]);
public stream_data: string | undefined = $state();
constructor(file: PdfFile) {
@ -38,8 +39,19 @@ export default class FileViewState {
.catch(err => console.error(err));
}
public selectPath(newPath: string[]) {
invoke<Primitive>("get_prim_by_path", { id: this.file.id, path: this.mergePaths(newPath) })
public async selectPath(newPath: string[]) {
if (newPath.at(-1) === "Data") {
let _path = newPath.slice(0, newPath.length - 1)
const _prim = await invoke<Primitive>("get_prim_by_path", { id: this.file.id, path: this.formatPaths(_path) })
this.stream_data = await invoke<string>("get_stream_data", { id: this.file.id, path: this.formatPaths(newPath) })
this.prim = new Primitive(_prim);
this.path = _path;
this.highlighted_prim = "Data";
return;
} else {
this.stream_data = undefined;
}
invoke<Primitive>("get_prim_by_path", { id: this.file.id, path: this.formatPaths(newPath) })
.then(result => {
let _prim = new Primitive(result)
if (_prim.isContainer()) {
@ -47,7 +59,7 @@ export default class FileViewState {
this.path = newPath
return;
} else {
this.highlightedPrim = _prim;
this.highlighted_prim = _prim.key;
this.selectPath(newPath.slice(0, newPath.length - 1))
}
})
@ -55,7 +67,7 @@ export default class FileViewState {
}
public selectPathHandler(event: PathSelectedEvent) {
console.log("Selecting path", event.detail);
if (event.detail.file_id !== this.file.id) {
return;
}
@ -63,7 +75,7 @@ export default class FileViewState {
}
public getMergedPath() {
return this.mergePaths(this.path);
return this.formatPaths(this.path);
}
public popPath() {
@ -93,7 +105,7 @@ export default class FileViewState {
}
public mergePaths(paths: string[]) {
public formatPaths(paths: string[]) {
if (paths.length == 0) {
return "/";
}

View File

@ -0,0 +1,15 @@
import type { Trace } from "./Primitive.svelte";
export class PrimitiveView {
constructor(
public depth: number,
public key: string,
public ptype: string,
public sub_type: string,
public value: string,
public container: boolean,
public expanded: boolean,
public path: Trace[],
public active: boolean,
) { }
}

View File

@ -3,11 +3,12 @@ export default class TreeViewRequest {
public key: string;
public children: TreeViewRequest[];
public displayName: string;
public active: boolean;
expand: boolean;
constructor(
key: string,
children: TreeViewRequest[]
children: TreeViewRequest[],
expand: boolean = false,
) {
if (key.startsWith("Page")) {
this.displayName = "Page " + key.slice(4);
@ -16,13 +17,21 @@ export default class TreeViewRequest {
}
this.key = key;
this.children = children;
this.active = true;
this.expand = expand;
}
static TRAILER = new TreeViewRequest("Trailer", [new TreeViewRequest("Root", [])]);
static fromPageCount(pageCount: number) {
let roots = [TreeViewRequest.TRAILER];
for (let i = 0; i < pageCount; i++) {
roots.push(new TreeViewRequest("Page" + (i + 1), []));
}
return roots;
}
static TRAILER = new TreeViewRequest("Trailer", [new TreeViewRequest("Root", [], true)], true);
public clone(): TreeViewRequest {
return new TreeViewRequest(this.key, this.children.map(child => child.clone()));
return new TreeViewRequest(this.key, this.children.map(child => child.clone()), this.expand);
}
public getChild(key: string) {
@ -30,16 +39,17 @@ export default class TreeViewRequest {
}
public addChild(key: string) {
let child = new TreeViewRequest(key, [])
this.expand = true;
let child = new TreeViewRequest(key, [], true)
this.children.push(child);
return child;
}
public clearChildren() {
this.children = [];
}
public removeChild(key: string) {
this.children = this.children.filter(child => child.key !== key);
}
}
public setExpand(expand: boolean) {
this.expand = expand;
}
}

View File

@ -1,79 +1,160 @@
import { invoke } from "@tauri-apps/api/core";
import Primitive from "./Primitive.svelte";
import TreeViewRequest from "./TreeViewRequest.svelte";
import type { PathSelectedEvent } from "../events/PathSelectedEvent";
import { PrimitiveView } from "./PrimitiveView";
import type { Trace } from "./Primitive.svelte";
import type FileViewState from "./FileViewState.svelte";
export default class TreeViewState {
private root: TreeViewRequest = $state(new TreeViewRequest("Trailer", [new TreeViewRequest("Root", [])]));
public view: Primitive | undefined = $state();
active: boolean = $state(false);
private request: TreeViewRequest[];
private activeRequest: Map<string, TreeViewRequest> = $state(new Map());
private activeEntries: Map<string, PrimitiveView[]> = $state(new Map());
file_id: string;
constructor(file_id: string, root: TreeViewRequest) {
console.log("Creating tree view state", file_id, root);
constructor(file_id: string, fState: FileViewState) {
this.request = TreeViewRequest.fromPageCount(+fState.file.page_count);
this.activeRequest.set(this.request[0].key, this.request[0]);
this.file_id = file_id;
this.root = root;
}
public loadTreeView() {
this.setTreeViewRequest(this.getRoot());
public getEntryCount() {
let count = 0;
this.activeEntries.forEach((value, key) => {
count += value.length - 1;
});
return this.request.length + count;
}
public getRoot(): TreeViewRequest {
return this.root.clone();
public getEntries(start: number, end: number): PrimitiveView[] {
let i = 0;
let result: PrimitiveView[] = [];
for (let request of this.request) {
if (this.activeEntries.has(request.key)) {
let entries = this.activeEntries.get(request.key);
for (let entry of entries) {
if (i >= start) {
result.push(entry);
}
i += 1;
if (i >= end) {
return result;
}
}
} else {
if (i >= start) {
result.push(
new PrimitiveView(
0,
request.key,
"Dictionary",
"-",
"-",
true,
false,
[{ key: request.key, last_jump: request.key } as Trace],
true,
))
}
i += 1;
if (i >= end) {
return result;
}
}
}
return result;
}
public setTreeViewRequest(treeViewRequest: TreeViewRequest) {
console.log("Loading tree view", treeViewRequest);
invoke<Primitive>("get_prim_tree_by_path", {
public toView(request: TreeViewRequest): PrimitiveView {
return new PrimitiveView(
0,
request.key,
"Dictionary",
"-",
"-",
true,
false,
[{ key: request.key, last_jump: request.key } as Trace],
false,
);
}
public async loadTreeView() {
const activeRequests = Array.from(this.activeRequest.values());
await this.updateTreeViewRequest(activeRequests);
}
public getRoot(): TreeViewRequest[] {
let _roots: TreeViewRequest[] = [];
this.request.forEach((root) => {
_roots.push(root.clone());
});
return _roots;
}
public async updateTreeViewRequest(treeViewRequests: TreeViewRequest[]) {
let result = await invoke<PrimitiveView[]>("get_prim_tree_by_path", {
id: this.file_id,
path: treeViewRequest,
})
.then((result) => {
this.view = new Primitive(result);
this.root = treeViewRequest;
})
.catch((err) => console.error(err));
paths: treeViewRequests,
});
for (let i = 0; i < treeViewRequests.length; i++) {
let request = treeViewRequests[i];
if (request.expand) {
this.activeRequest.set(request.key, request);
} else {
this.activeRequest.delete(request.key);
}
let prim = result.filter(r => r.path[0].key == request.key);
if (prim) {
this.activeEntries.set(request.key, prim);
}
}
}
public expandTree(path: string[]) {
public async expandTree(path: string[]) {
if (path.length == 0) {
console.error("Empty path");
return;
}
console.log("Expanding tree", this.getRoot(), path);
let root = this.getRoot();
let root = this.activeRequest.get(path[0]);
if (!root) {
root = this.getRoot().find((root) => root.key === path[0]);
if (!root) {
console.error("Root not found for path: " + path);
return;
}
}
root.setExpand(true);
let node = root;
for (let key of path.slice(1, path.length)) {
let _node: TreeViewRequest | undefined = node.getChild(key);
if (_node) {
_node.setExpand(true);
node = _node;
} else {
node = node.addChild(key);
}
}
this.setTreeViewRequest(root);
await this.updateTreeViewRequest([root]);
}
public collapseTree(path: string[]) {
public async collapseTree(path: string[]) {
if (path.length == 0) {
console.error("Empty path");
return;
}
if (path.length == 1) {
this.view = this.view?.withoutChildren();
let root: TreeViewRequest | undefined = this.activeRequest.get(path[0]);
if (!root) {
console.error("Root not found for path: " + path);
return;
}
let root: TreeViewRequest = this.root;
let node: TreeViewRequest | undefined = root;
for (let key of path.slice(1, path.length - 1)) {
for (let key of path.slice(1, path.length)) {
node = node?.getChild(key);
}
if (node) {
node.removeChild(path[path.length - 1]);
this.setTreeViewRequest(root);
node.setExpand(false);
await this.updateTreeViewRequest([root]);
}
}
}

View File

@ -28,7 +28,7 @@ export default {
bound: 'rgba(0, 0, 0, 0.29)',
text: '#dadada',
text_sec: '#838686',
text_hint: '#5adada',
text_hint: '#5cc4c4c2',
}
}
}