diff --git a/.gitmodules b/.gitmodules index 486389c..648d724 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "apps/hub"] path = apps/hub url = https://github.com/asepharyana/asepharyana-hub-hub.git +[submodule "apps/tools"] + path = apps/tools + url = https://github.com/asepharyana/asepharyana-hub-tools.git diff --git a/apps/tools b/apps/tools new file mode 160000 index 0000000..a00ad62 --- /dev/null +++ b/apps/tools @@ -0,0 +1 @@ +Subproject commit a00ad62f6c05f51eb92d1a670a17ad2e15f5144e diff --git a/apps/tools/README.md b/apps/tools/README.md deleted file mode 100644 index 5795723..0000000 --- a/apps/tools/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# Tools — Document Scanner & Media Processing - -Self-hosted, no-install document scanner dan media processing tools yang jalan di browser. Alternatif dari CamScanner, ilovepdf, compressjpeg — tanpa upload ke pihak ketiga. - -## Tech Stack - -- **Frontend**: Next.js 16 + TypeScript + shadcn/ui + Tailwind v4 + Framer Motion -- **Backend**: Rust (Axum gateway + worker pool with Tokio) -- **Queue**: NATS JetStream (job queue + progress pub/sub) -- **Cache**: Redis (job metadata, rate limiting) -- **Image Processing**: `image` + `imageproc` crates (edge detection, warp, binarization, deskew) -- **OCR**: Tesseract via `leptess` crate (optional feature) - -## Architecture - -``` -Browser → Next.js (frontend) → Rust Gateway (Axum) → NATS Queue → Workers (Tokio+Rayon) - ↕ ↕ - Redis Temp Storage -``` - -## Directory Structure - -``` -apps/tools/ -├── frontend/ # Next.js 16 SPA -│ ├── src/ -│ │ ├── app/ # Pages + API routes -│ │ ├── components/# shadcn/ui components -│ │ └── hooks/ # Custom hooks (useJobStatus, useUpload) -│ ├── package.json -│ └── next.config.ts -├── backend/ # Rust workspace -│ ├── common/ # Shared types, errors, NATS constants -│ ├── gateway/ # Axum API server (upload, job, WS, download) -│ ├── workers/ # Processing workers (scanner, image, PDF) -│ └── wasm/ # WASM image processing (future) -├── scripts/ -│ └── entrypoint.sh -└── Dockerfile -``` - -## Development - -```bash -# Start Redis + NATS -docker compose -f infra/compose/shared.yml -f infra/compose/nats.yml up -d - -# Start Rust workers -cd apps/tools/backend -REDIS_URL=redis://localhost:6379 NATS_URL=nats://localhost:4222 cargo run --bin workers - -# Start Rust gateway (another terminal) -REDIS_URL=redis://localhost:6379 NATS_URL=nats://localhost:4222 \ - STORAGE_PATH=/tmp/tools cargo run --bin gateway - -# Start Next.js frontend (another terminal) -cd apps/tools/frontend -bun dev --port 3002 -``` - -## Build - -```bash -docker build -f infra/docker/tools.Dockerfile -t tools:latest . -``` - -## Pipeline Stages (Document Scanner) - -1. **Preprocess** — Load, resize (max 2000px), grayscale -2. **Edge Detection** — Canny with adaptive threshold + morphological close -3. **Corner Detection** — Contour analysis with fallback chain -4. **Perspective Warp** — DLT homography + bilinear interpolation -5. **Shadow Removal** — Background subtraction + CLAHE -6. **Binarization** — Sauvola local threshold (integral image accelerated) -7. **Deskew** — Hough transform line detection -8. **Enhance** — Unsharp mask + contrast adjustment -9. **OCR** — Tesseract (English + Indonesian) -10. **PDF Generation** — Searchable PDF with invisible text layer diff --git a/apps/tools/backend/Cargo.lock b/apps/tools/backend/Cargo.lock deleted file mode 100644 index 97944f6..0000000 --- a/apps/tools/backend/Cargo.lock +++ /dev/null @@ -1,3816 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "ab_glyph" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "aligned" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" -dependencies = [ - "as-slice", -] - -[[package]] -name = "aligned-vec" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" -dependencies = [ - "equator", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" - -[[package]] -name = "arc-swap" -version = "1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" -dependencies = [ - "rustversion", -] - -[[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.119", -] - -[[package]] -name = "arrayvec" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" - -[[package]] -name = "as-slice" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "async-nats" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a798aab0c0203b31d67d501e5ed1f3ac6c36a329899ce47fc93c3bea53f3ae89" -dependencies = [ - "base64", - "bytes", - "futures", - "memchr", - "nkeys", - "nuid", - "once_cell", - "pin-project", - "portable-atomic", - "rand 0.8.7", - "regex", - "ring", - "rustls-native-certs", - "rustls-pemfile", - "rustls-webpki 0.102.8", - "serde", - "serde_json", - "serde_nanos", - "serde_repr", - "thiserror 1.0.69", - "time", - "tokio", - "tokio-rustls", - "tokio-util", - "tokio-websockets", - "tracing", - "tryhard", - "url", -] - -[[package]] -name = "async-trait" -version = "0.1.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "av-scenechange" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" -dependencies = [ - "aligned", - "anyhow", - "arg_enum_proc_macro", - "arrayvec", - "log", - "num-rational", - "num-traits", - "pastey", - "rayon", - "thiserror 2.0.19", - "v_frame", - "y4m", -] - -[[package]] -name = "av1-grain" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" -dependencies = [ - "anyhow", - "arrayvec", - "log", - "nom 8.0.0", - "num-rational", - "v_frame", -] - -[[package]] -name = "avif-serialize" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "axum" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" -dependencies = [ - "axum-core", - "base64", - "bytes", - "form_urlencoded", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "multer", - "percent-encoding", - "pin-project-lite", - "serde_core", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sha1", - "sync_wrapper", - "tokio", - "tokio-tungstenite", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "backon" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" -dependencies = [ - "fastrand", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bindgen" -version = "0.64.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" -dependencies = [ - "bitflags 1.3.2", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex 1.3.0", - "syn 1.0.109", - "which", -] - -[[package]] -name = "bit_field" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" - -[[package]] -name = "bitstream-io" -version = "4.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" -dependencies = [ - "no_std_io2", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array", -] - -[[package]] -name = "built" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - -[[package]] -name = "bytemuck" -version = "1.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" - -[[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.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" -dependencies = [ - "serde", -] - -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher", -] - -[[package]] -name = "cc" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex 2.0.1", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom 7.1.3", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "futures-core", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version", - "subtle", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "data-encoding" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" - -[[package]] -name = "defmt" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" -dependencies = [ - "bitflags 1.3.2", - "defmt-macros", -] - -[[package]] -name = "defmt-macros" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" -dependencies = [ - "defmt-parser", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "defmt-parser" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" -dependencies = [ - "thiserror 2.0.19", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "serde_core", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "displaydoc" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "ecb" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a8bfa975b1aec2145850fcaa1c6fe269a16578c44705a532ae3edc92b8881c7" -dependencies = [ - "cipher", -] - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "sha2", - "signature", - "subtle", -] - -[[package]] -name = "either" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equator" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" -dependencies = [ - "equator-macro", -] - -[[package]] -name = "equator-macro" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "exr" -version = "1.74.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" -dependencies = [ - "bit_field", - "half", - "lebe", - "miniz_oxide", - "num-complex", - "pulp", - "rayon-core", - "smallvec", - "zune-inflate", -] - -[[package]] -name = "fastrand" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" - -[[package]] -name = "fax" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" - -[[package]] -name = "futures-executor" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" - -[[package]] -name = "futures-macro" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "futures-sink" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" - -[[package]] -name = "futures-task" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" - -[[package]] -name = "futures-util" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", -] - -[[package]] -name = "gif" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" -dependencies = [ - "color_quant", - "weezl", -] - -[[package]] -name = "glob" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "http" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "bytes", - "http", - "http-body", - "hyper", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "color_quant", - "exr", - "gif", - "image-webp", - "moxcms", - "num-traits", - "png", - "qoi", - "ravif", - "rayon", - "rgb", - "tiff", - "zune-core", - "zune-jpeg", -] - -[[package]] -name = "image-webp" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" -dependencies = [ - "byteorder-lite", - "quick-error", -] - -[[package]] -name = "imageproc" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602b4e8a4cc3e98372b766cd184ab532999bc0e839b7469e759511ccabc65d77" -dependencies = [ - "ab_glyph", - "approx", - "getrandom 0.2.17", - "image", - "itertools 0.12.1", - "nalgebra", - "num", - "rand 0.8.7", - "rand_distr", - "rayon", -] - -[[package]] -name = "imgref" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "block-padding", - "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.119", -] - -[[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.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jiff" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e184d09547b80eb7e20d141ba2fb1fbac843ca53f4cf1b31210adc4c1adc6e16" -dependencies = [ - "defmt", - "jiff-core", - "jiff-static", - "jiff-tzdb-platform", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", - "windows-link", -] - -[[package]] -name = "jiff-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" -dependencies = [ - "defmt", -] - -[[package]] -name = "jiff-static" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323da076b7a6faf914dc677cb05a4b907742ff7375c8322c9e7f5061e5e0e9de" -dependencies = [ - "jiff-core", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "jiff-tzdb" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" - -[[package]] -name = "jiff-tzdb-platform" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" -dependencies = [ - "jiff-tzdb", -] - -[[package]] -name = "jobserver" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" -dependencies = [ - "getrandom 0.4.3", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "lebe" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" - -[[package]] -name = "leptess" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8964e3d3270be667dda2d0026e8c77011bafaad33936011b93750489987513" -dependencies = [ - "tesseract-plumbing", - "thiserror 1.0.69", -] - -[[package]] -name = "leptonica-plumbing" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7a74c43d6f090d39158d233f326f47cd8bba545217595c93662b4e31156f42" -dependencies = [ - "leptonica-sys", - "libc", - "thiserror 1.0.69", -] - -[[package]] -name = "leptonica-sys" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da627c72b2499a8106f4dd33143843015e4a631f445d561f3481f7fba35b6151" -dependencies = [ - "bindgen", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "libfuzzer-sys" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" -dependencies = [ - "arbitrary", - "cc", -] - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "loop9" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" -dependencies = [ - "imgref", -] - -[[package]] -name = "lopdf" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fa2559e99ba0f26a12458aabc754432c805bbb8cba516c427825a997af1fb7" -dependencies = [ - "aes", - "bitflags 2.13.1", - "cbc", - "chrono", - "ecb", - "encoding_rs", - "flate2", - "indexmap", - "itoa", - "jiff", - "log", - "md-5", - "nom 8.0.0", - "nom_locate", - "rand 0.9.5", - "rangemap", - "rayon", - "sha2", - "stringprep", - "thiserror 2.0.19", - "time", - "weezl", -] - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - -[[package]] -name = "matrixmultiply" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7" -dependencies = [ - "autocfg", - "rawpointer", -] - -[[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 = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "mime" -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.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "moxcms" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "multer" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http", - "httparse", - "memchr", - "mime", - "spin", - "version_check", -] - -[[package]] -name = "nalgebra" -version = "0.32.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5c17de023a86f59ed79891b2e5d5a94c705dbe904a5b5c9c952ea6221b03e4" -dependencies = [ - "approx", - "matrixmultiply", - "nalgebra-macros", - "num-complex", - "num-rational", - "num-traits", - "simba", - "typenum", -] - -[[package]] -name = "nalgebra-macros" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "nkeys" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879011babc47a1c7fdf5a935ae3cfe94f34645ca0cac1c7f6424b36fc743d1bf" -dependencies = [ - "data-encoding", - "ed25519", - "ed25519-dalek", - "getrandom 0.2.17", - "log", - "rand 0.8.7", - "signatory", -] - -[[package]] -name = "no_std_io2" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" -dependencies = [ - "memchr", -] - -[[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 = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - -[[package]] -name = "nom_locate" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d" -dependencies = [ - "bytecount", - "memchr", - "nom 8.0.0", -] - -[[package]] -name = "noop_proc_macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "nuid" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc895af95856f929163a0aa20c26a78d26bfdc839f51b9d5aa7a5b79e52b7e83" -dependencies = [ - "rand 0.8.7", -] - -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "bytemuck", - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[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.119", -] - -[[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-iter" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" -dependencies = [ - "num-integer", - "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "owned_ttf_parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" -dependencies = [ - "ttf-parser", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pastey" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" - -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags 2.13.1", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "portable-atomic" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" - -[[package]] -name = "portable-atomic-util" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "profiling" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" -dependencies = [ - "profiling-procmacros", -] - -[[package]] -name = "profiling-procmacros" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "pulp" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" -dependencies = [ - "bytemuck", - "cfg-if", - "libm", - "num-complex", - "paste", - "pulp-wasm-simd-flag", - "raw-cpuid", - "reborrow", - "version_check", -] - -[[package]] -name = "pulp-wasm-simd-flag" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" - -[[package]] -name = "pxfm" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" - -[[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 = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_distr" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" -dependencies = [ - "num-traits", - "rand 0.8.7", -] - -[[package]] -name = "rangemap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" - -[[package]] -name = "rav1e" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" -dependencies = [ - "aligned-vec", - "arbitrary", - "arg_enum_proc_macro", - "arrayvec", - "av-scenechange", - "av1-grain", - "bitstream-io", - "built", - "cfg-if", - "interpolate_name", - "itertools 0.14.0", - "libc", - "libfuzzer-sys", - "log", - "maybe-rayon", - "new_debug_unreachable", - "noop_proc_macro", - "num-derive", - "num-traits", - "paste", - "profiling", - "rand 0.9.5", - "rand_chacha 0.9.0", - "simd_helpers", - "thiserror 2.0.19", - "v_frame", - "wasm-bindgen", -] - -[[package]] -name = "ravif" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" -dependencies = [ - "avif-serialize", - "imgref", - "loop9", - "quick-error", - "rav1e", - "rayon", - "rgb", -] - -[[package]] -name = "raw-cpuid" -version = "11.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "rawpointer" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" - -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "reborrow" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" - -[[package]] -name = "redis" -version = "0.28.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e37ec3fd44bea2ec947ba6cc7634d7999a6590aca7c35827c250bc0de502bda6" -dependencies = [ - "arc-swap", - "backon", - "bytes", - "combine", - "futures-channel", - "futures-util", - "itoa", - "num-bigint", - "percent-encoding", - "pin-project-lite", - "ryu", - "sha1_smol", - "socket2 0.5.10", - "tokio", - "tokio-util", - "url", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "regex" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "rgb" -version = "0.8.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.13.1", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustls" -version = "0.23.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki 0.103.13", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "safe_arch" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "schannel" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.13.1", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_nanos" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93142f0367a4cc53ae0fead1bcda39e85beccfad3dcd717656cacab94b12985" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] - -[[package]] -name = "serde_repr" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "signatory" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e303f8205714074f6068773f0e29527e0453937fe837c9717d066635b65f31" -dependencies = [ - "pkcs8", - "rand_core 0.6.4", - "signature", - "zeroize", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - -[[package]] -name = "simba" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae" -dependencies = [ - "approx", - "num-complex", - "num-traits", - "paste", - "wide", -] - -[[package]] -name = "simd-adler32" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" - -[[package]] -name = "simd_helpers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" -dependencies = [ - "quote", -] - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "spin" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tesseract-plumbing" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a25fbbb95169954a9262a565fbfb001c4d9dad271d48142e6632a3e2b7314b35" -dependencies = [ - "leptonica-plumbing", - "tesseract-sys", - "thiserror 1.0.69", -] - -[[package]] -name = "tesseract-sys" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd33f6f216124cfaf0fa86c2c0cdf04da39b6257bd78c5e44fa4fa98c3a5857b" -dependencies = [ - "bindgen", - "leptonica-sys", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" -dependencies = [ - "thiserror-impl 2.0.19", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "thread_local" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "tiff" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error", - "weezl", - "zune-jpeg", -] - -[[package]] -name = "time" -version = "0.3.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" - -[[package]] -name = "time-macros" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.6.5", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite", -] - -[[package]] -name = "tokio-util" -version = "0.7.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "libc", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-websockets" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591660438b3038dd04d16c938271c79e7e06260ad2ea2885a4861bfb238605d" -dependencies = [ - "base64", - "bytes", - "futures-core", - "futures-sink", - "http", - "httparse", - "rand 0.8.7", - "ring", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tokio-util", - "webpki-roots 0.26.11", -] - -[[package]] -name = "tools-common" -version = "0.1.0" -dependencies = [ - "async-nats", - "chrono", - "redis", - "serde", - "serde_json", - "thiserror 2.0.19", - "tracing", - "uuid", -] - -[[package]] -name = "tools-gateway" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-nats", - "axum", - "chrono", - "futures", - "redis", - "serde", - "serde_json", - "thiserror 2.0.19", - "tokio", - "tokio-util", - "tools-common", - "tower", - "tower-http", - "tracing", - "tracing-subscriber", - "uuid", -] - -[[package]] -name = "tools-wasm" -version = "0.1.0" -dependencies = [ - "console_error_panic_hook", - "image", - "serde", - "serde_json", - "wasm-bindgen", -] - -[[package]] -name = "tools-workers" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-nats", - "async-trait", - "chrono", - "futures", - "image", - "imageproc", - "leptess", - "lopdf", - "nalgebra", - "rayon", - "redis", - "serde", - "serde_json", - "thiserror 2.0.19", - "tokio", - "tools-common", - "tracing", - "tracing-subscriber", - "uuid", -] - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags 2.13.1", - "bytes", - "http", - "http-body", - "http-body-util", - "pin-project-lite", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-serde" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" -dependencies = [ - "serde", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "serde", - "serde_json", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", - "tracing-serde", -] - -[[package]] -name = "tryhard" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fe58ebd5edd976e0fe0f8a14d2a04b7c81ef153ea9a54eebc42e67c2c23b4e5" -dependencies = [ - "pin-project-lite", - "tokio", -] - -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" - -[[package]] -name = "tungstenite" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" -dependencies = [ - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand 0.9.5", - "sha1", - "thiserror 2.0.19", -] - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" -dependencies = [ - "getrandom 0.4.3", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "v_frame" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" -dependencies = [ - "aligned-vec", - "num-traits", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.119", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.9", -] - -[[package]] -name = "webpki-roots" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "weezl" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" - -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix", -] - -[[package]] -name = "wide" -version = "0.7.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" -dependencies = [ - "bytemuck", - "safe_arch", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "y4m" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - -[[package]] -name = "zune-core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" - -[[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.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" -dependencies = [ - "zune-core", -] diff --git a/apps/tools/backend/Cargo.toml b/apps/tools/backend/Cargo.toml deleted file mode 100644 index 49c57c9..0000000 --- a/apps/tools/backend/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[workspace] -resolver = "2" - -members = [ - "common", - "gateway", - "workers", - "wasm", -] - -default-members = [ - "common", - "gateway", - "workers", -] - -[workspace.package] -version = "0.1.0" -edition = "2021" -license = "MIT" - -[workspace.dependencies] -serde = { version = "1", features = ["derive"] } -serde_json = "1" -uuid = { version = "1", features = ["v4", "serde"] } -chrono = { version = "0.4", features = ["serde"] } -tokio = { version = "1", features = ["full"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] } -thiserror = "2" -async-nats = "0.39" -redis = { version = "0.28", features = ["tokio-comp", "connection-manager", "aio"] } -image = "0.25" -imageproc = "0.25" -lopdf = "0.36" -reqwest = { version = "0.12", features = ["json"] } -anyhow = "1" \ No newline at end of file diff --git a/apps/tools/backend/common/Cargo.toml b/apps/tools/backend/common/Cargo.toml deleted file mode 100644 index 3534d35..0000000 --- a/apps/tools/backend/common/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "tools-common" -version.workspace = true -edition.workspace = true -license.workspace = true - -[dependencies] -serde.workspace = true -serde_json.workspace = true -uuid.workspace = true -chrono.workspace = true -thiserror.workspace = true -tracing.workspace = true -async-nats.workspace = true -redis.workspace = true \ No newline at end of file diff --git a/apps/tools/backend/common/src/error.rs b/apps/tools/backend/common/src/error.rs deleted file mode 100644 index 559d2a8..0000000 --- a/apps/tools/backend/common/src/error.rs +++ /dev/null @@ -1,104 +0,0 @@ -use thiserror::Error; - -/// Errors that can occur during file upload. -#[derive(Debug, Error)] -pub enum UploadError { - #[error("Invalid MIME type: {0}")] - InvalidMime(String), - - #[error("File too large: {0} bytes exceeds maximum of {1} bytes")] - FileTooLarge(u64, u64), - - #[error("I/O error: {0}")] - Io(#[from] std::io::Error), - - #[error("Virus or suspicious content detected")] - VirusDetected, - - #[error("Invalid tool: {0}")] - InvalidTool(String), - - #[error("Missing file in upload")] - MissingFile, - - #[error("Missing tool parameter")] - MissingTool, - - #[error("Serialization error: {0}")] - Serde(#[from] serde_json::Error), -} - -/// Errors during processing pipeline execution. -#[derive(Debug, Error)] -pub enum PipelineError { - #[error("Failed to load image: {0}")] - ImageLoad(String), - - #[error("Edge detection failed: {0}")] - EdgeDetection(String), - - #[error("Corner detection failed: {0}")] - CornerDetection(String), - - #[error("Perspective warp failed: {0}")] - Warp(String), - - #[error("Shadow removal failed: {0}")] - ShadowRemoval(String), - - #[error("Binarization failed: {0}")] - Binarization(String), - - #[error("OCR processing failed: {0}")] - Ocr(String), - - #[error("PDF generation failed: {0}")] - PdfGeneration(String), - - #[error("Pipeline timed out")] - Timeout, - - #[error("Internal error: {0}")] - Internal(String), -} - -/// Errors related to NATS messaging. -#[derive(Debug, Error)] -pub enum NatsError { - #[error("Failed to publish message: {0}")] - Publish(String), - - #[error("Failed to subscribe: {0}")] - Subscribe(String), - - #[error("JetStream error: {0}")] - JetStream(String), - - #[error("Connection timeout")] - Timeout, - - #[error("NATS connection error: {0}")] - Connection(String), -} - -/// Errors related to Redis operations. -#[derive(Debug, Error)] -pub enum RedisError { - #[error("Redis connection failed: {0}")] - Connection(String), - - #[error("Redis query failed: {0}")] - Query(String), - - #[error("Serialization error: {0}")] - Serialization(#[from] serde_json::Error), - - #[error("Key not found: {0}")] - NotFound(String), -} - -impl From for RedisError { - fn from(e: redis::RedisError) -> Self { - RedisError::Query(e.to_string()) - } -} \ No newline at end of file diff --git a/apps/tools/backend/common/src/lib.rs b/apps/tools/backend/common/src/lib.rs deleted file mode 100644 index 2d0bf26..0000000 --- a/apps/tools/backend/common/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod error; -pub mod nats; -pub mod types; \ No newline at end of file diff --git a/apps/tools/backend/common/src/nats.rs b/apps/tools/backend/common/src/nats.rs deleted file mode 100644 index 1ef2dc8..0000000 --- a/apps/tools/backend/common/src/nats.rs +++ /dev/null @@ -1,104 +0,0 @@ -/// NATS subject constants for the tools service. -/// -/// Subject naming convention: -/// tools..jobs.{job_id} — Job submission queue -/// tools..progress.{job_id} — Progress update fan-out -/// tools.scheduler.cleanup — Cron-triggered cleanup - -// ── Job Subjects ── - -pub const SCAN_JOBS: &str = "tools.scan.jobs"; -pub const SCAN_PROGRESS: &str = "tools.scan.progress"; -pub const IMAGE_JOBS: &str = "tools.image.jobs"; -pub const IMAGE_PROGRESS: &str = "tools.image.progress"; -pub const PDF_JOBS: &str = "tools.pdf.jobs"; -pub const PDF_PROGRESS: &str = "tools.pdf.progress"; -pub const VIDEO_JOBS: &str = "tools.video.jobs"; -pub const VIDEO_PROGRESS: &str = "tools.video.progress"; -pub const AUDIO_JOBS: &str = "tools.audio.jobs"; -pub const AUDIO_PROGRESS: &str = "tools.audio.progress"; - -// ── Scheduler Subjects ── - -pub const SCHEDULER_CLEANUP: &str = "tools.scheduler.cleanup"; - -// ── Stream Names ── - -pub const STREAM_JOBS: &str = "tools-jobs"; -pub const STREAM_PROGRESS: &str = "tools-progress"; - -// ── Stream Configuration ── - -/// Returns the stream configuration for jobs. -/// Max age: 24h, storage: file (persistent on disk). -pub fn jobs_stream_config() -> async_nats::jetstream::stream::Config { - use async_nats::jetstream::stream::Config; - Config { - name: STREAM_JOBS.to_string(), - subjects: vec![ - "tools.scan.jobs.*".to_string(), - "tools.image.jobs.*".to_string(), - "tools.pdf.jobs.*".to_string(), - "tools.video.jobs.*".to_string(), - "tools.audio.jobs.*".to_string(), - "tools.scheduler.>".to_string(), - ], - max_age: std::time::Duration::from_secs(24 * 3600), - storage: async_nats::jetstream::stream::StorageType::File, - ..Default::default() - } -} - -/// Returns the stream configuration for progress events. -/// Max age: 1h, storage: memory (no persistence needed). -pub fn progress_stream_config() -> async_nats::jetstream::stream::Config { - use async_nats::jetstream::stream::Config; - Config { - name: STREAM_PROGRESS.to_string(), - subjects: vec![ - "tools.scan.progress.*".to_string(), - "tools.image.progress.*".to_string(), - "tools.pdf.progress.*".to_string(), - "tools.video.progress.*".to_string(), - "tools.audio.progress.*".to_string(), - ], - max_age: std::time::Duration::from_secs(3600), - storage: async_nats::jetstream::stream::StorageType::Memory, - ..Default::default() - } -} - -/// Build a job subject for a given tool and job ID. -pub fn job_subject(tool_group: &str, job_id: &str) -> String { - format!("tools.{}.jobs.{}", tool_group, job_id) -} - -/// Build a progress subject for a given tool and job ID. -pub fn progress_subject(tool_group: &str, job_id: &str) -> String { - format!("tools.{}.progress.{}", tool_group, job_id) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_subject_format() { - assert_eq!(job_subject("scan", "abc-123"), "tools.scan.jobs.abc-123"); - assert_eq!( - progress_subject("scan", "abc-123"), - "tools.scan.progress.abc-123" - ); - assert_eq!( - job_subject("image", "def-456"), - "tools.image.jobs.def-456" - ); - assert_eq!(SCHEDULER_CLEANUP, "tools.scheduler.cleanup"); - } - - #[test] - fn test_stream_names() { - assert_eq!(STREAM_JOBS, "tools-jobs"); - assert_eq!(STREAM_PROGRESS, "tools-progress"); - } -} \ No newline at end of file diff --git a/apps/tools/backend/common/src/types.rs b/apps/tools/backend/common/src/types.rs deleted file mode 100644 index 6e01d17..0000000 --- a/apps/tools/backend/common/src/types.rs +++ /dev/null @@ -1,228 +0,0 @@ -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -/// Status of a processing job. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum JobStatus { - Queued, - Processing { - stage: String, - progress: u8, - }, - Completed, - NeedsManualCrop, - Failed(String), -} - -/// Available tool types. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum Tool { - Scan, - ImageCompress, - ImageResize, - ImageConvert, - RemoveBg, - PdfMerge, - PdfSplit, - ImagesToPdf, - PdfCompress, - PdfToImages, - VideoCompress, - AudioExtract, - VideoTrim, - GifMaker, - AudioConvert, -} - -impl Tool { - /// Returns the NATS subject prefix for this tool. - pub fn subject_prefix(&self) -> &'static str { - match self { - Tool::Scan => "tools.scan", - Tool::ImageCompress - | Tool::ImageResize - | Tool::ImageConvert - | Tool::RemoveBg => "tools.image", - Tool::PdfMerge - | Tool::PdfSplit - | Tool::ImagesToPdf - | Tool::PdfCompress - | Tool::PdfToImages => "tools.pdf", - Tool::VideoCompress | Tool::VideoTrim | Tool::GifMaker => "tools.video", - Tool::AudioExtract | Tool::AudioConvert => "tools.audio", - } - } - - pub fn as_str(&self) -> &'static str { - match self { - Tool::Scan => "scan", - Tool::ImageCompress => "image-compress", - Tool::ImageResize => "image-resize", - Tool::ImageConvert => "image-convert", - Tool::RemoveBg => "remove-bg", - Tool::PdfMerge => "pdf-merge", - Tool::PdfSplit => "pdf-split", - Tool::ImagesToPdf => "images-to-pdf", - Tool::PdfCompress => "pdf-compress", - Tool::PdfToImages => "pdf-to-images", - Tool::VideoCompress => "video-compress", - Tool::AudioExtract => "audio-extract", - Tool::VideoTrim => "video-trim", - Tool::GifMaker => "gif-maker", - Tool::AudioConvert => "audio-convert", - } - } - - pub fn from_str(s: &str) -> Option { - match s { - "scan" => Some(Tool::Scan), - "image-compress" => Some(Tool::ImageCompress), - "image-resize" => Some(Tool::ImageResize), - "image-convert" => Some(Tool::ImageConvert), - "remove-bg" => Some(Tool::RemoveBg), - "pdf-merge" => Some(Tool::PdfMerge), - "pdf-split" => Some(Tool::PdfSplit), - "images-to-pdf" => Some(Tool::ImagesToPdf), - "pdf-compress" => Some(Tool::PdfCompress), - "pdf-to-images" => Some(Tool::PdfToImages), - "video-compress" => Some(Tool::VideoCompress), - "audio-extract" => Some(Tool::AudioExtract), - "video-trim" => Some(Tool::VideoTrim), - "gif-maker" => Some(Tool::GifMaker), - "audio-convert" => Some(Tool::AudioConvert), - _ => None, - } - } -} - -/// Options for document scanning. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ScanOptions { - pub ocr: bool, - pub enhance: bool, - pub output_format: OutputFormat, - pub dpi: u32, - pub quality: u8, - pub language: String, - pub color_mode: ColorMode, - pub page_size: PageSize, -} - -impl Default for ScanOptions { - fn default() -> Self { - Self { - ocr: true, - enhance: true, - output_format: OutputFormat::Pdf, - dpi: 300, - quality: 90, - language: "eng+ind".to_string(), - color_mode: ColorMode::BlackAndWhite, - page_size: PageSize::A4, - } - } -} - -/// Options for image tools. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ImageOptions { - pub quality: Option, - pub width: Option, - pub height: Option, - pub format: Option, - pub fit: Option, - pub bg_color: Option<[u8; 3]>, -} - -/// Options for PDF tools. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PdfOptions { - pub quality: Option, - pub pages: Option, - pub dpi: Option, - pub page_size: Option, - pub margin_mm: Option, -} - -/// A complete job record. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Job { - pub id: Uuid, - pub tool: Tool, - pub status: JobStatus, - pub file_path: String, - pub result_path: Option, - pub file_size: u64, - pub options: serde_json::Value, - pub created_at: DateTime, - pub ttl_seconds: u64, -} - -/// Progress update sent via NATS and forwarded via WebSocket. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct JobProgress { - pub job_id: Uuid, - pub status: JobStatus, - pub stage: String, - pub progress: u8, - pub message: String, -} - -/// Response returned after successful upload. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UploadResponse { - pub job_id: Uuid, - pub status: String, - pub tool: String, - pub ws_url: String, - pub created_at: DateTime, - pub estimated_seconds: u8, -} - -/// Job status response. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct JobStatusResponse { - pub job_id: Uuid, - pub status: String, - pub tool: String, - pub progress: u8, - pub stage: String, - pub message: String, - pub result: Option, - pub created_at: DateTime, - pub error: Option, -} - -/// Result metadata included in status response. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ResultInfo { - pub download_url: String, - pub file_size: u64, - pub file_name: String, - pub preview_url: Option, -} - -/// Output format for scan results. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum OutputFormat { - Pdf, - Jpeg, - Png, -} - -/// Color mode for processed output. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ColorMode { - BlackAndWhite, - Grayscale, - Color, -} - -/// Page size for PDF output. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum PageSize { - A4, - Letter, - Auto, -} \ No newline at end of file diff --git a/apps/tools/backend/gateway/Cargo.toml b/apps/tools/backend/gateway/Cargo.toml deleted file mode 100644 index bdc83c3..0000000 --- a/apps/tools/backend/gateway/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "tools-gateway" -version.workspace = true -edition.workspace = true -license.workspace = true - -[dependencies] -tools-common = { path = "../common" } - -axum = { version = "0.8", features = ["multipart", "ws"] } -tower = "0.5" -tower-http = { version = "0.6", features = ["cors", "trace", "limit"] } -tokio.workspace = true -tokio-util = { version = "0.7", features = ["io"] } -serde.workspace = true -serde_json.workspace = true -uuid.workspace = true -chrono.workspace = true -tracing.workspace = true -tracing-subscriber.workspace = true -async-nats.workspace = true -redis.workspace = true -thiserror.workspace = true -anyhow.workspace = true -futures = "0.3" \ No newline at end of file diff --git a/apps/tools/backend/gateway/src/config.rs b/apps/tools/backend/gateway/src/config.rs deleted file mode 100644 index d0642cb..0000000 --- a/apps/tools/backend/gateway/src/config.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::path::PathBuf; - -/// Application configuration loaded from environment variables. -#[derive(Debug, Clone)] -pub struct AppConfig { - pub port: u16, - pub nats_url: String, - pub redis_url: String, - pub storage_path: PathBuf, - pub max_file_size_mb: u64, - pub job_ttl_seconds: u64, - pub rate_limit_per_minute: u32, - pub rust_log: String, -} - -impl AppConfig { - /// Load configuration from environment variables with sensible defaults. - pub fn from_env() -> Self { - Self { - port: env_or_default("GATEWAY_PORT", "3001") - .parse() - .unwrap_or(3001), - nats_url: env_or_default("NATS_URL", "nats://localhost:4222"), - redis_url: env_or_default("REDIS_URL", "redis://localhost:6379"), - storage_path: PathBuf::from(env_or_default("STORAGE_PATH", "/data/tools")), - max_file_size_mb: env_or_default("MAX_FILE_SIZE_MB", "50") - .parse() - .unwrap_or(50), - job_ttl_seconds: env_or_default("JOB_TTL_SECONDS", "3600") - .parse() - .unwrap_or(3600), - rate_limit_per_minute: env_or_default("RATE_LIMIT_PER_MINUTE", "30") - .parse() - .unwrap_or(30), - rust_log: env_or_default("RUST_LOG", "info"), - } - } - - pub fn max_file_size_bytes(&self) -> u64 { - self.max_file_size_mb * 1024 * 1024 - } -} - -fn env_or_default(key: &str, default: &str) -> String { - std::env::var(key).unwrap_or_else(|_| default.to_string()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_default_config() { - let config = AppConfig::from_env(); - assert_eq!(config.port, 3001); - assert_eq!(config.nats_url, "nats://localhost:4222"); - assert_eq!(config.redis_url, "redis://localhost:6379"); - assert_eq!(config.max_file_size_mb, 50); - assert_eq!(config.job_ttl_seconds, 3600); - assert_eq!(config.rate_limit_per_minute, 30); - } - - #[test] - fn test_file_size_bytes() { - let config = AppConfig::from_env(); - assert_eq!(config.max_file_size_bytes(), 50 * 1024 * 1024); - } - - #[test] - fn test_env_override() { - std::env::set_var("GATEWAY_PORT", "9999"); - let config = AppConfig::from_env(); - assert_eq!(config.port, 9999); - std::env::remove_var("GATEWAY_PORT"); - } -} \ No newline at end of file diff --git a/apps/tools/backend/gateway/src/main.rs b/apps/tools/backend/gateway/src/main.rs deleted file mode 100644 index 5cca287..0000000 --- a/apps/tools/backend/gateway/src/main.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::net::SocketAddr; -use std::sync::Arc; - -use axum::{ - routing::{get, post}, - Router, -}; -use tower_http::cors::{Any, CorsLayer}; -use tower_http::limit::RequestBodyLimitLayer; -use tower_http::trace::TraceLayer; -use tracing_subscriber::EnvFilter; - -mod config; -mod metrics; -mod middleware; -mod nats; -mod redis; -mod routes; - -use config::AppConfig; -use metrics::Metrics; -use routes::health::AppState; - -#[tokio::main] -async fn main() { - // Load config - let config = AppConfig::from_env(); - - // Init logging - tracing_subscriber::fmt() - .with_env_filter(EnvFilter::new(&config.rust_log)) - .init(); - - tracing::info!("Starting tools-gateway..."); - - // Init Redis client - let redis_client = redis::create_client(&config.redis_url) - .expect("Failed to create Redis client"); - tracing::info!("Redis client created for {}", config.redis_url); - - // Init NATS connection - let nats = nats::publisher::NatsPublisher::connect(&config.nats_url) - .await - .expect("Failed to connect to NATS"); - tracing::info!("Connected to NATS at {}", config.nats_url); - - // Ensure NATS streams exist - if let Err(e) = ensure_nats_streams(&nats).await { - tracing::warn!("Failed to create NATS streams: {}", e); - } - - // Init metrics - let metrics = Metrics::new(); - - // Shared state - let state = Arc::new(AppState { - redis: redis_client, - nats, - config: config.clone(), - metrics, - }); - - // Build router - let app = Router::new() - .route("/api/upload", post(routes::upload::upload_handler)) - .route("/api/job/{id}", get(routes::job::job_status_handler)) - .route( - "/api/job/{id}/preview", - get(routes::job::job_preview_handler), - ) - .route("/api/job/{id}/ws", get(routes::ws::ws_handler)) - .route("/api/download/{id}", get(routes::download::download_handler)) - .route("/health", get(routes::health::health_handler)) - .route("/metrics", get(routes::health::metrics_handler)) - .layer(TraceLayer::new_for_http()) - .layer(CorsLayer::new().allow_origin(Any)) - .layer(RequestBodyLimitLayer::new( - ((config.max_file_size_mb + 1) * 1024 * 1024) as usize, - )) - .with_state(state); - - // Start server - let addr = SocketAddr::from(([0, 0, 0, 0], config.port)); - tracing::info!("Gateway listening on {}", addr); - - let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - axum::serve(listener, app) - .with_graceful_shutdown(shutdown_signal()) - .await - .unwrap(); -} - -/// Ensure required NATS JetStream streams exist. -async fn ensure_nats_streams( - nats: &async_nats::Client, -) -> Result<(), Box> { - let js = async_nats::jetstream::new(nats.clone()); - - match js - .get_or_create_stream(tools_common::nats::jobs_stream_config()) - .await - { - Ok(_) => tracing::info!("NATS stream 'tools-jobs' ready"), - Err(e) => tracing::warn!("Failed to create tools-jobs stream: {}", e), - } - - match js - .get_or_create_stream(tools_common::nats::progress_stream_config()) - .await - { - Ok(_) => tracing::info!("NATS stream 'tools-progress' ready"), - Err(e) => tracing::warn!("Failed to create tools-progress stream: {}", e), - } - - Ok(()) -} - -/// Handle graceful shutdown on SIGINT/SIGTERM. -async fn shutdown_signal() { - let ctrl_c = async { - tokio::signal::ctrl_c() - .await - .expect("Failed to install Ctrl+C handler"); - }; - - #[cfg(unix)] - let terminate = async { - tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) - .expect("Failed to install SIGTERM handler") - .recv() - .await; - }; - - #[cfg(not(unix))] - let terminate = std::future::pending::<()>(); - - tokio::select! { - _ = ctrl_c => {}, - _ = terminate => {}, - } - - tracing::info!("Shutting down gateway..."); -} \ No newline at end of file diff --git a/apps/tools/backend/gateway/src/metrics.rs b/apps/tools/backend/gateway/src/metrics.rs deleted file mode 100644 index d49e866..0000000 --- a/apps/tools/backend/gateway/src/metrics.rs +++ /dev/null @@ -1,181 +0,0 @@ -use std::collections::HashMap; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Mutex; - -/// Simple Prometheus metrics collector. -pub struct Metrics { - /// Counter: tools_jobs_total{tool, status} - jobs_total: Mutex>, - /// Counter: tools_uploaded_files_total{tool, status} - uploaded_files_total: Mutex>, - /// Histogram buckets for processing duration (ms) - duration_buckets: Vec, - /// Histogram: tools_processing_duration_ms{tool} - duration_histogram: Mutex>>, - /// Gauge: tools_queue_depth{tool} - queue_depth: Mutex>, - /// Counter: tools_rate_limit_hits{tool} - rate_limit_hits: Mutex>, - /// Counter: cleanup deleted files - cleanup_deleted_files: AtomicU64, -} - -impl Metrics { - pub fn new() -> Self { - Self { - jobs_total: Mutex::new(HashMap::new()), - uploaded_files_total: Mutex::new(HashMap::new()), - duration_buckets: vec![ - 100.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16000.0, 32000.0, - ], - duration_histogram: Mutex::new(HashMap::new()), - queue_depth: Mutex::new(HashMap::new()), - rate_limit_hits: Mutex::new(HashMap::new()), - cleanup_deleted_files: AtomicU64::new(0), - } - } - - pub fn increment_jobs_total(&self, tool: &str, status: &str) { - if let Ok(mut map) = self.jobs_total.lock() { - map.entry((tool.to_string(), status.to_string())) - .or_insert_with(|| AtomicU64::new(0)) - .fetch_add(1, Ordering::Relaxed); - } - } - - pub fn increment_uploaded_files(&self, tool: &str, status: &str) { - if let Ok(mut map) = self.uploaded_files_total.lock() { - map.entry((tool.to_string(), status.to_string())) - .or_insert_with(|| AtomicU64::new(0)) - .fetch_add(1, Ordering::Relaxed); - } - } - - #[allow(unused)] - pub fn record_duration(&self, tool: &str, duration_ms: f64) { - if let Ok(mut map) = self.duration_histogram.lock() { - let entry = map - .entry(tool.to_string()) - .or_insert_with(|| { - (0..self.duration_buckets.len()) - .map(|_| AtomicU64::new(0)) - .collect() - }); - for (i, bucket) in self.duration_buckets.iter().enumerate() { - if duration_ms <= *bucket { - if let Some(b) = entry.get(i) { - b.fetch_add(1, Ordering::Relaxed); - } - } - } - } - } - - pub fn set_queue_depth(&self, tool: &str, depth: u64) { - if let Ok(mut map) = self.queue_depth.lock() { - map.entry(tool.to_string()) - .or_insert_with(|| AtomicU64::new(0)) - .store(depth, Ordering::Relaxed); - } - } - - #[allow(unused)] - pub fn increment_rate_limit_hits(&self, tool: &str) { - if let Ok(mut map) = self.rate_limit_hits.lock() { - map.entry(tool.to_string()) - .or_insert_with(|| AtomicU64::new(0)) - .fetch_add(1, Ordering::Relaxed); - } - } - - #[allow(unused)] - pub fn increment_cleanup_deleted(&self) { - self.cleanup_deleted_files.fetch_add(1, Ordering::Relaxed); - } - - /// Format all metrics as Prometheus text format. - pub fn format(&self) -> String { - let mut output = String::new(); - - output.push_str("# HELP tools_jobs_total Total jobs processed\n"); - output.push_str("# TYPE tools_jobs_total counter\n"); - if let Ok(map) = self.jobs_total.lock() { - for ((tool, status), count) in map.iter() { - let val = count.load(Ordering::Relaxed); - output.push_str(&format!( - "tools_jobs_total{{tool=\"{}\",status=\"{}\"}} {}\n", - tool, status, val - )); - } - } - - output.push_str("# HELP tools_uploaded_files_total Total uploaded files\n"); - output.push_str("# TYPE tools_uploaded_files_total counter\n"); - if let Ok(map) = self.uploaded_files_total.lock() { - for ((tool, status), count) in map.iter() { - let val = count.load(Ordering::Relaxed); - output.push_str(&format!( - "tools_uploaded_files_total{{tool=\"{}\",status=\"{}\"}} {}\n", - tool, status, val - )); - } - } - - output.push_str("# HELP tools_processing_duration_ms Processing duration histogram\n"); - output.push_str("# TYPE tools_processing_duration_ms histogram\n"); - if let Ok(map) = self.duration_histogram.lock() { - for (tool, buckets) in map.iter() { - for (i, bucket) in self.duration_buckets.iter().enumerate() { - if let Some(b) = buckets.get(i) { - let val = b.load(Ordering::Relaxed); - if val > 0 { - output.push_str(&format!( - "tools_processing_duration_ms_bucket{{tool=\"{}\",le=\"{}\"}} {}\n", - tool, bucket, val - )); - } - } - } - } - } - - output.push_str("# HELP tools_queue_depth Current queue depth\n"); - output.push_str("# TYPE tools_queue_depth gauge\n"); - if let Ok(map) = self.queue_depth.lock() { - for (tool, depth) in map.iter() { - let val = depth.load(Ordering::Relaxed); - output.push_str(&format!( - "tools_queue_depth{{tool=\"{}\"}} {}\n", - tool, val - )); - } - } - - output.push_str("# HELP tools_rate_limit_hits Total rate limit violations\n"); - output.push_str("# TYPE tools_rate_limit_hits counter\n"); - if let Ok(map) = self.rate_limit_hits.lock() { - for (tool, count) in map.iter() { - let val = count.load(Ordering::Relaxed); - output.push_str(&format!( - "tools_rate_limit_hits{{tool=\"{}\"}} {}\n", - tool, val - )); - } - } - - output.push_str("# HELP tools_cleanup_deleted_files Total files deleted by cleanup\n"); - output.push_str("# TYPE tools_cleanup_deleted_files counter\n"); - output.push_str(&format!( - "tools_cleanup_deleted_files {}\n", - self.cleanup_deleted_files.load(Ordering::Relaxed) - )); - - output - } -} - -impl Default for Metrics { - fn default() -> Self { - Self::new() - } -} \ No newline at end of file diff --git a/apps/tools/backend/gateway/src/middleware/error_handler.rs b/apps/tools/backend/gateway/src/middleware/error_handler.rs deleted file mode 100644 index 01cc712..0000000 --- a/apps/tools/backend/gateway/src/middleware/error_handler.rs +++ /dev/null @@ -1,66 +0,0 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, - Json, -}; -use serde_json::json; - -/// Unified JSON error response format. -#[derive(Debug)] -pub struct AppError { - pub status_code: StatusCode, - pub code: String, - pub message: String, -} - -impl AppError { - pub fn bad_request(message: impl Into) -> Self { - Self { - status_code: StatusCode::BAD_REQUEST, - code: "bad_request".to_string(), - message: message.into(), - } - } - - pub fn not_found(message: impl Into) -> Self { - Self { - status_code: StatusCode::NOT_FOUND, - code: "not_found".to_string(), - message: message.into(), - } - } - - pub fn too_large(message: impl Into) -> Self { - Self { - status_code: StatusCode::PAYLOAD_TOO_LARGE, - code: "file_too_large".to_string(), - message: message.into(), - } - } - - pub fn rate_limited(retry_after: u64) -> Self { - Self { - status_code: StatusCode::TOO_MANY_REQUESTS, - code: "rate_limit_exceeded".to_string(), - message: format!("Rate limit exceeded. Retry after {} seconds", retry_after), - } - } - - pub fn internal(message: impl Into) -> Self { - Self { - status_code: StatusCode::INTERNAL_SERVER_ERROR, - code: "internal_error".to_string(), - message: message.into(), - } - } -} - -impl IntoResponse for AppError { - fn into_response(self) -> Response { - let body = json!({ - "error": self.message, - "code": self.code, - }); - (self.status_code, Json(body)).into_response() - } -} \ No newline at end of file diff --git a/apps/tools/backend/gateway/src/middleware/mod.rs b/apps/tools/backend/gateway/src/middleware/mod.rs deleted file mode 100644 index b96207f..0000000 --- a/apps/tools/backend/gateway/src/middleware/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod error_handler; -pub mod request_id; \ No newline at end of file diff --git a/apps/tools/backend/gateway/src/middleware/request_id.rs b/apps/tools/backend/gateway/src/middleware/request_id.rs deleted file mode 100644 index 585ed5e..0000000 --- a/apps/tools/backend/gateway/src/middleware/request_id.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::future::Future; -use std::pin::Pin; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::task::{Context, Poll}; - -use axum::{extract::Request, response::Response}; -use tower::{Layer, Service}; -use uuid::Uuid; - -/// Middleware that adds a unique X-Request-Id header to every request. -#[derive(Clone, Default)] -pub struct RequestIdLayer; - -impl Layer for RequestIdLayer { - type Service = RequestIdMiddleware; - - fn layer(&self, inner: S) -> Self::Service { - RequestIdMiddleware { - inner, - counter: AtomicU64::new(0), - } - } -} - -pub struct RequestIdMiddleware { - inner: S, - counter: AtomicU64, -} - -impl Clone for RequestIdMiddleware { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - counter: AtomicU64::new(self.counter.load(Ordering::Relaxed)), - } - } -} - -impl Service> for RequestIdMiddleware -where - S: Service, Response = Response>, - S::Future: Send + 'static, - S::Error: 'static, - ReqBody: Send + 'static, - ResBody: Default + Send + 'static, -{ - type Response = Response; - type Error = S::Error; - type Future = Pin> + Send>>; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx) - } - - fn call(&mut self, req: Request) -> Self::Future { - let request_id = Uuid::new_v4().to_string(); - let (mut parts, body) = req.into_parts(); - parts - .headers - .insert("x-request-id", request_id.parse().unwrap()); - - let req = Request::from_parts(parts, body); - let fut = self.inner.call(req); - - Box::pin(async move { - let mut response: Response = fut.await?; - response - .headers_mut() - .insert("x-request-id", request_id.parse().unwrap()); - Ok(response) - }) - } -} \ No newline at end of file diff --git a/apps/tools/backend/gateway/src/nats/mod.rs b/apps/tools/backend/gateway/src/nats/mod.rs deleted file mode 100644 index 21e3f07..0000000 --- a/apps/tools/backend/gateway/src/nats/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod publisher; \ No newline at end of file diff --git a/apps/tools/backend/gateway/src/nats/publisher.rs b/apps/tools/backend/gateway/src/nats/publisher.rs deleted file mode 100644 index 36ee1f8..0000000 --- a/apps/tools/backend/gateway/src/nats/publisher.rs +++ /dev/null @@ -1,64 +0,0 @@ -use async_nats::Client; -use tools_common::error::NatsError; -use tools_common::nats; -use tools_common::types::{Job, JobProgress, Tool}; - -/// NATS publisher for job and progress messages. -pub struct NatsPublisher; - -impl NatsPublisher { - /// Connect to NATS server. - pub async fn connect(url: &str) -> Result { - async_nats::connect(url) - .await - .map_err(|e| NatsError::Connection(e.to_string())) - } - - /// Publish a job to the appropriate NATS subject. - pub async fn publish_job(nats: &Client, tool: &Tool, job: &Job) -> Result<(), NatsError> { - let prefix = tool.subject_prefix(); - let subject = nats::job_subject(prefix, &job.id.to_string()); - let payload = serde_json::to_vec(job) - .map_err(|e| NatsError::Publish(e.to_string()))?; - - nats.publish(subject, payload.into()) - .await - .map_err(|e| NatsError::Publish(e.to_string()))?; - - tracing::debug!( - job_id = %job.id, - tool = %tool.as_str(), - "Published job to NATS" - ); - - Ok(()) - } - - /// Publish a progress update to the NATS progress subject. - pub async fn publish_progress( - nats: &Client, - progress: &JobProgress, - ) -> Result<(), NatsError> { - let tool_prefix = ""; // We need the tool from somewhere — stored in progress - let subject = format!("tools.*.progress.{}", progress.job_id); - let payload = serde_json::to_vec(progress) - .map_err(|e| NatsError::Publish(e.to_string()))?; - - nats.publish(subject, payload.into()) - .await - .map_err(|e| NatsError::Publish(e.to_string()))?; - - Ok(()) - } - - /// Subscribe to NATS progress updates for a specific job. - pub async fn subscribe_progress( - nats: &Client, - job_id: &str, - ) -> Result { - let subject = format!("tools.*.progress.{}", job_id); - nats.subscribe(subject) - .await - .map_err(|e| NatsError::Subscribe(e.to_string())) - } -} \ No newline at end of file diff --git a/apps/tools/backend/gateway/src/redis/job.rs b/apps/tools/backend/gateway/src/redis/job.rs deleted file mode 100644 index d1c3879..0000000 --- a/apps/tools/backend/gateway/src/redis/job.rs +++ /dev/null @@ -1,78 +0,0 @@ -use redis::{AsyncCommands, RedisError}; -use uuid::Uuid; - -use tools_common::types::Job; - -/// Repository for job CRUD operations on Redis. -pub struct JobRepository; - -impl JobRepository { - /// Create a new job record in Redis with TTL. - pub async fn create( - conn: &mut impl AsyncCommands, - job: &Job, - ) -> Result<(), Box> { - let key = format!("job:{}", job.id); - let json = serde_json::to_string(job)?; - let _: () = conn - .set_ex(key, json, job.ttl_seconds) - .await - .map_err(|e: RedisError| -> Box { Box::new(e) })?; - Ok(()) - } - - /// Get a job by ID from Redis. - pub async fn get( - conn: &mut impl AsyncCommands, - job_id: Uuid, - ) -> Result> { - let key = format!("job:{}", job_id); - let json: String = conn.get(&key).await.map_err(|_| { - Box::new(std::io::Error::new( - std::io::ErrorKind::NotFound, - format!("Job {} not found", job_id), - )) as Box - })?; - let job: Job = serde_json::from_str(&json)?; - Ok(job) - } - - /// Update the status of a job in Redis and refresh TTL. - pub async fn update_status( - conn: &mut impl AsyncCommands, - job_id: Uuid, - status: &tools_common::types::JobStatus, - result_path: Option, - ttl_seconds: u64, - ) -> Result<(), Box> { - let key = format!("job:{}", job_id); - let json: String = conn - .get(&key) - .await - .map_err(|e: RedisError| -> Box { Box::new(e) })?; - let mut job: Job = serde_json::from_str(&json)?; - job.status = status.clone(); - if let Some(path) = result_path { - job.result_path = Some(path); - } - let json = serde_json::to_string(&job)?; - let _: () = conn - .set_ex(key, json, ttl_seconds) - .await - .map_err(|e: RedisError| -> Box { Box::new(e) })?; - Ok(()) - } - - /// Delete a job from Redis. - pub async fn delete( - conn: &mut impl AsyncCommands, - job_id: Uuid, - ) -> Result<(), Box> { - let key = format!("job:{}", job_id); - let _: usize = conn - .del(key) - .await - .map_err(|e: RedisError| -> Box { Box::new(e) })?; - Ok(()) - } -} \ No newline at end of file diff --git a/apps/tools/backend/gateway/src/redis/mod.rs b/apps/tools/backend/gateway/src/redis/mod.rs deleted file mode 100644 index 0005372..0000000 --- a/apps/tools/backend/gateway/src/redis/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod job; -pub mod ratelimit; - -use redis::Client; - -/// Create a Redis client. -pub fn create_client(url: &str) -> Result { - Client::open(url) -} \ No newline at end of file diff --git a/apps/tools/backend/gateway/src/redis/ratelimit.rs b/apps/tools/backend/gateway/src/redis/ratelimit.rs deleted file mode 100644 index c770a79..0000000 --- a/apps/tools/backend/gateway/src/redis/ratelimit.rs +++ /dev/null @@ -1,50 +0,0 @@ -use redis::AsyncCommands; - -/// Sliding window rate limiter using Redis sorted sets. -pub struct RateLimiter; - -impl RateLimiter { - /// Check if a request is within the rate limit. - pub async fn check( - conn: &mut impl AsyncCommands, - ip: &str, - tool: &str, - max_per_minute: u32, - ) -> Result> { - let key = format!("ratelimit:{}:{}", ip, tool); - let now = chrono::Utc::now().timestamp_millis(); - let window_start = now - 60_000; - - // Remove entries outside the window - let _: usize = conn.zrembyscore(&key, 0, window_start).await?; - - // Add current entry - let _: usize = conn - .zadd(&key, format!("{}:{}", ip, now), now as f64) - .await?; - - // Set TTL on the key (cleanup) - let _: usize = conn.expire(&key, 120).await?; - - // Count entries in window - let count: u32 = conn.zcount(&key, window_start, now).await?; - - Ok(count <= max_per_minute) - } - - /// Get remaining requests within the current window. - pub async fn remaining( - conn: &mut impl AsyncCommands, - ip: &str, - tool: &str, - max_per_minute: u32, - ) -> Result> { - let key = format!("ratelimit:{}:{}", ip, tool); - let now = chrono::Utc::now().timestamp_millis(); - let window_start = now - 60_000; - - let count: u32 = conn.zcount(&key, window_start, now).await?; - - Ok(max_per_minute.saturating_sub(count)) - } -} \ No newline at end of file diff --git a/apps/tools/backend/gateway/src/routes/download.rs b/apps/tools/backend/gateway/src/routes/download.rs deleted file mode 100644 index 5a7d14a..0000000 --- a/apps/tools/backend/gateway/src/routes/download.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::sync::Arc; - -use axum::{ - extract::{Path, State}, - http::{header, StatusCode}, - response::{IntoResponse, Response}, -}; -use tokio_util::io::ReaderStream; -use uuid::Uuid; - -use crate::routes::health::AppState; -use tools_common::types::JobStatus; - -/// Handle GET /api/download/{id} -pub async fn download_handler( - State(state): State>, - Path(id): Path, -) -> Result { - let mut conn = state.redis.get_multiplexed_async_connection().await.map_err(|_| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - JsonResponse(serde_json::json!({ "error": "Redis connection failed" })), - ) - })?; - - let job = crate::redis::job::JobRepository::get(&mut conn, id) - .await - .map_err(|_| { - ( - StatusCode::NOT_FOUND, - JsonResponse(serde_json::json!({ "error": "Job not found or expired" })), - ) - })?; - - // Verify job is completed - if job.status != JobStatus::Completed { - return Err(( - StatusCode::BAD_REQUEST, - JsonResponse(serde_json::json!({ - "error": "Job is not completed yet", - "status": match job.status { - JobStatus::Queued => "queued", - JobStatus::Processing { .. } => "processing", - JobStatus::NeedsManualCrop => "needs_manual_crop", - JobStatus::Failed(_) => "failed", - _ => "unknown", - } - })), - )); - } - - let result_path = job.result_path.ok_or_else(|| { - ( - StatusCode::NOT_FOUND, - JsonResponse(serde_json::json!({ "error": "Result file path not found" })), - ) - })?; - - // Open file - let file = tokio::fs::File::open(&result_path).await.map_err(|e| { - ( - StatusCode::NOT_FOUND, - JsonResponse(serde_json::json!({ "error": format!("File not found: {}", e) })), - ) - })?; - - let metadata = file.metadata().await.map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - JsonResponse(serde_json::json!({ - "error": format!("Failed to read metadata: {}", e) - })), - ) - })?; - - // Determine content type - let ext = result_path.rsplit('.').next().unwrap_or("bin").to_string(); - let content_type = match ext.as_str() { - "pdf" => "application/pdf", - "jpg" | "jpeg" => "image/jpeg", - "png" => "image/png", - "webp" => "image/webp", - "mp4" => "video/mp4", - "mp3" => "audio/mpeg", - "zip" => "application/zip", - _ => "application/octet-stream", - }; - - // Generate filename for download - let file_name = format!( - "{}_{}.{}", - job.tool.as_str(), - job.id.to_string().split('-').next().unwrap_or("result"), - ext - ); - - // Stream the file - let stream = ReaderStream::new(file); - let body = axum::body::Body::from_stream(stream); - - let response = Response::builder() - .header(header::CONTENT_TYPE, content_type) - .header( - header::CONTENT_DISPOSITION, - format!("attachment; filename=\"{}\"", file_name), - ) - .header(header::CONTENT_LENGTH, metadata.len().to_string()) - .body(body) - .unwrap(); - - Ok(response) -} - -/// Wrapper for JSON error responses. -pub struct JsonResponse(pub serde_json::Value); - -impl IntoResponse for JsonResponse { - fn into_response(self) -> Response { - (StatusCode::OK, axum::Json(self.0)).into_response() - } -} \ No newline at end of file diff --git a/apps/tools/backend/gateway/src/routes/health.rs b/apps/tools/backend/gateway/src/routes/health.rs deleted file mode 100644 index 39d6983..0000000 --- a/apps/tools/backend/gateway/src/routes/health.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::sync::Arc; - -use axum::{extract::State, Json}; -use redis::AsyncCommands; -use serde::Serialize; - -use crate::metrics::Metrics; - -/// Shared application state accessible from all handlers. -pub struct AppState { - pub redis: redis::Client, - pub nats: async_nats::Client, - pub config: crate::config::AppConfig, - pub metrics: Metrics, -} - -/// Health check response. -#[derive(Serialize)] -pub struct HealthResponse { - pub status: String, - pub version: String, - pub redis: String, - pub nats: String, - pub uptime_seconds: u64, -} - -/// Handle GET /health -pub async fn health_handler( - State(state): State>, -) -> Json { - let redis_status = { - match state.redis.get_multiplexed_async_connection().await { - Ok(mut conn) => match redis::cmd("PING").query_async::(&mut conn).await { - Ok(_) => "connected".to_string(), - Err(_) => "error".to_string(), - }, - Err(_) => "disconnected".to_string(), - } - }; - - let nats_status = if state - .nats - .publish("tools.health.check", b"ping".to_vec().into()) - .await - .is_ok() - { - "connected".to_string() - } else { - "disconnected".to_string() - }; - - Json(HealthResponse { - status: "ok".to_string(), - version: env!("CARGO_PKG_VERSION").to_string(), - redis: redis_status, - nats: nats_status, - uptime_seconds: std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0), - }) -} - -/// Handle GET /metrics -pub async fn metrics_handler( - State(state): State>, -) -> Result)> { - Ok(state.metrics.format()) -} \ No newline at end of file diff --git a/apps/tools/backend/gateway/src/routes/job.rs b/apps/tools/backend/gateway/src/routes/job.rs deleted file mode 100644 index 302455b..0000000 --- a/apps/tools/backend/gateway/src/routes/job.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::sync::Arc; - -use axum::{ - extract::{Path, State}, - http::StatusCode, - Json, -}; -use uuid::Uuid; - -use crate::routes::health::AppState; -use tools_common::types::*; - -/// Handle GET /api/job/{id} -pub async fn job_status_handler( - State(state): State>, - Path(id): Path, -) -> Result, (StatusCode, Json)> { - let mut conn = state.redis.get_multiplexed_async_connection().await.map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": format!("Redis error: {}", e) })), - ) - })?; - - let job = crate::redis::job::JobRepository::get(&mut conn, id) - .await - .map_err(|_| { - ( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": "Job not found or expired" })), - ) - })?; - - let status_str = match &job.status { - JobStatus::Queued => "queued", - JobStatus::Processing { .. } => "processing", - JobStatus::Completed => "completed", - JobStatus::NeedsManualCrop => "needs_manual_crop", - JobStatus::Failed(_) => "failed", - }; - - let (progress, stage, message) = match &job.status { - JobStatus::Processing { stage, progress } => (*progress, stage.clone(), String::new()), - JobStatus::Failed(msg) => (0, String::new(), msg.clone()), - JobStatus::Completed => (100, "complete".to_string(), "Processing complete".to_string()), - JobStatus::Queued => (0, "queued".to_string(), "Waiting in queue".to_string()), - JobStatus::NeedsManualCrop => { - (0, "manual_crop".to_string(), "Manual crop needed".to_string()) - } - }; - - let result = if job.status == JobStatus::Completed { - let file_name = job - .result_path - .as_ref() - .and_then(|p| std::path::Path::new(p).file_name()) - .and_then(|n| n.to_str()) - .unwrap_or("result") - .to_string(); - Some(ResultInfo { - download_url: format!("/api/download/{}", job.id), - file_size: job.file_size, - file_name, - preview_url: Some(format!("/api/job/{}/preview", job.id)), - }) - } else { - None - }; - - let error = match &job.status { - JobStatus::Failed(msg) => Some(msg.clone()), - _ => None, - }; - - Ok(Json(JobStatusResponse { - job_id: job.id, - status: status_str.to_string(), - tool: job.tool.as_str().to_string(), - progress, - stage, - message, - result, - created_at: job.created_at, - error, - })) -} - -/// Handle GET /api/job/{id}/preview -pub async fn job_preview_handler( - State(state): State>, - Path(id): Path, -) -> Result<(StatusCode, [(String, String); 2], Vec), (StatusCode, Json)> { - let mut conn = state.redis.get_multiplexed_async_connection().await.map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": format!("Redis error: {}", e) })), - ) - })?; - - let job = crate::redis::job::JobRepository::get(&mut conn, id) - .await - .map_err(|_| { - ( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": "Job not found or expired" })), - ) - })?; - - let result_path = job.result_path.ok_or_else(|| { - ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "No result available yet" })), - ) - })?; - - let data = tokio::fs::read(&result_path).await.map_err(|e| { - ( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": format!("File not found: {}", e) })), - ) - })?; - - let ext = result_path.rsplit('.').next().unwrap_or("bin").to_string(); - let content_type = match ext.as_str() { - "jpg" | "jpeg" => "image/jpeg", - "png" => "image/png", - "webp" => "image/webp", - "pdf" => "application/pdf", - _ => "application/octet-stream", - }; - - Ok(( - StatusCode::OK, - [ - ("Content-Type".to_string(), content_type.to_string()), - ( - "Cache-Control".to_string(), - "private, max-age=300".to_string(), - ), - ], - data, - )) -} \ No newline at end of file diff --git a/apps/tools/backend/gateway/src/routes/mod.rs b/apps/tools/backend/gateway/src/routes/mod.rs deleted file mode 100644 index e6c8dc9..0000000 --- a/apps/tools/backend/gateway/src/routes/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod download; -pub mod health; -pub mod job; -pub mod upload; -pub mod ws; \ No newline at end of file diff --git a/apps/tools/backend/gateway/src/routes/upload.rs b/apps/tools/backend/gateway/src/routes/upload.rs deleted file mode 100644 index 40c9674..0000000 --- a/apps/tools/backend/gateway/src/routes/upload.rs +++ /dev/null @@ -1,261 +0,0 @@ -use std::sync::Arc; - -use axum::{ - extract::{Multipart, State}, - http::StatusCode, - Json, -}; -use chrono::Utc; -use tokio::fs; -use uuid::Uuid; - -use crate::routes::health::AppState; -use tools_common::error::UploadError; -use tools_common::types::*; - -/// Handle POST /api/upload -pub async fn upload_handler( - State(state): State>, - mut multipart: Multipart, -) -> Result, (StatusCode, Json)> { - let config = &state.config; - let max_size = config.max_file_size_bytes(); - - // Extract fields from multipart - let mut file_data: Option<(String, Vec)> = None; - let mut tool_str: Option = None; - let mut options: serde_json::Value = serde_json::Value::Null; - - while let Ok(Some(field)) = multipart.next_field().await { - let name = field.name().unwrap_or("").to_string(); - match name.as_str() { - "file" => { - let filename = field.file_name().unwrap_or("unknown").to_string(); - let data = field.bytes().await.map_err(|e| { - ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "error": "Failed to read file", - "detail": e.to_string() - })), - ) - })?; - file_data = Some((filename, data.to_vec())); - } - "tool" => { - tool_str = Some(field.text().await.unwrap_or_default()); - } - "options" => { - let text = field.text().await.unwrap_or_default(); - if !text.is_empty() { - options = serde_json::from_str(&text).unwrap_or(serde_json::Value::Null); - } - } - _ => {} - } - } - - // Validate fields - let (filename, data) = file_data.ok_or_else(|| { - ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "No file provided" })), - ) - })?; - - let tool_str = tool_str.ok_or_else(|| { - ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "No tool specified" })), - ) - })?; - - let tool = Tool::from_str(&tool_str).ok_or_else(|| { - ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": format!("Unknown tool: {}", tool_str) })), - ) - })?; - - // Validate file size - let file_size = data.len() as u64; - if file_size > max_size { - return Err(( - StatusCode::PAYLOAD_TOO_LARGE, - Json(serde_json::json!({ - "error": format!("File too large: {} bytes (max {} bytes)", file_size, max_size) - })), - )); - } - - // Validate MIME type based on tool - let ext = filename.rsplit('.').next().unwrap_or("").to_lowercase(); - validate_mime(&tool, &ext).map_err(|e| { - ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": e.to_string() })), - ) - })?; - - // Verify magic bytes - if !verify_magic_bytes(&data, &ext) { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "File content does not match extension" })), - )); - } - - // Create directories - let upload_dir = config.storage_path.join("upload"); - fs::create_dir_all(&upload_dir).await.map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": format!("Storage error: {}", e) })), - ) - })?; - - // Generate job ID and save file - let job_id = Uuid::new_v4(); - let storage_filename = format!("{}.{}", job_id, ext); - let file_path = upload_dir.join(&storage_filename); - fs::write(&file_path, &data).await.map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": format!("Failed to save file: {}", e) })), - ) - })?; - - // Create job record - let job = Job { - id: job_id, - tool: tool.clone(), - status: JobStatus::Queued, - file_path: file_path.to_string_lossy().to_string(), - result_path: None, - file_size, - options: options.clone(), - created_at: Utc::now(), - ttl_seconds: config.job_ttl_seconds, - }; - - // Save to Redis - { - let mut conn = state.redis.get_multiplexed_async_connection().await.map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": format!("Redis error: {}", e) })), - ) - })?; - crate::redis::job::JobRepository::create(&mut conn, &job) - .await - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": format!("Failed to create job: {}", e) })), - ) - })?; - } - - // Publish to NATS - crate::nats::publisher::NatsPublisher::publish_job(&state.nats, &tool, &job) - .await - .map_err(|e| { - tracing::error!("Failed to publish job to NATS: {}", e); - }); - - // Update metrics - state.metrics.increment_jobs_total(tool.as_str(), "queued"); - - // Return response - Ok(Json(UploadResponse { - job_id, - status: "queued".to_string(), - tool: tool_str, - ws_url: format!("/api/job/{}/ws", job_id), - created_at: job.created_at, - estimated_seconds: match tool { - Tool::Scan => 5, - _ => 3, - }, - })) -} - -fn validate_mime(tool: &Tool, ext: &str) -> Result<(), UploadError> { - let image_exts = ["jpg", "jpeg", "png", "webp", "heic", "bmp", "tiff", "tif"]; - let pdf_exts = ["pdf"]; - let video_exts = ["mp4", "webm", "avi", "mov", "mkv"]; - let audio_exts = ["mp3", "wav", "flac", "aac", "ogg", "m4a"]; - - match tool { - Tool::Scan - | Tool::ImageCompress - | Tool::ImageResize - | Tool::ImageConvert - | Tool::RemoveBg => { - if !image_exts.contains(&ext) { - return Err(UploadError::InvalidMime(format!( - "Expected image file, got .{}", - ext - ))); - } - } - Tool::PdfMerge | Tool::PdfSplit | Tool::PdfCompress | Tool::PdfToImages => { - if !pdf_exts.contains(&ext) { - return Err(UploadError::InvalidMime(format!( - "Expected PDF file, got .{}", - ext - ))); - } - } - Tool::ImagesToPdf => { - if !image_exts.contains(&ext) { - return Err(UploadError::InvalidMime(format!( - "Expected image file, got .{}", - ext - ))); - } - } - Tool::VideoCompress | Tool::VideoTrim | Tool::GifMaker => { - if !video_exts.contains(&ext) { - return Err(UploadError::InvalidMime(format!( - "Expected video file, got .{}", - ext - ))); - } - } - Tool::AudioExtract => { - if !video_exts.contains(&ext) { - return Err(UploadError::InvalidMime(format!( - "Expected video file, got .{}", - ext - ))); - } - } - Tool::AudioConvert => { - if !audio_exts.contains(&ext) { - return Err(UploadError::InvalidMime(format!( - "Expected audio file, got .{}", - ext - ))); - } - } - } - Ok(()) -} - -fn verify_magic_bytes(data: &[u8], ext: &str) -> bool { - if data.is_empty() { - return false; - } - match ext { - "jpg" | "jpeg" => data.starts_with(&[0xFF, 0xD8, 0xFF]), - "png" => data.starts_with(&[0x89, 0x50, 0x4E, 0x47]), - "webp" => data.len() > 12 && &data[0..4] == b"RIFF" && &data[8..12] == b"WEBP", - "gif" => data.starts_with(b"GIF8"), - "bmp" => data.starts_with(b"BM"), - "pdf" => data.starts_with(b"%PDF"), - "mp4" => data.len() > 8 && (&data[4..8] == b"ftyp" || &data[4..8] == b"ftyp"), - "heic" => data.len() > 12 && &data[4..12] == b"ftypheic", - _ => true, - } -} \ No newline at end of file diff --git a/apps/tools/backend/gateway/src/routes/ws.rs b/apps/tools/backend/gateway/src/routes/ws.rs deleted file mode 100644 index ad1d8e9..0000000 --- a/apps/tools/backend/gateway/src/routes/ws.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::sync::Arc; - -use axum::{ - extract::{ - ws::{Message, WebSocket}, - Path, State, WebSocketUpgrade, - }, - response::IntoResponse, -}; -use futures::{SinkExt, StreamExt}; -use tokio::sync::mpsc; -use uuid::Uuid; - -use crate::routes::health::AppState; -use tools_common::types::{JobProgress, JobStatus}; - -/// Handle WebSocket upgrade at /api/job/{id}/ws. -pub async fn ws_handler( - ws: WebSocketUpgrade, - State(state): State>, - Path(job_id): Path, -) -> impl IntoResponse { - ws.on_upgrade(move |socket| handle_ws(socket, state, job_id)) -} - -async fn handle_ws(ws: WebSocket, state: Arc, job_id: Uuid) { - let (mut sender, mut receiver) = ws.split(); - - // Subscribe to NATS progress updates - let nats = state.nats.clone(); - let subject = format!("tools.*.progress.{}", job_id); - let mut subscriber = match nats.subscribe(subject).await { - Ok(sub) => sub, - Err(e) => { - tracing::error!("Failed to subscribe to NATS: {}", e); - let _ = sender - .send(Message::Text( - serde_json::json!({ - "type": "error", - "job_id": job_id, - "status": "failed", - "error": format!("Connection error: {}", e) - }) - .to_string() - .into(), - )) - .await; - return; - } - }; - - // Send initial status from Redis - if let Ok(mut conn) = state.redis.get_multiplexed_async_connection().await { - if let Ok(job) = crate::redis::job::JobRepository::get(&mut conn, job_id).await { - let init_msg = serde_json::json!({ - "type": "status", - "job_id": job_id, - "status": match &job.status { - JobStatus::Queued => "queued", - JobStatus::Processing { .. } => "processing", - JobStatus::Completed => "completed", - JobStatus::NeedsManualCrop => "needs_manual_crop", - JobStatus::Failed(_) => "failed", - }, - "progress": match &job.status { - JobStatus::Processing { progress, .. } => *progress, - JobStatus::Completed => 100, - _ => 0, - }, - }); - let _ = sender - .send(Message::Text(init_msg.to_string().into())) - .await; - } - } - - // Channel for NATS messages - let (tx, mut rx) = mpsc::channel::(32); - - // Spawn NATS listener - let tx_clone = tx.clone(); - let nats_listener = tokio::spawn(async move { - loop { - tokio::select! { - msg = subscriber.next() => { - match msg { - Some(nats_msg) => { - if let Ok(progress) = serde_json::from_slice::(&nats_msg.payload) { - let json = serde_json::json!({ - "type": "progress", - "job_id": progress.job_id, - "status": match &progress.status { - JobStatus::Queued => "queued", - JobStatus::Processing { .. } => "processing", - JobStatus::Completed => "completed", - JobStatus::NeedsManualCrop => "needs_manual_crop", - JobStatus::Failed(_) => "failed", - }, - "progress": progress.progress, - "stage": progress.stage, - "message": progress.message, - }); - let _ = tx_clone.send(json.to_string()).await; - } - } - None => break, - } - } - _ = tokio::time::sleep(tokio::time::Duration::from_secs(30)) => { - // Keepalive ping - let _ = tx_clone.send(serde_json::json!({"type": "ping"}).to_string()).await; - } - } - } - }); - - // Forward messages from channel to WebSocket - let ws_sender = tokio::spawn(async move { - while let Some(msg) = rx.recv().await { - if sender.send(Message::Text(msg.into())).await.is_err() { - break; - } - } - }); - - // Listen for client close - let ws_receiver = tokio::spawn(async move { - while let Some(Ok(_)) = receiver.next().await { - // Client messages ignored (we only forward server→client) - } - }); - - // Wait for either task to complete (connection closed) - tokio::select! { - _ = ws_sender => {}, - _ = ws_receiver => {}, - } - - // Cancel NATS listener - nats_listener.abort(); -} \ No newline at end of file diff --git a/apps/tools/backend/rust-toolchain.toml b/apps/tools/backend/rust-toolchain.toml deleted file mode 100644 index 7bcfaa2..0000000 --- a/apps/tools/backend/rust-toolchain.toml +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -channel = "1.85" \ No newline at end of file diff --git a/apps/tools/backend/wasm/Cargo.toml b/apps/tools/backend/wasm/Cargo.toml deleted file mode 100644 index 7c88a55..0000000 --- a/apps/tools/backend/wasm/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "tools-wasm" -version.workspace = true -edition.workspace = true -license.workspace = true - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wasm-bindgen = "0.2" -image.workspace = true -console_error_panic_hook = "0.1" -serde.workspace = true -serde_json.workspace = true - -# Skip wasm crate from default cargo check -# Full build requires: wasm-pack build --target web \ No newline at end of file diff --git a/apps/tools/backend/wasm/src/lib.rs b/apps/tools/backend/wasm/src/lib.rs deleted file mode 100644 index dd22d00..0000000 --- a/apps/tools/backend/wasm/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -use wasm_bindgen::prelude::*; - -/// Placeholder for WASM image processing. -/// Full implementation in Phase 2.2. -#[wasm_bindgen] -pub fn greet() -> String { - "tools-wasm: ready".to_string() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_greet() { - assert_eq!(greet(), "tools-wasm: ready"); - } -} \ No newline at end of file diff --git a/apps/tools/backend/workers/Cargo.toml b/apps/tools/backend/workers/Cargo.toml deleted file mode 100644 index 1981c62..0000000 --- a/apps/tools/backend/workers/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "tools-workers" -version.workspace = true -edition.workspace = true -license.workspace = true - -[dependencies] -tools-common = { path = "../common" } - -tokio.workspace = true -serde.workspace = true -serde_json.workspace = true -uuid.workspace = true -chrono.workspace = true -tracing.workspace = true -tracing-subscriber.workspace = true -async-nats.workspace = true -redis.workspace = true -thiserror.workspace = true -anyhow.workspace = true -image.workspace = true -imageproc.workspace = true -nalgebra = "0.32" -lopdf.workspace = true -rayon = "1" -futures = "0.3" -async-trait = "0.1" -leptess = { version = "0.14", optional = true } - -[features] -default = [] -tesseract = ["leptess"] \ No newline at end of file diff --git a/apps/tools/backend/workers/src/audio/mod.rs b/apps/tools/backend/workers/src/audio/mod.rs deleted file mode 100644 index 7527523..0000000 --- a/apps/tools/backend/workers/src/audio/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -// Audio processing module. -// TODO: Phase 4 - implement convert, trim \ No newline at end of file diff --git a/apps/tools/backend/workers/src/config.rs b/apps/tools/backend/workers/src/config.rs deleted file mode 100644 index dc341d8..0000000 --- a/apps/tools/backend/workers/src/config.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::path::PathBuf; - -/// Worker configuration loaded from environment variables. -#[derive(Debug, Clone)] -pub struct WorkerConfig { - pub nats_url: String, - pub redis_url: String, - pub storage_path: PathBuf, - pub concurrency: u32, - pub job_ttl_seconds: u64, - pub rust_log: String, -} - -impl WorkerConfig { - pub fn from_env() -> Self { - Self { - nats_url: env_or_default("NATS_URL", "nats://localhost:4222"), - redis_url: env_or_default("REDIS_URL", "redis://localhost:6379"), - storage_path: PathBuf::from(env_or_default("STORAGE_PATH", "/data/tools")), - concurrency: env_or_default("TOOLS_WORKER_CONCURRENCY", "4") - .parse() - .unwrap_or(4), - job_ttl_seconds: env_or_default("JOB_TTL_SECONDS", "3600") - .parse() - .unwrap_or(3600), - rust_log: env_or_default("RUST_LOG", "info"), - } - } -} - -fn env_or_default(key: &str, default: &str) -> String { - std::env::var(key).unwrap_or_else(|_| default.to_string()) -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/image/mod.rs b/apps/tools/backend/workers/src/image/mod.rs deleted file mode 100644 index 72a441e..0000000 --- a/apps/tools/backend/workers/src/image/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::config::WorkerConfig; -use tools_common::types::Job; - -/// Process an image tool job. -pub async fn process_job( - job: Job, - _redis: &redis::Client, - _config: &WorkerConfig, -) -> Result<(), Box> { - tracing::info!(job_id = %job.id, tool = %job.tool.as_str(), "Processing image job (stub)"); - // TODO: Phase 2.2 - implement actual image processing - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - tracing::info!(job_id = %job.id, "Image job completed"); - Ok(()) -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/main.rs b/apps/tools/backend/workers/src/main.rs deleted file mode 100644 index 885b276..0000000 --- a/apps/tools/backend/workers/src/main.rs +++ /dev/null @@ -1,39 +0,0 @@ -mod config; -mod image; -mod nats; -mod pdf; -mod scanner; -mod scheduler; -mod video; -mod audio; - -use tracing_subscriber::EnvFilter; - -#[tokio::main] -async fn main() { - let config = config::WorkerConfig::from_env(); - - tracing_subscriber::fmt() - .with_env_filter(EnvFilter::new(&config.rust_log)) - .init(); - - tracing::info!("Starting tools-workers..."); - - // Connect to NATS - let nats = nats::consumer::JobConsumer::connect(&config.nats_url) - .await - .expect("Failed to connect to NATS"); - tracing::info!("Connected to NATS at {}", config.nats_url); - - // Connect to Redis - let redis = nats::consumer::JobConsumer::connect_redis(&config.redis_url) - .await - .expect("Failed to connect to Redis"); - tracing::info!("Connected to Redis at {}", config.redis_url); - - // Start NATS consumers (blocks forever) - tracing::info!("Starting job consumers..."); - if let Err(e) = nats::consumer::JobConsumer::start(&nats, &redis, &config).await { - tracing::error!("Consumer error: {}", e); - } -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/nats/consumer.rs b/apps/tools/backend/workers/src/nats/consumer.rs deleted file mode 100644 index 447e4ac..0000000 --- a/apps/tools/backend/workers/src/nats/consumer.rs +++ /dev/null @@ -1,186 +0,0 @@ -use async_nats::Client; -use futures::StreamExt; -use redis::AsyncCommands; -use uuid::Uuid; - -use tools_common::types::{Job, JobStatus}; - -use crate::config::WorkerConfig; - -/// NATS consumer setup and management. -pub struct JobConsumer; - -impl JobConsumer { - /// Connect to NATS. - pub async fn connect(url: &str) -> Result> { - Ok(async_nats::connect(url).await?) - } - - /// Connect to Redis. - pub async fn connect_redis( - url: &str, - ) -> Result> { - Ok(redis::Client::open(url)?) - } - - /// Start consuming job messages from NATS for all tool groups. - pub async fn start( - nats: &Client, - redis: &redis::Client, - config: &WorkerConfig, - ) -> Result<(), Box> { - // Subscribe to scan jobs - let scan_sub = nats - .queue_subscribe("tools.scan.jobs.>", "scan-workers".to_string()) - .await?; - tracing::info!("Subscribed to tools.scan.jobs.>"); - - // Subscribe to image jobs - let image_sub = nats - .queue_subscribe("tools.image.jobs.>", "image-workers".to_string()) - .await?; - tracing::info!("Subscribed to tools.image.jobs.>"); - - // Subscribe to pdf jobs - let pdf_sub = nats - .queue_subscribe("tools.pdf.jobs.>", "pdf-workers".to_string()) - .await?; - tracing::info!("Subscribed to tools.pdf.jobs.>"); - - // Subscribe to cleanup scheduler - let cleanup_sub = nats - .subscribe("tools.scheduler.cleanup".to_string()) - .await?; - tracing::info!("Subscribed to tools.scheduler.cleanup"); - - let redis_clone = redis.clone(); - let config_clone = config.clone(); - - // Process messages concurrently - tokio::select! { - _ = Self::process_subscription(scan_sub, redis.clone(), config.clone()) => {}, - _ = Self::process_subscription(image_sub, redis.clone(), config.clone()) => {}, - _ = Self::process_subscription(pdf_sub, redis.clone(), config.clone()) => {}, - _ = Self::process_cleanup(cleanup_sub, config_clone) => {}, - } - - Ok(()) - } - - /// Process messages from a NATS subscription. - async fn process_subscription( - mut sub: async_nats::Subscriber, - redis: redis::Client, - config: WorkerConfig, - ) { - while let Some(msg) = sub.next().await { - if let Ok(job) = serde_json::from_slice::(&msg.payload) { - let redis = redis.clone(); - let config = config.clone(); - - tokio::spawn(async move { - let tool = job.tool.clone(); - tracing::info!( - job_id = %job.id, - tool = %tool.as_str(), - "Received job" - ); - - match Self::dispatch_job(tool, job, &redis, &config).await { - Ok(()) => tracing::info!("Job completed successfully"), - Err(e) => tracing::error!("Job failed: {}", e), - } - }); - } - } - } - - /// Process cleanup scheduler messages. - async fn process_cleanup(mut sub: async_nats::Subscriber, config: WorkerConfig) { - while let Some(msg) = sub.next().await { - tracing::info!("Running cleanup cycle"); - let redis_url = config.redis_url.clone(); - match redis::Client::open(redis_url.as_str()) { - Ok(client) => { - match crate::scheduler::cleanup::CleanupScheduler::run( - &config.storage_path, - &client, - config.job_ttl_seconds, - ) - .await - { - Ok(result) => { - tracing::info!( - "Cleanup: {} files deleted, {} bytes freed", - result.files_deleted, - result.bytes_freed - ); - } - Err(e) => { - tracing::error!("Cleanup failed: {}", e); - } - } - } - Err(e) => { - tracing::error!("Failed to create Redis client for cleanup: {}", e); - } - } - // Consume the message (no ack for core NATS) - let _ = msg; - } - } - - /// Dispatch a job to the appropriate handler based on tool type. - async fn dispatch_job( - tool: tools_common::types::Tool, - job: Job, - redis: &redis::Client, - config: &WorkerConfig, - ) -> Result<(), Box> { - match tool { - tools_common::types::Tool::Scan => { - crate::scanner::process_job(job, redis, config).await - } - tools_common::types::Tool::ImageCompress - | tools_common::types::Tool::ImageResize - | tools_common::types::Tool::ImageConvert - | tools_common::types::Tool::RemoveBg => { - crate::image::process_job(job, redis, config).await - } - tools_common::types::Tool::PdfMerge - | tools_common::types::Tool::PdfSplit - | tools_common::types::Tool::ImagesToPdf - | tools_common::types::Tool::PdfCompress - | tools_common::types::Tool::PdfToImages => { - crate::pdf::process_job(job, redis, config).await - } - _ => { - tracing::warn!(tool = %tool.as_str(), "Tool handler not yet implemented"); - Ok(()) - } - } - } - - /// Update job result in Redis after processing. - pub async fn update_job_result( - conn: &mut impl AsyncCommands, - job_id: Uuid, - result_path: &str, - ttl_seconds: u64, - ) -> Result<(), Box> { - let key = format!("job:{}", job_id); - let json: String = conn - .get(&key) - .await - .map_err(|e| -> Box { Box::new(e) })?; - let mut job: Job = serde_json::from_str(&json)?; - job.status = JobStatus::Completed; - job.result_path = Some(result_path.to_string()); - let updated = serde_json::to_string(&job)?; - let _: () = conn - .set_ex(key, updated, ttl_seconds) - .await - .map_err(|e| -> Box { Box::new(e) })?; - Ok(()) - } -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/nats/mod.rs b/apps/tools/backend/workers/src/nats/mod.rs deleted file mode 100644 index 4e6ecfa..0000000 --- a/apps/tools/backend/workers/src/nats/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod consumer; -pub mod progress; \ No newline at end of file diff --git a/apps/tools/backend/workers/src/nats/progress.rs b/apps/tools/backend/workers/src/nats/progress.rs deleted file mode 100644 index 0833434..0000000 --- a/apps/tools/backend/workers/src/nats/progress.rs +++ /dev/null @@ -1,82 +0,0 @@ -use redis::AsyncCommands; -use uuid::Uuid; - -use tools_common::types::{JobStatus, Tool}; - -/// Reports progress from worker to NATS and Redis. -pub struct ProgressReporter { - redis: redis::Client, - nats: async_nats::Client, - job_id: Uuid, - tool: Tool, -} - -impl ProgressReporter { - pub fn new(redis: redis::Client, nats: async_nats::Client, job_id: Uuid, tool: Tool) -> Self { - Self { - redis, - nats, - job_id, - tool, - } - } - - /// Report progress: updates Redis and publishes to NATS. - pub async fn report( - &self, - status: JobStatus, - stage: &str, - progress: u8, - message: &str, - ) -> Result<(), Box> { - // Update Redis - if let Ok(mut conn) = self.redis.get_multiplexed_async_connection().await { - let key = format!("job:{}", self.job_id); - if let Ok(json) = conn.get::<_, String>(&key).await { - if let Ok(mut job) = serde_json::from_str::(&json) { - job.status = status.clone(); - let updated = serde_json::to_string(&job).unwrap_or(json); - let _: Result<(), _> = conn.set_ex(key, updated, job.ttl_seconds).await; - } - } - } - - // Publish to NATS - let progress_msg = tools_common::types::JobProgress { - job_id: self.job_id, - status, - stage: stage.to_string(), - progress, - message: message.to_string(), - }; - - let subject = format!("tools.{}.progress.{}", self.tool.subject_prefix(), self.job_id); - if let Ok(payload) = serde_json::to_vec(&progress_msg) { - let _ = self.nats.publish(subject, payload.into()).await; - } - - tracing::debug!( - job_id = %self.job_id, - stage = %stage, - progress = %progress, - "Progress update" - ); - - Ok(()) - } - - pub fn job_id(&self) -> Uuid { - self.job_id - } -} - -impl Clone for ProgressReporter { - fn clone(&self) -> Self { - Self { - redis: self.redis.clone(), - nats: self.nats.clone(), - job_id: self.job_id, - tool: self.tool.clone(), - } - } -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/pdf/mod.rs b/apps/tools/backend/workers/src/pdf/mod.rs deleted file mode 100644 index 88aa88c..0000000 --- a/apps/tools/backend/workers/src/pdf/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::config::WorkerConfig; -use tools_common::types::Job; - -/// Process a PDF tool job. -pub async fn process_job( - job: Job, - _redis: &redis::Client, - _config: &WorkerConfig, -) -> Result<(), Box> { - tracing::info!(job_id = %job.id, tool = %job.tool.as_str(), "Processing PDF job (stub)"); - // TODO: Phase 3 - implement actual PDF processing - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - tracing::info!(job_id = %job.id, "PDF job completed"); - Ok(()) -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/scanner/binarize.rs b/apps/tools/backend/workers/src/scanner/binarize.rs deleted file mode 100644 index 2310f1b..0000000 --- a/apps/tools/backend/workers/src/scanner/binarize.rs +++ /dev/null @@ -1,246 +0,0 @@ -use image::{GrayImage, Luma}; - -/// Apply Sauvola local threshold for clean black-and-white output. -/// -/// Sauvola: T(x,y) = m(x,y) * [1 + k * (s(x,y)/R - 1)] -/// where m = local mean, s = local std dev, R = 128, k = 0.2 -pub fn sauvola_threshold(img: &GrayImage, window_size: u32, k: f64) -> GrayImage { - let (w, h) = (img.width(), img.height()); - let half_win = (window_size / 2) as i32; - let mut output = GrayImage::new(w, h); - - // Integral images for O(1) mean and variance computation - let integral = compute_integral_image(img); - let integral_sq = compute_integral_image_sq(img); - - for y in 0..h { - for x in 0..w { - let (mean, variance) = local_stats( - &integral, - &integral_sq, - x as i32, - y as i32, - half_win, - w as i32, - h as i32, - ); - let std_dev = variance.sqrt(); - let threshold = mean * (1.0 + k * (std_dev / 128.0 - 1.0)); - - let pixel = img.get_pixel(x, y)[0] as f64; - output.put_pixel(x, y, Luma([if pixel > threshold { 255 } else { 0 }])); - } - } - - output -} - -/// Compute integral image for O(1) sum queries. -fn compute_integral_image(img: &GrayImage) -> Vec { - let (w, h) = (img.width() as usize, img.height() as usize); - let mut integral = vec![0u64; (w + 1) * (h + 1)]; - - for y in 0..h { - for x in 0..w { - let idx = (y + 1) * (w + 1) + (x + 1); - let pixel = img.get_pixel(x as u32, y as u32)[0] as u64; - integral[idx] = pixel - + integral[(y + 1) * (w + 1) + x] - + integral[y * (w + 1) + (x + 1)] - - integral[y * (w + 1) + x]; - } - } - - integral -} - -/// Compute squared integral image for O(1) variance queries. -fn compute_integral_image_sq(img: &GrayImage) -> Vec { - let (w, h) = (img.width() as usize, img.height() as usize); - let mut integral = vec![0u64; (w + 1) * (h + 1)]; - - for y in 0..h { - for x in 0..w { - let idx = (y + 1) * (w + 1) + (x + 1); - let pixel = img.get_pixel(x as u32, y as u32)[0] as u64; - let pixel_sq = pixel * pixel; - integral[idx] = pixel_sq - + integral[(y + 1) * (w + 1) + x] - + integral[y * (w + 1) + (x + 1)] - - integral[y * (w + 1) + x]; - } - } - - integral -} - -/// Compute local mean and variance for a window around (x, y) using integral images. -fn local_stats( - integral: &[u64], - integral_sq: &[u64], - x: i32, - y: i32, - half_win: i32, - w: i32, - h: i32, -) -> (f64, f64) { - let x1 = (x - half_win).max(0); - let y1 = (y - half_win).max(0); - let x2 = (x + half_win).min(w - 1); - let y2 = (y + half_win).min(h - 1); - - let width = (w + 1) as usize; - let area = ((x2 - x1 + 1) * (y2 - y1 + 1)) as f64; - - if area <= 0.0 { - return (0.0, 0.0); - } - - // Sum from integral image - let idx_tl = (y1) as usize * width + (x1) as usize; - let idx_tr = (y1) as usize * width + (x2 + 1) as usize; - let idx_bl = (y2 + 1) as usize * width + (x1) as usize; - let idx_br = (y2 + 1) as usize * width + (x2 + 1) as usize; - - let sum = integral[idx_br] - .wrapping_sub(integral[idx_tr]) - .wrapping_sub(integral[idx_bl]) - .wrapping_add(integral[idx_tl]); - - // Sum of squares - let sum_sq = integral_sq[idx_br] - .wrapping_sub(integral_sq[idx_tr]) - .wrapping_sub(integral_sq[idx_bl]) - .wrapping_add(integral_sq[idx_tl]); - - let mean = sum as f64 / area; - let variance = (sum_sq as f64 / area) - mean * mean; - - (mean, variance.max(0.0)) -} - -/// Otsu global threshold (fallback for when Sauvola is too slow). -#[allow(dead_code)] -pub fn otsu_threshold(img: &GrayImage) -> GrayImage { - let (w, h) = (img.width(), img.height()); - let total_pixels = w * h; - - // Compute histogram - let mut hist = [0u32; 256]; - for pixel in img.iter() { - hist[*pixel as usize] += 1; - } - - // Normalize to probabilities - let mut prob = [0.0f64; 256]; - for i in 0..256 { - prob[i] = hist[i] as f64 / total_pixels as f64; - } - - // Find threshold that maximizes between-class variance - let mut best_threshold = 128u8; - let mut best_variance = 0.0f64; - - for t in 1..255 { - let w0: f64 = prob[..t].iter().sum(); - let w1: f64 = prob[t..].iter().sum(); - - if w0 < 1e-6 || w1 < 1e-6 { - continue; - } - - let mut mean0 = 0.0f64; - let mut mean1 = 0.0f64; - - for i in 0..t { - mean0 += i as f64 * prob[i] / w0; - } - for i in t..256 { - mean1 += i as f64 * prob[i] / w1; - } - - let variance = w0 * w1 * (mean0 - mean1).powi(2); - if variance > best_variance { - best_variance = variance; - best_threshold = t as u8; - } - } - - // Apply threshold - let mut output = GrayImage::new(w, h); - for y in 0..h { - for x in 0..w { - let pixel = img.get_pixel(x, y)[0]; - output.put_pixel(x, y, Luma([if pixel > best_threshold { 255 } else { 0 }])); - } - } - - output -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_sauvola_on_simple_image() { - // Create document-like image: white background with dark text lines - let mut img = GrayImage::new(100, 100); - // White background - for y in 0..100 { - for x in 0..100 { - img.put_pixel(x, y, Luma([220])); - } - } - // Dark text lines (simulated with thin dark rectangles) - for y in 0..100 { - for x in 0..100 { - // Alternate thin dark "text" lines - if y % 10 < 3 && x > 10 && x < 90 { - img.put_pixel(x, y, Luma([30])); - } - } - } - - let result = sauvola_threshold(&img, 25, 0.2); - // Text line at y=1 should be black (0) - let text_pixel1 = result.get_pixel(50, 1)[0]; - let text_pixel2 = result.get_pixel(50, 2)[0]; - assert_eq!(text_pixel1, 0, "Text line at y=1 should be black (0), got {}", text_pixel1); - assert_eq!(text_pixel2, 0, "Text line at y=2 should be black (0), got {}", text_pixel2); - // Background at y=5 should be white (255) - let bg_pixel = result.get_pixel(50, 5)[0]; - assert_eq!(bg_pixel, 255, "Background at y=5 should be white (255), got {}", bg_pixel); - } - - #[test] - fn test_integral_image() { - let mut img = GrayImage::new(4, 4); - img.put_pixel(0, 0, Luma([1])); - img.put_pixel(1, 0, Luma([2])); - img.put_pixel(0, 1, Luma([3])); - img.put_pixel(1, 1, Luma([4])); - - let integral = compute_integral_image(&img); - let width = 5; // (w+1) - // Sum of all 4 pixels at (2,2) - let sum = integral[2 * width + 2]; - assert_eq!(sum, 1 + 2 + 3 + 4); // 10 - } - - #[test] - fn test_otsu_on_bimodal() { - // Create a bimodal image: half black, half white - let mut img = GrayImage::new(50, 50); - for y in 0..50 { - for x in 0..50 { - let val = if x < 25 { 30 } else { 200 }; - img.put_pixel(x, y, Luma([val])); - } - } - let result = otsu_threshold(&img); - // Should threshold correctly at ~115 - assert_eq!(result.get_pixel(10, 25)[0], 0); // dark side - assert_eq!(result.get_pixel(35, 25)[0], 255); // light side - } -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/scanner/corners.rs b/apps/tools/backend/workers/src/scanner/corners.rs deleted file mode 100644 index 57fe961..0000000 --- a/apps/tools/backend/workers/src/scanner/corners.rs +++ /dev/null @@ -1,177 +0,0 @@ -use image::GrayImage; -use imageproc::contours::find_contours; - -use tools_common::error::PipelineError; - -/// Represents a detected corner point. -pub type CornerPoint = (f64, f64); - -/// The fallback reason if corner detection fails. -pub enum FallbackReason { - NoContours, - NoRectangularContour, - TooSmall, -} - -/// Find the 4 corners of the document from an edge image. -pub fn detect_corners(edges: &GrayImage) -> Result<[CornerPoint; 4], FallbackReason> { - let contours = find_contours::(edges); - if contours.is_empty() { - return Err(FallbackReason::NoContours); - } - - // Convert contours to use i32 coordinates - let contour_points: Vec> = contours - .iter() - .map(|c| c.points.iter().map(|p| (p.x as i32, p.y as i32)).collect()) - .collect(); - - // Sort by area descending - let mut sorted: Vec<_> = contour_points.iter().collect(); - sorted.sort_by(|a, b| { - contour_area_slice(b) - .partial_cmp(&contour_area_slice(a)) - .unwrap_or(std::cmp::Ordering::Equal) - }); - - for points in sorted.iter().take(5) { - if let Some(corners) = approx_quadrilateral(points) { - let ordered = order_corners(&corners); - return Ok(ordered); - } - } - - // Fallback: use bounding rect of largest contour - if let Some(largest) = sorted.first() { - let rect = bounding_rect_slice(largest); - let corners = vec![ - (rect.0 as f64, rect.1 as f64), - (rect.2 as f64, rect.1 as f64), - (rect.2 as f64, rect.3 as f64), - (rect.0 as f64, rect.3 as f64), - ]; - return Ok(order_corners(&corners)); - } - - Err(FallbackReason::NoContours) -} - -/// Compute the area of a contour using the Shoelace formula. -fn contour_area_slice(points: &[(i32, i32)]) -> f64 { - let n = points.len(); - if n < 3 { - return 0.0; - } - let mut area = 0.0; - for i in 0..n { - let j = (i + 1) % n; - area += points[i].0 as f64 * points[j].1 as f64; - area -= points[j].0 as f64 * points[i].1 as f64; - } - area.abs() / 2.0 -} - -/// Approximate a contour to a quadrilateral. -fn approx_quadrilateral(points: &[(i32, i32)]) -> Option> { - let n = points.len(); - if n < 4 { - return None; - } - - let top = points.iter().min_by(|a, b| a.1.cmp(&b.1))?; - let bottom = points.iter().max_by(|a, b| a.1.cmp(&b.1))?; - let left = points.iter().min_by(|a, b| a.0.cmp(&b.0))?; - let right = points.iter().max_by(|a, b| a.0.cmp(&b.0))?; - - Some(vec![ - (left.0 as f64, left.1 as f64), - (right.0 as f64, top.1 as f64), - (right.0 as f64, bottom.1 as f64), - (left.0 as f64, bottom.1 as f64), - ]) -} - -/// Order 4 corners: top-left, top-right, bottom-right, bottom-left. -fn order_corners(points: &[CornerPoint]) -> [CornerPoint; 4] { - let mut pts: Vec = points.to_vec(); - let mut ordered = [(0.0, 0.0); 4]; - - if pts.len() >= 4 { - // Sort by position - // TL = min(x+y), BR = max(x+y) - pts.sort_by(|a, b| { - (a.0 + a.1) - .partial_cmp(&(b.0 + b.1)) - .unwrap_or(std::cmp::Ordering::Equal) - }); - ordered[0] = pts[0]; // TL - ordered[2] = pts[3]; // BR - - // TR = max(x - y), BL = min(x - y) - pts.sort_by(|a, b| { - (a.0 - a.1) - .partial_cmp(&(b.0 - b.1)) - .unwrap_or(std::cmp::Ordering::Equal) - }); - ordered[1] = pts[3]; // TR - ordered[3] = pts[0]; // BL - } - - ordered -} - -/// Compute bounding rectangle: (left, top, right, bottom). -fn bounding_rect_slice(points: &[(i32, i32)]) -> (i32, i32, i32, i32) { - let left = points.iter().map(|p| p.0).min().unwrap_or(0); - let top = points.iter().map(|p| p.1).min().unwrap_or(0); - let right = points.iter().map(|p| p.0).max().unwrap_or(0); - let bottom = points.iter().map(|p| p.1).max().unwrap_or(0); - (left, top, right, bottom) -} - -/// Detect corners with fallback: full resolution, then half, then error. -pub fn detect_corners_with_fallback( - edges: &GrayImage, -) -> Result<[CornerPoint; 4], PipelineError> { - // Attempt 1: Full resolution - if let Ok(corners) = detect_corners(edges) { - return Ok(corners); - } - - // Attempt 2: Half resolution - let (w, h) = (edges.width() / 2, edges.height() / 2); - if w > 10 && h > 10 { - let half = image::imageops::resize( - edges, - w, - h, - image::imageops::FilterType::Lanczos3, - ); - if let Ok(corners) = detect_corners(&half) { - return Ok(corners.map(|(x, y)| (x * 2.0, y * 2.0))); - } - } - - Err(PipelineError::CornerDetection( - "Could not detect document corners automatically".to_string(), - )) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_contour_area_slice() { - let points = vec![(0, 0), (100, 0), (100, 100), (0, 100)]; - let area = contour_area_slice(&points); - assert!((area - 10000.0).abs() < 1.0); - } - - #[test] - fn test_bounding_rect_slice() { - let points = vec![(10, 20), (100, 30), (90, 150), (5, 140)]; - let rect = bounding_rect_slice(&points); - assert_eq!(rect, (5, 20, 100, 150)); - } -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/scanner/deskew.rs b/apps/tools/backend/workers/src/scanner/deskew.rs deleted file mode 100644 index 861c9ac..0000000 --- a/apps/tools/backend/workers/src/scanner/deskew.rs +++ /dev/null @@ -1,197 +0,0 @@ -use image::{GrayImage, Luma}; -use image::imageops; - -/// Detect and correct small rotation (<5°) of text lines using Hough transform. -pub fn deskew(img: &GrayImage) -> GrayImage { - let lines = hough_lines(img, 10, 50); - - if lines.is_empty() { - return img.clone(); - } - - // Compute median angle of all detected lines - let angles: Vec = lines - .iter() - .map(|line| line.angle_deg()) - .filter(|a| a.abs() < 45.0) // Skip vertical lines - .collect(); - - if angles.is_empty() { - return img.clone(); - } - - let median_angle = median(&angles); - - // Skip if angle is very small (<0.5°) - if median_angle.abs() < 0.5 { - return img.clone(); - } - - // Rotate image - rotate_image(img, median_angle) -} - -/// Represents a line detected by Hough transform. -#[derive(Debug, Clone)] -struct HoughLine { - rho: f64, - theta: f64, -} - -impl HoughLine { - fn angle_deg(&self) -> f64 { - self.theta.to_degrees() - 90.0 - } -} - -/// Simple Hough line detection. -fn hough_lines(img: &GrayImage, threshold: u32, _max_lines: usize) -> Vec { - let (w, h) = (img.width() as i32, img.height() as i32); - let max_rho = ((w * w + h * h) as f64).sqrt().ceil() as i32; - - let theta_step = 1.0_f64.to_radians(); - let num_thetas = 180; - - // Accumulator - let mut accumulator = - vec![vec![0u32; (2 * max_rho + 1) as usize]; num_thetas]; - - // Vote - for y in 0..h { - for x in 0..w { - if img.get_pixel(x as u32, y as u32)[0] > 128 { - for t_idx in 0..num_thetas { - let theta = t_idx as f64 * theta_step; - let rho = (x as f64 * theta.cos() + y as f64 * theta.sin()).round() as i32; - let rho_idx = rho + max_rho; - if rho_idx >= 0 && (rho_idx as usize) < accumulator[t_idx].len() { - accumulator[t_idx][rho_idx as usize] += 1; - } - } - } - } - } - - // Find local maxima above threshold - let mut lines = Vec::new(); - for t_idx in 0..num_thetas { - let theta = t_idx as f64 * theta_step; - for (r_idx, &count) in accumulator[t_idx].iter().enumerate() { - if count > threshold { - let rho = r_idx as i32 - max_rho; - lines.push(HoughLine { - rho: rho as f64, - theta, - }); - } - } - } - - // Sort by votes (descending) and take top N - lines.sort_by(|a, b| { - let a_idx = (a.theta / theta_step).round() as usize; - let b_idx = (b.theta / theta_step).round() as usize; - let a_rho_idx = (a.rho + max_rho as f64).round() as usize; - let b_rho_idx = (b.rho + max_rho as f64).round() as usize; - let a_count = accumulator[a_idx.min(num_thetas - 1)][a_rho_idx.min(accumulator[0].len() - 1)]; - let b_count = accumulator[b_idx.min(num_thetas - 1)][b_rho_idx.min(accumulator[0].len() - 1)]; - b_count.cmp(&a_count) - }); - - lines.truncate(100); - lines -} - -/// Compute median of a sorted slice of f64 values. -fn median(values: &[f64]) -> f64 { - if values.is_empty() { - return 0.0; - } - let mut sorted = values.to_vec(); - sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); - let mid = sorted.len() / 2; - if sorted.len() % 2 == 0 { - (sorted[mid - 1] + sorted[mid]) / 2.0 - } else { - sorted[mid] - } -} - -/// Rotate an image by the given angle in degrees. -fn rotate_image(img: &GrayImage, angle_deg: f64) -> GrayImage { - let angle_rad = angle_deg.to_radians(); - let (w, h) = (img.width(), img.height()); - - // Compute new image dimensions to fit the rotated content - let cos = angle_rad.cos().abs(); - let sin = angle_rad.sin().abs(); - let new_w = (w as f64 * cos + h as f64 * sin).ceil() as u32; - let new_h = (w as f64 * sin + h as f64 * cos).ceil() as u32; - let new_w = new_w.max(1); - let new_h = new_h.max(1); - - let mut output = GrayImage::new(new_w, new_h); - let cx = w as f64 / 2.0; - let cy = h as f64 / 2.0; - let new_cx = new_w as f64 / 2.0; - let new_cy = new_h as f64 / 2.0; - - // Backward mapping - for out_y in 0..new_h { - for out_x in 0..new_w { - // Translate to origin, rotate, translate back - let dx = out_x as f64 - new_cx; - let dy = out_y as f64 - new_cy; - let src_x = dx * cos + dy * sin + cx; - let src_y = -dx * sin + dy * cos + cy; - - if src_x >= 0.0 && src_x < w as f64 - 1.0 && src_y >= 0.0 && src_y < h as f64 - 1.0 { - // Bilinear interpolation - let x0 = src_x.floor() as u32; - let y0 = src_y.floor() as u32; - let x1 = (x0 + 1).min(w - 1); - let y1 = (y0 + 1).min(h - 1); - let fx = src_x - x0 as f64; - let fy = src_y - y0 as f64; - - let p00 = img.get_pixel(x0, y0)[0] as f64; - let p10 = img.get_pixel(x1, y0)[0] as f64; - let p01 = img.get_pixel(x0, y1)[0] as f64; - let p11 = img.get_pixel(x1, y1)[0] as f64; - - let val = p00 * (1.0 - fx) * (1.0 - fy) - + p10 * fx * (1.0 - fy) - + p01 * (1.0 - fx) * fy - + p11 * fx * fy; - - output.put_pixel(out_x, out_y, Luma([val.round().clamp(0.0, 255.0) as u8])); - } else { - output.put_pixel(out_x, out_y, Luma([255])); // White padding - } - } - } - - output -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_median_odd() { - let v = vec![1.0, 3.0, 5.0]; - assert!((median(&v) - 3.0).abs() < 0.001); - } - - #[test] - fn test_median_even() { - let v = vec![1.0, 2.0, 3.0, 4.0]; - assert!((median(&v) - 2.5).abs() < 0.001); - } - - #[test] - fn test_empty() { - assert!((median(&[]) - 0.0).abs() < 0.001); - } -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/scanner/edge.rs b/apps/tools/backend/workers/src/scanner/edge.rs deleted file mode 100644 index d5bca26..0000000 --- a/apps/tools/backend/workers/src/scanner/edge.rs +++ /dev/null @@ -1,77 +0,0 @@ -use image::{GrayImage}; -use imageproc::edges::canny; -use imageproc::filter::gaussian_blur_f32; -use imageproc::distance_transform::Norm; -use imageproc::morphology::close; - -use tools_common::error::PipelineError; - -/// Detect edges using Canny algorithm with adaptive threshold. -pub fn detect_edges(img: &GrayImage) -> Result { - // 1. Gaussian blur for noise reduction - let blurred = gaussian_blur_f32(img, 3.0); - - // 2. First attempt: Canny with standard thresholds - let edges = canny(&blurred, 50.0, 150.0); - - // 3. Morphological close to connect broken edges - let closed = close(&edges, Norm::L1, 5); - - // 4. Check edge coverage - let edge_count = count_non_zero(&closed); - let total_pixels = (closed.width() * closed.height()) as u32; - - // If too few edges (<1%), retry with lower thresholds - if edge_count < total_pixels / 100 { - let edges2 = canny(&blurred, 20.0, 80.0); - let closed2 = close(&edges2, Norm::L1, 5); - let edge_count2 = count_non_zero(&closed2); - - if edge_count2 < total_pixels / 200 { - return Err(PipelineError::EdgeDetection( - "Too few edges detected even with low threshold".to_string(), - )); - } - return Ok(closed2); - } - - Ok(closed) -} - -/// Count non-zero (white) pixels in a binary image. -fn count_non_zero(img: &GrayImage) -> u32 { - let mut count = 0u32; - for pixel in img.iter() { - if *pixel > 0 { - count += 1; - } - } - count -} - -#[cfg(test)] -mod tests { - use super::*; - use image::Luma; - - #[test] - fn test_edge_detection_on_simple_image() { - let mut img = GrayImage::new(200, 200); - for y in 30..170 { - for x in 30..170 { - img.put_pixel(x, y, Luma([255])); - } - } - let result = detect_edges(&img); - assert!(result.is_ok()); - let edges = result.unwrap(); - assert!(count_non_zero(&edges) > 0); - } - - #[test] - fn test_empty_image_returns_error() { - let img = GrayImage::new(100, 100); - let result = detect_edges(&img); - assert!(result.is_err()); - } -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/scanner/enhance.rs b/apps/tools/backend/workers/src/scanner/enhance.rs deleted file mode 100644 index 42ea097..0000000 --- a/apps/tools/backend/workers/src/scanner/enhance.rs +++ /dev/null @@ -1,134 +0,0 @@ -use image::{GrayImage, Luma}; -use imageproc::filter::gaussian_blur_f32; - -/// Apply final sharpening and contrast optimization. -pub fn enhance_final(img: &GrayImage) -> GrayImage { - let sharpened = unsharp_mask(img, 1.0, 1.0); - adjust_contrast(&sharpened, 1.2) -} - -/// Unsharp mask: add high-frequency detail back to the image. -/// result = img + amount * (img - blurred) -pub fn unsharp_mask(img: &GrayImage, sigma: f64, amount: f64) -> GrayImage { - let (w, h) = (img.width(), img.height()); - let blurred = gaussian_blur_f32(img, sigma as f32); - - let mut output = GrayImage::new(w, h); - for y in 0..h { - for x in 0..w { - let orig = img.get_pixel(x, y)[0] as f64; - let blur = blurred.get_pixel(x, y)[0] as f64; - let mask = orig - blur; - let result = (orig + amount * mask).clamp(0.0, 255.0) as u8; - output.put_pixel(x, y, Luma([result])); - } - } - output -} - -/// Adjust contrast by scaling pixel values around the mean. -pub fn adjust_contrast(img: &GrayImage, factor: f64) -> GrayImage { - let (w, h) = (img.width(), img.height()); - let mean = mean_value(img); - - let mut output = GrayImage::new(w, h); - for y in 0..h { - for x in 0..w { - let pixel = img.get_pixel(x, y)[0] as f64; - let adjusted = ((pixel - mean) * factor + mean).clamp(0.0, 255.0) as u8; - output.put_pixel(x, y, Luma([adjusted])); - } - } - output -} - -/// Remove salt-and-pepper noise using a median-like filter. -#[allow(dead_code)] -pub fn remove_noise(img: &GrayImage, threshold: u8) -> GrayImage { - let (w, h) = (img.width(), img.height()); - let mut output = GrayImage::new(w, h); - - for y in 1..h - 1 { - for x in 1..w - 1 { - let center = img.get_pixel(x, y)[0]; - // Check if pixel is significantly different from neighbors - let mut neighbors = Vec::new(); - for dy in -1i32..=1 { - for dx in -1i32..=1 { - if dx == 0 && dy == 0 { - continue; - } - neighbors.push( - img.get_pixel((x as i32 + dx) as u32, (y as i32 + dy) as u32)[0], - ); - } - } - let min = *neighbors.iter().min().unwrap_or(&0); - let max = *neighbors.iter().max().unwrap_or(&255); - - if (center as i16 - min as i16).abs() > threshold as i16 - || (center as i16 - max as i16).abs() > threshold as i16 - { - // Replace with median - neighbors.sort(); - output.put_pixel(x, y, Luma([neighbors[neighbors.len() / 2]])); - } else { - output.put_pixel(x, y, Luma([center])); - } - } - } - - // Copy edges - for x in 0..w { - output.put_pixel(x, 0, *img.get_pixel(x, 0)); - output.put_pixel(x, h - 1, *img.get_pixel(x, h - 1)); - } - for y in 0..h { - output.put_pixel(0, y, *img.get_pixel(0, y)); - output.put_pixel(w - 1, y, *img.get_pixel(w - 1, y)); - } - - output -} - -/// Compute mean pixel value. -fn mean_value(img: &GrayImage) -> f64 { - let sum: u64 = img.iter().map(|&p| p as u64).sum(); - let count = img.width() as u64 * img.height() as u64; - if count > 0 { - sum as f64 / count as f64 - } else { - 128.0 - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_unsharp_mask_no_change() { - // Uniform image should remain unchanged - let img = GrayImage::from_pixel(50, 50, Luma([128])); - let result = unsharp_mask(&img, 1.0, 0.0); - assert_eq!(result.get_pixel(25, 25)[0], 128); - } - - #[test] - fn test_contrast_increase() { - let mut img = GrayImage::new(10, 10); - img.put_pixel(0, 0, Luma([100])); - img.put_pixel(1, 0, Luma([200])); - let result = adjust_contrast(&img, 2.0); - // With factor > 1, contrast increases - let diff_orig = (200 - 100) as f64; - let diff_result = (result.get_pixel(1, 0)[0] as f64) - (result.get_pixel(0, 0)[0] as f64); - // The difference after contrast adjustment should be greater than original - assert!( - diff_result.abs() > diff_orig.abs() * 0.5, - "diff_orig={}, diff_result={}", - diff_orig, - diff_result - ); - } -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/scanner/mod.rs b/apps/tools/backend/workers/src/scanner/mod.rs deleted file mode 100644 index 3fd35a0..0000000 --- a/apps/tools/backend/workers/src/scanner/mod.rs +++ /dev/null @@ -1,80 +0,0 @@ -pub mod binarize; -pub mod corners; -pub mod deskew; -pub mod edge; -pub mod enhance; -pub mod ocr; -pub mod pdf; -pub mod pipeline; -pub mod preprocess; -pub mod shadow; -pub mod warp; - -use crate::config::WorkerConfig; -use crate::nats::progress::ProgressReporter; -use tools_common::types::{Job, JobStatus, Tool}; - -/// Process a scan job through the full pipeline. -pub async fn process_job( - job: Job, - redis: &redis::Client, - config: &WorkerConfig, -) -> Result<(), Box> { - tracing::info!(job_id = %job.id, "Processing scan job"); - - let nats = async_nats::connect(&config.nats_url).await?; - let progress = ProgressReporter::new(redis.clone(), nats, job.id, Tool::Scan); - - progress - .report( - JobStatus::Processing { - stage: "preprocess".to_string(), - progress: 5, - }, - "preprocess", - 5, - "Memproses gambar...", - ) - .await?; - - let result = pipeline::process(&job, config, &progress).await; - - match result { - Ok(scan_result) => { - progress - .report(JobStatus::Completed, "complete", 100, "Scan selesai") - .await?; - - let mut conn = redis.get_multiplexed_async_connection().await?; - crate::nats::consumer::JobConsumer::update_job_result( - &mut conn, - job.id, - &scan_result.output_path, - job.ttl_seconds, - ) - .await?; - - tracing::info!( - job_id = %job.id, - output = %scan_result.output_path, - duration_ms = %scan_result.processing_time_ms, - "Scan job completed" - ); - - Ok(()) - } - Err(e) => { - progress - .report( - JobStatus::Failed(e.to_string()), - "error", - 0, - &format!("Gagal: {}", e), - ) - .await?; - - tracing::error!(job_id = %job.id, error = %e, "Scan job failed"); - Err(e) - } - } -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/scanner/ocr.rs b/apps/tools/backend/workers/src/scanner/ocr.rs deleted file mode 100644 index 20c57f1..0000000 --- a/apps/tools/backend/workers/src/scanner/ocr.rs +++ /dev/null @@ -1,112 +0,0 @@ -use image::GrayImage; - -use tools_common::error::PipelineError; - -/// OCR result with text and word-level bounding boxes. -pub struct OcrResult { - pub full_text: String, - pub words: Vec, - pub confidence: f32, -} - -/// A single word detected by OCR with its bounding box. -#[derive(Debug, Clone)] -pub struct OcrWord { - pub text: String, - pub bbox: Bbox, - pub confidence: i32, -} - -/// Bounding box coordinates. -#[derive(Debug, Clone)] -pub struct Bbox { - pub x: i32, - pub y: i32, - pub width: i32, - pub height: i32, -} - -/// Initialize Tesseract OCR engine. -/// Uses leptess crate which binds to libtesseract. -/// Falls back gracefully if Tesseract is not installed. -#[cfg(feature = "tesseract")] -fn init_tesseract(lang: &str) -> Result { - let tessdata_prefix = std::env::var("TESSDATA_PREFIX") - .unwrap_or_else(|_| "/usr/share/tesseract-ocr/5/tessdata".to_string()); - - let mut tess = leptess::LepTess::new(Some(&tessdata_prefix), lang) - .map_err(|e| PipelineError::Ocr(format!("Failed to init Tesseract: {}", e)))?; - - Ok(tess) -} - -/// Run OCR on a grayscale image and return extracted text. -/// Uses Tesseract via leptess crate when the "tesseract" feature is enabled. -/// Falls back to a placeholder when Tesseract is unavailable. -pub fn ocr_text(img: &GrayImage, lang: &str) -> Result { - #[cfg(feature = "tesseract")] - { - let mut tess = init_tesseract(lang)?; - - let width = img.width() as i32; - let height = img.height() as i32; - - // Set image from memory - tess.set_image_from_mem(&img.to_vec(), width, height, 1, width) - .map_err(|e| PipelineError::Ocr(format!("Failed to set image: {}", e)))?; - - tess.set_source_resolution(300); - - // Set PSM to automatic - tess.set_page_seg_mode(3); - - let text = tess.get_utf8_text() - .map_err(|e| PipelineError::Ocr(format!("OCR failed: {}", e)))?; - - let words = tess.get_words() - .iter() - .map(|w| OcrWord { - text: w.text.clone(), - bbox: Bbox { - x: w.x, - y: w.y, - width: w.w, - height: w.h, - }, - confidence: w.confidence, - }) - .collect(); - - let confidence = if words.is_empty() { - 0.0 - } else { - words.iter().map(|w| w.confidence as f32).sum::() / words.len() as f32 - }; - - Ok(OcrResult { - full_text: text, - words, - confidence, - }) - } - - #[cfg(not(feature = "tesseract"))] - { - tracing::warn!("Tesseract feature not enabled, OCR returning placeholder"); - Ok(OcrResult { - full_text: String::new(), - words: Vec::new(), - confidence: 0.0, - }) - } -} - -/// Run OCR on a grayscale image, returning only the text. -pub fn ocr_text_only(img: &GrayImage, lang: &str) -> Result { - ocr_text(img, lang).map(|r| r.full_text) -} - -/// Run OCR with word-level bounding boxes. -pub fn ocr_words(img: &GrayImage, lang: &str) -> Result, PipelineError> { - ocr_text(img, lang).map(|r| r.words) -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/scanner/pdf.rs b/apps/tools/backend/workers/src/scanner/pdf.rs deleted file mode 100644 index 969ffee..0000000 --- a/apps/tools/backend/workers/src/scanner/pdf.rs +++ /dev/null @@ -1,190 +0,0 @@ -use image::GrayImage; -use lopdf::{Document, Object, Stream, Dictionary}; - -use tools_common::error::PipelineError; - -/// A4 page dimensions in points (1 pt = 1/72 inch). -pub const A4_WIDTH_PT: f64 = 595.28; -pub const A4_HEIGHT_PT: f64 = 841.89; - -/// Generate a searchable PDF with JPEG image + invisible OCR text layer. -pub fn generate_searchable_pdf( - image_data: &[u8], - _ocr_text: &str, - words: &[super::ocr::OcrWord], - page_width: f64, - page_height: f64, -) -> Result, PipelineError> { - let mut doc = Document::new(); - - // ── Pages object ── - let pages_id = doc.new_object_id(); - let mut pages = Dictionary::new(); - pages.set("Type", Object::Name("Pages".as_bytes().to_vec())); - pages.set("Kids", Object::Array(vec![])); - pages.set("Count", Object::Integer(0)); - doc.objects.insert(pages_id, Object::Dictionary(pages)); - - // ── Image XObject ── - let mut img_dict = Dictionary::new(); - img_dict.set("Type", Object::Name("XObject".as_bytes().to_vec())); - img_dict.set("Subtype", Object::Name("Image".as_bytes().to_vec())); - img_dict.set("Width", Object::Integer(page_width as i64)); - img_dict.set("Height", Object::Integer(page_height as i64)); - img_dict.set("ColorSpace", Object::Name("DeviceGray".as_bytes().to_vec())); - img_dict.set("BitsPerComponent", Object::Integer(8)); - img_dict.set("Filter", Object::Name("DCTDecode".as_bytes().to_vec())); - - let image_stream = Stream::new(img_dict, image_data.to_vec()); - let image_id = doc.add_object(Object::Stream(image_stream)); - - // ── Content stream: place image + invisible text ── - let mut content = Vec::new(); - - // Place image at full page - content.extend_from_slice(b"q\n"); - content.extend_from_slice( - format!("{} 0 0 {} 0 0 cm\n", page_width, page_height).as_bytes(), - ); - content.extend_from_slice(b"/Im0 Do\n"); - content.extend_from_slice(b"Q\n"); - - // Add invisible text layer (searchable) - for word in words { - let x = word.bbox.x as f64 / 300.0 * 72.0; - let y = page_height - (word.bbox.y as f64 / 300.0 * 72.0); - let font_size = (word.bbox.height as f64 / 300.0 * 72.0 * 0.8).max(4.0); - - content.extend_from_slice(b"BT\n"); - content.extend_from_slice(b"3 Tr\n"); // Rendering mode: invisible (neither fill nor stroke) - content.extend_from_slice( - format!("/F1 {} Tf\n{} {} Td\n", font_size, x, y - font_size).as_bytes(), - ); - content.extend_from_slice( - format!("({}) Tj\n", escape_pdf_string(&word.text)).as_bytes(), - ); - content.extend_from_slice(b"ET\n"); - } - - let content_stream = Stream::new(Dictionary::new(), content); - let content_id = doc.add_object(Object::Stream(content_stream)); - - // ── Font dictionary ── - let mut font_dict = Dictionary::new(); - let mut f1 = Dictionary::new(); - f1.set("Type", Object::Name("Font".as_bytes().to_vec())); - f1.set("Subtype", Object::Name("Type1".as_bytes().to_vec())); - f1.set("BaseFont", Object::Name("Helvetica".as_bytes().to_vec())); - font_dict.set("F1", Object::Dictionary(f1)); - - // ── Resources dictionary ── - let mut xobject_dict = Dictionary::new(); - xobject_dict.set("Im0", Object::Reference(image_id)); - - let mut resources = Dictionary::new(); - resources.set("XObject", Object::Dictionary(xobject_dict)); - resources.set("Font", Object::Dictionary(font_dict)); - - // ── Page object ── - let page_id = doc.new_object_id(); - let mut page = Dictionary::new(); - page.set("Type", Object::Name("Page".as_bytes().to_vec())); - page.set("Parent", Object::Reference(pages_id)); - page.set( - "MediaBox", - Object::Array(vec![ - Object::Real(0.0), - Object::Real(0.0), - Object::Real(page_width as f32), - Object::Real(page_height as f32), - ]), - ); - page.set("Contents", Object::Reference(content_id)); - page.set("Resources", Object::Dictionary(resources)); - - doc.objects.insert(page_id, Object::Dictionary(page)); - - // ── Update pages object ── - if let Some(Object::Dictionary(ref mut pages_dict)) = doc.objects.get_mut(&pages_id) { - pages_dict.set("Count", Object::Integer(1)); - pages_dict.set("Kids", Object::Array(vec![Object::Reference(page_id)])); - } - - // ── Save ── - let mut output = Vec::new(); - doc.save_to(&mut output) - .map_err(|e| PipelineError::PdfGeneration(e.to_string()))?; - - Ok(output) -} - -/// Escape special characters for PDF string literals. -fn escape_pdf_string(s: &str) -> String { - let mut result = String::with_capacity(s.len()); - for c in s.chars() { - match c { - '(' => result.push_str("\\("), - ')' => result.push_str("\\)"), - '\\' => result.push_str("\\\\"), - '\n' => result.push_str("\\n"), - '\r' => result.push_str("\\r"), - '\t' => result.push_str("\\t"), - other => result.push(other), - } - } - result -} - -/// Compress grayscale image as JPEG bytes. -pub fn compress_image_jpeg(img: &GrayImage, quality: u8) -> Result, PipelineError> { - let mut bytes = Vec::new(); - let rgb = image::DynamicImage::ImageLuma8(img.clone()).into_rgb8(); - let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut bytes, quality); - encoder - .encode( - rgb.as_raw(), - img.width(), - img.height(), - image::ExtendedColorType::Rgb8, - ) - .map_err(|e| PipelineError::PdfGeneration(format!("JPEG compression failed: {}", e)))?; - Ok(bytes) -} - -/// Compress RGB image data as JPEG bytes. -pub fn compress_rgb_image_jpeg( - data: &[u8], - width: u32, - height: u32, - quality: u8, -) -> Result, PipelineError> { - let mut bytes = Vec::new(); - let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut bytes, quality); - encoder - .encode(data, width, height, image::ExtendedColorType::Rgb8) - .map_err(|e| PipelineError::PdfGeneration(format!("JPEG compression failed: {}", e)))?; - Ok(bytes) -} - -#[cfg(test)] -mod tests { - use super::*; - use image::Luma; - - #[test] - fn test_escape_pdf_string() { - assert_eq!(escape_pdf_string("hello"), "hello"); - assert_eq!(escape_pdf_string("(parens)"), "\\(parens\\)"); - assert_eq!(escape_pdf_string("back\\slash"), "back\\\\slash"); - } - - #[test] - fn test_jpeg_compression() { - let img = GrayImage::from_pixel(100, 100, Luma([128])); - let result = compress_image_jpeg(&img, 90); - assert!(result.is_ok(), "JPEG compression failed: {:?}", result.err()); - let bytes = result.unwrap(); - assert!(!bytes.is_empty()); - assert_eq!(&bytes[0..2], &[0xFF, 0xD8]); - } -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/scanner/pipeline.rs b/apps/tools/backend/workers/src/scanner/pipeline.rs deleted file mode 100644 index 6ac6eed..0000000 --- a/apps/tools/backend/workers/src/scanner/pipeline.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::path::Path; -use std::time::Instant; - -use image::DynamicImage; -use tools_common::error::PipelineError; -use tools_common::types::Job; - -use crate::config::WorkerConfig; -use crate::nats::progress::ProgressReporter; - -use super::binarize::sauvola_threshold; -use super::corners::detect_corners_with_fallback; -use super::deskew::deskew; -use super::edge::detect_edges; -use super::enhance::enhance_final; -use super::preprocess::preprocess; -use super::shadow::remove_shadow; -use super::warp::warp_perspective; - -/// Result of the scanning pipeline. -pub struct ScanResult { - pub output_path: String, - pub page_count: u32, - pub file_size: u64, - pub ocr_text: Option, - pub processing_time_ms: u64, -} - -/// Run the full scanner pipeline with all stages. -pub async fn process( - job: &Job, - config: &WorkerConfig, - progress: &ProgressReporter, -) -> Result> { - let start = Instant::now(); - let input_path = Path::new(&job.file_path); - - // Create output directory - let output_dir = config.storage_path.join("output"); - tokio::fs::create_dir_all(&output_dir).await?; - - // Stage 1: Load & Preprocess (0-15%) - report(progress, "preprocess", 5, "Memuat dan meresize gambar...").await; - let gray = preprocess(input_path) - .map_err(|e| format!("Preprocess failed: {}", e))?; - - // Stage 2: Edge Detection (15-30%) - report(progress, "edge_detection", 20, "Mendeteksi tepi dokumen...").await; - let edges = detect_edges(&gray).map_err(|e| format!("Edge detection failed: {}", e))?; - - // Stage 3: Corner Detection (30-40%) - report(progress, "corner_detection", 35, "Mencari sudut dokumen...").await; - let corners = detect_corners_with_fallback(&edges)?; - - // Stage 4: Perspective Warp (40-55%) - report(progress, "warp", 45, "Meluruskan perspektif dokumen...").await; - let image = image::open(input_path) - .map_err(|e| PipelineError::ImageLoad(e.to_string()))?; - let warped = warp_perspective(&image, corners)?; - - // Stage 5: Shadow Removal (55-70%) - report(progress, "shadow_removal", 60, "Menghilangkan bayangan...").await; - let warped_gray = warped.to_luma8(); - let clean = remove_shadow(&warped_gray); - - // Stage 6: Binarization (70-80%) - report(progress, "binarization", 75, "Mengubah ke hitam-putih...").await; - let binary = sauvola_threshold(&clean, 30, 0.2); - - // Stage 7: Deskew (80-87%) - report(progress, "deskew", 82, "Meluruskan teks...").await; - let final_img = deskew(&binary); - - // Stage 8: Enhance (87-93%) - report(progress, "enhance", 90, "Mengoptimalkan kualitas...").await; - let final_img = enhance_final(&final_img); - - // Stage 9: Save output (93-100%) - report(progress, "save", 95, "Menyimpan hasil...").await; - - let output_filename = format!("{}.png", progress.job_id()); - let output_path = output_dir.join(&output_filename); - - final_img.save(&output_path)?; - - let elapsed = start.elapsed().as_millis() as u64; - - tracing::info!( - job_id = %progress.job_id(), - duration_ms = elapsed, - "Pipeline complete" - ); - - Ok(ScanResult { - output_path: output_path.to_string_lossy().to_string(), - page_count: 1, - file_size: tokio::fs::metadata(&output_path).await.map(|m| m.len()).unwrap_or(0), - ocr_text: None, - processing_time_ms: elapsed, - }) -} - -/// Helper to report progress. -async fn report(progress: &ProgressReporter, stage: &str, pct: u8, msg: &str) { - let _ = progress - .report( - tools_common::types::JobStatus::Processing { - stage: stage.to_string(), - progress: pct, - }, - stage, - pct, - msg, - ) - .await; -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/scanner/preprocess.rs b/apps/tools/backend/workers/src/scanner/preprocess.rs deleted file mode 100644 index 4519cb8..0000000 --- a/apps/tools/backend/workers/src/scanner/preprocess.rs +++ /dev/null @@ -1,74 +0,0 @@ -use image::{DynamicImage, GrayImage, Luma}; -use image::imageops::FilterType; - -use tools_common::error::PipelineError; - -/// Maximum dimension for processing (edge detection works fine at this resolution). -const MAX_DIMENSION: u32 = 2000; - -/// Load image from file path. -pub fn load_image(path: &std::path::Path) -> Result { - image::open(path).map_err(|e| PipelineError::ImageLoad(e.to_string())) -} - -/// Resize image if it exceeds the maximum dimension, preserving aspect ratio. -/// Uses Lanczos3 filter for sharpest downscale. -pub fn safe_resize(img: &DynamicImage) -> DynamicImage { - let (w, h) = (img.width(), img.height()); - let max_dim = w.max(h) as f64; - - if max_dim > MAX_DIMENSION as f64 { - let scale = MAX_DIMENSION as f64 / max_dim; - let new_w = (w as f64 * scale) as u32; - let new_h = (h as f64 * scale) as u32; - img.resize_exact(new_w.max(1), new_h.max(1), FilterType::Lanczos3) - } else { - img.clone() - } -} - -/// Convert to grayscale (Luma8). -pub fn to_grayscale(img: &DynamicImage) -> GrayImage { - img.to_luma8() -} - -/// Full preprocess pipeline: load → resize → grayscale. -pub fn preprocess(path: &std::path::Path) -> Result { - let img = load_image(path)?; - let resized = safe_resize(&img); - Ok(to_grayscale(&resized)) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_safe_resize_no_resize() { - // Image smaller than MAX_DIMENSION should not be resized - let img = DynamicImage::new_luma8(800, 600); - let result = safe_resize(&img); - assert_eq!(result.width(), 800); - assert_eq!(result.height(), 600); - } - - #[test] - fn test_safe_resize_downscale() { - // 12MP image (4000x3000) should be resized to ≤2000px - let img = DynamicImage::new_luma8(4000, 3000); - let result = safe_resize(&img); - assert!(result.width() <= 2000); - assert!(result.height() <= 2000); - // Aspect ratio preserved: 4000/3000 = 1.333 - let ratio = result.width() as f64 / result.height() as f64; - assert!((ratio - 4.0 / 3.0).abs() < 0.01); - } - - #[test] - fn test_to_grayscale() { - let img = DynamicImage::new_rgba8(100, 100); - let gray = to_grayscale(&img); - assert_eq!(gray.width(), 100); - assert_eq!(gray.height(), 100); - } -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/scanner/shadow.rs b/apps/tools/backend/workers/src/scanner/shadow.rs deleted file mode 100644 index aa0b0ad..0000000 --- a/apps/tools/backend/workers/src/scanner/shadow.rs +++ /dev/null @@ -1,161 +0,0 @@ -use image::{GrayImage, Luma}; -use imageproc::filter::gaussian_blur_f32; - -/// Remove uneven lighting and shadows from a grayscale document image. -/// -/// Algorithm: -/// 1. Large Gaussian blur to estimate background illumination -/// 2. Subtract background from original -/// 3. Apply CLAHE for local contrast normalization -pub fn remove_shadow(img: &GrayImage) -> GrayImage { - let (w, h) = (img.width(), img.height()); - - // 1. Large Gaussian blur for illumination estimate - let blur_radius = (w.min(h) as f64 / 50.0).max(15.0); - let background = gaussian_blur_f32(img, blur_radius as f32); - - // 2. Subtract background - let bg_mean = mean_pixel(&background); - let mut corrected = GrayImage::new(w, h); - - for y in 0..h { - for x in 0..w { - let orig = img.get_pixel(x, y)[0] as f32; - let bg = background.get_pixel(x, y)[0] as f32; - let corrected_val = (orig - bg + bg_mean).clamp(0.0, 255.0) as u8; - corrected.put_pixel(x, y, Luma([corrected_val])); - } - } - - // 3. Apply CLAHE - apply_clahe(&corrected, 8, 4) -} - -/// Compute mean pixel value of a grayscale image. -fn mean_pixel(img: &GrayImage) -> f32 { - let sum: u32 = img.iter().map(|&p| p as u32).sum(); - let count = img.width() * img.height(); - if count > 0 { - sum as f32 / count as f32 - } else { - 0.0 - } -} - -/// Contrast Limited Adaptive Histogram Equalization. -/// Divides the image into tiles and applies histogram equalization to each. -fn apply_clahe(img: &GrayImage, tile_size: u32, clip_limit: u8) -> GrayImage { - let (w, h) = (img.width(), img.height()); - let tiles_x = (w + tile_size - 1) / tile_size; - let tiles_y = (h + tile_size - 1) / tile_size; - - let mut output = GrayImage::new(w, h); - - for ty in 0..tiles_y { - for tx in 0..tiles_x { - let start_x = tx * tile_size; - let start_y = ty * tile_size; - let end_x = (start_x + tile_size).min(w); - let end_y = (start_y + tile_size).min(h); - - // Compute histogram for this tile - let mut hist = [0u32; 256]; - for y in start_y..end_y { - for x in start_x..end_x { - hist[img.get_pixel(x, y)[0] as usize] += 1; - } - } - - // Clip histogram - let tile_pixels = (end_x - start_x) * (end_y - start_y); - let clip_limit_count = tile_pixels as u32 * clip_limit as u32 / 255 / 10; - let mut excess = 0u32; - for count in hist.iter_mut() { - if *count > clip_limit_count { - excess += *count - clip_limit_count; - *count = clip_limit_count; - } - } - // Redistribute excess - let add_per_bin = excess / 256; - for count in hist.iter_mut() { - *count += add_per_bin; - } - - // Build CDF - let mut cdf = [0u32; 256]; - cdf[0] = hist[0]; - for i in 1..256 { - cdf[i] = cdf[i - 1] + hist[i]; - } - let cdf_min = cdf.iter().find(|&&v| v > 0).copied().unwrap_or(0); - - // Apply equalization to this tile - for y in start_y..end_y { - for x in start_x..end_x { - let pixel = img.get_pixel(x, y)[0] as usize; - let equalized = if cdf_max(cdf) > cdf_min { - ((cdf[pixel].saturating_sub(cdf_min)) as f64 - / (cdf_max(cdf).saturating_sub(cdf_min)) as f64 - * 255.0) as u8 - } else { - pixel as u8 - }; - output.put_pixel(x, y, Luma([equalized])); - } - } - } - } - - output -} - -/// Get the maximum value in the CDF array. -fn cdf_max(cdf: [u32; 256]) -> u32 { - *cdf.iter().max().unwrap_or(&0) -} - -/// Retinex-based shadow removal (alternative algorithm). -#[allow(dead_code)] -fn retinex_shadow_removal(img: &GrayImage) -> GrayImage { - let (w, h) = (img.width(), img.height()); - let blurred = gaussian_blur_f32(img, 30.0); - - let mut output = GrayImage::new(w, h); - for y in 0..h { - for x in 0..w { - let orig = img.get_pixel(x, y)[0] as f32; - let bg = blurred.get_pixel(x, y)[0] as f32; - if bg > 0.0 { - let retinex = (orig / bg).ln() * 255.0; - output.put_pixel(x, y, Luma([retinex.clamp(0.0, 255.0) as u8])); - } else { - output.put_pixel(x, y, Luma([0])); - } - } - } - output -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_shadow_removal_uniform() { - // Uniform image should remain uniform - let img = GrayImage::from_pixel(100, 100, Luma([128])); - let result = remove_shadow(&img); - assert_eq!(result.width(), 100); - assert_eq!(result.height(), 100); - // The result should have fewer dark pixels than a shadowed version - let dark_count = result.iter().filter(|&&p| p < 50).count(); - assert!(dark_count < 100); // Very few dark pixels - } - - #[test] - fn test_mean_pixel() { - let img = GrayImage::from_pixel(10, 10, Luma([100])); - assert!((mean_pixel(&img) - 100.0).abs() < 1.0); - } -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/scanner/warp.rs b/apps/tools/backend/workers/src/scanner/warp.rs deleted file mode 100644 index 930209e..0000000 --- a/apps/tools/backend/workers/src/scanner/warp.rs +++ /dev/null @@ -1,208 +0,0 @@ -use image::{DynamicImage, GrayImage, Luma}; -use nalgebra::{Matrix3, SVD}; - -use tools_common::error::PipelineError; - -use crate::scanner::corners::CornerPoint; - -/// Compute homography matrix from 4 point correspondences using DLT algorithm. -pub fn compute_homography( - src: &[CornerPoint; 4], - dst: &[CornerPoint; 4], -) -> Result<[[f64; 3]; 3], PipelineError> { - // Build 8x9 matrix A from 4 point correspondences - // Each correspondence (x,y) -> (x',y') gives 2 rows: - // [-x, -y, -1, 0, 0, 0, x*x', y*x', x'] - // [ 0, 0, 0, -x, -y, -1, x*y', y*y', y'] - let mut a = nalgebra::DMatrix::::zeros(8, 9); - - for i in 0..4 { - let x = src[i].0; - let y = src[i].1; - let xp = dst[i].0; - let yp = dst[i].1; - - // First row - a[(i * 2, 0)] = -x; - a[(i * 2, 1)] = -y; - a[(i * 2, 2)] = -1.0; - a[(i * 2, 3)] = 0.0; - a[(i * 2, 4)] = 0.0; - a[(i * 2, 5)] = 0.0; - a[(i * 2, 6)] = x * xp; - a[(i * 2, 7)] = y * xp; - a[(i * 2, 8)] = xp; - - // Second row - a[(i * 2 + 1, 0)] = 0.0; - a[(i * 2 + 1, 1)] = 0.0; - a[(i * 2 + 1, 2)] = 0.0; - a[(i * 2 + 1, 3)] = -x; - a[(i * 2 + 1, 4)] = -y; - a[(i * 2 + 1, 5)] = -1.0; - a[(i * 2 + 1, 6)] = x * yp; - a[(i * 2 + 1, 7)] = y * yp; - a[(i * 2 + 1, 8)] = yp; - } - - // Solve Ah = 0 via SVD: h = last column of V - let svd = SVD::new(a, true, true); - if let Some(v_t) = &svd.v_t { - let nrows = v_t.nrows(); - if nrows > 0 { - let h_vec: Vec = v_t.row(nrows - 1).iter().copied().collect(); - if h_vec.len() >= 9 { - let h = [ - [h_vec[0], h_vec[1], h_vec[2]], - [h_vec[3], h_vec[4], h_vec[5]], - [h_vec[6], h_vec[7], h_vec[8]], - ]; - return Ok(h); - } - } - } - - Err(PipelineError::Warp("SVD decomposition failed".to_string())) -} - -/// Invert a 3x3 homography matrix. -pub fn invert_homography(h: &[[f64; 3]; 3]) -> [[f64; 3]; 3] { - let m = Matrix3::new(h[0][0], h[0][1], h[0][2], h[1][0], h[1][1], h[1][2], h[2][0], h[2][1], h[2][2]); - let inv = m - .try_inverse() - .unwrap_or(Matrix3::identity()); - [ - [inv[(0, 0)], inv[(0, 1)], inv[(0, 2)]], - [inv[(1, 0)], inv[(1, 1)], inv[(1, 2)]], - [inv[(2, 0)], inv[(2, 1)], inv[(2, 2)]], - ] -} - -/// Apply homography to a point (forward mapping). -pub fn apply_homography(h: &[[f64; 3]; 3], x: f64, y: f64) -> (f64, f64) { - let z = h[2][0] * x + h[2][1] * y + h[2][2]; - if z.abs() < 1e-10 { - return (x, y); - } - let xp = (h[0][0] * x + h[0][1] * y + h[0][2]) / z; - let yp = (h[1][0] * x + h[1][1] * y + h[1][2]) / z; - (xp, yp) -} - -/// Bilinear interpolation at sub-pixel coordinates. -fn bilinear_interpolate(img: &GrayImage, x: f64, y: f64) -> Luma { - let x0 = x.floor() as i32; - let y0 = y.floor() as i32; - let x1 = x0 + 1; - let y1 = y0 + 1; - - let w = img.width() as i32; - let h = img.height() as i32; - - // Clamp coordinates - let x0 = x0.clamp(0, w - 1); - let x1 = x1.clamp(0, w - 1); - let y0 = y0.clamp(0, h - 1); - let y1 = y1.clamp(0, h - 1); - - let fx = x - x0 as f64; - let fy = y - y0 as f64; - - let p00 = img.get_pixel(x0 as u32, y0 as u32)[0] as f64; - let p10 = img.get_pixel(x1 as u32, y0 as u32)[0] as f64; - let p01 = img.get_pixel(x0 as u32, y1 as u32)[0] as f64; - let p11 = img.get_pixel(x1 as u32, y1 as u32)[0] as f64; - - let val = p00 * (1.0 - fx) * (1.0 - fy) - + p10 * fx * (1.0 - fy) - + p01 * (1.0 - fx) * fy - + p11 * fx * fy; - - Luma([val.round().clamp(0.0, 255.0) as u8]) -} - -/// Apply perspective warp to correct the document perspective. -/// Takes the original color image and 4 corners, returns warped image. -pub fn warp_perspective( - img: &DynamicImage, - corners: [CornerPoint; 4], -) -> Result { - let [tl, tr, br, bl] = corners; - - // Compute target width and height (preserve aspect ratio) - let width_top = distance(tl, tr); - let width_bot = distance(bl, br); - let width = width_top.max(width_bot).ceil() as u32; - - let height_left = distance(tl, bl); - let height_right = distance(tr, br); - let height = height_left.max(height_right).ceil() as u32; - - // Clamp output dimensions - let width = width.min(3000).max(1); - let height = height.min(3000).max(1); - - let src = [tl, tr, br, bl]; - let dst = [ - (0.0, 0.0), - (width as f64, 0.0), - (width as f64, height as f64), - (0.0, height as f64), - ]; - - let h = compute_homography(&src, &dst)?; - let h_inv = invert_homography(&h); - - let gray = img.to_luma8(); - let mut output = GrayImage::new(width, height); - - // Backward mapping: for each output pixel, find source pixel - for y in 0..height { - for x in 0..width { - let (sx, sy) = apply_homography(&h_inv, x as f64, y as f64); - let pixel = bilinear_interpolate(&gray, sx, sy); - output.put_pixel(x, y, pixel); - } - } - - Ok(DynamicImage::ImageLuma8(output)) -} - -/// Euclidean distance between two points. -fn distance(a: CornerPoint, b: CornerPoint) -> f64 { - ((a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)).sqrt() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_homography_identity() { - // Identity mapping should produce identity matrix - let src = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)]; - let dst = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)]; - let h = compute_homography(&src, &dst).unwrap(); - let (xp, yp) = apply_homography(&h, 50.0, 50.0); - assert!((xp - 50.0).abs() < 1.0); - assert!((yp - 50.0).abs() < 1.0); - } - - #[test] - fn test_invert_homography() { - let h = [[2.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 1.0]]; - let inv = invert_homography(&h); - let (xp, yp) = apply_homography(&inv, 100.0, 100.0); - assert!((xp - 50.0).abs() < 0.001); - assert!((yp - 50.0).abs() < 0.001); - } - - #[test] - fn test_bilinear_interpolate() { - let mut img = GrayImage::new(3, 3); - img.put_pixel(0, 0, Luma([100])); - img.put_pixel(1, 0, Luma([200])); - let pixel = bilinear_interpolate(&img, 0.5, 0.0); - assert_eq!(pixel[0], 150); // Midpoint between 100 and 200 - } -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/scheduler/cleanup.rs b/apps/tools/backend/workers/src/scheduler/cleanup.rs deleted file mode 100644 index e06d2ae..0000000 --- a/apps/tools/backend/workers/src/scheduler/cleanup.rs +++ /dev/null @@ -1,81 +0,0 @@ -use redis::AsyncCommands; - -/// Cleanup expired files and Redis keys. -/// Scans storage directory and removes files older than TTL. -pub struct CleanupScheduler; - -impl CleanupScheduler { - /// Run a single cleanup cycle. - pub async fn run( - storage_path: &std::path::Path, - redis_client: &redis::Client, - ttl_seconds: u64, - ) -> Result> { - let mut result = CleanupResult::default(); - let now = std::time::SystemTime::now(); - - // Clean up upload files - let upload_dir = storage_path.join("upload"); - if upload_dir.exists() { - let mut entries = tokio::fs::read_dir(&upload_dir).await?; - while let Some(entry) = entries.next_entry().await? { - if let Ok(metadata) = entry.metadata().await { - if let Ok(modified) = metadata.modified() { - if now - .duration_since(modified) - .map(|d| d.as_secs() > ttl_seconds) - .unwrap_or(false) - { - if let Ok(_) = tokio::fs::remove_file(entry.path()).await { - result.files_deleted += 1; - result.bytes_freed += metadata.len(); - } - } - } - } - } - } - - // Clean up output files - let output_dir = storage_path.join("output"); - if output_dir.exists() { - let mut entries = tokio::fs::read_dir(&output_dir).await?; - while let Some(entry) = entries.next_entry().await? { - if let Ok(metadata) = entry.metadata().await { - if let Ok(modified) = metadata.modified() { - if now - .duration_since(modified) - .map(|d| d.as_secs() > ttl_seconds) - .unwrap_or(false) - { - if let Ok(_) = tokio::fs::remove_file(entry.path()).await { - result.files_deleted += 1; - result.bytes_freed += metadata.len(); - } - } - } - } - } - } - - // Clean up orphaned Redis keys - if let Ok(mut conn) = redis_client.get_multiplexed_async_connection().await { - // Scan for expired job keys - let _: Result<(), _> = redis::cmd("SCAN") - .arg(0) - .arg("MATCH") - .arg("job:*") - .query_async(&mut conn) - .await; - } - - Ok(result) - } -} - -#[derive(Debug, Default)] -pub struct CleanupResult { - pub files_deleted: u64, - pub bytes_freed: u64, - pub orphan_keys: u64, -} \ No newline at end of file diff --git a/apps/tools/backend/workers/src/scheduler/mod.rs b/apps/tools/backend/workers/src/scheduler/mod.rs deleted file mode 100644 index 08559a6..0000000 --- a/apps/tools/backend/workers/src/scheduler/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -/// Auto-cleanup scheduler for expired files and Redis keys. -/// TODO: Phase 1.4 - implement cleanup logic -pub mod cleanup; \ No newline at end of file diff --git a/apps/tools/backend/workers/src/video/mod.rs b/apps/tools/backend/workers/src/video/mod.rs deleted file mode 100644 index a2b155d..0000000 --- a/apps/tools/backend/workers/src/video/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -// Video processing module. -// TODO: Phase 4 - implement compress, extract audio, trim, GIF maker \ No newline at end of file diff --git a/apps/tools/frontend/biome.json b/apps/tools/frontend/biome.json deleted file mode 100644 index d984b2a..0000000 --- a/apps/tools/frontend/biome.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "extends": ["../../biome.json"] -} \ No newline at end of file diff --git a/apps/tools/frontend/components.json b/apps/tools/frontend/components.json deleted file mode 100644 index bf85cf5..0000000 --- a/apps/tools/frontend/components.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "base-nova", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/app/globals.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "iconLibrary": "lucide", - "rtl": false, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - } -} \ No newline at end of file diff --git a/apps/tools/frontend/next.config.ts b/apps/tools/frontend/next.config.ts deleted file mode 100644 index 67131f2..0000000 --- a/apps/tools/frontend/next.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - reactCompiler: true, - turbopack: { - root: process.cwd(), - }, -}; - -export default nextConfig; \ No newline at end of file diff --git a/apps/tools/frontend/package.json b/apps/tools/frontend/package.json deleted file mode 100644 index 7064d6a..0000000 --- a/apps/tools/frontend/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "tools-frontend", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev --port 3002", - "build": "next build", - "start": "next start", - "lint": "biome check", - "format": "biome format --write" - }, - "dependencies": { - "@shadcn/react": "^0.2.1", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "framer-motion": "^12.42.2", - "lucide-react": "^1.26.0", - "next": "16.2.11", - "next-themes": "^0.4.6", - "react": "19.2.8", - "react-dom": "19.2.8", - "sonner": "^2.0.7", - "tailwind-merge": "^3.6.0", - "tw-animate-css": "^1.4.0" - }, - "devDependencies": { - "@biomejs/biome": "2.5.5", - "@tailwindcss/postcss": "^4", - "@types/node": "^26", - "@types/react": "^19", - "@types/react-dom": "^19", - "tailwindcss": "^4", - "typescript": "^5.9.3" - } -} \ No newline at end of file diff --git a/apps/tools/frontend/postcss.config.mjs b/apps/tools/frontend/postcss.config.mjs deleted file mode 100644 index 5f04293..0000000 --- a/apps/tools/frontend/postcss.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -const config = { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; - -export default config; \ No newline at end of file diff --git a/apps/tools/frontend/public/manifest.json b/apps/tools/frontend/public/manifest.json deleted file mode 100644 index c53498d..0000000 --- a/apps/tools/frontend/public/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "Tools — Asep Haryana", - "short_name": "Tools", - "description": "Document Scanner, Image & PDF Tools", - "start_url": "/", - "display": "standalone", - "background_color": "#0a0a1a", - "theme_color": "#0a0a1a", - "icons": [ - { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, - { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" } - ] -} diff --git a/apps/tools/frontend/src/app/api/download/[id]/route.ts b/apps/tools/frontend/src/app/api/download/[id]/route.ts deleted file mode 100644 index 68cf115..0000000 --- a/apps/tools/frontend/src/app/api/download/[id]/route.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -const RUST_GATEWAY = process.env.RUST_GATEWAY_URL || "http://localhost:3001"; - -export async function GET( - _request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const { id } = await params; - try { - const response = await fetch(`${RUST_GATEWAY}/api/download/${id}`); - - if (!response.ok) { - const data = await response.json().catch(() => null); - return NextResponse.json( - data ?? { error: "Download failed" }, - { status: response.status }, - ); - } - - // Stream the file back - const blob = await response.blob(); - const contentType = - response.headers.get("content-type") || "application/octet-stream"; - const contentDisposition = - response.headers.get("content-disposition") || - "attachment; filename=\"result\""; - - return new NextResponse(blob, { - headers: { - "Content-Type": contentType, - "Content-Disposition": contentDisposition, - }, - }); - } catch (error) { - console.error("Download proxy error:", error); - return NextResponse.json( - { error: "Failed to download file" }, - { status: 500 }, - ); - } -} \ No newline at end of file diff --git a/apps/tools/frontend/src/app/api/job/[id]/route.ts b/apps/tools/frontend/src/app/api/job/[id]/route.ts deleted file mode 100644 index 4df8e37..0000000 --- a/apps/tools/frontend/src/app/api/job/[id]/route.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -const RUST_GATEWAY = process.env.RUST_GATEWAY_URL || "http://localhost:3001"; - -export async function GET( - _request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const { id } = await params; - try { - const response = await fetch(`${RUST_GATEWAY}/api/job/${id}`); - const data = await response.json(); - - if (!response.ok) { - return NextResponse.json(data, { status: response.status }); - } - - return NextResponse.json(data); - } catch (error) { - console.error("Job status proxy error:", error); - return NextResponse.json( - { error: "Failed to fetch job status" }, - { status: 500 }, - ); - } -} \ No newline at end of file diff --git a/apps/tools/frontend/src/app/api/job/[id]/ws/route.ts b/apps/tools/frontend/src/app/api/job/[id]/ws/route.ts deleted file mode 100644 index e728947..0000000 --- a/apps/tools/frontend/src/app/api/job/[id]/ws/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { NextResponse } from "next/server"; - -// WebSocket is handled directly by the client connecting to the Rust gateway. -// Next.js App Router cannot proxy WebSocket connections in route handlers. -// The client-side useJobStatus hook connects directly to ws://localhost:3001/api/job/{id}/ws -// In production, configure the WebSocket to connect to wss://tools.asepharyana.my.id/api/job/{id}/ws -export function GET() { - return NextResponse.json( - { - note: "WebSocket connections go directly to the Rust gateway", - ws_url: - process.env.NEXT_PUBLIC_WS_URL || "ws://localhost:3001/api/job/{id}/ws", - }, - ); -} \ No newline at end of file diff --git a/apps/tools/frontend/src/app/api/upload/route.ts b/apps/tools/frontend/src/app/api/upload/route.ts deleted file mode 100644 index abfba9f..0000000 --- a/apps/tools/frontend/src/app/api/upload/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -const RUST_GATEWAY = process.env.RUST_GATEWAY_URL || "http://localhost:3001"; - -export async function POST(request: NextRequest) { - try { - const formData = await request.formData(); - const file = formData.get("file"); - const tool = formData.get("tool"); - const options = formData.get("options"); - - if (!file || !tool) { - return NextResponse.json( - { error: "Missing file or tool parameter" }, - { status: 400 }, - ); - } - - // Forward to Rust gateway - const gatewayForm = new FormData(); - gatewayForm.append("file", file); - gatewayForm.append("tool", tool as string); - if (options) { - gatewayForm.append("options", options as string); - } - - const response = await fetch(`${RUST_GATEWAY}/api/upload`, { - method: "POST", - body: gatewayForm, - }); - - const data = await response.json(); - - if (!response.ok) { - return NextResponse.json(data, { status: response.status }); - } - - return NextResponse.json(data, { status: 202 }); - } catch (error) { - console.error("Upload proxy error:", error); - return NextResponse.json( - { error: "Failed to process upload" }, - { status: 500 }, - ); - } -} \ No newline at end of file diff --git a/apps/tools/frontend/src/app/audio/convert/page.tsx b/apps/tools/frontend/src/app/audio/convert/page.tsx deleted file mode 100644 index 1a83723..0000000 --- a/apps/tools/frontend/src/app/audio/convert/page.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import { useState, useCallback } from "react"; -import { Mic } from "lucide-react"; -import { ToolLayout } from "@/components/tools/tool-layout"; -import { UploadZone } from "@/components/tools/upload-zone"; -import { useUpload } from "@/hooks/use-upload"; -import { useJobStatus } from "@/hooks/use-job-status"; -import { ProgressBar } from "@/components/tools/progress-bar"; -import { ResultPreview } from "@/components/tools/result-preview"; - -type PageState = "upload" | "processing" | "result" | "error"; - -export default function AudioConvertPage() { - const [pageState, setPageState] = useState("upload"); - const [jobId, setJobId] = useState(null); - const [errorMsg, setErrorMsg] = useState(null); - - const { upload } = useUpload({ - tool: "audio-convert", - options: { quality: 80 }, - }); - - const handleComplete = useCallback(() => setPageState("result"), []); - const handleError = useCallback( - (err: string) => { - setErrorMsg(err); - setPageState("error"); - }, - [], - ); - - const { progress, stage, message, status, result } = useJobStatus(jobId, { - onComplete: handleComplete, - onError: handleError, - }); - - const handleUpload = useCallback( - async (file: File) => { - setErrorMsg(null); - const res = await upload(file); - if (res) { - setJobId(res.job_id); - setPageState("processing"); - } - }, - [upload], - ); - - const handleRetry = useCallback(() => { - setPageState("upload"); - setJobId(null); - setErrorMsg(null); - }, []); - - return ( - - {pageState === "upload" && ( - - )} - - {pageState === "processing" && jobId && ( - - )} - - {pageState === "result" && result && ( - - )} - - {pageState === "error" && ( -
-

- {errorMsg || "Terjadi kesalahan"} -

- -
- )} -
- ); -} diff --git a/apps/tools/frontend/src/app/globals.css b/apps/tools/frontend/src/app/globals.css deleted file mode 100644 index dbf6aef..0000000 --- a/apps/tools/frontend/src/app/globals.css +++ /dev/null @@ -1,121 +0,0 @@ -@import "tailwindcss"; -@import "tw-animate-css"; - -@custom-variant dark (&:is(.dark *)); - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-border: var(--border); - --color-ring: var(--ring); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --font-sans: "Geist", sans-serif; - --font-mono: "Geist Mono", monospace; -} - -:root { - --radius: 0.625rem; - --background: oklch(0.97 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0.042 265.755); - --primary-foreground: oklch(0.985 0 0); - --muted: oklch(0.965 0.001 286.375); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.965 0.001 286.375); - --accent-foreground: oklch(0.205 0.042 265.755); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0.004 286.375); - --ring: oklch(0.205 0.042 265.755); -} - -.dark { - --background: oklch(0.07 0.015 265); - --foreground: oklch(0.985 0 0); - --card: oklch(0.12 0.02 265); - --card-foreground: oklch(0.985 0 0); - --primary: oklch(0.7 0.15 265); - --primary-foreground: oklch(0.07 0.015 265); - --muted: oklch(0.15 0.02 265); - --muted-foreground: oklch(0.6 0.02 265); - --accent: oklch(0.15 0.02 265); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.2 0.02 265); - --ring: oklch(0.7 0.15 265); -} - -* { - border-color: var(--border); -} - -body { - background: var(--background); - color: var(--foreground); - font-family: var(--font-sans); -} - -/* Glass effect */ -.glass { - background: oklch(from var(--card) l c h / 0.6); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid oklch(from var(--border) l c h / 0.5); -} - -/* Gradient text */ -.gradient-text { - background: linear-gradient(135deg, var(--primary), oklch(0.6 0.2 265)); - -webkit-background-clip: text; - background-clip: text; - color: transparent; -} - -/* Terminal cursor blink */ -@keyframes blink { - 0%, - 100% { - opacity: 1; - } - 50% { - opacity: 0; - } -} - -.cursor-blink::after { - content: "█"; - animation: blink 1s step-end infinite; - color: var(--primary); -} - -/* Custom scrollbar */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: var(--muted); -} - -::-webkit-scrollbar-thumb { - background: var(--muted-foreground); - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: var(--primary); -} \ No newline at end of file diff --git a/apps/tools/frontend/src/app/health/route.ts b/apps/tools/frontend/src/app/health/route.ts deleted file mode 100644 index eccbe58..0000000 --- a/apps/tools/frontend/src/app/health/route.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NextResponse } from "next/server"; - -const RUST_GATEWAY = process.env.RUST_GATEWAY_URL || "http://localhost:3001"; - -export async function GET() { - try { - const response = await fetch(`${RUST_GATEWAY}/health`, { - signal: AbortSignal.timeout(5000), - }); - const data = await response.json(); - return NextResponse.json(data); - } catch { - return NextResponse.json( - { status: "error", message: "Gateway unreachable" }, - { status: 503 }, - ); - } -} \ No newline at end of file diff --git a/apps/tools/frontend/src/app/image/compress/page.tsx b/apps/tools/frontend/src/app/image/compress/page.tsx deleted file mode 100644 index f9cc09c..0000000 --- a/apps/tools/frontend/src/app/image/compress/page.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import { useState, useCallback } from "react"; -import { ImageDown } from "lucide-react"; -import { ToolLayout } from "@/components/tools/tool-layout"; -import { UploadZone } from "@/components/tools/upload-zone"; -import { useUpload } from "@/hooks/use-upload"; -import { useJobStatus } from "@/hooks/use-job-status"; -import { ProgressBar } from "@/components/tools/progress-bar"; -import { ResultPreview } from "@/components/tools/result-preview"; - -type PageState = "upload" | "processing" | "result" | "error"; - -export default function ImageCompressPage() { - const [pageState, setPageState] = useState("upload"); - const [jobId, setJobId] = useState(null); - const [errorMsg, setErrorMsg] = useState(null); - - const { upload } = useUpload({ - tool: "image-compress", - options: { quality: 80 }, - }); - - const handleComplete = useCallback(() => setPageState("result"), []); - const handleError = useCallback( - (err: string) => { - setErrorMsg(err); - setPageState("error"); - }, - [], - ); - - const { progress, stage, message, status, result } = useJobStatus(jobId, { - onComplete: handleComplete, - onError: handleError, - }); - - const handleUpload = useCallback( - async (file: File) => { - setErrorMsg(null); - const res = await upload(file); - if (res) { - setJobId(res.job_id); - setPageState("processing"); - } - }, - [upload], - ); - - const handleRetry = useCallback(() => { - setPageState("upload"); - setJobId(null); - setErrorMsg(null); - }, []); - - return ( - - {pageState === "upload" && ( - - )} - - {pageState === "processing" && jobId && ( - - )} - - {pageState === "result" && result && ( - - )} - - {pageState === "error" && ( -
-

- {errorMsg || "Terjadi kesalahan"} -

- -
- )} -
- ); -} \ No newline at end of file diff --git a/apps/tools/frontend/src/app/image/convert/page.tsx b/apps/tools/frontend/src/app/image/convert/page.tsx deleted file mode 100644 index ca3b1cc..0000000 --- a/apps/tools/frontend/src/app/image/convert/page.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import { useState, useCallback } from "react"; -import { Repeat } from "lucide-react"; -import { ToolLayout } from "@/components/tools/tool-layout"; -import { UploadZone } from "@/components/tools/upload-zone"; -import { useUpload } from "@/hooks/use-upload"; -import { useJobStatus } from "@/hooks/use-job-status"; -import { ProgressBar } from "@/components/tools/progress-bar"; -import { ResultPreview } from "@/components/tools/result-preview"; - -type PageState = "upload" | "processing" | "result" | "error"; - -export default function ImageConvertPage() { - const [pageState, setPageState] = useState("upload"); - const [jobId, setJobId] = useState(null); - const [errorMsg, setErrorMsg] = useState(null); - - const { upload } = useUpload({ - tool: "image-convert", - options: { quality: 80 }, - }); - - const handleComplete = useCallback(() => setPageState("result"), []); - const handleError = useCallback( - (err: string) => { - setErrorMsg(err); - setPageState("error"); - }, - [], - ); - - const { progress, stage, message, status, result } = useJobStatus(jobId, { - onComplete: handleComplete, - onError: handleError, - }); - - const handleUpload = useCallback( - async (file: File) => { - setErrorMsg(null); - const res = await upload(file); - if (res) { - setJobId(res.job_id); - setPageState("processing"); - } - }, - [upload], - ); - - const handleRetry = useCallback(() => { - setPageState("upload"); - setJobId(null); - setErrorMsg(null); - }, []); - - return ( - - {pageState === "upload" && ( - - )} - - {pageState === "processing" && jobId && ( - - )} - - {pageState === "result" && result && ( - - )} - - {pageState === "error" && ( -
-

- {errorMsg || "Terjadi kesalahan"} -

- -
- )} -
- ); -} diff --git a/apps/tools/frontend/src/app/image/remove-bg/page.tsx b/apps/tools/frontend/src/app/image/remove-bg/page.tsx deleted file mode 100644 index 73ae8c4..0000000 --- a/apps/tools/frontend/src/app/image/remove-bg/page.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import { useState, useCallback } from "react"; -import { Shrink } from "lucide-react"; -import { ToolLayout } from "@/components/tools/tool-layout"; -import { UploadZone } from "@/components/tools/upload-zone"; -import { useUpload } from "@/hooks/use-upload"; -import { useJobStatus } from "@/hooks/use-job-status"; -import { ProgressBar } from "@/components/tools/progress-bar"; -import { ResultPreview } from "@/components/tools/result-preview"; - -type PageState = "upload" | "processing" | "result" | "error"; - -export default function ImageRemoveBgPage() { - const [pageState, setPageState] = useState("upload"); - const [jobId, setJobId] = useState(null); - const [errorMsg, setErrorMsg] = useState(null); - - const { upload } = useUpload({ - tool: "image-remove-bg", - options: { quality: 80 }, - }); - - const handleComplete = useCallback(() => setPageState("result"), []); - const handleError = useCallback( - (err: string) => { - setErrorMsg(err); - setPageState("error"); - }, - [], - ); - - const { progress, stage, message, status, result } = useJobStatus(jobId, { - onComplete: handleComplete, - onError: handleError, - }); - - const handleUpload = useCallback( - async (file: File) => { - setErrorMsg(null); - const res = await upload(file); - if (res) { - setJobId(res.job_id); - setPageState("processing"); - } - }, - [upload], - ); - - const handleRetry = useCallback(() => { - setPageState("upload"); - setJobId(null); - setErrorMsg(null); - }, []); - - return ( - - {pageState === "upload" && ( - - )} - - {pageState === "processing" && jobId && ( - - )} - - {pageState === "result" && result && ( - - )} - - {pageState === "error" && ( -
-

- {errorMsg || "Terjadi kesalahan"} -

- -
- )} -
- ); -} diff --git a/apps/tools/frontend/src/app/image/resize/page.tsx b/apps/tools/frontend/src/app/image/resize/page.tsx deleted file mode 100644 index c194245..0000000 --- a/apps/tools/frontend/src/app/image/resize/page.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import { useState, useCallback } from "react"; -import { ImageResize } from "lucide-react"; -import { ToolLayout } from "@/components/tools/tool-layout"; -import { UploadZone } from "@/components/tools/upload-zone"; -import { useUpload } from "@/hooks/use-upload"; -import { useJobStatus } from "@/hooks/use-job-status"; -import { ProgressBar } from "@/components/tools/progress-bar"; -import { ResultPreview } from "@/components/tools/result-preview"; - -type PageState = "upload" | "processing" | "result" | "error"; - -export default function ImageResizePage() { - const [pageState, setPageState] = useState("upload"); - const [jobId, setJobId] = useState(null); - const [errorMsg, setErrorMsg] = useState(null); - - const { upload } = useUpload({ - tool: "image-resize", - options: { quality: 80 }, - }); - - const handleComplete = useCallback(() => setPageState("result"), []); - const handleError = useCallback( - (err: string) => { - setErrorMsg(err); - setPageState("error"); - }, - [], - ); - - const { progress, stage, message, status, result } = useJobStatus(jobId, { - onComplete: handleComplete, - onError: handleError, - }); - - const handleUpload = useCallback( - async (file: File) => { - setErrorMsg(null); - const res = await upload(file); - if (res) { - setJobId(res.job_id); - setPageState("processing"); - } - }, - [upload], - ); - - const handleRetry = useCallback(() => { - setPageState("upload"); - setJobId(null); - setErrorMsg(null); - }, []); - - return ( - - {pageState === "upload" && ( - - )} - - {pageState === "processing" && jobId && ( - - )} - - {pageState === "result" && result && ( - - )} - - {pageState === "error" && ( -
-

- {errorMsg || "Terjadi kesalahan"} -

- -
- )} -
- ); -} diff --git a/apps/tools/frontend/src/app/layout.tsx b/apps/tools/frontend/src/app/layout.tsx deleted file mode 100644 index 057219e..0000000 --- a/apps/tools/frontend/src/app/layout.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { Metadata } from "next"; -import { ThemeProvider } from "next-themes"; - -import "./globals.css"; -import { Header } from "@/components/tools/header"; -import { Footer } from "@/components/tools/footer"; - -export const metadata: Metadata = { - title: "Tools — Asep Haryana", - description: - "Self-hosted document scanner, image tools & PDF tools. No upload to third-party servers.", - manifest: "/manifest.json", -}; - -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( - - - -
-
{children}
-