updates incoming

This commit is contained in:
Kilian Schuettler 2025-02-17 01:23:53 +01:00
parent 0e9f020e26
commit f484c9b54c
17 changed files with 1267 additions and 543 deletions

File diff suppressed because it is too large Load Diff

View File

@ -249,7 +249,7 @@ impl<'a> IntoIterator for &'a Dictionary {
#[derive(Clone, Debug, PartialEq, DataSize)]
pub struct PdfStream {
pub info: Dictionary,
pub (crate) inner: StreamInner,
pub inner: StreamInner,
}
#[derive(Clone, Debug, PartialEq, DataSize)]

74
src-tauri/Cargo.lock generated
View File

@ -1011,6 +1011,12 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04d2cd9c18b9f454ed67da600630b021a8a80bf33f8c95896ab33aaf1c26b728"
[[package]]
name = "data-encoding"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
[[package]]
name = "datasize"
version = "0.2.15"
@ -3681,6 +3687,7 @@ name = "pdf-forge"
version = "0.1.0"
dependencies = [
"image 0.25.5",
"indexmap 1.9.3",
"lazy_static",
"pathfinder_geometry",
"pathfinder_rasterize",
@ -3694,6 +3701,7 @@ dependencies = [
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-opener",
"tauri-plugin-websocket",
"uuid",
]
@ -4811,6 +4819,17 @@ dependencies = [
"stable_deref_trait",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if 1.0.0",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.8"
@ -5350,6 +5369,25 @@ dependencies = [
"zbus",
]
[[package]]
name = "tauri-plugin-websocket"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af3ac71aec5fb0ae5441e830cd075b1cbed49ac3d39cb975a4894ea8fa2e62b9"
dependencies = [
"futures-util",
"http",
"log",
"rand 0.8.5",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.11",
"tokio",
"tokio-tungstenite",
]
[[package]]
name = "tauri-runtime"
version = "2.3.0"
@ -5607,6 +5645,22 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-tungstenite"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4bf6fecd69fcdede0ec680aaf474cdab988f9de6bc73d3758f0160e3b7025a"
dependencies = [
"futures-util",
"log",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tungstenite",
"webpki-roots",
]
[[package]]
name = "tokio-util"
version = "0.7.13"
@ -5775,6 +5829,26 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413083a99c579593656008130e29255e54dcaae495be556cc26888f211648c24"
dependencies = [
"byteorder",
"bytes",
"data-encoding",
"http",
"httparse",
"log",
"rand 0.8.5",
"rustls",
"rustls-pki-types",
"sha1",
"thiserror 2.0.11",
"utf-8",
]
[[package]]
name = "tuple"
version = "0.5.2"

View File

@ -33,3 +33,5 @@ image = { version = "0.25.5", features = ["jpeg"] }
pdf_render = { path = "../../pdf-render/render" }
pathfinder_rasterize = { git = "https://github.com/s3bk/pathfinder_rasterizer" }
pathfinder_geometry = { git = "https://github.com/servo/pathfinder" }
indexmap = "1.9.3"
tauri-plugin-websocket = "2"

View File

@ -11,14 +11,13 @@ use crate::render::Renderer;
use image::DynamicImage;
use lazy_static::lazy_static;
use pdf::file::{File, FileOptions, NoLog, ObjectCache, StreamCache};
use pdf::object::{Object, PlainRef};
use pdf::object::{Object, PlainRef, Stream, Updater};
use pdf::primitive::{Dictionary, Primitive};
use pdf::xref::XRef;
use regex::Regex;
use retrieval::{get_prim_by_path_with_file, get_stream_data_by_path_with_file};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::ops::DerefMut;
use std::path::Path;
use std::sync::{Arc, RwLock};
use tauri::ipc::{InvokeResponseBody, Response};
@ -26,7 +25,6 @@ use tauri::{Manager, State};
use uuid::Uuid;
type CosFile = File<Vec<u8>, ObjectCache, StreamCache, NoLog>;
#[derive(Serialize, Debug, Clone)]
pub struct XRefTableModel {
pub size: usize,
@ -113,7 +111,7 @@ fn get_all_files(session: State<Session>) -> Vec<PdfFile> {
session
.get_all_files()
.iter()
.map(|s| s.pdf_file.clone())
.map(|s| s.read().unwrap().pdf_file.clone())
.collect()
}
@ -129,7 +127,9 @@ fn close_file(id: &str, session: State<Session>) {
#[tauri::command]
fn get_file_by_id(id: &str, session: State<Session>) -> Result<PdfFile, String> {
session.get_file(id).map(|file| file.pdf_file.clone())
let lock = session.get_file(id)?;
let file = lock.read().unwrap();
Ok(file.pdf_file.clone())
}
#[tauri::command]
@ -183,7 +183,8 @@ fn to_pdf_file(path: &str, file: &CosFile) -> Result<PdfFile, String> {
#[tauri::command]
fn get_contents(id: &str, path: &str, session: State<Session>) -> Result<ContentsModel, String> {
let file = session.get_file(id)?;
let lock = session.get_file(id)?;
let file = lock.read().unwrap();
let (_, page_prim, _) = get_prim_by_path_with_file(path, &file.cos_file)?;
let resolver = file.cos_file.resolver();
@ -208,11 +209,30 @@ fn get_stream_data_as_string(
path: &str,
session: State<Session>,
) -> Result<String, String> {
let file = session.get_file(id)?;
let lock = session.get_file(id)?;
let file = lock.read().unwrap();
let data = get_stream_data_by_path_with_file(path, &file.cos_file)?;
Ok(String::from_utf8_lossy(&data).into_owned())
}
#[tauri::command]
async fn update_stream_data_as_string(
id: &str,
path: &str,
new_data: &str,
session: State<'_, Session>,
) -> Result<(), String> {
let lock = session.get_file(id)?;
let mut file = lock.write().unwrap();
let (_, _, trace) = get_prim_by_path_with_file(path, &file.cos_file)?;
let i = trace.len() - 1;
let id = t!(trace[i].last_jump.parse::<u64>());
let plain_ref = PlainRef { id, gen: 0 };
let stream = Stream::new((), new_data.as_bytes());
let _ = t!(file.cos_file.update(plain_ref, stream));
Ok(())
}
fn format_img_as_response(img: DynamicImage) -> Result<Response, String> {
use std::mem;
@ -248,7 +268,8 @@ async fn get_stream_data_as_image(
path: &str,
session: State<'_, Session>,
) -> Result<Response, String> {
let file = session.get_file(id)?;
let lock = session.get_file(id)?;
let file = lock.read().unwrap();
let img = retrieval::get_image_by_path(path, &file.cos_file)?;
format_img_as_response(img)
}
@ -259,21 +280,39 @@ async fn get_page_by_num(
num: u32,
session: State<'_, Session>,
) -> Result<Response, String> {
let file = session.get_file(id)?;
render_page_by_path(id, &format!("Page{}", num), session).await
}
#[tauri::command]
async fn render_page_by_path(
id: &str,
path: &str,
session: State<'_, Session>,
) -> Result<Response, String> {
let lock = session.get_file(id)?;
let file = lock.read().unwrap();
let page = get_page_by_path(path, &file.cos_file)?;
let mut renderer = Renderer::new(&file.cos_file, 150);
let img = renderer.render(num)?;
let img = renderer.render(&page)?;
format_img_as_response(img)
}
fn get_page_by_path(path: &str, file: &CosFile) -> Result<pdf::object::Page, String> {
let resolver = file.resolver();
let (_, page_prim, _) = get_prim_by_path_with_file(path, file)?;
let page = t!(pdf::object::Page::from_primitive(page_prim, &resolver));
Ok(page)
}
#[tauri::command]
fn get_prim_by_path(
id: &str,
path: &str,
session: State<Session>,
) -> Result<PrimitiveModel, String> {
let file = session.get_file(id)?;
let lock = session.get_file(id)?;
let file = lock.read().unwrap();
get_prim_model_by_path_with_file(path, &file.cos_file)
}
@ -313,7 +352,8 @@ fn get_prim_tree_by_path(
paths: Vec<TreeViewRequest>,
session: State<Session>,
) -> Result<Vec<PrimitiveTreeView>, String> {
let file = session.get_file(id)?;
let lock = session.get_file(id)?;
let file = lock.read().unwrap();
let results = paths
.into_iter()
@ -659,9 +699,11 @@ impl PrimitiveTreeView {
}
#[tauri::command]
fn get_xref_table(id: &str, session: State<Session>) -> Result<XRefTableModel, String> {
let file = session.get_file(id)?;
let lock = session.get_file(id)?;
let file = lock.read().unwrap();
get_xref_table_model_with_file(&file.cos_file)
}
fn get_xref_table_model_with_file(file: &CosFile) -> Result<XRefTableModel, String> {
let resolver = file.resolver();
let x_ref_table = file.get_xref();
@ -719,7 +761,7 @@ fn get_xref_table_model_with_file(file: &CosFile) -> Result<XRefTableModel, Stri
}
struct Session {
files: RwLock<HashMap<String, Arc<SessionFile>>>,
files: RwLock<HashMap<String, Arc<RwLock<SessionFile>>>>,
}
struct SessionFile {
@ -734,20 +776,15 @@ impl Session {
}
}
fn get_file(&self, id: &str) -> Result<Arc<SessionFile>, String> {
fn get_file(&self, id: &str) -> Result<Arc<RwLock<SessionFile>>, String> {
let lock = self.files.read().unwrap();
lock.get(id)
.cloned()
.ok_or(format!(" File {} not found!", id))
}
fn get_all_files(&self) -> Vec<Arc<SessionFile>> {
self.files
.read()
.unwrap()
.values()
.map(|f| f.clone())
.collect()
fn get_all_files(&self) -> Vec<Arc<RwLock<SessionFile>>> {
self.files.read().unwrap().values().cloned().collect()
}
fn get_all_file_ids(&self) -> Vec<String> {
@ -763,10 +800,10 @@ impl Session {
if let Ok(mut files) = self.files.write() {
return match files.insert(
pdf_file.id.clone(),
Arc::new(SessionFile {
Arc::new(RwLock::new(SessionFile {
pdf_file: pdf_file,
cos_file: cos_file,
}),
})),
) {
Some(_) => Err("File could not be uploaded!".to_string()),
None => Ok(()),
@ -802,7 +839,8 @@ pub fn run() {
get_contents,
get_stream_data_as_string,
get_stream_data_as_image,
get_page_by_num
get_page_by_num,
update_stream_data_as_string
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@ -5,6 +5,7 @@ use crate::{t, CosFile};
use image::{DynamicImage, RgbaImage};
use pathfinder_geometry::transform2d::Transform2F;
use pathfinder_rasterize::Rasterizer;
use pdf::object::Page;
use pdf_render::{render_page, Cache, SceneBackend};
pub struct Renderer<'a> {
@ -22,9 +23,8 @@ impl<'a> Renderer<'a> {
}
}
pub fn render(&mut self, page_num: u32) -> Result<DynamicImage, String> {
pub fn render(&mut self, page: &Page) -> Result<DynamicImage, String> {
let resolver = self.file.resolver();
let page = t!(self.file.get_page(page_num - 1));
let mut backend = SceneBackend::new(&mut self.cache);
t!(render_page(&mut backend, &resolver, &page, self.transform));
let scene = backend.finish();

View File

@ -97,6 +97,7 @@
closeTab={closeFile}
openTab={upload}
selectTab={(state) => (fState = state)}
reload={() => fState?.reload()}
></TitleBar>
</div>
<div class="fileview_container" style="height: {fileViewHeight}px">

View File

@ -1,6 +1,4 @@
<script lang="ts">
import ContentsView from "./ContentsView.svelte";
import RenderedPageView from "./RenderedPageView.svelte";
import {Pane, Splitpanes} from "svelte-splitpanes";
import {PageViewState} from "../models/PageViewState.svelte";
@ -8,30 +6,14 @@
import StreamEditor from "./StreamEditor.svelte";
let {fState, height}: {fState: FileViewState, height: number} = $props();
let states: PageViewState[] = [];
let state: PageViewState | undefined = $state(undefined);
$effect(() => {
if (!(fState.container_prim && fState.container_prim.isPage())) return;
let page_num = fState.container_prim.getPageNumber();
if (state && state.file_id == fState.file.id && state.page_num === page_num) return;
state = states.find(s => s.file_id === fState.file.id && s.page_num == page_num)
if (!state) {
state = new PageViewState(fState.file.id, page_num, (m) => fState.logError(m));
states.push(state);
}
})
let state: PageViewState | undefined = $derived(fState.pageViewState);
</script>
{#if fState.container_prim && fState.container_prim.isPage() && state}
{#if state}
<Splitpanes theme="forge-movable">
<Pane minSize={1}>
<div class="overflow-hidden">
<StreamEditor stream_data={state.contents} height={height - 1}></StreamEditor>
<StreamEditor save={state.handleSave} stream_data={state.contents.toDisplay()} height={height - 1}></StreamEditor>
</div>
</Pane>
<Pane minSize={1}>

View File

@ -2,12 +2,12 @@
import type FileViewState from "../models/FileViewState.svelte";
import type Primitive from "../models/Primitive.svelte";
import PrimitiveIcon from "./PrimitiveIcon.svelte";
import {Pane, Splitpanes} from "svelte-splitpanes";
import { Pane, Splitpanes } from "svelte-splitpanes";
import StreamDataView from "./StreamDataView.svelte";
const cellH = 29;
const headerOffset = 24;
let {fState, height}: { fState: FileViewState; height: number } =
let { fState, height }: { fState: FileViewState; height: number } =
$props();
let fillerHeight: number = $state(0);
let firstEntry = $state(0);
@ -20,9 +20,6 @@
let tableHeight = $derived(prim ? prim.children.length * cellH : 0);
let bodyHeight = $derived(height);
let editorHeight = $derived(
Math.max(800, bodyHeight - tableHeight - headerOffset),
);
let locallySelected: Primitive | undefined = $state(undefined);
$effect(() => {
@ -50,53 +47,65 @@
fState.selectPath(_path);
}
</script>
<Splitpanes theme="forge-movable">
<Splitpanes theme="forge-movable">
<Pane minSize={1}>
{#if prim && prim.children && prim.children.length > 0}
<div
class="overflow-auto"
onscroll={handleScroll}
style="height: {bodyHeight}px"
class="overflow-auto"
onscroll={handleScroll}
style="height: {bodyHeight}px"
>
<div class="w-[851px]">
<table style="position: relative; top: {scrollY}px">
<thead>
<tr>
<td class="page-cell t-header border-forge-prim">Key
</td
>
<td class="ref-cell t-header border-forge-prim">Type
</td
>
<td class="cell t-header border-forge-sec">Value</td>
</tr>
<tr>
<td class="page-cell t-header border-forge-prim"
>Key
</td>
<td class="ref-cell t-header border-forge-prim"
>Type
</td>
<td class="cell t-header border-forge-sec"
>Value</td
>
</tr>
</thead>
</table>
<div class="container" style="height: {tableHeight}px">
<table>
<tbody>
<tr class="filler" style="height: {fillerHeight}px"
></tr>
{#each entriesToDisplay as entry}
<tr
class="filler"
style="height: {fillerHeight}px"
></tr>
{#each entriesToDisplay as entry}
<tr
class:selected={entry.key ===
locallySelected?.key}
locallySelected?.key}
class="row"
onclick={() => handlePrimClick(entry)}
ondblclick={() => handlePrimDbLClick(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}
ondblclick={() =>
handlePrimDbLClick(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>
@ -105,17 +114,18 @@
{/if}
</Pane>
<Pane
size={fState.container_prim?.stream_data ? 50 : 0}
minSize={fState.container_prim?.stream_data ? 1 : 0}
maxSize={fState.container_prim?.stream_data ? 100 : 0}
size={fState.container_prim?.stream_data ? 50 : 0}
minSize={fState.container_prim?.stream_data ? 1 : 0}
maxSize={fState.container_prim?.stream_data ? 100 : 0}
>
<div style:height={height} class="bg-forge-dark">
<div style:height class="bg-forge-dark">
{#if fState.container_prim?.stream_data}
<StreamDataView {fState} {height}></StreamDataView>
{/if}
</div>
</Pane>
</Splitpanes>
<style lang="postcss">
.key-field {
display: flex;

View File

@ -1,29 +1,33 @@
<script lang="ts">
import type FileViewState from "../models/FileViewState.svelte";
import StreamEditor from "./StreamEditor.svelte";
import type {StreamData} from "../models/StreamData.svelte";
import type { StreamData } from "../models/StreamData.svelte";
import RenderedPageView from "./RenderedPageView.svelte";
let {fState, height}: { fState: FileViewState, height: number } = $props()
let streamData: StreamData | undefined = $derived(fState.container_prim?.stream_data);
let {
fState,
height,
}: {
fState: FileViewState;
height: number;
} = $props();
let streamData: StreamData | undefined = $derived(
fState.container_prim?.stream_data,
);
</script>
{#if streamData}
{#if streamData.type === "Image"}
<RenderedPageView img={streamData.imageData} {height}></RenderedPageView>
{:else}
{#if streamData.textData}
<StreamEditor
stream_data={streamData.textData}
{height}
></StreamEditor>
{/if}
<RenderedPageView img={streamData.imageData} {height}
></RenderedPageView>
{:else if streamData.textData}
<StreamEditor
stream_data={streamData.textData}
{height}
save={(updated_data) => fState.saveStreamData(updated_data)}
></StreamEditor>
{/if}
{/if}
<style lang="postcss">
</style>
</style>

View File

@ -1,20 +1,32 @@
<script lang="ts">
import { onMount } from "svelte";
import { UploadOutline } from "flowbite-svelte-icons";
import * as monaco from "monaco-editor";
monaco.editor.defineTheme('forge-dark', {
monaco.editor.defineTheme("forge-dark", {
base: "vs-dark",
inherit: true,
rules: [],
colors: {
"editor.background": '#1E1F22FF'
}
"editor.background": "#1E1F22FF",
},
});
let { stream_data, height }: { stream_data: string; height: number } =
$props();
let {
stream_data,
height,
save,
}: {
stream_data: string;
height: number;
save: (updated_data: string) => void;
} = $props();
let editorContainer: HTMLElement;
let editor: monaco.editor.IStandaloneCodeEditor;
let isEdited = $state(false);
let last_data: string | undefined = $state(undefined);
onMount(() => {
editor = monaco.editor.create(editorContainer, {
value: "",
@ -26,6 +38,27 @@
automaticLayout: true,
});
loadContents(stream_data);
editor.onDidChangeModelContent(() => {
isEdited = editor.getValue() !== stream_data;
});
editor.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
(context, args) => {
if (isEdited) {
save(editor.getValue());
}
},
);
window.addEventListener("keydown", (event) => {
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
event.preventDefault();
}
});
return () => {
editor.dispose();
};
@ -38,11 +71,36 @@
function loadContents(stream_data: string | undefined) {
if (!stream_data || !editor) return;
if (last_data === stream_data) {
return;
}
isEdited = false;
last_data = stream_data;
editor.setValue(stream_data);
}
</script>
<div bind:this={editorContainer} style:height = {height - 1 + "px"}></div>
<div class="relative">
<div bind:this={editorContainer} style:height={height + "px"}></div>
{#if isEdited}
<button onclick={() => save(editor.getValue())} class="save-button">
<UploadOutline size="xl" />
</button>
{/if}
</div>
<style lang="postcss">
.save-button {
@apply p-2 bg-forge-sec text-forge-text border-t border-forge-bound cursor-pointer;
position: absolute;
right: 2em;
bottom: 0px;
width: 48px;
height: 48px;
}
.save-button:hover {
background-color: #3c3d41;
}
</style>

View File

@ -6,6 +6,7 @@
CaretDownOutline,
PlusOutline,
FilePdfSolid,
ArrowsRepeatOutline,
} from "flowbite-svelte-icons";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { homeDir } from "@tauri-apps/api/path";
@ -21,12 +22,14 @@
selectTab,
closeTab,
openTab: newFile,
reload
}: {
fStates: FileViewState[];
fState: FileViewState | undefined;
selectTab: (arg0: FileViewState) => void;
closeTab: (arg0: FileViewState) => void;
openTab: () => void;
reload: () => void;
} = $props();
let dropdownVisible = $state(false);
@ -167,7 +170,15 @@
{/if}
</div>
<div class="titlebar-button-group">
<button
onclick={reload}
class="titlebar-button"
id="titlebar-maximize"
>
<ArrowsRepeatOutline />
</button>
<button
onclick={minimize}
class="titlebar-button"

View File

@ -5,7 +5,6 @@
import type {TreeViewModel} from "../models/TreeViewModel";
import TreeViewEntry from "./TreeViewEntry.svelte";
const rowHeight = 24;
let {
fState,
height,
@ -17,54 +16,36 @@
const file_id = $derived(fState.file.id);
let scrollY = $state(0);
let scrollContainer: HTMLElement;
let fillerHeight: number = $state(0);
let states: TreeViewState[] = [];
let treeState: TreeViewState | undefined = $state(undefined);
let treeState: TreeViewState | undefined = $state(fState.treeState);
let totalHeight: number = $state(100);
let entries: TreeViewModel[] = $state([]);
let stickies: TreeViewModel[] = $state([]);
let fillerHeight: number = $derived(
treeState ? treeState.filler_height : 0,
);
let totalHeight: number = $derived(treeState ? treeState.total_height : 0);
let entries: TreeViewModel[] = $derived(treeState ? treeState.entries : []);
let stickies: TreeViewModel[] = $derived(
treeState ? treeState.stickies : [],
);
$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;
treeState?.loadTreeView(scrollY, height);
}
scrollY = scrollContainer.scrollTop;
let firstEntry = Math.floor(scrollY / rowHeight);
let lastEntry = Math.ceil((scrollY + height) / rowHeight);
let entryCount = treeState?.getEntryCount();
totalHeight = Math.max(entryCount * rowHeight, 0);
fillerHeight = firstEntry * rowHeight;
let entriesAndStickies = treeState.getEntries(firstEntry, lastEntry);
entries = entriesAndStickies[0];
stickies = entriesAndStickies[1];
}
)
function handleSelect(prim: TreeViewModel) {
if (prim.expanded && prim.container) {
treeState
?.collapseTree(prim.path.map((path) => path.key))
.then(() => {
updateTreeView();
treeState?.loadTreeView(scrollY, height);
});
return;
} else if (prim.container) {
treeState
?.expandTree(prim.path.map((path) => path.key))
.then(() => {
updateTreeView();
treeState?.loadTreeView(scrollY, height);
});
}
fState.selectPathHandler(
@ -73,19 +54,22 @@
prim.path.map((path) => path.key),
),
);
updateTreeView();
treeState?.loadTreeView(scrollY, height);
}
function handleScroll(event: Event & { currentTarget: HTMLElement }) {
updateTreeView();
scrollY = event.currentTarget.scrollTop;
treeState?.loadTreeView(scrollY, height);
}
</script>
<div bind:this={scrollContainer} onscroll={handleScroll} class="overflow-auto" style="height: {height}px">
<div onscroll={handleScroll} class="overflow-auto" style="height: {height}px">
{#if entries}
<div style="height: {totalHeight}px; width: 100%">
<table class="border-b-2 border-forge-bound bg-forge-prim" style="position: relative; top: {scrollY}px">
<table
class="border-b-2 border-forge-bound bg-forge-prim"
style="position: relative; top: {scrollY}px"
>
<thead>
<tr>
{#each stickies as entry}
@ -99,8 +83,9 @@
</tr>
</thead>
</table>
<div class="filler"
style="height: {fillerHeight}px; width: 100%"
<div
class="filler"
style="height: {fillerHeight}px; width: 100%"
></div>
<div>
{#each entries as entry}

View File

@ -1,3 +1,42 @@
export default interface ContentModel {
readonly parts: string[][];
const EOF: string = "\n%-------------------% EOF %-------------------%\n\n";
const CONTENTS: string = "%----------------% Contents[%d] %--------------%\n\n";
const CONTENTS_PATTERN = /%----------------% Contents\[\d+] %--------------%\n\n([\s\S]*?)(?=%-------------------% EOF %-------------------%|$)/gm;
export default class ContentModel {
parts: string[][];
constructor(parts: string[][]) {
this.parts = parts;
}
public toDisplay(): string {
let text = "";
if (this.parts.length > 1) {
let i = 0;
for (let part of this.parts) {
text += CONTENTS.replace("%d", i.toString());
for (let line of part) {
text += " " + line + "\n";
}
text += EOF;
i++;
}
} else if (this.parts.length == 1) {
text = this.parts[0].join("\n");
}
return text;
}
public fromDisplay(display: string): ContentModel {
const parts: string[][] = [];
let match;
while ((match = CONTENTS_PATTERN.exec(display)) !== null) {
const lines = match[1].split("\n").map(line => line.trim()).filter(line => line !== "");
parts.push(lines);
}
if (parts.length === 0) {
parts.push(display.split("\n").map(line => line.trim()));
}
return new ContentModel(parts);
}
}

View File

@ -9,6 +9,8 @@ import {StreamData} from "./StreamData.svelte";
import {ForgeNotification} from "./ForgeNotification.svelte";
import {Mutex} from 'async-mutex';
import {RawImageData} from "./RawImageData";
import TreeViewState from "./TreeViewState.svelte";
import {PageViewState} from "./PageViewState.svelte";
export default class FileViewState {
public file: PdfFile;
@ -24,6 +26,10 @@ export default class FileViewState {
public selected_leaf_prim: Primitive | undefined = $state();
public xref_entries: XRefEntry[] = $state([]);
public treeState: TreeViewState | undefined = $state();
pageViewStates: Map<number, PageViewState> = new Map();
public pageViewState: PageViewState | undefined = $state();
public notifications: ForgeNotification[] = $state([]);
notificationMutex = new Mutex();
@ -33,6 +39,7 @@ export default class FileViewState {
this.selectPath(this.path);
this.loadXrefEntries()
this.notificationMutex = new Mutex();
this.treeState = new TreeViewState(this);
}
getLastJump(): string | number | undefined {
@ -43,6 +50,31 @@ export default class FileViewState {
return this.container_prim?.getFirstJump()
}
public saveStreamData(updated_data: string) {
if (!this.selected_leaf_prim) return;
if (!this.container_prim?.stream_data) return;
invoke("update_stream_data_as_string", {
id: this.file.id,
path: this.formatPaths(this.path),
newData: updated_data
})
.then(() => {
console.log("saved");
this.reload();
this.container_prim?.stream_data?.setData(updated_data);
})
.catch(err => this.logError(err));
}
public reload() {
this.container_prim = undefined;
this.selected_leaf_prim = undefined;
this.loadXrefEntries();
this.selectPath(this.path);
this.treeState?.reload();
this.reloadPageState();
}
public logError(message: string) {
this.notificationMutex.acquire().then(release => {
try {
@ -54,6 +86,10 @@ export default class FileViewState {
});
}
public setTreeViewState(state: TreeViewState) {
this.treeState = state;
}
public async deleteNotification(timestamp: number) {
const release = await this.notificationMutex.acquire();
try {
@ -95,6 +131,7 @@ export default class FileViewState {
if (newPrim.isContainer()) {
this.container_prim = newPrim;
this.path = newPath
this.loadPageState().catch(err => this.logError(err));
return;
}
this.selected_leaf_prim = newPrim;
@ -174,4 +211,21 @@ export default class FileViewState {
}
private async reloadPageState() {
this.pageViewState = undefined;
this.pageViewStates = new Map();
}
private async loadPageState() {
if (!(this.container_prim && this.container_prim.isPage())) return;
let page_num = this.container_prim.getPageNumber();
if (this.pageViewState && this.pageViewState.page_num === page_num) return;
this.pageViewState = this.pageViewStates.get(page_num);
if (!this.pageViewState) {
this.pageViewState = new PageViewState(page_num, this);
this.pageViewStates.set(page_num, this.pageViewState);
}
}
}

View File

@ -1,62 +1,55 @@
import type ContentModel from "./ContentModel.svelte";
import ContentModel from "./ContentModel.svelte";
import {invoke} from "@tauri-apps/api/core";
import {RawImageData} from "./RawImageData";
import type FileViewState from "./FileViewState.svelte";
export class PageViewState {
file_id: string;
page_num: number;
errorHandler: (arg0: string) => void;
fState: FileViewState;
img_data: RawImageData | undefined = $state();
contents: string = $state("");
contents: ContentModel = $state(new ContentModel([]));
constructor(file_id: string, page_num: number, errorHandler: (arg0: string) => void) {
this.file_id = file_id;
constructor(page_num: number, fState: FileViewState) {
this.file_id = fState.file.id;
this.page_num = page_num;
this.errorHandler = errorHandler;
this.load();
this.fState = fState;
this.load(page_num);
}
public async load() {
invoke<ContentModel>("get_contents", {id: this.file_id, path: "Page" + this.page_num})
private logError(err: string) {
this.fState.logError(err);
}
public async load(page_num: number) {
invoke<ContentModel>("get_contents", {id: this.file_id, path: "Page" + page_num})
.then((result) => {
this.contents = this.mapToString(result);
this.loadImage();
this.page_num = page_num;
this.contents = new ContentModel(result.parts);
this.loadImage(page_num);
})
.catch((err) => this.errorHandler(err));
.catch(this.logError);
}
public async loadImage() {
public async loadImage(page_num: number) {
let result = await invoke<ArrayBuffer>("get_page_by_num", {
id: this.file_id,
num: this.page_num,
}).catch(err => this.errorHandler(err));
num: page_num,
})
.catch(this.logError);
if (result) {
this.img_data = new RawImageData(result);
}
}
private mapToString(model: ContentModel | undefined): string {
if (!model) return "";
let text = "";
if (model.parts.length > 1) {
let i = 0;
for (let part of model.parts) {
text +=
"%----------------% Contents[" +
i +
"] %--------------%\n\n";
for (let line of part) {
text += " " + line + "\n";
}
text +=
"\n%-------------------% EOF %-------------------%\n\n";
i++;
}
} else {
text = model.parts[0].join("\n");
}
return text;
public handleSave(newData: string) {
invoke("update_contents", {id: this.file_id, path: "Page" + this.page_num, newData: newData})
.then((_) => {
this.loadImage(this.page_num);
})
.catch(this.logError);
}
}

View File

@ -4,21 +4,29 @@ import {TreeViewModel} from "./TreeViewModel";
import type {Trace} from "./Primitive.svelte";
import type FileViewState from "./FileViewState.svelte";
const ROW_HEIGHT = 24;
const MAX_STICKIES: number = 5;
export default class TreeViewState {
private initialRequest: TreeViewRequest[];
private activeRequest: Map<string, TreeViewRequest> = $state(new Map());
private activeEntries: Map<string, TreeViewModel[]> = $state(new Map());
scrollY: number = 0;
private activeRequest: Map<string, TreeViewRequest> = new Map();
private activeEntries: Map<string, TreeViewModel[]> = new Map();
public total_height: number = $state(100);
public filler_height: number = $state(0);
public entries: TreeViewModel[] = $state([]);
public stickies: TreeViewModel[] = $state([]);
private scrollY = 0;
private height = 100;
file_id: string;
constructor(file_id: string, fState: FileViewState) {
constructor(fState: FileViewState) {
this.initialRequest = [TreeViewRequest.TRAILER];
this.initialRequest = this.initialRequest.concat(TreeViewRequest.fromPageCount(+fState.file.page_count));
this.activeRequest.set(this.initialRequest[0].key, this.initialRequest[0]);
this.file_id = file_id;
this.file_id = fState.file.id;
}
public getEntryCount() {
@ -106,9 +114,22 @@ export default class TreeViewState {
);
}
public async loadTreeView() {
public async reload() {
await this.loadTreeView(this.scrollY, this.height);
}
public async loadTreeView(scrollY: number, height: number) {
const activeRequests = Array.from(this.activeRequest.values());
await this.updateTreeViewRequest(activeRequests);
let firstEntry = Math.floor(scrollY / ROW_HEIGHT);
let lastEntry = Math.ceil((scrollY + height) / ROW_HEIGHT);
this.total_height = this.getEntryCount() * ROW_HEIGHT;
this.filler_height = firstEntry * ROW_HEIGHT;
let entriesAndStickies = this.getEntries(firstEntry, lastEntry);
this.entries = entriesAndStickies[0];
this.stickies = entriesAndStickies[1];
this.scrollY = scrollY;
this.height = height;
}
public getRoot(): TreeViewRequest[] {