From 98d34ad5c3914a5209f8928035aea71adf8c765c Mon Sep 17 00:00:00 2001 From: Kilian Schuettler Date: Mon, 10 Feb 2025 12:07:25 +0100 Subject: [PATCH] Added image rendering and notifications --- package.json | 1 + src-tauri/Cargo.lock | 486 +++++++++++++++++++++++- src-tauri/Cargo.toml | 4 +- src-tauri/src/lib.rs | 362 +++++++----------- src-tauri/src/tests.rs | 64 +++- src/components/App.svelte | 113 +++--- src/components/ContentsView.svelte | 83 ++-- src/components/FileView.svelte | 56 +-- src/components/Footer.svelte | 2 +- src/components/NotificationModal.svelte | 49 +++ src/components/PrimitiveView.svelte | 132 +++---- src/components/StreamDataView.svelte | 27 ++ src/components/StreamEditor.svelte | 12 +- src/components/ToolbarLeft.svelte | 24 +- src/components/ToolbarRight.svelte | 26 +- src/components/TreeView.svelte | 139 +++---- src/components/TreeViewEntry.svelte | 93 +++++ src/models/FileViewState.svelte.ts | 122 ++++-- src/models/ForgeNotification.svelte.ts | 27 ++ src/models/Primitive.svelte.ts | 39 +- src/models/PrimitiveModel.ts | 11 + src/models/PrimitiveView.ts | 15 - src/models/StreamData.ts | 9 + src/models/TreeViewRequest.svelte.ts | 2 +- src/models/TreeViewState.svelte.ts | 90 +++-- src/utils.ts | 28 +- yarn.lock | 9 +- 27 files changed, 1382 insertions(+), 643 deletions(-) create mode 100644 src/components/NotificationModal.svelte create mode 100644 src/components/StreamDataView.svelte create mode 100644 src/components/TreeViewEntry.svelte create mode 100644 src/models/ForgeNotification.svelte.ts create mode 100644 src/models/PrimitiveModel.ts delete mode 100644 src/models/PrimitiveView.ts create mode 100644 src/models/StreamData.ts diff --git a/package.json b/package.json index 894ed09..576b92f 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@tauri-apps/plugin-dialog": "~2", "@tauri-apps/plugin-fs": "~2", "@tauri-apps/plugin-opener": "^2", + "async-mutex": "^0.5.0", "flowbite-svelte": "^0.47.4", "flowbite-svelte-icons": "^2.0.2", "monaco-editor": "^0.52.2", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index fa58a0c..c2217db 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -55,6 +55,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -97,6 +103,29 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "ashpd" version = "0.10.2" @@ -296,6 +325,29 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" +dependencies = [ + "arrayvec", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -323,6 +375,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -338,6 +396,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + [[package]] name = "block" version = "0.1.6" @@ -405,6 +469,12 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "built" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b" + [[package]] name = "bumpalo" version = "3.16.0" @@ -423,6 +493,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.9.0" @@ -514,6 +590,8 @@ version = "1.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -609,6 +687,12 @@ dependencies = [ "objc", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.7" @@ -720,12 +804,37 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1088,6 +1197,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1420,6 +1544,16 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.31.1" @@ -1585,6 +1719,16 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1901,6 +2045,45 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" + [[package]] name = "indexmap" version = "1.9.3" @@ -1942,6 +2125,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1976,6 +2170,15 @@ dependencies = [ "datasize", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -2042,6 +2245,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "jpeg-decoder" version = "0.3.1" @@ -2110,6 +2322,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libappindicator" version = "0.9.0" @@ -2164,6 +2382,16 @@ dependencies = [ "rle-decode-fast", ] +[[package]] +name = "libfuzzer-sys" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.7.4" @@ -2212,6 +2440,15 @@ version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "mac" version = "0.1.1" @@ -2247,6 +2484,16 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "md5" version = "0.7.0" @@ -2274,6 +2521,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.3" @@ -2370,12 +2623,69 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2731,6 +3041,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.3" @@ -2750,7 +3066,7 @@ dependencies = [ "globalcache", "indexmap 2.7.1", "istring", - "itertools", + "itertools 0.13.0", "jpeg-decoder", "libflate", "log", @@ -2767,6 +3083,9 @@ dependencies = [ name = "pdf-forge" version = "0.1.0" dependencies = [ + "base64 0.21.7", + "fax", + "image", "lazy_static", "pdf", "regex", @@ -3087,6 +3406,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn 2.0.96", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.32.0" @@ -3195,12 +3548,82 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.12.1", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand 0.8.5", + "rand_chacha 0.3.1", + "simd_helpers", + "system-deps", + "thiserror 1.0.69", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-window-handle" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.8" @@ -3313,6 +3736,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" + [[package]] name = "rle-decode-fast" version = "1.0.3" @@ -3616,6 +4045,15 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -4254,6 +4692,17 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.37" @@ -4654,6 +5103,17 @@ dependencies = [ "serde", ] +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "version-compare" version = "0.2.0" @@ -5649,6 +6109,30 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.2.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5207570..d394cef 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -28,4 +28,6 @@ tauri-plugin-dialog = "2" uuid = { version = "1.12.0", features = ["v4"] } regex = "1.10.3" lazy_static = "1.4.0" - +fax = "0.2" +base64 = "0.21" +image = { version = "0.25.5", features = ["jpeg"] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8cb9c35..756c942 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,34 +1,34 @@ +mod retrieval; + #[cfg(test)] mod tests; extern crate pdf; use crate::pdf::object::Resolve; - +use base64; +use image::codecs::jpeg::JpegEncoder; +use image::ImageFormat; use lazy_static::lazy_static; use pdf::file::{File, FileOptions, NoLog, ObjectCache, StreamCache}; -use pdf::object::{Object, ObjectWrite, PlainRef, Stream, Trace}; +use pdf::object::{ImageXObject, Object, PlainRef}; use pdf::primitive::{Dictionary, Primitive}; use pdf::xref::XRef; use regex::Regex; +use retrieval::{ + get_pdf_stream_by_path_with_file, get_prim_by_path_with_file, get_stream_data_by_path_with_file, +}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, VecDeque}; +use std::io::Cursor; use std::ops::DerefMut; use std::path::Path; -use std::sync::{Mutex, MutexGuard}; +use std::sync::{Arc, Mutex, MutexGuard, RwLock}; use tauri::{Manager, State}; use uuid::Uuid; type CosFile = File, ObjectCache, StreamCache, NoLog>; -macro_rules! t { - ($result:expr) => {{ - match $result { - Ok(f) => f, - Err(e) => return Err(e.to_string()), - } - }}; -} #[derive(Serialize, Debug, Clone)] pub struct XRefTableModel { pub size: usize, @@ -111,50 +111,35 @@ pub struct ContentsModel { } #[tauri::command] -fn get_all_files(session: State>) -> Vec { - let files = &session.lock().unwrap().files; - files - .values() - .map(|sf| sf.pdf_file.clone()) - .collect::>() +fn get_all_files(session: State) -> Vec { + session.get_all_files().iter().map(|s| s.pdf_file.clone()).collect() } #[tauri::command] -fn get_all_file_ids(session: State>) -> Vec { - let files = &session.lock().unwrap().files; - files - .values() - .map(|sf| sf.pdf_file.id.clone()) - .collect::>() +fn get_all_file_ids(session: State) -> Vec { + session.get_all_file_ids() } #[tauri::command] -fn close_file(id: &str, session: State>) { - session.lock().unwrap().deref_mut().handle_close(&id); +fn close_file(id: &str, session: State) { + session.handle_close(id); } #[tauri::command] -fn get_file_by_id(id: &str, session: State>) -> Result { - let session_guard = session - .lock() - .map_err(|_| "Failed to lock the session mutex.".to_string())?; - let file = &get_file_from_state(id, &session_guard)?; - Ok(file.pdf_file.clone()) +fn get_file_by_id(id: &str, session: State) -> Result { + session.get_file(id).map(|file| file.pdf_file.clone()) } #[tauri::command] -fn upload(path: &str, session: State>) -> Result { +fn upload(path: &str, session: State) -> Result { let file = t!(FileOptions::cached().open(path)); let pdf_file = to_pdf_file(path, &file)?; + let id = pdf_file.id.clone(); - session - .lock() - .unwrap() - .deref_mut() - .handle_upload(&pdf_file, file); + session.handle_upload(pdf_file, file)?; - Ok(pdf_file.id.to_string()) + Ok(id) } fn to_pdf_file(path: &str, file: &CosFile) -> Result { @@ -198,12 +183,10 @@ fn to_pdf_file(path: &str, file: &CosFile) -> Result { fn get_contents( id: &str, path: &str, - session: State>, + session: State, ) -> Result { - let session_guard = session - .lock() - .map_err(|_| "Failed to lock the session mutex.".to_string())?; - let file = get_file_from_state(id, &session_guard)?; + + let file = session.get_file(id)?; let (_, page_prim, _) = get_prim_by_path_with_file(path, &file.cos_file)?; let resolver = file.cos_file.resolver(); @@ -223,112 +206,87 @@ fn get_contents( } #[tauri::command] -fn get_stream_data( +fn get_stream_data_as_string( id: &str, path: &str, - session: State>, -) -> Result, 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_stream_data_by_path_with_file(path, &file.cos_file) + session: State, +) -> Result { + + let file = session.get_file(id)?; + let data = get_stream_data_by_path_with_file(path, &file.cos_file)?; + Ok(String::from_utf8_lossy(&data).into_owned()) } -fn get_stream_data_by_path_with_file(path: &str, file: &CosFile) -> Result, String> { - let mut steps = Step::parse(path); - if steps - .pop_back() - .filter(|last| *last == Step::Data) - .is_none() - { - return Err(format!("Path {} does not end with Data", path)); - } - let (_, prim, _) = get_prim_by_steps_with_file(steps, file)?; +#[tauri::command] +fn get_stream_data_as_image( + id: &str, + path: &str, + session: State, +) -> Result { + use base64::prelude::*; - let Primitive::Stream(stream) = prim else { - return Err(format!("Path {} does not point to a stream", path)); - }; - let resolver = file.resolver(); - let data = t!(t!(Stream::::from_stream(stream, &resolver)).data(&resolver)); - Ok(data.to_vec()) + let file = session.get_file(id)?; + let img = retrieval::get_image_by_path(path, &file.cos_file)?; + let mut writer = Cursor::new(Vec::new()); + match img.write_to(&mut writer, ImageFormat::Jpeg) { + Ok(_) => Ok(format!( + "data:image/{};base64,{}", + "jpeg", + BASE64_STANDARD.encode(writer.into_inner()) + )), + Err(e) => Err(e.to_string()), + } } #[tauri::command] fn get_prim_by_path( id: &str, path: &str, - session: State>, + session: State, ) -> Result { - let session_guard = session - .lock() - .map_err(|_| "Failed to lock the session mutex.".to_string())?; - let file = get_file_from_state(id, &session_guard)?; + let file = session.get_file(id)?; get_prim_model_by_path_with_file(path, &file.cos_file) } fn get_prim_model_by_path_with_file(path: &str, file: &CosFile) -> Result { - let (step, prim, trace) = get_prim_by_path_with_file(path, file)?; + let steps = Step::parse(path); + if steps.len() == 0 { + return Err(String::from(format!("{:?} is not a valid path!", steps))); + } + if steps.back().unwrap() == &Step::Data { + return handle_data_step(steps, file); + } + + let (_, prim, trace) = retrieval::get_prim_by_steps_with_file(steps, file)?; Ok(PrimitiveModel::from_primitive_with_children(&prim, trace)) } -fn get_prim_by_path_with_file( - path: &str, - file: &CosFile, -) -> Result<(Step, Primitive, Vec), String> { - get_prim_by_steps_with_file(Step::parse(path), file) -} - -fn get_prim_by_steps_with_file( - mut steps: VecDeque, - file: &CosFile, -) -> Result<(Step, Primitive, Vec), String> { - if steps.len() == 0 { - return Err(String::from(format!("{:?} is not a valid path!", steps))); +fn handle_data_step(mut steps: VecDeque, file: &CosFile) -> Result { + let _data = steps.pop_back(); + let (_, prim, trace) = retrieval::get_prim_by_steps_with_file(steps, file)?; + if let Primitive::Stream(stream) = prim { + let sub_type = match stream.info.get("Subtype") { + Some(element) => match element { + Primitive::Name(name) => name.as_str().to_string(), + _ => String::from("-"), + }, + _ => String::from("-"), + }; + return Ok(PrimitiveModel::as_data(sub_type, trace)); } - let step = steps.pop_front().unwrap(); - let (mut parent, trace) = resolve_parent(step.clone(), file)?; - - let mut last_jump = trace.last_jump.clone(); - let mut trace = vec![trace]; - - let mut current_prim = &parent; - while !steps.is_empty() { - let step = steps.pop_front().unwrap(); - - current_prim = resolve_step(¤t_prim, &step)?; - if let Primitive::Reference(xref) = current_prim { - last_jump = xref.id.to_string(); - parent = resolve_p_ref(xref.clone(), file)?; - current_prim = &parent; - } - trace.push(PathTrace::new(step.get_key(), last_jump.clone())); - } - Ok((step, current_prim.clone(), trace)) -} - -fn resolve_parent(step: Step, file: &CosFile) -> Result<(Primitive, PathTrace), String> { - let parent = match step { - Step::Page(page_num) => return retrieve_page(page_num, file), - Step::Number(obj_num) => resolve_xref(obj_num, file)?, - Step::Trailer => retrieve_trailer(file), - _ => return Err(String::from(format!("{:?} is not a valid path!", step))), - }; - Ok((parent, PathTrace::new(step.get_key(), step.get_key()))) + Err(String::from("Parent of Data is not a stream")) } #[tauri::command] fn get_prim_tree_by_path( id: &str, paths: Vec, - session: State>, + session: State, ) -> Result, 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)?; + + let file = session.get_file(id)?; let results = paths .into_iter() @@ -346,7 +304,7 @@ fn get_prim_tree_by_path_with_file( file: &CosFile, ) -> Result, String> { let step = node.step()?; - let (parent, trace) = resolve_parent(step.clone(), file)?; + let (parent, trace) = retrieval::resolve_parent(step.clone(), file)?; let trace = vec![trace]; let mut parent_model: PrimitiveModel; @@ -373,9 +331,9 @@ fn expand( return Ok(()); } let step = node.step()?; - let prim = resolve_step(parent, &step)?; + let prim = retrieval::resolve_step(parent, &step)?; if let Primitive::Reference(x_ref) = prim { - let jump = resolve_xref(x_ref.id, file)?; + let jump = retrieval::resolve_xref(x_ref.id, file)?; 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(); @@ -402,72 +360,6 @@ fn expand_children( Ok(()) } -fn resolve_step<'a>(current_prim: &'a Primitive, step: &Step) -> Result<&'a Primitive, String> { - Ok(match step { - Step::Number(index) => match current_prim { - Primitive::Array(prim_array) => { - let i = index.clone() as usize; - if prim_array.len() <= i { - return Err(String::from(format!( - "{} index out of bounds!", - step.get_key() - ))); - } - &prim_array[i] - } - p => { - return Err(String::from(format!( - "{} is not indexed with numbers!", - p.get_debug_name() - ))) - } - }, - Step::String(key) => match current_prim { - Primitive::Dictionary(dict) => match dict.get(key) { - Some(prim) => prim, - None => { - return Err(String::from(format!( - "Key {} does not exist in Dictionary!", - key - ))) - } - }, - Primitive::Stream(stream) => match stream.info.get(key) { - Some(prim) => prim, - None => { - return Err(String::from(format!( - "Key {} does not exist in Info Dictionary!", - key - ))) - } - }, - p => { - return Err(String::from(format!( - "{} has no String paths!", - p.get_debug_name() - ))) - } - }, - _ => return Err(format!("Invalid Step: {}", step.get_key())), - }) -} - -fn retrieve_trailer(file: &CosFile) -> Primitive { - let mut updater = FileOptions::uncached().storage(); - file.trailer.to_primitive(&mut updater).unwrap() -} - -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(( - resolve_p_ref(p_ref, file)?, - PathTrace::new(format!("Page{}", page_num), p_ref.id.to_string()), - )) -} #[derive(Debug, PartialEq, Clone)] pub enum Step { String(String), @@ -530,27 +422,6 @@ impl Step { } } -fn resolve_xref(id: u64, file: &CosFile) -> Result { - let plain_ref = PlainRef { id, gen: 0 }; - resolve_p_ref(plain_ref, file) -} - -fn resolve_p_ref(plain_ref: PlainRef, file: &CosFile) -> Result { - file.resolver() - .resolve(plain_ref) - .map_err(|e| e.to_string()) -} - -fn get_file_from_state<'a>( - id: &str, - session_guard: &'a MutexGuard, -) -> Result<&'a SessionFile, String> { - session_guard - .files - .get(id) - .ok_or_else(|| format!("File with id {} does not exist!", id)) -} - fn append_path(key: String, path: &Vec) -> Vec { let mut new_path = path.clone(); let last_jump = new_path.last().unwrap().last_jump.clone(); @@ -596,6 +467,18 @@ impl PrimitiveModel { } } + fn as_data(sub_type: String, trace: Vec) -> PrimitiveModel { + PrimitiveModel { + key: "Data".to_string(), + ptype: "Stream Data".to_string(), + sub_type: sub_type, + value: "".to_string(), + children: Vec::new(), + trace: append_path("Data".to_string(), &trace), + expanded: false, + } + } + fn format_dict_content(dict: &Dictionary) -> String { let mut result = String::from("{"); let mut count = 0; @@ -748,11 +631,9 @@ impl PrimitiveTreeView { } } #[tauri::command] -fn get_xref_table(id: &str, session: State>) -> Result { - let session_guard = session - .lock() - .map_err(|_| "Failed to lock the session mutex.".to_string())?; - let file = get_file_from_state(id, &session_guard)?; +fn get_xref_table(id: &str, session: State) -> Result { + + let file = session.get_file(id)?; get_xref_table_model_with_file(&file.cos_file) } fn get_xref_table_model_with_file(file: &CosFile) -> Result { @@ -812,7 +693,7 @@ fn get_xref_table_model_with_file(file: &CosFile) -> Result, + files: RwLock>>, } struct SessionFile { @@ -820,30 +701,48 @@ struct SessionFile { cos_file: CosFile, } -unsafe impl Send for SessionFile {} -unsafe impl Sync for SessionFile {} impl Session { fn load() -> Session { Session { - files: HashMap::new(), + files: RwLock::new(HashMap::new()), } } - fn handle_upload(&mut self, pdf_file: &PdfFile, cos_file: CosFile) { - self.files.insert( - pdf_file.id.clone(), - SessionFile { - pdf_file: pdf_file.clone(), - cos_file: cos_file, - }, - ); + fn get_file(&self, id: &str) -> Result, String> { + let lock = self.files.read().unwrap(); + lock.get(id).cloned().ok_or(format!(" File {} not found!", id)) } - fn handle_close(&mut self, id: &str) { - self.files.remove(id); + fn get_all_files(&self) -> Vec> { + self.files.read().unwrap().values().map(|f| f.clone()).collect() + } + + fn get_all_file_ids(&self) -> Vec { + self.files.read().unwrap().keys().map(|f| f.clone()).collect() + } + + fn handle_upload(&self, pdf_file: PdfFile, cos_file: CosFile) -> Result<(), String> { + if let Ok(mut files) = self.files.write() { + return match files.insert( + pdf_file.id.clone(), + Arc::new(SessionFile { + pdf_file: pdf_file, + cos_file: cos_file, + }, + )) { + Some(_) => Err("File could not be uploaded!".to_string()), + None => Ok(()), + } + }; + Err("Lock could not be acquired!".to_string()) + } + + fn handle_close(&self, id: &str) { + self.files.write().unwrap().remove(id); } } + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -851,7 +750,7 @@ pub fn run() { .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_opener::init()) .setup(|app| { - app.manage(Mutex::new(Session::load())); + app.manage(Session::load()); Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -864,7 +763,8 @@ pub fn run() { get_prim_tree_by_path, get_xref_table, get_contents, - get_stream_data + get_stream_data_as_string, + get_stream_data_as_image ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/tests.rs b/src-tauri/src/tests.rs index 180fadd..658e93b 100644 --- a/src-tauri/src/tests.rs +++ b/src-tauri/src/tests.rs @@ -3,16 +3,21 @@ extern crate pdf; #[cfg(test)] mod tests { + use crate::retrieval::{get_image_by_path, get_pdf_stream_by_path_with_file}; 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, TreeViewRequest, }; + use image::codecs::jpeg::JpegEncoder; + use image::imageops::FilterType; + use image::ImageFormat; use pdf::content::{display_ops, serialize_ops, Op}; use pdf::file::FileOptions; use pdf::object::{Object, ObjectWrite, Page, PlainRef, Resolve}; use pdf::primitive::Primitive; + use std::io::Cursor; use std::time::Instant; macro_rules! timed { ($func_call:expr, $label:expr) => {{ @@ -107,27 +112,36 @@ mod tests { ); } } + #[test] fn test_read_by_path() { let file = timed!( - FileOptions::cached().open(FILE_PATH).unwrap(), + FileOptions::cached() + .open("/home/kschuettler/Dokumente/TestFiles/402Study.pdf") + .unwrap(), "Loading file" ); - let path = "/Trailer/Root/Pages"; + let path = "/Page4/Resources/XObject/X13/Data"; let message = format!("Retrieval of {:?}", path); let prim = timed!(get_prim_model_by_path_with_file(path, &file), message); - print_node(prim.unwrap(), 0); + match prim { + Ok(prim) => print_node(prim, 0), + Err(e) => println!("Error: {}", e), + } } fn print_node(node: PrimitiveModel, depth: usize) { let spaces = " ".repeat(depth); - println!("{:?}", node.trace); - println!("{}{} | {} | {}", spaces, node.key, node.ptype, node.value); + println!( + "{}{} | {} | {} | {}", + spaces, node.key, node.ptype, node.sub_type, node.value + ); for child in node.children { print_node(child, depth + 1); } } + #[test] fn test_read_trailer() { let file = timed!( @@ -198,6 +212,44 @@ mod tests { get_stream_data_by_path_with_file("1/Contents/1/Data", &file).unwrap(), "get content 1" ); - println!("{}", content1); + println!("{}", String::from_utf8(content1).unwrap()); + } + + #[test] + fn test_read_img_by_path() { + let file = timed!( + FileOptions::cached() + .open("/home/kschuettler/Dokumente/TestFiles/402Study.pdf") + .unwrap(), + "Loading file" + ); + let path = "/Page4/Resources/XObject/X13/Data"; + + let img = timed!(get_image_by_path(path, &file), "get image").expect("Failed to get image"); + let w = img.width(); + let h = img.height(); + let bytes = (h * w * 3) as usize; + println!("Image Dimensions: {}-{}", w, h); + println!("Image Bytes: {}", bytes); + let mut writer = Cursor::new(Vec::new()); + timed!( + img.write_to(&mut writer, ImageFormat::Jpeg), + "save image jpeg" + ); + let size = writer.get_ref().len(); + println!( + "size: {} bytes {}% compression", + size, + 100.0 - ((size as f32 / bytes as f32) * 100.0) + ); + let mut writer = Cursor::new(Vec::new()); + let mut enc = JpegEncoder::new_with_quality(&mut writer, 10); + timed!(enc.encode_image(&img), "encode image jpeg 10"); + let size = writer.get_ref().len(); + println!( + "size: {} bytes {}% compression", + size, + 100.0 - ((size as f32 / bytes as f32) * 100.0) + ); } } diff --git a/src/components/App.svelte b/src/components/App.svelte index 74bd7c5..eee28ef 100644 --- a/src/components/App.svelte +++ b/src/components/App.svelte @@ -1,8 +1,8 @@ - +
@@ -112,31 +109,29 @@
- {#if fState} {:else} {/if} + - +
@@ -168,102 +163,102 @@ } :global(.splitpanes.forge-movable) - :global(.splitpanes__splitter:hover:before), + :global(.splitpanes__splitter:hover:before), :global(.splitpanes.forge-movable) - :global(.splitpanes__splitter:hover:after) { + :global(.splitpanes__splitter:hover:after) { background-color: var(--boundary-color); } :global(.splitpanes.forge-movable) - :global(.splitpanes__splitter:first-child) { + :global(.splitpanes__splitter:first-child) { cursor: auto; } :global(.forge-movable.splitpanes) - :global(.splitpanes) - :global(.splitpanes__splitter) { + :global(.splitpanes) + :global(.splitpanes__splitter) { z-index: 1; } :global(.forge-movable.splitpanes--vertical) - > :global(.splitpanes__splitter), + > :global(.splitpanes__splitter), :global(.forge-movable) - :global(.splitpanes--vertical) - > :global(.splitpanes__splitter) { + :global(.splitpanes--vertical) + > :global(.splitpanes__splitter) { width: 2px; border-left: 1px solid var(--boundary-color); cursor: col-resize; } :global(.forge-movable.splitpanes--vertical) - > :global(.splitpanes__splitter:before), + > :global(.splitpanes__splitter:before), :global(.forge-movable.splitpanes--vertical) - > :global(.splitpanes__splitter:after), + > :global(.splitpanes__splitter:after), :global(.forge-movable) - :global(.splitpanes--vertical) - > :global(.splitpanes__splitter:before), + :global(.splitpanes--vertical) + > :global(.splitpanes__splitter:before), :global(.forge-movable) - :global(.splitpanes--vertical) - > :global(.splitpanes__splitter:after) { + :global(.splitpanes--vertical) + > :global(.splitpanes__splitter:after) { transform: translateY(-50%); width: 1px; height: 40px; } :global(.forge-movable.splitpanes--vertical) - > :global(.splitpanes__splitter:before), + > :global(.splitpanes__splitter:before), :global(.forge-movable) - :global(.splitpanes--vertical) - > :global(.splitpanes__splitter:before) { + :global(.splitpanes--vertical) + > :global(.splitpanes__splitter:before) { margin-left: -2px; } :global(.forge-movable.splitpanes--vertical) - > :global(.splitpanes__splitter:after), + > :global(.splitpanes__splitter:after), :global(.forge-movable) - :global(.splitpanes--vertical) - > :global(.splitpanes__splitter:after) { + :global(.splitpanes--vertical) + > :global(.splitpanes__splitter:after) { margin-left: 1px; } :global(.forge-movable.splitpanes--horizontal) - > :global(.splitpanes__splitter), + > :global(.splitpanes__splitter), :global(.forge-movable) - :global(.splitpanes--horizontal) - > :global(.splitpanes__splitter) { + :global(.splitpanes--horizontal) + > :global(.splitpanes__splitter) { height: 2px; border-top: 1px solid var(--boundary-color); cursor: row-resize; } :global(.forge-movable.splitpanes--horizontal) - > :global(.splitpanes__splitter:before), + > :global(.splitpanes__splitter:before), :global(.forge-movable.splitpanes--horizontal) - > :global(.splitpanes__splitter:after), + > :global(.splitpanes__splitter:after), :global(.forge-movable) - :global(.splitpanes--horizontal) - > :global(.splitpanes__splitter:before), + :global(.splitpanes--horizontal) + > :global(.splitpanes__splitter:before), :global(.forge-movable) - :global(.splitpanes--horizontal) - > :global(.splitpanes__splitter:after) { + :global(.splitpanes--horizontal) + > :global(.splitpanes__splitter:after) { transform: translateX(-50%); width: 40px; height: 3px; } :global(.forge-movable.splitpanes--horizontal) - > :global(.splitpanes__splitter:before), + > :global(.splitpanes__splitter:before), :global(.forge-movable) - :global(.splitpanes--horizontal) - > :global(.splitpanes__splitter:before) { + :global(.splitpanes--horizontal) + > :global(.splitpanes__splitter:before) { margin-top: -2px; } :global(.forge-movable.splitpanes--horizontal) - > :global(.splitpanes__splitter:after), + > :global(.splitpanes__splitter:after), :global(.forge-movable) - :global(.splitpanes--horizontal) - > :global(.splitpanes__splitter:after) { + :global(.splitpanes--horizontal) + > :global(.splitpanes__splitter:after) { margin-top: 1px; } diff --git a/src/components/ContentsView.svelte b/src/components/ContentsView.svelte index 25d49ac..4e13d46 100644 --- a/src/components/ContentsView.svelte +++ b/src/components/ContentsView.svelte @@ -1,34 +1,18 @@ -
+ diff --git a/src/components/FileView.svelte b/src/components/FileView.svelte index fa5feca..ecd0917 100644 --- a/src/components/FileView.svelte +++ b/src/components/FileView.svelte @@ -1,29 +1,25 @@ + +
+
+ Notifications: +
+ +
+
+ {#each notifications as notification} +
+ {notification.getDate() + ":"} 

{notification.level}

  +

{" - " + notification.message}

+
+
+ {/each} +
+ + + diff --git a/src/components/PrimitiveView.svelte b/src/components/PrimitiveView.svelte index db85f73..de34bdd 100644 --- a/src/components/PrimitiveView.svelte +++ b/src/components/PrimitiveView.svelte @@ -1,20 +1,19 @@ -{#if fState.stream_data} - -{/if} {#if prim && prim.children && prim.children.length > 0} -
+
- +
- - - - - + + + + +
KeyTypeValue
KeyTypeValue
-
-
- - - - {#each entriesToDisplay as entry} - - (fState.highlighted_prim = entry.key)} - ondblclick={() => handlePrimSelect(entry)} - > - - - - - {/each} - -
-
- -

- {entry.key} -

-
-
{entry.ptype}{entry.value}
-
- {#if fState.stream_data} - - {/if} +
+ + + + {#each entriesToDisplay as entry} + handlePrimClick(entry)} + ondblclick={() => handlePrimDbLClick(entry)} + > + + + + + {/each} + +
+
+ +

+ {entry.key} +

+
+
{entry.ptype}{entry.value}
+ {#if fState.container_prim?.stream_data} + + {/if}
{/if} diff --git a/src/components/StreamDataView.svelte b/src/components/StreamDataView.svelte new file mode 100644 index 0000000..d61cf81 --- /dev/null +++ b/src/components/StreamDataView.svelte @@ -0,0 +1,27 @@ + +{#if streamData.type === "Image"} +
+ x-object +
+{:else} + + +{/if} + + \ No newline at end of file diff --git a/src/components/StreamEditor.svelte b/src/components/StreamEditor.svelte index e90230c..a7c261e 100644 --- a/src/components/StreamEditor.svelte +++ b/src/components/StreamEditor.svelte @@ -1,9 +1,15 @@
+ +
diff --git a/src/components/TreeView.svelte b/src/components/TreeView.svelte index 0bb0327..12901cb 100644 --- a/src/components/TreeView.svelte +++ b/src/components/TreeView.svelte @@ -1,12 +1,9 @@ -
+
{#if entries}
-
+ + + {#each stickies as entry} + {#if entry.active} + handleSelect(entry)} + > + {/if} + {/each} + + + +
- {#each entries as entry} - {#if entry.active} - {#if entry.depth == 0} -
+
+ {#each entries as entry} + {#if entry.active} + handleSelect(entry)} + {entry} + > {/if} - - {/if} - {/each} + {/each} +
{/if}
@@ -155,12 +127,6 @@ width: 100%; text-align: center; } - .item { - @apply text-sm rounded-sm; - text-align: center; - display: flex; - flex-direction: row; - } .caret { @apply text-forge-sec; @@ -170,21 +136,6 @@ .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 { diff --git a/src/components/TreeViewEntry.svelte b/src/components/TreeViewEntry.svelte new file mode 100644 index 0000000..ab4a244 --- /dev/null +++ b/src/components/TreeViewEntry.svelte @@ -0,0 +1,93 @@ + + {#if entry.depth == 0} +
+ {/if} + + + \ No newline at end of file diff --git a/src/models/FileViewState.svelte.ts b/src/models/FileViewState.svelte.ts index 10edfcd..7231070 100644 --- a/src/models/FileViewState.svelte.ts +++ b/src/models/FileViewState.svelte.ts @@ -1,20 +1,30 @@ import type PdfFile from "./PdfFile"; import type XRefEntry from "./XRefEntry"; import Primitive from "./Primitive.svelte"; -import { invoke } from "@tauri-apps/api/core"; -import TreeViewRequest from "./TreeViewRequest.svelte"; +import {invoke} from "@tauri-apps/api/core"; import type XRefTable from "./XRefTable"; -import TreeViewState from "./TreeViewState.svelte"; -import type { PathSelectedEvent } from "../events/PathSelectedEvent"; +import type {PathSelectedEvent} from "../events/PathSelectedEvent"; +import type {PrimitiveModel} from "./PrimitiveModel"; +import {StreamData} from "./StreamData"; +import {ForgeNotification} from "./ForgeNotification.svelte"; +import {Mutex} from 'async-mutex'; export default class FileViewState { + public file: PdfFile; + + public treeMode: boolean = $state(true); + public pageMode: boolean = $state(false); + public xRefShowing: boolean = $state(true); + public notificationsShowing: boolean = $state(false); + public path: string[] = $state(["Trailer"]); - public file: PdfFile; - public prim: Primitive | undefined = $state(); - public highlighted_prim: string | undefined = $state(); + public container_prim: Primitive | undefined = $state(); + public selected_leaf_prim: Primitive | undefined = $state(); public xref_entries: XRefEntry[] = $state([]); - public stream_data: string | undefined = $state(); + + public notifications: ForgeNotification[] = $state([]); + notificationMutex = new Mutex(); constructor(file: PdfFile) { @@ -24,48 +34,92 @@ export default class FileViewState { } getLastJump(): string | number | undefined { - return this.prim?.getLastJump() + return this.container_prim?.getLastJump() } getFirstJump(): string | number | undefined { - return this.prim?.getFirstJump() + return this.container_prim?.getFirstJump() + } + + public async logError(message: string) { + const release = await this.notificationMutex.acquire(); + try { + console.error(message) + this.notifications.push(new ForgeNotification(Date.now(), "ERROR", message)); + } finally { + release(); // Always release the lock + } + } + + public async deleteNotification(timestamp: number) { + const release = await this.notificationMutex.acquire(); + try { + this.notifications = this.notifications.filter(n => n.timestamp != timestamp); + } finally { + release(); // Always release the lock + } + } + + public async clearNotifications() { + const release = await this.notificationMutex.acquire(); + try { + this.notifications = []; + } finally { + release(); // Always release the lock + } } public loadXrefEntries() { - invoke("get_xref_table", { id: this.file.id }) + invoke("get_xref_table", {id: this.file.id}) .then(result => { this.xref_entries = result.entries; }) - .catch(err => console.error(err)); + .catch(err => this.logError(err)); } public async selectPath(newPath: string[]) { - if (newPath.at(-1) === "Data") { - let _path = newPath.slice(0, newPath.length - 1) - const _prim = await invoke("get_prim_by_path", { id: this.file.id, path: this.formatPaths(_path) }) - this.stream_data = await invoke("get_stream_data", { id: this.file.id, path: this.formatPaths(newPath) }) - this.prim = new Primitive(_prim); - this.path = _path; - this.highlighted_prim = "Data"; + if (this.container_prim?.pathEquals(newPath)) { return; - } else { - this.stream_data = undefined; } - invoke("get_prim_by_path", { id: this.file.id, path: this.formatPaths(newPath) }) - .then(result => { - let _prim = new Primitive(result) - if (_prim.isContainer()) { - this.prim = _prim; - this.path = newPath - return; - } else { - this.highlighted_prim = _prim.key; - this.selectPath(newPath.slice(0, newPath.length - 1)) - } - }) - .catch(err => console.error(err)); + const result = await invoke("get_prim_by_path", { + id: this.file.id, + path: this.formatPaths(newPath) + }).catch(err => this.logError(err)) + if (!result) return; + + let newPrim = new Primitive(result) + + if (newPrim.isContainer()) { + this.container_prim = newPrim; + this.path = newPath + return; + } + this.selected_leaf_prim = newPrim; + await this.selectPath(newPath.slice(0, newPath.length - 1)).catch(err => this.logError(err)); + await this.loadStreamData(this.container_prim, newPrim,).catch(err => this.logError(err)); } + public async loadStreamData(container: Primitive | undefined, leaf: Primitive | undefined) { + if (!container || !leaf) return; + if (leaf.ptype === "Stream Data" && + leaf.isChildOf(container) + ) { + const path = this.formatPaths(this.path) + "/Data"; + if (leaf.sub_type === "Image") { + const data = await invoke("get_stream_data_as_image", {id: this.file.id, path: path}) + .catch(err => this.logError(err)) + if (!data) return; + container.stream_data = new StreamData(leaf.sub_type, data); + } else { + const data = await invoke("get_stream_data_as_string", {id: this.file.id, path: path}) + .catch(err => this.logError(err)) + if (!data) return; + container.stream_data = new StreamData(leaf.sub_type, data); + } + } + } + + public selectPathHandler(event: PathSelectedEvent) { if (event.detail.file_id !== this.file.id) { diff --git a/src/models/ForgeNotification.svelte.ts b/src/models/ForgeNotification.svelte.ts new file mode 100644 index 0000000..dd10f86 --- /dev/null +++ b/src/models/ForgeNotification.svelte.ts @@ -0,0 +1,27 @@ +export class ForgeNotification { + public timestamp: number; + date: Date; + public level: string; + public message: string; + public read: boolean = false; + constructor(timestamp: number, level: string, message: string) { + this.timestamp = timestamp; + this.level = level; + this.message = message; + this.date = new Date(timestamp); + } + + public getDate(): string { + const hours = this.date.getHours().toString().padStart(2, '0'); + const minutes = this.date.getMinutes().toString().padStart(2, '0'); + const seconds = this.date.getSeconds().toString().padStart(2, '0'); + return `${hours}:${minutes}:${seconds}`; + } + + public isError() { + return this.level === "ERROR"; + } + public isDebug() { + return this.level === "DEBUG"; + } +} \ No newline at end of file diff --git a/src/models/Primitive.svelte.ts b/src/models/Primitive.svelte.ts index 57f5a41..4da3883 100644 --- a/src/models/Primitive.svelte.ts +++ b/src/models/Primitive.svelte.ts @@ -1,4 +1,9 @@ +import {tracesAreEqual, tracesAreEqual2} from "../utils"; +import type {PrimitiveModel} from "./PrimitiveModel"; +import type {StreamData} from "./StreamData"; + export default class Primitive { + // input from api public key: string; public ptype: string; public sub_type: string; @@ -7,8 +12,11 @@ export default class Primitive { public trace: Trace[] = $state([]); public expanded: boolean = $state(false); + //local state + public stream_data: StreamData | undefined = $state(); + constructor( - p: Primitive + p: PrimitiveModel ) { this.key = p.key; this.ptype = p.ptype; @@ -33,9 +41,19 @@ export default class Primitive { return this.trace.map(path => path.key); } + public traceEquals(trace: Trace[]): boolean { + return tracesAreEqual(trace, this.trace) + } + public pathEquals(path: string[]): boolean { + return tracesAreEqual2(this.trace, path) + } + public getLastJump(): string | number { let path = this.trace[this.trace.length - 1].last_jump; - if (path === "/") { return path }; + if (path === "/") { + return path + } + ; return +path; } @@ -52,8 +70,14 @@ export default class Primitive { public getFirstJump(): string | number | undefined { let path = this.trace[0].last_jump; - if (path === "Trailer") { return path }; - if (path.startsWith("Page")) { return path }; + if (path === "Trailer") { + return path + } + ; + if (path.startsWith("Page")) { + return path + } + ; return +path; } @@ -64,6 +88,13 @@ export default class Primitive { expanded: false, }); } + + public isChildOf(prim: Primitive | undefined): boolean { + if (!prim) { + return false; + } + return tracesAreEqual(this.trace.slice(0, -1), prim.trace); + } } export interface Trace { diff --git a/src/models/PrimitiveModel.ts b/src/models/PrimitiveModel.ts new file mode 100644 index 0000000..c9631d6 --- /dev/null +++ b/src/models/PrimitiveModel.ts @@ -0,0 +1,11 @@ +import type {Trace} from "./Primitive.svelte" + +export interface PrimitiveModel { + readonly key: string, + readonly ptype: string, + readonly sub_type: string, + readonly value: string, + readonly children: PrimitiveModel[], + readonly trace: Trace[], + readonly expanded: boolean, +} diff --git a/src/models/PrimitiveView.ts b/src/models/PrimitiveView.ts deleted file mode 100644 index e407344..0000000 --- a/src/models/PrimitiveView.ts +++ /dev/null @@ -1,15 +0,0 @@ -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, - ) { } -} diff --git a/src/models/StreamData.ts b/src/models/StreamData.ts new file mode 100644 index 0000000..2f204be --- /dev/null +++ b/src/models/StreamData.ts @@ -0,0 +1,9 @@ +export class StreamData { + // Image, Text + public type: string; + public data: string; + constructor(type: string, value: string) { + this.type = type; + this.data = value; + } +} \ No newline at end of file diff --git a/src/models/TreeViewRequest.svelte.ts b/src/models/TreeViewRequest.svelte.ts index f50b290..2af16ee 100644 --- a/src/models/TreeViewRequest.svelte.ts +++ b/src/models/TreeViewRequest.svelte.ts @@ -21,7 +21,7 @@ export default class TreeViewRequest { } static fromPageCount(pageCount: number) { - let roots = [TreeViewRequest.TRAILER]; + let roots = []; for (let i = 0; i < pageCount; i++) { roots.push(new TreeViewRequest("Page" + (i + 1), [])); } diff --git a/src/models/TreeViewState.svelte.ts b/src/models/TreeViewState.svelte.ts index d34e8a5..91c75cb 100644 --- a/src/models/TreeViewState.svelte.ts +++ b/src/models/TreeViewState.svelte.ts @@ -1,19 +1,23 @@ -import { invoke } from "@tauri-apps/api/core"; +import {invoke} from "@tauri-apps/api/core"; import TreeViewRequest from "./TreeViewRequest.svelte"; -import { PrimitiveView } from "./PrimitiveView"; -import type { Trace } from "./Primitive.svelte"; +import {TreeViewModel} from "./TreeViewModel"; +import type {Trace} from "./Primitive.svelte"; import type FileViewState from "./FileViewState.svelte"; +const MAX_STICKIES: number = 5; export default class TreeViewState { - private request: TreeViewRequest[]; + + private initialRequest: TreeViewRequest[]; private activeRequest: Map = $state(new Map()); - private activeEntries: Map = $state(new Map()); + private activeEntries: Map = $state(new Map()); + scrollY: number = 0; file_id: string; 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.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; } @@ -22,28 +26,33 @@ export default class TreeViewState { this.activeEntries.forEach((value, key) => { count += value.length - 1; }); - return this.request.length + count; + return this.initialRequest.length + count; } - 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) { + public getEntries(start: number, end: number): [TreeViewModel[], TreeViewModel[]] { + let stickies: TreeViewModel[] = []; + let totalIndex = 0; + let result: TreeViewModel[] = []; + for (let request of this.initialRequest) { + let entries = this.activeEntries.get(request.key); + if (entries) { + for (let i = 0; i < entries.length; i++) { + let entry = entries[i]; + if (totalIndex > 0 && totalIndex == start) { + stickies = this.findParents(entries, i); + } + if (totalIndex >= start) { result.push(entry); } - i += 1; - if (i >= end) { - return result; + totalIndex += 1; + if (totalIndex >= end) { + return [result, stickies]; } } } else { - if (i >= start) { + if (totalIndex >= start) { result.push( - new PrimitiveView( + new TreeViewModel( 0, request.key, "Dictionary", @@ -51,21 +60,40 @@ export default class TreeViewState { "-", true, false, - [{ key: request.key, last_jump: request.key } as Trace], + [{key: request.key, last_jump: request.key} as Trace], true, )) } - i += 1; - if (i >= end) { - return result; + totalIndex += 1; + if (totalIndex >= end) { + return [result, stickies]; } } } - return result; + return [result, stickies]; } - public toView(request: TreeViewRequest): PrimitiveView { - return new PrimitiveView( + private findParents(entries: TreeViewModel[], start: number): TreeViewModel[] { + + let parents = []; + let lastDepth = entries[start].depth; + for (let i = start; i >= 0; i--) { + let entry = entries[i]; + if (entry.depth < lastDepth) { + parents.push(entry); + if (entry.depth == 0) { + return parents.reverse().slice(0, MAX_STICKIES); + } + lastDepth = entry.depth; + + } + } + return parents.reverse().slice(0, MAX_STICKIES); + } + + + public toView(request: TreeViewRequest): TreeViewModel { + return new TreeViewModel( 0, request.key, "Dictionary", @@ -73,7 +101,7 @@ export default class TreeViewState { "-", true, false, - [{ key: request.key, last_jump: request.key } as Trace], + [{key: request.key, last_jump: request.key} as Trace], false, ); } @@ -85,7 +113,7 @@ export default class TreeViewState { public getRoot(): TreeViewRequest[] { let _roots: TreeViewRequest[] = []; - this.request.forEach((root) => { + this.initialRequest.forEach((root) => { _roots.push(root.clone()); }); return _roots; @@ -93,7 +121,7 @@ export default class TreeViewState { public async updateTreeViewRequest(treeViewRequests: TreeViewRequest[]) { - let result = await invoke("get_prim_tree_by_path", { + let result = await invoke("get_prim_tree_by_path", { id: this.file_id, paths: treeViewRequests, }); diff --git a/src/utils.ts b/src/utils.ts index 1928358..fd4d92e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ -import type {Action} from "@sveltejs/kit"; +import type { Action } from "@sveltejs/kit"; +import type { Trace } from "./models/Primitive.svelte"; export function arraysAreEqual(arr1: string[], arr2: string[]) { if (arr1.length !== arr2.length) { @@ -12,6 +13,31 @@ export function arraysAreEqual(arr1: string[], arr2: string[]) { return true; // All elements match } + +export function tracesAreEqual(arr1: Trace[], arr2: Trace[]) { + if (arr1.length !== arr2.length) { + return false; // Arrays of different lengths are not equal + } + for (let i = 0; i < arr1.length; i++) { + if (arr1[i].key !== arr2[i].key) { + return false; // Mismatched element found + } + } + return true; // All elements match +} + +export function tracesAreEqual2(arr1: Trace[], arr2: string[]) { + if (arr1.length !== arr2.length) { + return false; // Arrays of different lengths are not equal + } + for (let i = 0; i < arr1.length; i++) { + if (arr1[i].key !== arr2[i]) { + return false; // Mismatched element found + } + } + return true; // All elements match +} + export function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } diff --git a/yarn.lock b/yarn.lock index 60b6410..c919ee6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -603,6 +603,13 @@ aria-query@^5.3.1: resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz" integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== +async-mutex@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.5.0.tgz#353c69a0b9e75250971a64ac203b0ebfddd75482" + integrity sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA== + dependencies: + tslib "^2.4.0" + autoprefixer@^10.4.20: version "10.4.20" resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz" @@ -1812,7 +1819,7 @@ ts-interface-checker@^0.1.9: resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -tslib@^2.1.0: +tslib@^2.1.0, tslib@^2.4.0: version "2.8.1" resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==