Added image rendering and notifications

This commit is contained in:
Kilian Schuettler 2025-02-10 12:07:25 +01:00
parent f6fa5d7269
commit 98d34ad5c3
27 changed files with 1382 additions and 643 deletions

View File

@ -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",

486
src-tauri/Cargo.lock generated
View File

@ -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"

View File

@ -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"] }

View File

@ -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<Vec<u8>, 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<Mutex<Session>>) -> Vec<PdfFile> {
let files = &session.lock().unwrap().files;
files
.values()
.map(|sf| sf.pdf_file.clone())
.collect::<Vec<PdfFile>>()
fn get_all_files(session: State<Session>) -> Vec<PdfFile> {
session.get_all_files().iter().map(|s| s.pdf_file.clone()).collect()
}
#[tauri::command]
fn get_all_file_ids(session: State<Mutex<Session>>) -> Vec<String> {
let files = &session.lock().unwrap().files;
files
.values()
.map(|sf| sf.pdf_file.id.clone())
.collect::<Vec<String>>()
fn get_all_file_ids(session: State<Session>) -> Vec<String> {
session.get_all_file_ids()
}
#[tauri::command]
fn close_file(id: &str, session: State<Mutex<Session>>) {
session.lock().unwrap().deref_mut().handle_close(&id);
fn close_file(id: &str, session: State<Session>) {
session.handle_close(id);
}
#[tauri::command]
fn get_file_by_id(id: &str, session: State<Mutex<Session>>) -> Result<PdfFile, 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)?;
Ok(file.pdf_file.clone())
fn get_file_by_id(id: &str, session: State<Session>) -> Result<PdfFile, String> {
session.get_file(id).map(|file| file.pdf_file.clone())
}
#[tauri::command]
fn upload(path: &str, session: State<Mutex<Session>>) -> Result<String, String> {
fn upload(path: &str, session: State<Session>) -> Result<String, String> {
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<PdfFile, String> {
@ -198,12 +183,10 @@ fn to_pdf_file(path: &str, file: &CosFile) -> Result<PdfFile, String> {
fn get_contents(
id: &str,
path: &str,
session: State<Mutex<Session>>,
session: State<Session>,
) -> Result<ContentsModel, 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 (_, 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<Mutex<Session>>,
) -> Result<Vec<u8>, 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<Session>,
) -> Result<String, String> {
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<Vec<u8>, 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<Session>,
) -> Result<String, String> {
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::<Primitive>::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<Mutex<Session>>,
session: State<Session>,
) -> Result<PrimitiveModel, 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)?;
get_prim_model_by_path_with_file(path, &file.cos_file)
}
fn get_prim_model_by_path_with_file(path: &str, file: &CosFile) -> Result<PrimitiveModel, String> {
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<PathTrace>), String> {
get_prim_by_steps_with_file(Step::parse(path), file)
}
fn get_prim_by_steps_with_file(
mut steps: VecDeque<Step>,
file: &CosFile,
) -> Result<(Step, Primitive, Vec<PathTrace>), String> {
if steps.len() == 0 {
return Err(String::from(format!("{:?} is not a valid path!", steps)));
fn handle_data_step(mut steps: VecDeque<Step>, file: &CosFile) -> Result<PrimitiveModel, String> {
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(&current_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<TreeViewRequest>,
session: State<Mutex<Session>>,
session: State<Session>,
) -> Result<Vec<PrimitiveTreeView>, String> {
let session_guard = session
.lock()
.map_err(|_| "Failed to lock the session mutex.".to_string())?;
let file = get_file_from_state(id, &session_guard)?;
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<Vec<PrimitiveTreeView>, 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<Primitive, String> {
let plain_ref = PlainRef { id, gen: 0 };
resolve_p_ref(plain_ref, file)
}
fn resolve_p_ref(plain_ref: PlainRef, file: &CosFile) -> Result<Primitive, String> {
file.resolver()
.resolve(plain_ref)
.map_err(|e| e.to_string())
}
fn get_file_from_state<'a>(
id: &str,
session_guard: &'a MutexGuard<Session>,
) -> 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<PathTrace>) -> Vec<PathTrace> {
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<PathTrace>) -> 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<Mutex<Session>>) -> Result<XRefTableModel, 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)?;
fn get_xref_table(id: &str, session: State<Session>) -> Result<XRefTableModel, String> {
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<XRefTableModel, String> {
@ -812,7 +693,7 @@ fn get_xref_table_model_with_file(file: &CosFile) -> Result<XRefTableModel, Stri
}
struct Session {
files: HashMap<String, SessionFile>,
files: RwLock<HashMap<String, Arc<SessionFile>>>,
}
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<Arc<SessionFile>, 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<Arc<SessionFile>> {
self.files.read().unwrap().values().map(|f| f.clone()).collect()
}
fn get_all_file_ids(&self) -> Vec<String> {
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");

View File

@ -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)
);
}
}

View File

@ -1,8 +1,8 @@
<script lang="ts">
import WelcomeScreen from "./WelcomeScreen.svelte";
import { Pane, Splitpanes } from "svelte-splitpanes";
import { open } from "@tauri-apps/plugin-dialog";
import { invoke } from "@tauri-apps/api/core";
import {Pane, Splitpanes} from "svelte-splitpanes";
import {open} from "@tauri-apps/plugin-dialog";
import {invoke} from "@tauri-apps/api/core";
import type PdfFile from "../models/PdfFile";
import ToolbarLeft from "./ToolbarLeft.svelte";
import FileView from "./FileView.svelte";
@ -11,16 +11,16 @@
import TitleBar from "./TitleBar.svelte";
import ToolbarRight from "./ToolbarRight.svelte";
import Footer from "./Footer.svelte";
import NotificationModal from "./NotificationModal.svelte";
const footerHeight: number = 30;
const titleBarHeight: number = 30;
const tabBarHeight: number = 30;
let files: PdfFile[] = $state([]);
let innerHeight: number = $state(1060);
let errorMessage: string = $state("");
let xrefTableWidth: number = $state(0);
let xrefTableShowing: boolean = $state(false);
let treeShowing: boolean = $state(true);
let notificationsShowing: boolean = $state(false);
let pagesShowing: boolean = $state(false);
let fileViewHeight: number = $derived(
Math.max(innerHeight - footerHeight - tabBarHeight - titleBarHeight, 0),
@ -41,7 +41,6 @@
}
})
.catch((error) => {
errorMessage = error;
console.error("File retrieval failed: " + error);
});
}
@ -60,13 +59,13 @@
let file_path = await open({
multiple: false,
directory: false,
filters: [{ name: "pdf", extensions: ["pdf"] }],
filters: [{name: "pdf", extensions: ["pdf"]}],
});
if (file_path === null || Array.isArray(file_path)) {
return;
}
invoke<String>("upload", { path: file_path })
invoke<String>("upload", {path: file_path})
.then((result) => {
invoke<PdfFile[]>("get_all_files")
.then((result_list) => {
@ -78,12 +77,10 @@
}
})
.catch((error) => {
errorMessage = error;
console.error("Fetch all files failed with: " + error);
});
})
.catch((error) => {
errorMessage = error;
console.error("File upload failed with: " + error);
});
}
@ -93,7 +90,7 @@
}
function closeFile(file: PdfFile) {
invoke("close_file", { id: file.id })
invoke("close_file", {id: file.id})
.then((_) => {
files = files.filter((f) => f.id != file.id);
if (file === selected_file) {
@ -104,7 +101,7 @@
}
</script>
<svelte:window bind:innerHeight />
<svelte:window bind:innerHeight/>
<main style="height: {innerHeight}px; overflow: hidden;">
<div style="height: {titleBarHeight}px">
<TitleBar></TitleBar>
@ -112,31 +109,29 @@
<div class="fileview_container" style="height: {fileViewHeight + 30}px">
<Splitpanes theme="forge-movable" dblClickSplitter={false}>
<Pane size={2.5} minSize={1.5} maxSize={4}>
<ToolbarLeft bind:tree={treeShowing} bind:pages={pagesShowing}
<ToolbarLeft {fState}
></ToolbarLeft>
</Pane>
<Pane>
<TabBar
{files}
{selected_file}
closeTab={closeFile}
openTab={upload}
selectTab={selectFile}
{files}
{selected_file}
closeTab={closeFile}
openTab={upload}
selectTab={selectFile}
></TabBar>
{#if fState}
<FileView
{treeShowing}
{xrefTableShowing}
{pagesShowing}
{fState}
height={fileViewHeight}
{fState}
height={fileViewHeight}
></FileView>
{:else}
<WelcomeScreen {upload}></WelcomeScreen>
{/if}
</Pane>
<Pane size={2.5} minSize={1.5} maxSize={4}>
<ToolbarRight bind:xref={xrefTableShowing}></ToolbarRight>
<ToolbarRight {fState}></ToolbarRight>
</Pane>
</Splitpanes>
</div>
@ -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;
}

View File

@ -1,34 +1,18 @@
<script lang="ts">
import { invoke } from "@tauri-apps/api/core";
import {invoke} from "@tauri-apps/api/core";
import type ContentModel from "../models/ContentModel.svelte";
import { onMount } from "svelte";
import {onMount} from "svelte";
import * as monaco from "monaco-editor";
import type FileViewState from "../models/FileViewState.svelte";
import StreamEditor from "./StreamEditor.svelte";
let { fState, height }: { fState: FileViewState; height: number } =
let {fState, height}: { fState: FileViewState; height: number } =
$props();
let h = $derived(height);
let path = $derived(fState.prim?.getFirstJump()?.toString());
let path = $derived(fState.container_prim?.getFirstJump()?.toString());
let id = $derived(fState.file.id);
let contents: ContentModel | undefined = $state(undefined);
let editorContainer: HTMLElement;
let editor: monaco.editor.IStandaloneCodeEditor;
onMount(() => {
editor = monaco.editor.create(editorContainer, {
value: "",
language: "plaintext",
theme: "vs-dark",
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
automaticLayout: true,
});
return () => {
editor.dispose();
};
});
let contentsModel: ContentModel | undefined = $state(undefined);
let contents: string | undefined = $derived(mapToString(contentsModel));
$effect(() => {
loadContents(path, id);
@ -37,36 +21,41 @@
function loadContents(path: string | undefined, id: string) {
if (!path || !id) return;
invoke<ContentModel>("get_contents", { id, path })
invoke<ContentModel>("get_contents", {id, path})
.then((result) => {
contents = result;
if (contents && editor) {
let text = "";
if (contents.parts.length > 1) {
let i = 0;
for (let part of contents.parts) {
text +=
"%----------------% Contents[" +
i +
"] %--------------%\n\n";
for (let line of part) {
text += " " + line + "\n";
}
text +=
"\n%-------------------% EOF %-------------------%\n\n";
i++;
}
} else {
text = contents.parts[0].join("\n");
}
editor.setValue(text);
}
contentsModel = result;
})
.catch((err) => console.error(err));
}
function mapToString(model: ContentModel | undefined) {
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;
}
</script>
<div bind:this={editorContainer} style="height: {h}px; width: 100%;"></div>
<StreamEditor stream_data={contents} height={height}></StreamEditor>
<style lang="postcss">
</style>

View File

@ -1,29 +1,25 @@
<script lang="ts">
import { Pane, Splitpanes } from "svelte-splitpanes";
import {Pane, Splitpanes} from "svelte-splitpanes";
import XRefTable from "./XRefTable.svelte";
import PrimitiveView from "./PrimitiveView.svelte";
import TreeView from "./TreeView.svelte";
import type FileViewState from "../models/FileViewState.svelte";
import ContentsView from "./ContentsView.svelte";
import type { PathSelectedEvent } from "../events/PathSelectedEvent";
import { onMount } from "svelte";
import type {PathSelectedEvent} from "../events/PathSelectedEvent";
import {onMount} from "svelte";
import NotificationModal from "./NotificationModal.svelte";
let {
treeShowing,
xrefTableShowing,
pagesShowing,
fState,
height,
}: {
treeShowing: boolean;
xrefTableShowing: boolean;
pagesShowing: boolean;
fState: FileViewState;
height: number;
} = $props();
let width: number = $state(0);
let xRefTableWidth = $derived(xrefTableShowing ? 281 : 0);
let splitPanesWidth: number = $derived(width - xRefTableWidth);
let modalWidth = $derived(fState.xRefShowing || fState.notificationsShowing ? 281 : 0);
let splitPanesWidth: number = $derived(width - modalWidth);
function handleKeydown(event: KeyboardEvent) {
// Check for "Alt + Left Arrow"
@ -48,6 +44,7 @@
window.removeEventListener("mousedown", handleMouseButton);
};
});
function pathSelectedHandler(event: PathSelectedEvent) {
fState.selectPathHandler(event);
}
@ -55,37 +52,42 @@
<div bind:clientWidth={width} class="file-view-container">
<div
class="flex flex-row"
style="width: {splitPanesWidth}px; height: {height}px"
class="flex flex-row"
style="width: {splitPanesWidth}px; height: {height}px"
>
<Splitpanes theme="forge-movable" pushOtherPanes={false}>
<Pane
size={treeShowing || pagesShowing ? 15 : 0}
minSize={treeShowing || pagesShowing ? 2 : 0}
maxSize={treeShowing || pagesShowing ? 100 : 0}
size={fState.pageMode || fState.treeMode ? 15 : 0}
minSize={fState.pageMode || fState.treeMode ? 2 : 0}
maxSize={fState.pageMode || fState.treeMode ? 100 : 0}
>
<TreeView {fState} {height}></TreeView>
</Pane>
<Pane minSize={1}>
<div>
<PrimitiveView {fState} {height}></PrimitiveView>
<div class="bg-forge-dark w-full" style="height: {height}px">
{#if fState.treeMode}
<PrimitiveView {fState} {height}></PrimitiveView>
{/if}
{#if fState.pageMode}
<div class="overflow-hidden">
<ContentsView {fState} {height}></ContentsView>
</div>
{/if}
</div>
</Pane>
{#if fState.prim?.isPage()}
<Pane minSize={1}>
<div class="overflow-hidden">
<ContentsView {fState} {height}></ContentsView>
</div>
</Pane>
{/if}
</Splitpanes>
</div>
{#if xrefTableShowing}
<div class="xref-modal" class:visible={xrefTableShowing}>
{#if fState.xRefShowing}
<div class="xref-modal" class:visible={fState.xRefShowing}>
<XRefTable {fState} {height}></XRefTable>
</div>
{/if}
{#if fState.notificationsShowing}
<div class="xref-modal" class:visible={fState.notificationsShowing}>
<NotificationModal {fState}></NotificationModal>
</div>
{/if}
</div>
<style lang="postcss">

View File

@ -21,7 +21,7 @@
footerHeight,
}: { fState: FileViewState | undefined; footerHeight: number } = $props();
let elements: Path[] | undefined = $derived(
fState && fState.prim ? toElements(fState.prim.trace) : undefined,
fState && fState.container_prim ? toElements(fState.container_prim.trace) : undefined,
);
function toElements(path: Trace[]): Path[] {

View File

@ -0,0 +1,49 @@
<script lang="ts">
import type FileViewState from "../models/FileViewState.svelte";
import {
TrashBinSolid
} from "flowbite-svelte-icons";
let {fState}: { fState: FileViewState } = $props();
let notifications = $derived(fState.notifications);
</script>
<div class="border-l border-r border-forge-sec bg-forge-sec flex flex-row" style="width: 100%; height: 24px">
<div class="text-sm p-1">
Notifications:
</div>
<button class="clear" onclick={ () => fState.clearNotifications()}>
<div class="img">
<TrashBinSolid size="sm"/>
</div>
</button>
</div>
<div class="flex flex-col">
{#each notifications as notification}
<div class="text-xs flex flex-row">
<small class="whitespace-nowrap">{notification.getDate() + ":"}</small>&nbsp <p class:error={notification.isError()} class:debug={notification.isDebug()}> {notification.level} </p> &nbsp
<p> {" - " + notification.message}</p>
</div>
<div class="bg-forge-bound mt-1 mb-1" style="height: 1px; width: 100%"></div>
{/each}
</div>
<style lang="postcss">
.error {
@apply text-red-600;
}
.clear {
@apply hover:bg-forge-acc h-[24px] w-[24px] rounded-2xl;
position: fixed;
right: 0;
top: 0;
}
.img {
position: fixed;
right: 4px;
top: 4px;
}
</style>

View File

@ -1,20 +1,19 @@
<script lang="ts">
import type FileViewState from "../models/FileViewState.svelte";
import type Primitive from "../models/Primitive.svelte";
import ContentsView from "./ContentsView.svelte";
import PrimitiveIcon from "./PrimitiveIcon.svelte";
import StreamEditor from "./StreamEditor.svelte";
import StreamDataView from "./StreamDataView.svelte";
const cellH = 29;
const headerOffset = 24;
let { fState, height }: { fState: FileViewState; height: number } =
const headerOffset = 0;
let {fState, height}: { fState: FileViewState; height: number } =
$props();
let fillerHeight: number = $state(0);
let firstEntry = $state(0);
let lastEntry = $state(100);
let scrollY = $state(0);
let prim = $derived(fState.prim);
let prim = $derived(fState.container_prim);
let entriesToDisplay: Primitive[] = $derived(
prim ? prim.children.slice(firstEntry, lastEntry) : [],
);
@ -22,19 +21,17 @@
let bodyHeight = $derived(height - headerOffset);
let editorHeight = $derived(Math.max(800, bodyHeight - tableHeight));
let imageUrl = "";
// Example: Simulating a binary Uint8Array (normally fetched from an API)
let binaryData = new Uint8Array([fState.stream_data]);
let locallySelected: Primitive | undefined = $state(undefined);
$effect(() => {
locallySelected = fState.selected_leaf_prim;
});
if (binaryData.length > 0) {
const blob = new Blob([binaryData], { type: "image/png" });
imageUrl = URL.createObjectURL(blob);
}
function handlePrimSelect(prim: Primitive) {
const _path: string[] = fState.copyPath();
_path.push(prim?.key);
fState.selectPath(_path);
function handlePrimClick(prim: Primitive) {
locallySelected = prim;
if (!prim.isContainer()) {
handlePrimDbLClick(prim);
}
}
function handleScroll(event: Event & { currentTarget: HTMLElement }) {
@ -44,70 +41,61 @@
fillerHeight = firstEntry * cellH;
}
function handlePrimDbLClick(prim: Primitive) {
const _path: string[] = fState.copyPath();
_path.push(prim.key);
fState.selectPath(_path);
}
</script>
{#if fState.stream_data}
<img src={fState.stream_data} />
{/if}
{#if prim && prim.children && prim.children.length > 0}
<div class="overflow-x-auto">
<div class="overflow-auto" onscroll={handleScroll}
style="height: {bodyHeight}px">
<div class="w-[851px]">
<table>
<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
onscroll={handleScroll}
class="overflow-y-auto"
style="height: {bodyHeight}px"
>
<div class="container" style="height: {tableHeight}px">
<table>
<tbody>
<tr class="filler" style="height: {fillerHeight}px"
></tr>
{#each entriesToDisplay as entry}
<tr
class:selected={entry.key ===
fState.highlighted_prim}
class="row"
onclick={() =>
(fState.highlighted_prim = entry.key)}
ondblclick={() => handlePrimSelect(entry)}
>
<td class="page-cell t-data">
<div class="key-field">
<PrimitiveIcon
ptype={entry.ptype}
/>
<p class="text-left">
{entry.key}
</p>
</div>
</td>
<td class="ref-cell t-data"
>{entry.ptype}</td
>
<td class="cell t-data">{entry.value}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{#if fState.stream_data}
<StreamEditor
stream_data={fState.stream_data}
height={editorHeight}
></StreamEditor>
{/if}
<div class="container" style="height: {tableHeight}px">
<table>
<tbody>
<tr class="filler" style="height: {fillerHeight}px"
></tr>
{#each entriesToDisplay as entry}
<tr
class:selected={entry.key === 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}
</tbody>
</table>
</div>
{#if fState.container_prim?.stream_data}
<StreamDataView {fState} {editorHeight}></StreamDataView>
{/if}
</div>
</div>
{/if}

View File

@ -0,0 +1,27 @@
<script lang="ts">
import type FileViewState from "../models/FileViewState.svelte";
import StreamEditor from "./StreamEditor.svelte";
import type {StreamData} from "../models/StreamData";
let {fState, editorHeight}: { fState: FileViewState, editorHeight: number } = $props()
let streamData: StreamData = $derived(fState.container_prim?.stream_data ?);
</script>
{#if streamData.type === "Image"}
<div class="w-full m-auto">
<img alt="x-object" src={streamData.data}/>
</div>
{:else}
<StreamEditor
stream_data={streamData.data}
height={editorHeight}
></StreamEditor>
{/if}
<style lang="postcss">
</style>

View File

@ -1,9 +1,15 @@
<script lang="ts">
import { invoke } from "@tauri-apps/api/core";
import type ContentModel from "../models/ContentModel.svelte";
import { onMount } from "svelte";
import * as monaco from "monaco-editor";
monaco.editor.defineTheme('forge-dark', {
base: "vs-dark",
inherit: true,
rules: [],
colors: {
"editor.background": '#1E1F22FF'
}
});
let { stream_data, height }: { stream_data: string; height: number } =
$props();
@ -14,7 +20,7 @@
editor = monaco.editor.create(editorContainer, {
value: "",
language: "plaintext",
theme: "vs-dark",
theme: "forge-dark",
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,

View File

@ -1,29 +1,31 @@
<script lang="ts">
import { BookOpenSolid, CodeBranchSolid } from "flowbite-svelte-icons";
import type FileViewState from "../models/FileViewState.svelte";
let {
tree = $bindable(false),
pages = $bindable(false),
}: { tree: boolean; pages: boolean } = $props();
fState
}: { fState: FileViewState | undefined } = $props();
function toggleTree() {
tree = !tree;
if (tree) {
pages = false;
if (!fState) return;
fState.treeMode = !fState.treeMode;
if (fState.treeMode) {
fState.pageMode = false;
}
}
function togglePages() {
pages = !pages;
if (pages) {
tree = false;
if (!fState) return;
fState.pageMode = !fState.pageMode;
if (fState.pageMode) {
fState.treeMode = false;
}
}
</script>
<div class="grid grid-cols-1">
<button
class={tree ? "tool-button active" : "tool-button"}
class={fState?.treeMode ? "tool-button active" : "tool-button"}
onclick={toggleTree}
>
<div class="justify-center flex m-0">
@ -33,7 +35,7 @@
</button>
<button
id="#page"
class={pages ? "tool-button active" : "tool-button"}
class={fState?.pageMode ? "tool-button active" : "tool-button"}
onclick={togglePages}
>
<div class="justify-center flex">

View File

@ -1,22 +1,42 @@
<script lang="ts">
import {ToolbarButton} from "flowbite-svelte";
import {ListOutline} from "flowbite-svelte-icons";
import type FileViewState from "../models/FileViewState.svelte";
let {xref = $bindable(false)}: { xref: boolean } = $props()
let {fState}: { fState: FileViewState | undefined } = $props()
function toggleXref() {
xref = !xref;
if (!fState) return;
if (fState.notificationsShowing) {
fState.notificationsShowing = false;
}
fState.xRefShowing = !fState.xRefShowing;
}
function toggleNots() {
if (!fState) return;
if (fState.xRefShowing) {
fState.xRefShowing = false;
}
fState.notificationsShowing = !fState.notificationsShowing;
}
</script>
<div class="grid grid-cols-1">
<button class={ xref ? "tool-button active" : "tool-button" } onclick={toggleXref}>
<button class={ fState?.xRefShowing ? "tool-button active" : "tool-button" } onclick={toggleXref}>
<div class="justify-center flex text-forge-text">
<ListOutline/>
</div>
<b class="button-title">XRef</b>
</button>
<button class={ fState?.notificationsShowing ? "tool-button active" : "tool-button" } onclick={toggleNots}>
<div class="justify-center flex text-forge-text">
<ListOutline/>
</div>
<b class="button-title">Notifications</b>
</button>
</div>

View File

@ -1,12 +1,9 @@
<script lang="ts">
import PrimitiveIcon from "./PrimitiveIcon.svelte";
import TreeViewState from "../models/TreeViewState.svelte";
import TreeViewRequest from "../models/TreeViewRequest.svelte";
import { PathSelectedEvent } from "../events/PathSelectedEvent";
import { CaretDownOutline, CaretRightOutline } from "flowbite-svelte-icons";
import type { PrimitiveView } from "../models/PrimitiveView";
import { onMount } from "svelte";
import {PathSelectedEvent} from "../events/PathSelectedEvent";
import type FileViewState from "../models/FileViewState.svelte";
import type {TreeViewModel} from "../models/TreeViewModel";
import TreeViewEntry from "./TreeViewEntry.svelte";
const rowHeight = 24;
let {
@ -19,40 +16,45 @@
const file_id = $derived(fState.file.id);
let scrollY = $state(0);
let scrollContainer: HTMLElement;
let fillerHeight: number = $state(0);
let states: TreeViewState[] = [];
let firstEntry = $state(0);
let lastEntry = $state(100);
let scrollY = $state(0);
let entryCount = $state(0);
let treeState: TreeViewState | undefined = $state(undefined);
let totalHeight: number = $state(100);
let entries: PrimitiveView[] | undefined = $state([]);
let entries: TreeViewModel[] = $state([]);
let stickies: TreeViewModel[] = $state([]);
$effect(() => {
console.log("Effect triggered")
treeState = states.find((state) => state.file_id === file_id);
updateTreeView();
});
async function updateTreeView() {
if (!treeState || treeState.file_id !== file_id) {
console.log("New State")
let newState = new TreeViewState(file_id, fState);
await newState.loadTreeView();
states.push(newState);
treeState = newState;
totalHeight = treeState.getEntryCount() * rowHeight;
}
firstEntry = Math.floor(scrollY / rowHeight);
lastEntry = Math.ceil((scrollY + height) / rowHeight);
entryCount = treeState?.getEntryCount();
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;
entries = treeState.getEntries(firstEntry, lastEntry);
let entriesAndStickies = treeState.getEntries(firstEntry, lastEntry);
entries = entriesAndStickies[0];
stickies = entriesAndStickies[1];
}
function handleSelect(prim: PrimitiveView) {
function handleSelect(prim: TreeViewModel) {
if (prim.expanded && prim.container) {
treeState
?.collapseTree(prim.path.map((path) => path.key))
@ -76,72 +78,42 @@
updateTreeView();
}
function formatDisplayKey(key: string) {
if (key.startsWith("Page") && !key.startsWith("Pages")) {
return key.replace("Page", "Page ");
}
return key;
}
function handleScroll(event: Event & { currentTarget: HTMLElement }) {
scrollY = event.currentTarget.scrollTop;
updateTreeView();
}
</script>
<div onscroll={handleScroll} class="overflow-auto" style="height: {height}px">
<div bind:this={scrollContainer} onscroll={handleScroll} class="overflow-auto" style="height: {height}px">
{#if entries}
<div style="height: {totalHeight}px; width: 100%">
<div
class="filler"
style="height: {fillerHeight}px; width: 100%"
<table class="border-b-2 border-forge-bound bg-forge-prim" style="position: relative; top: {scrollY}px">
<thead>
<tr>
{#each stickies as entry}
{#if entry.active}
<TreeViewEntry
{entry}
onclick={() => handleSelect(entry)}
></TreeViewEntry>
{/if}
{/each}
</tr>
</thead>
</table>
<div class="filler"
style="height: {fillerHeight}px; width: 100%"
></div>
{#each entries as entry}
{#if entry.active}
{#if entry.depth == 0}
<div
style="height: 1px; width: 100%;"
class="bg-forge-bound"
></div>
<div>
{#each entries as entry}
{#if entry.active}
<TreeViewEntry
onclick={() => handleSelect(entry)}
{entry}
></TreeViewEntry>
{/if}
<button
class="row text-sm hover:bg-forge-sec w-full group whitespace-nowrap"
style="height: {rowHeight}px;"
onclick={() => handleSelect(entry)}
>
<div style="margin-left: {entry.depth * 1.25}em">
{#if entry.container}
<div>
<span
class="caret group-hover:text-forge-text_hint"
>{#if entry.expanded}<CaretDownOutline
/>{:else}<CaretRightOutline
/>{/if}</span
>
</div>
{:else}
<span class="no-caret"></span>
{/if}
</div>
<div>
<PrimitiveIcon ptype={entry.ptype} />
</div>
<div class="pl-1 prim_name whitespace-nowrap">
{formatDisplayKey(entry.key)}
<div
class="details group-hover:text-forge-text_hint"
>
{" | " +
entry.value +
" | " +
entry.sub_type +
" | " +
entry.ptype}
</div>
</div>
</button>
{/if}
{/each}
{/each}
</div>
</div>
{/if}
</div>
@ -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 {

View File

@ -0,0 +1,93 @@
<script lang="ts">
const rowHeight = 24;
import PrimitiveIcon from "./PrimitiveIcon.svelte";
import type {TreeViewModel} from "../models/TreeViewModel";
import {CaretDownOutline, CaretRightOutline} from "flowbite-svelte-icons";
let {entry, onclick = undefined}: { entry: TreeViewModel, onclick: any } = $props();
function formatDisplayKey(key: string) {
if (key.startsWith("Page") && !key.startsWith("Pages")) {
return key.replace("Page", "Page ");
}
return key;
}
</script>
{#if entry.depth == 0}
<div
style="height: 1px; width: 100%;"
class="bg-forge-bound"
></div>
{/if}
<button
class="row text-sm hover:bg-forge-sec w-full group whitespace-nowrap"
style="height: {rowHeight}px;"
onclick={onclick}
>
<div style="margin-left: {entry.depth * 1.25}em">
{#if entry.container}
<div>
<span
class="caret group-hover:text-forge-text_hint"
>{#if entry.expanded}<CaretDownOutline
/>{:else}<CaretRightOutline
/>{/if}</span
>
</div>
{:else}
<span class="no-caret"></span>
{/if}
</div>
<div>
<PrimitiveIcon ptype={entry.ptype}/>
</div>
<div class="pl-1 prim_name whitespace-nowrap">
<p>
{formatDisplayKey(entry.key)}
</p>
<div
class="details group-hover:text-forge-text_hint"
>
{" | " +
entry.value +
" | " +
entry.sub_type +
" | " +
entry.ptype}
</div>
</div>
</button>
<style lang="postcss">
.prim_name {
display: flex;
flex-direction: row;
bottom: 0;
width: 100%;
text-align: center;
}
.caret {
@apply text-forge-sec;
cursor: pointer;
user-select: none;
}
.details {
@apply ml-1 text-forge-prim whitespace-nowrap font-extralight;
}
.row {
text-align: center;
display: flex;
flex-direction: row;
user-select: none;
}
.no-caret {
@apply pl-5;
user-select: none;
}
</style>

View File

@ -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<XRefTable>("get_xref_table", { id: this.file.id })
invoke<XRefTable>("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<Primitive>("get_prim_by_path", { id: this.file.id, path: this.formatPaths(_path) })
this.stream_data = await invoke<string>("get_stream_data", { id: this.file.id, path: this.formatPaths(newPath) })
this.prim = new Primitive(_prim);
this.path = _path;
this.highlighted_prim = "Data";
if (this.container_prim?.pathEquals(newPath)) {
return;
} else {
this.stream_data = undefined;
}
invoke<Primitive>("get_prim_by_path", { id: this.file.id, path: this.formatPaths(newPath) })
.then(result => {
let _prim = new Primitive(result)
if (_prim.isContainer()) {
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<PrimitiveModel>("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<string>("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<string>("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) {

View File

@ -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";
}
}

View File

@ -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 {

View File

@ -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,
}

View File

@ -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,
) { }
}

9
src/models/StreamData.ts Normal file
View File

@ -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;
}
}

View File

@ -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), []));
}

View File

@ -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<string, TreeViewRequest> = $state(new Map());
private activeEntries: Map<string, PrimitiveView[]> = $state(new Map());
private activeEntries: Map<string, TreeViewModel[]> = $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<PrimitiveView[]>("get_prim_tree_by_path", {
let result = await invoke<TreeViewModel[]>("get_prim_tree_by_path", {
id: this.file_id,
paths: treeViewRequests,
});

View File

@ -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<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@ -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==