getting there

* stream view and page list working
This commit is contained in:
Kilian Schuettler 2025-02-01 03:10:46 +01:00
parent 7e71cb947b
commit 570d495faa
15 changed files with 354 additions and 213 deletions

View File

@ -59,6 +59,7 @@ pub struct PrimitiveModel {
pub value: String,
pub children: Vec<PrimitiveModel>,
pub trace: Vec<PathTrace>,
pub expanded: bool,
}
#[derive(Serialize, Debug, Clone)]
@ -79,12 +80,12 @@ pub struct PageModel {
page_num: u64,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TreeViewNode {
pub struct TreeViewRequest {
key: String,
children: Vec<TreeViewNode>,
children: Vec<TreeViewRequest>,
}
impl TreeViewNode {
impl TreeViewRequest {
fn step(&self) -> Result<Step, String> {
Step::parse_step(&self.key)
}
@ -303,7 +304,7 @@ fn resolve_parent(step: Step, file: &CosFile) -> Result<(Primitive, PathTrace),
#[tauri::command]
fn get_prim_tree_by_path(
id: &str,
path: TreeViewNode,
path: TreeViewRequest,
session: State<Mutex<Session>>,
) -> Result<PrimitiveModel, String> {
let session_guard = session
@ -315,7 +316,7 @@ fn get_prim_tree_by_path(
}
fn get_prim_tree_by_path_with_file(
node: TreeViewNode,
node: TreeViewRequest,
file: &CosFile,
) -> Result<PrimitiveModel, String> {
let step = node.step()?;
@ -332,7 +333,7 @@ fn get_prim_tree_by_path_with_file(
}
fn expand(
node: &TreeViewNode,
node: &TreeViewRequest,
parent_model: &mut PrimitiveModel,
parent: &Primitive,
file: &CosFile,
@ -357,7 +358,7 @@ fn expand(
}
fn expand_children(
node: &TreeViewNode,
node: &TreeViewRequest,
file: &CosFile,
prim: &Primitive,
mut expanded: &mut PrimitiveModel,
@ -562,6 +563,7 @@ impl PrimitiveModel {
value: value,
children: Vec::new(),
trace: path,
expanded: false,
}
}
@ -604,6 +606,7 @@ impl PrimitiveModel {
}
fn add_children(&mut self, primitive: &Primitive, path: Vec<PathTrace>) {
self.expanded = true;
match primitive {
Primitive::Dictionary(dict) => dict.iter().for_each(|(name, value)| {
self.add_child(
@ -623,6 +626,7 @@ impl PrimitiveModel {
value: "".to_string(),
children: vec![],
trace: append_path("Data".to_string(), &path),
expanded: false,
});
stream.info.iter().for_each(|(name, value)| {
self.add_child(

View File

@ -6,7 +6,7 @@ mod tests {
use crate::{
get_prim_by_path_with_file, get_prim_model_by_path_with_file,
get_prim_tree_by_path_with_file, get_stream_data_by_path_with_file,
get_xref_table_model_with_file, to_pdf_file, PathTrace, PrimitiveModel, TreeViewNode,
get_xref_table_model_with_file, to_pdf_file, PathTrace, PrimitiveModel, TreeViewRequest,
};
use pdf::content::{display_ops, serialize_ops, Op};
@ -61,25 +61,25 @@ mod tests {
"Loading file"
);
let mut path = Vec::new();
path.push(TreeViewNode {
path.push(TreeViewRequest {
key: "Index".to_string(),
children: vec![TreeViewNode {
children: vec![TreeViewRequest {
key: "1".to_string(),
children: vec![],
}],
});
path.push(TreeViewNode {
path.push(TreeViewRequest {
key: "Info".to_string(),
children: vec![],
});
path.push(TreeViewNode {
path.push(TreeViewRequest {
key: "Root".to_string(),
children: vec![TreeViewNode {
children: vec![TreeViewRequest {
key: "Pages".to_string(),
children: vec![],
}],
});
let root = TreeViewNode {
let root = TreeViewRequest {
key: "/".to_string(),
children: path,
};

View File

@ -7,10 +7,10 @@
import { createEventDispatcher, onMount } from "svelte";
import PageList from "./PageList.svelte";
import ContentsView from "./ContentsView.svelte";
import TreeViewNode from "../models/TreeViewNode.svelte";
import TreeViewRequest from "../models/TreeViewRequest.svelte";
import TreeViewState from "../models/TreeViewState.svelte";
import type { PathSelectedEvent } from "../events/PathSelectedEvent";
TreeViewRequest;
let {
treeShowing,
xrefTableShowing,
@ -52,10 +52,8 @@
window.removeEventListener("mousedown", handleMouseButton);
};
});
function pathSelectedHandler(event: PathSelectedEvent) {
console.log("Path selected", event.detail.path);
fState.selectPath(event.detail.path);
fState.selectPathHandler(event);
}
</script>
@ -73,11 +71,15 @@
{#if pagesShowing}
<PageList {fState} h={height}></PageList>
{:else if treeShowing}
<TreeView
bind:pathSelectedHandler
file_id={fState.file.id}
root={TreeViewNode.TRAILER}
></TreeView>
<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}
</Pane>

View File

@ -1,48 +1,37 @@
<script lang="ts">
import type FileViewState from "../models/FileViewState.svelte";
import PrimitiveIcon from "./PrimitiveIcon.svelte";
import type PageModel from "../models/PageModel";
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: PageModel | undefined = $state(undefined);
let selected_page_num: number | undefined = $state(undefined);
let pages = $derived(fState.file.pages);
function handlePageSelect(page: PageModel) {
selected = page;
fState.selectPath(["Page" + page.page_num]);
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">
<table>
<thead>
<tr>
<td class="page-cell t-header border-forge-prim">Page</td>
<td class="ref-cell t-header border-forge-sec">Ref</td>
</tr>
</thead>
</table>
<div class="overflow-y-auto" style="height: {h - 25}px">
<table>
<tbody>
{#each fState.file.pages as page}
<tr
class:selected={page === selected}
class="hover:bg-forge-sec"
ondblclick={() => handlePageSelect(page)}
>
<td class="page-cell t-data">
<div class="key-field">
<PrimitiveIcon ptype={"Reference"} />
<p class="text-left">
{page.key}
</p>
</div>
</td>
<td class="ref-cell t-data">{page.obj_num}</td>
</tr>
{/each}
</tbody>
</table>
<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>

View File

@ -3,12 +3,15 @@
import type Primitive from "../models/Primitive.svelte";
import ContentsView from "./ContentsView.svelte";
import PrimitiveIcon from "./PrimitiveIcon.svelte";
import StreamEditor from "./StreamEditor.svelte";
let { fState, height }: { fState: FileViewState; height: number } =
$props();
let bodyHeight = $derived(height - 24);
let prim = $derived(fState.prim);
let showContents = $state(false);
let tableHeight = $state(0);
let editorHeight = $derived(Math.max(800, bodyHeight - tableHeight));
$inspect(fState.highlightedPrim);
function handlePrimSelect(prim: Primitive) {
if (prim.isContainer()) {
@ -39,7 +42,7 @@
</thead>
</table>
<div class="overflow-y-auto" style="height: {bodyHeight}px">
<table>
<table bind:clientHeight={tableHeight}>
<tbody>
{#each prim.children as entry}
<tr
@ -63,6 +66,13 @@
{/each}
</tbody>
</table>
{#if fState.prim?.ptype === "Stream"}
<StreamEditor
fileId={fState.file.id}
path={fState.getMergedPath()}
height={editorHeight}
></StreamEditor>
{/if}
</div>
</div>
</div>

View File

@ -0,0 +1,56 @@
<script lang="ts">
import { invoke } from "@tauri-apps/api/core";
import type ContentModel from "../models/ContentModel.svelte";
import { onMount } from "svelte";
import * as monaco from "monaco-editor";
let {
fileId,
path,
height,
}: { fileId: string; path: string; height: number } = $props();
let contents: string | undefined = $state(undefined);
let editorContainer: HTMLElement;
let editor: monaco.editor.IStandaloneCodeEditor;
onMount(() => {
editor = monaco.editor.create(editorContainer, {
value: "",
language: "plaintext",
theme: "vs-dark",
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
automaticLayout: true,
});
return () => {
editor.dispose();
};
});
$effect(() => {
loadContents(path, fileId);
});
$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));
}
</script>
<div bind:this={editorContainer} style="height: {height}px; width: 100%;"></div>
<style lang="postcss">
</style>

View File

@ -7,15 +7,19 @@
import { createEventDispatcher } from "svelte";
let {
file_id,
parent_view,
parent_path,
treeState,
pathSelectedHandler = $bindable(),
details,
}: {
parent_view: Primitive;
file_id: string;
parent_view: Primitive | undefined;
parent_path: string[];
treeState: TreeViewState;
treeState: TreeViewState | undefined;
pathSelectedHandler: any;
details: boolean;
} = $props();
function copyPathAndAppend(key: string): string[] {
@ -35,73 +39,75 @@
function selectItem(child: Primitive) {
let _path = copyPathAndAppend(child.key);
console.log("Selecting from tree", _path);
treeState.expandTree(_path);
const event = new PathSelectedEvent(_path);
if (child.expanded) {
treeState?.collapseTree(_path);
} else {
treeState?.expandTree(_path);
}
const event = new PathSelectedEvent(file_id, _path);
pathSelectedHandler(event);
}
</script>
{#if parent_view}
<ul class="active">
{#each parent_view.children as child}
<li>
<div class="item">
{#if child.children.length > 0}
<button
onclick={() =>
treeState.collapseTree(
copyPathAndAppend(child.key),
)}
><span class="caret"><CaretDownOutline /></span
></button
{#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
>
{:else if child.isContainer()}
<button
onclick={() =>
treeState.expandTree(
copyPathAndAppend(child.key),
)}
><span class="caret"><CaretRightOutline /></span
></button
>
{:else}
<span class="no-caret"></span>
{/if}
<button
class="select-button"
ondblclick={() => selectItem(child)}
>
<div class="item">
<PrimitiveIcon ptype={child.ptype} />
<div class="row">
<p>
{child.key}
</p>
{#if child.sub_type}
<p class="ml-1 text-forge-sec small">
{child.sub_type}
</p>
{/if}
</div>
</div>
</button>
</div>
{#if child.children.length > 0}
<svelte:self
parent_view={child}
parent_path={copyPathAndAppend(child.key)}
{treeState}
bind:pathSelectedHandler
></svelte:self>
</div>
{:else}
<span class="no-caret"></span>
{/if}
</li>
{/each}
</ul>
<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;
@ -144,17 +150,14 @@
user-select: none;
}
/* Style the caret/arrow */
.caret {
@apply text-forge-sec;
cursor: pointer;
user-select: none; /* Prevent text selection */
user-select: none;
}
/* Create the caret/arrow with a unicode, and style it */
.caret::before {
display: inline-block;
margin-right: 6px;
.caret-active {
transform: rotate(90deg);
}
/* Show the nested list when the user clicks on the caret/arrow (with JavaScript) */

View File

@ -2,47 +2,79 @@
import TreeNode from "./TreeNode.svelte";
import PrimitiveIcon from "./PrimitiveIcon.svelte";
import TreeViewState from "../models/TreeViewState.svelte";
import type TreeViewNode from "../models/TreeViewNode.svelte";
import type TreeViewRequest from "../models/TreeViewRequest.svelte";
import { PathSelectedEvent } from "../events/PathSelectedEvent";
import { CaretDownOutline, CaretRightOutline } from "flowbite-svelte-icons";
let {
file_id,
root,
pathSelectedHandler = $bindable(),
active = true,
pathSelectedHandler,
active,
details,
}: {
file_id: string;
root: TreeViewNode;
root: TreeViewRequest;
pathSelectedHandler: any;
active: boolean;
details: boolean;
} = $props();
let h = $state(100);
let treeState = $state(new TreeViewState(file_id, root));
let states: TreeViewState[] = $state([]);
let treeState: TreeViewState | undefined = $derived(
states.find((state) => state.file_id === file_id),
);
if (active) {
loadTreeView();
}
function loadTreeView() {
if (!treeState) {
let newState = new TreeViewState(file_id, root);
newState.loadTreeView();
states.push(newState);
}
}
function select() {
loadTreeView();
pathSelectedHandler(new PathSelectedEvent(file_id, [root.key]));
}
</script>
<div bind:clientHeight={h} class="full-container">
<div class="overflow-auto" style="height: {h}px">
<ul id="myUL">
{#if treeState.view}
<li>
<div class="item">
<PrimitiveIcon ptype={"Dictionary"} />
{"Trailer "}
</div>
{#if active}
<TreeNode
bind:pathSelectedHandler
parent_view={treeState.view}
parent_path={treeState.view.getPath()}
{treeState}
></TreeNode>
{/if}
</li>
<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>
{/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}
</ul>
</div>
getTrace
</div>
getPath
</li>
{/if}
</ul>
<style lang="postcss">
.item {
@ -55,4 +87,10 @@ getPath
#myUL {
list-style-type: none;
}
/* Style the caret/arrow */
.caret {
@apply text-forge-sec;
cursor: pointer;
user-select: none; /* Prevent text selection */
}
</style>

View File

@ -1,9 +1,9 @@
export class PathSelectedEvent extends CustomEvent<{ path: string[] }> {
export class PathSelectedEvent extends CustomEvent<{ file_id: string, path: string[] }> {
static readonly eventName = 'pathselected';
constructor(path: string[]) {
constructor(file_id: string, path: string[]) {
super(PathSelectedEvent.eventName, {
detail: { path },
detail: { file_id, path },
bubbles: true
});
}

View File

@ -2,8 +2,10 @@ import type PdfFile from "./PdfFile";
import type XRefEntry from "./XRefEntry";
import Primitive from "./Primitive.svelte";
import { invoke } from "@tauri-apps/api/core";
import TreeViewNode from "./TreeViewNode.svelte";
import TreeViewRequest from "./TreeViewRequest.svelte";
import type XRefTable from "./XRefTable";
import TreeViewState from "./TreeViewState.svelte";
import type { PathSelectedEvent } from "../events/PathSelectedEvent";
export default class FileViewState {
@ -13,7 +15,6 @@ export default class FileViewState {
public highlightedPrim: Primitive | undefined = $state();
public xref_entries: XRefEntry[] = $state([]);
constructor(file: PdfFile) {
this.file = file;
@ -53,13 +54,14 @@ export default class FileViewState {
.catch(err => console.error(err));
}
public getTreeRoot() {
return this.treeState;
public selectPathHandler(event: PathSelectedEvent) {
console.log("Selecting path", event.detail);
if (event.detail.file_id !== this.file.id) {
return;
}
this.selectPath(event.detail.path);
}
public getMergedPath() {
return this.mergePaths(this.path);
}
@ -96,7 +98,7 @@ export default class FileViewState {
return "/";
}
if (paths[0] === "/") {
return "/" + paths.slice(1, paths.length).join("/")
return "Trailer/" + paths.slice(1, paths.length).join("/")
}
return paths.join("/");
}

View File

@ -5,6 +5,7 @@ export default class Primitive {
public value: string;
public children: Primitive[];
public trace: Trace[] = $state([]);
public expanded: boolean = $state(false);
constructor(
p: Primitive
@ -18,6 +19,7 @@ export default class Primitive {
this.children.push(new Primitive(child));
}
this.trace = [];
this.expanded = p.expanded;
for (let path of p.trace) {
this.trace.push(path);
}
@ -54,6 +56,14 @@ export default class Primitive {
if (path.startsWith("Page")) { return path };
return +path;
}
public withoutChildren(): Primitive {
return new Primitive({
...this,
children: [],
expanded: false,
});
}
}
export interface Trace {

View File

@ -1,33 +0,0 @@
export default class TreeViewNode {
public key: string;
public children: TreeViewNode[];
constructor(
key: string,
children: TreeViewNode[]
) {
this.key = key;
this.children = children;
}
static TRAILER = new TreeViewNode("Trailer", [new TreeViewNode("Root", [])]);
public getChild(key: string) {
return this.children.find(child => child.key === key);
}
public addChild(key: string) {
let child = new TreeViewNode(key, [])
this.children.push(child);
return child;
}
public clearChildren() {
this.children = [];
}
public removeChild(key: string) {
this.children = this.children.filter(child => child.key !== key);
}
}

View File

@ -0,0 +1,45 @@
export default class TreeViewRequest {
public key: string;
public children: TreeViewRequest[];
public displayName: string;
public active: boolean;
constructor(
key: string,
children: TreeViewRequest[]
) {
if (key.startsWith("Page")) {
this.displayName = "Page " + key.slice(4);
} else {
this.displayName = key;
}
this.key = key;
this.children = children;
this.active = true;
}
static TRAILER = new TreeViewRequest("Trailer", [new TreeViewRequest("Root", [])]);
public clone(): TreeViewRequest {
return new TreeViewRequest(this.key, this.children.map(child => child.clone()));
}
public getChild(key: string) {
return this.children.find(child => child.key === key);
}
public addChild(key: string) {
let child = new TreeViewRequest(key, [])
this.children.push(child);
return child;
}
public clearChildren() {
this.children = [];
}
public removeChild(key: string) {
this.children = this.children.filter(child => child.key !== key);
}
}

View File

@ -1,28 +1,39 @@
import { invoke } from "@tauri-apps/api/core";
import Primitive from "./Primitive.svelte";
import TreeViewNode from "./TreeViewNode.svelte";
import type FileViewState from "./FileViewState.svelte";
import TreeViewRequest from "./TreeViewRequest.svelte";
import type { PathSelectedEvent } from "../events/PathSelectedEvent";
export default class TreeViewState {
public root: TreeViewNode = $state(new TreeViewNode("Trailer", [new TreeViewNode("Root", [])]));
private root: TreeViewRequest = $state(new TreeViewRequest("Trailer", [new TreeViewRequest("Root", [])]));
public view: Primitive | undefined = $state();
active: boolean = $state(false);
file_id: string;
constructor(file_id: string, root: TreeViewNode) {
constructor(file_id: string, root: TreeViewRequest) {
console.log("Creating tree view state", file_id, root);
this.file_id = file_id;
this.root = root;
this.loadTreeView();
}
public loadTreeView() {
console.log("Loading tree view", this.root);
this.setTreeViewRequest(this.getRoot());
}
public getRoot(): TreeViewRequest {
return this.root.clone();
}
public setTreeViewRequest(treeViewRequest: TreeViewRequest) {
console.log("Loading tree view", treeViewRequest);
invoke<Primitive>("get_prim_tree_by_path", {
id: this.file_id,
path: this.root,
path: treeViewRequest,
})
.then((result) => {
this.view = new Primitive(result);
this.root = treeViewRequest;
})
.catch((err) => console.error(err));
}
@ -32,16 +43,18 @@ export default class TreeViewState {
console.error("Empty path");
return;
}
let node = this.root;
console.log("Expanding tree", this.getRoot(), path);
let root = this.getRoot();
let node = root;
for (let key of path.slice(1, path.length)) {
let _node: TreeViewNode | undefined = node.getChild(key);
let _node: TreeViewRequest | undefined = node.getChild(key);
if (_node) {
node = _node;
} else {
node = node.addChild(key);
}
}
this.loadTreeView();
this.setTreeViewRequest(root);
}
public collapseTree(path: string[]) {
@ -50,16 +63,17 @@ export default class TreeViewState {
return;
}
if (path.length == 1) {
this.root.clearChildren();
this.view = this.view?.withoutChildren();
return;
}
let node: TreeViewNode | undefined = this.root;
let root: TreeViewRequest = this.root;
let node: TreeViewRequest | undefined = root;
for (let key of path.slice(1, path.length - 1)) {
node = node?.getChild(key);
}
if (node) {
node.removeChild(path[path.length - 1]);
this.setTreeViewRequest(root);
}
this.loadTreeView();
}
}
}

View File

@ -27,7 +27,8 @@ export default {
active: 'rgb(44, 71, 73)',
bound: 'rgba(0, 0, 0, 0.29)',
text: '#dadada',
text_sec: '#6c6c6c',
text_sec: '#838686',
text_hint: '#5adada',
}
}
}