diff --git a/.gitignore b/.gitignore index 13a7ea0..e53490f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,17 @@ +# Node / TS node_modules/ -.zig-cache/ -zig-out/ +dist/ +build/ *.log + +# Cloudflare Workers +.wrangler/ +.dev.vars + +# OS .DS_Store + +# Python (legacy/) __pycache__/ *.pyc *.pyo -dist/ -build/ - -# Rust build artifacts -target/ diff --git a/src/rust/assets/JetBrainsMono-Regular.ttf b/assets/JetBrainsMono-Regular.ttf similarity index 100% rename from src/rust/assets/JetBrainsMono-Regular.ttf rename to assets/JetBrainsMono-Regular.ttf diff --git a/legacy/README.md b/legacy/README.md new file mode 100644 index 0000000..8f27ceb --- /dev/null +++ b/legacy/README.md @@ -0,0 +1,35 @@ +# legacy/ + +Reference implementations kept around as a parity oracle during the +TypeScript port. Not part of the published npm package. + +## python/ + +The original working Python proxy (commit `5212f71`) plus its companion +scripts: + +| file | role | +| -------------------- | -------------------------------------------------------- | +| `proxy.py` | the proxy itself — `python3 proxy.py` to run | +| `gen_atlas.py` | pre-bakes Menlo glyph atlas (originally for the Zig CLI) | +| `diff_render.py` | byte-compare PNGs from two renderers | +| `diff_transform.py` | byte-compare transformed request bodies | +| `visual_compare.py` | side-by-side PNG diff viewer | +| `install.js` | old postinstall (verified Python + Pillow) | +| `cli.js` | old npm `bin` shim that spawned `proxy.py` | + +### Running the legacy proxy + +```bash +cd legacy/python +python3 proxy.py --port 47821 +``` + +Requires Python 3.8+ and `Pillow` + `httpx` (`pip install Pillow httpx`). + +### Why it's still here + +Until the TypeScript port reaches output parity (byte-for-byte +transformed requests, visually identical PNGs), this is the reference +implementation we diff against. Once parity is verified end-to-end, +the entire `legacy/` directory will be removed in a single commit. diff --git a/bin/cli.js b/legacy/python/cli.js similarity index 100% rename from bin/cli.js rename to legacy/python/cli.js diff --git a/scripts/diff_render.py b/legacy/python/diff_render.py similarity index 100% rename from scripts/diff_render.py rename to legacy/python/diff_render.py diff --git a/scripts/diff_transform.py b/legacy/python/diff_transform.py similarity index 100% rename from scripts/diff_transform.py rename to legacy/python/diff_transform.py diff --git a/scripts/gen_atlas.py b/legacy/python/gen_atlas.py similarity index 100% rename from scripts/gen_atlas.py rename to legacy/python/gen_atlas.py diff --git a/scripts/install.js b/legacy/python/install.js similarity index 100% rename from scripts/install.js rename to legacy/python/install.js diff --git a/src/proxy.py b/legacy/python/proxy.py similarity index 100% rename from src/proxy.py rename to legacy/python/proxy.py diff --git a/scripts/visual_compare.py b/legacy/python/visual_compare.py similarity index 100% rename from scripts/visual_compare.py rename to legacy/python/visual_compare.py diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock deleted file mode 100644 index ceb9d3e..0000000 --- a/src/rust/Cargo.lock +++ /dev/null @@ -1,2105 +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 = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core", - "axum-macros", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-macros" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[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 = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "cc" -version = "1.2.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "clap" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[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-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[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 = "dashmap" -version = "6.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_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.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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 = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[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 = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[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-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "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", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "h2" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -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.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[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 = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "js-sys" -version = "0.3.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[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.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[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.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[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 = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[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 = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pixelpipe" -version = "0.1.0" -dependencies = [ - "ab_glyph", - "anyhow", - "axum", - "base64", - "bytes", - "clap", - "dashmap", - "once_cell", - "png", - "reqwest", - "serde", - "serde_json", - "sha2", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[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.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -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 = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[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", -] - -[[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 = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.11.1", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64", - "bytes", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots", -] - -[[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 = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[package]] -name = "rustls" -version = "0.23.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" -dependencies = [ - "web-time", - "zeroize", -] - -[[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.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[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_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 = "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 = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - -[[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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -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" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[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.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -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.52.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[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.11.1", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[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", -] - -[[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-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", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" - -[[package]] -name = "typenum" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[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 = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[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.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "web-sys" -version = "0.3.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[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 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[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_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[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_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[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 = "yoke" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" -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", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[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", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml deleted file mode 100644 index b63a5de..0000000 --- a/src/rust/Cargo.toml +++ /dev/null @@ -1,71 +0,0 @@ -[package] -name = "pixelpipe" -version = "0.1.0" -edition = "2021" -description = "Token-saving proxy for Claude Code that renders system prompt + tools as images" -license = "MIT" -repository = "https://github.com/your/pixelpipe" - -[[bin]] -name = "pixelpipe" -path = "src/main.rs" - -[dependencies] -# Async runtime -tokio = { version = "1", features = ["full"] } - -# HTTP server (incoming from Claude Code) -axum = { version = "0.7", features = ["macros"] } - -# HTTP client (upstream to api.anthropic.com) - rustls so no OpenSSL link -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream", "json", "http2"] } - -# JSON parse + manipulate -serde = { version = "1", features = ["derive"] } -serde_json = "1" - -# Font rasterization (TTF -> glyph bitmaps) -ab_glyph = "0.2" - -# PNG encoding -png = "0.17" - -# Base64 for image data URI -base64 = "0.22" - -# Hashing for render-cache keys (deterministic across turns) -sha2 = "0.10" - -# In-memory cache (thread-safe, no allocations on hit) -dashmap = "6" - -# Bytes manipulation -bytes = "1" - -# CLI args parsing -clap = { version = "4", features = ["derive"] } - -# Logging -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -# Misc -once_cell = "1" - -# Error handling -anyhow = "1" - -[profile.release] -opt-level = 3 -lto = "thin" -codegen-units = 1 -strip = true -panic = "abort" - -[profile.release-small] -inherits = "release" -opt-level = "z" - -[lib] -name = "pixelpipe" -path = "src/lib.rs" diff --git a/src/rust/examples/diff_render.rs b/src/rust/examples/diff_render.rs deleted file mode 100644 index e6847e5..0000000 --- a/src/rust/examples/diff_render.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! Diff harness companion: read a JSON spec {name: text, ...}, render each via -//! the Rust pipeline, write PNGs to /_.png, then emit -//! /manifest.json mapping name -> [filenames]. -//! -//! Run: cargo run --release --example diff_render -- - -use std::collections::BTreeMap; -use std::path::PathBuf; - -use pixelpipe::font::AtlasFont; -use pixelpipe::render::render_chunks; - -fn main() -> anyhow::Result<()> { - let mut args = std::env::args().skip(1); - let spec_path: PathBuf = args - .next() - .ok_or_else(|| anyhow::anyhow!("usage: diff_render "))? - .into(); - let out_dir: PathBuf = args - .next() - .ok_or_else(|| anyhow::anyhow!("usage: diff_render "))? - .into(); - - let spec_bytes = std::fs::read(&spec_path)?; - let spec: BTreeMap = serde_json::from_slice(&spec_bytes)?; - - let font = AtlasFont::load(5.0)?; - - let mut manifest: BTreeMap> = BTreeMap::new(); - for (name, text) in &spec { - let pngs = render_chunks(&font, text); - let mut filenames = Vec::with_capacity(pngs.len()); - for (i, png) in pngs.iter().enumerate() { - let fname = format!("{name}_{i}.png"); - std::fs::write(out_dir.join(&fname), &png.bytes)?; - filenames.push(fname); - } - manifest.insert(name.clone(), filenames); - } - - std::fs::write( - out_dir.join("manifest.json"), - serde_json::to_vec_pretty(&manifest)?, - )?; - - eprintln!("rendered {} samples", spec.len()); - Ok(()) -} diff --git a/src/rust/examples/render_sample.rs b/src/rust/examples/render_sample.rs deleted file mode 100644 index 0660f9d..0000000 --- a/src/rust/examples/render_sample.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Render a sample text → PNG so we can visually inspect + OCR-test. -//! Run: cargo run --example render_sample -- [size] - -use pixelpipe::font::AtlasFont; -use pixelpipe::render::render_chunks; - -fn main() -> anyhow::Result<()> { - let mut args = std::env::args().skip(1); - let out = args.next().unwrap_or_else(|| "/tmp/rust_sample.png".to_string()); - let size: f32 = args.next().and_then(|s| s.parse().ok()).unwrap_or(5.0); - - let font = AtlasFont::load(size)?; - println!("cell: {}x{}", font.cell_w, font.cell_h); - - let sample = "def fibonacci(n):\n if n <= 1: return n\n return fibonacci(n-1) + fibonacci(n-2)\nprint(fibonacci(10))"; - - let pngs = render_chunks(&font, sample); - println!("images: {}, dims: {:?}", pngs.len(), pngs.iter().map(|p| (p.width, p.height)).collect::>()); - - std::fs::write(&out, &pngs[0].bytes)?; - println!("wrote {} ({} bytes)", out, pngs[0].bytes.len()); - Ok(()) -} diff --git a/src/rust/examples/transform_only.rs b/src/rust/examples/transform_only.rs deleted file mode 100644 index 579b251..0000000 --- a/src/rust/examples/transform_only.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Headless transform: read a request body from file (or stdin), run it through -//! the Rust transform() with default Claude Code settings, write the transformed -//! body + info JSON to disk for offline diffing against Python's transform. -//! -//! Run: cargo run --release --example transform_only -- -//! -//! Produces: -//! /body_out.json — transformed body bytes (the JSON forwarded upstream) -//! /info.json — TransformInfo struct (dims, expected_image_tokens, etc.) -//! -//! Settings mirror bin/cli.js defaults: compress=true, all sub-flags on, -//! min_chars=2000, font_size=5pt. No env-var overrides. - -use std::path::PathBuf; - -use pixelpipe::font::AtlasFont; -use pixelpipe::transform::{transform, TransformConfig}; -use dashmap::DashMap; - -fn main() -> anyhow::Result<()> { - let mut args = std::env::args().skip(1); - let body_path: PathBuf = args - .next() - .ok_or_else(|| anyhow::anyhow!("usage: transform_only "))? - .into(); - let out_dir: PathBuf = args - .next() - .ok_or_else(|| anyhow::anyhow!("usage: transform_only "))? - .into(); - - std::fs::create_dir_all(&out_dir)?; - - let body = std::fs::read(&body_path)?; - let font = AtlasFont::load(5.0)?; - let render_cache = DashMap::new(); - let cfg = TransformConfig { - compress: true, - compress_tools: true, - compress_schemas: true, - compress_reminders: true, - compress_tool_results: true, - min_chars: 2000, - font: &font, - render_cache: &render_cache, - }; - - let (body_out, info) = transform(&body, &cfg); - - std::fs::write(out_dir.join("body_out.json"), &body_out)?; - std::fs::write( - out_dir.join("info.json"), - serde_json::to_vec_pretty(&info)?, - )?; - - eprintln!( - "rust transform: in={}B out={}B compressed={} imgs={} dims={:?}", - body.len(), - body_out.len(), - info.compressed, - info.images, - info.dims, - ); - Ok(()) -} diff --git a/src/rust/src/font.rs b/src/rust/src/font.rs deleted file mode 100644 index f1d0d31..0000000 --- a/src/rust/src/font.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Embedded TTF font + glyph rasterization at a fixed size. -//! -//! Pre-bakes the printable ASCII range (32-126) into a fixed-cell atlas at -//! load time. Renders text by blitting from the atlas into a u8 grayscale -//! buffer (0 = ink, 255 = paper). This matches the Python proxy's pipeline -//! that we verified produces 99.5% OCR accuracy on Opus 4.7. - -use ab_glyph::{Font, FontRef, PxScale, ScaleFont}; - -/// JetBrains Mono Regular, OFL-licensed. Bundled at compile time. -const FONT_DATA: &[u8] = include_bytes!("../assets/JetBrainsMono-Regular.ttf"); - -pub struct AtlasFont { - pub cell_w: u32, - pub cell_h: u32, - pub ascent: i32, - /// Glyph bitmaps, one per code point in [FIRST..=LAST]. Each bitmap is - /// cell_w * cell_h bytes (0 = ink, 255 = paper). - pub glyphs: Vec>, - pub first: u8, - pub last: u8, -} - -const FIRST: u8 = 32; -const LAST: u8 = 126; - -impl AtlasFont { - pub fn load(font_size_pt: f32) -> anyhow::Result { - let font = FontRef::try_from_slice(FONT_DATA)?; - // ab_glyph's PxScale is in *pixels* of the EM box. PIL/typography use - // points (1pt = 1/72 in, 1 px = 1/96 in @ 96 DPI). Convert pt → px. - // This gives us the same physical glyph size PIL produced (where - // Menlo 5pt OCR'd at 99.7%). - let px_height = font_size_pt * 96.0 / 72.0; - let scale = PxScale::from(px_height); - let scaled = font.as_scaled(scale); - - // Determine cell dimensions: use widest glyph width + line height. - let ascent = scaled.ascent().ceil() as i32; - let descent = scaled.descent().floor() as i32; - let line_h = (ascent - descent).max(1) as u32; - - // Use a consistent monospace advance from 'M'. - let advance_m = scaled.h_advance(font.glyph_id('M')).ceil().max(1.0) as u32; - // Cell width: tight to advance; allow 1 px of slop for descenders/spillover. - let cell_w = advance_m.max(1); - let cell_h = line_h; - - let mut glyphs = Vec::with_capacity((LAST - FIRST + 1) as usize); - for code in FIRST..=LAST { - let ch = code as char; - let glyph_id = font.glyph_id(ch); - let glyph = glyph_id.with_scale(scale); - let mut bitmap = vec![255u8; (cell_w * cell_h) as usize]; - if let Some(outlined) = font.outline_glyph(glyph) { - let bounds = outlined.px_bounds(); - let x_offset = bounds.min.x as i32; - let y_offset = bounds.min.y as i32 + ascent; - outlined.draw(|gx, gy, coverage| { - let px = gx as i32 + x_offset; - let py = gy as i32 + y_offset; - if px < 0 || py < 0 { return; } - let px = px as u32; - let py = py as u32; - if px >= cell_w || py >= cell_h { return; } - // Keep antialiased grayscale (0=black ink, 255=white paper). - // PIL+Menlo at 5pt with AA→99.7% OCR; 1-bit threshold killed - // legibility for ab_glyph. Vision encoder actually uses the - // grayscale gradients at tiny font sizes. - let ink = (coverage * 255.0).clamp(0.0, 255.0) as u8; - let cur = bitmap[(py * cell_w + px) as usize]; - let new_v = 255_u8.saturating_sub(ink); - if new_v < cur { - bitmap[(py * cell_w + px) as usize] = new_v; - } - }); - } - glyphs.push(bitmap); - } - - Ok(AtlasFont { - cell_w, - cell_h, - ascent, - glyphs, - first: FIRST, - last: LAST, - }) - } - - /// Blit one glyph into a destination buffer (255=paper, 0=ink) at (dx, dy). - /// Keeps the darker of (existing, glyph) — preserves antialiasing. - pub fn blit(&self, ch: u8, dst: &mut [u8], dst_w: u32, dst_h: u32, dx: u32, dy: u32) { - if ch < self.first || ch > self.last { return; } - let bitmap = &self.glyphs[(ch - self.first) as usize]; - for y in 0..self.cell_h { - let py = dy + y; - if py >= dst_h { break; } - for x in 0..self.cell_w { - let px = dx + x; - if px >= dst_w { break; } - let v = bitmap[(y * self.cell_w + x) as usize]; - let idx = (py * dst_w + px) as usize; - // Preserve grayscale: keep the darker value (paper=255 default). - if v < dst[idx] { - dst[idx] = v; - } - } - } - } -} diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs deleted file mode 100644 index 46cbb2c..0000000 --- a/src/rust/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod font; -pub mod render; -pub mod transform; diff --git a/src/rust/src/main.rs b/src/rust/src/main.rs deleted file mode 100644 index ccbcf53..0000000 --- a/src/rust/src/main.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! pixelpipe — token-saving proxy for Claude Code. -//! -//! Architecture mirrors the proven Python prototype: -//! 1. Accept HTTP from Claude Code on a local port. -//! 2. For POST /v1/messages: parse JSON body, replace the bulky system -//! prompt + tool descriptions + tool schemas + blocks -//! with a single rendered PNG image. Stub the original fields. -//! 3. Forward the modified request to api.anthropic.com untouched-otherwise. -//! 4. Return the response unchanged. Log effective-token math. -//! -//! Single static binary, no Python dependency. - -use clap::Parser; -use std::sync::Arc; - -mod proxy; -mod stats; -mod transform; -mod render; -mod font; - -#[derive(Parser, Debug, Clone)] -#[command(name = "pixelpipe", version, about, long_about = None)] -struct Args { - /// Port to listen on - #[arg(short, long, default_value_t = 47821)] - port: u16, - - /// Disable ALL compression (pure passthrough — for benchmarking) - #[arg(long)] - no_compress: bool, - - /// Don't compress tool descriptions - #[arg(long)] - no_tools: bool, - - /// Don't compress tool input_schemas (saves the most) - #[arg(long)] - no_schemas: bool, - - /// Don't compress blocks - #[arg(long)] - no_reminders: bool, - - /// Don't compress large tool_result content - #[arg(long)] - no_tool_results: bool, - - /// Font size for rendering (5pt is verified OCR floor) - #[arg(long, default_value_t = 5.0)] - font_size: f32, - - /// Minimum chars before triggering compression - #[arg(long, default_value_t = 2000)] - min_chars: usize, - - /// Upstream URL (default: https://api.anthropic.com) - #[arg(long, default_value = "https://api.anthropic.com")] - upstream: String, -} - -#[derive(Clone)] -pub struct AppState { - pub args: Arc, - pub client: reqwest::Client, - pub render_cache: Arc>>, - pub stats: Arc, - pub font: Arc, -} - -#[tokio::main(flavor = "multi_thread")] -async fn main() -> anyhow::Result<()> { - let args = Args::parse(); - - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("pixelpipe=info,info")), - ) - .with_target(false) - .init(); - - let port = args.port; - let compress = !args.no_compress; - let font_size = args.font_size; - - let state = AppState { - args: Arc::new(args), - client: reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(300)) - .build()?, - render_cache: Arc::new(dashmap::DashMap::new()), - stats: Arc::new(stats::Stats::default()), - font: Arc::new(font::AtlasFont::load(font_size)?), - }; - - println!("pixelpipe v{} starting", env!("CARGO_PKG_VERSION")); - println!(" port: {}", port); - println!(" compression: {}", if compress { "ON" } else { "OFF (passthrough)" }); - if compress { - println!(" font: JetBrains Mono {}pt", font_size); - } - println!(); - println!(" Point Claude Code at: ANTHROPIC_BASE_URL=http://127.0.0.1:{}", port); - println!(" Live stats: curl http://127.0.0.1:{}/proxy-stats", port); - println!(); - - proxy::serve(port, state).await -} - -// Submodules declared inline at top of file diff --git a/src/rust/src/proxy.rs b/src/rust/src/proxy.rs deleted file mode 100644 index 773dca4..0000000 --- a/src/rust/src/proxy.rs +++ /dev/null @@ -1,280 +0,0 @@ -//! HTTP server: accept from Claude Code, transform, forward to api.anthropic.com. - -use axum::{ - body::Bytes, - extract::State, - http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri}, - response::{IntoResponse, Response}, - routing::{any, get}, - Router, -}; -use std::collections::VecDeque; -use std::sync::Mutex; -use std::time::Instant; - -use crate::transform::{transform, TransformConfig, TransformInfo}; -use crate::AppState; - -// Connection-specific (hop-by-hop) headers + things we always strip. -const HOP_HEADERS: &[&str] = &[ - "connection", "keep-alive", "proxy-connection", "transfer-encoding", - "upgrade", "te", "host", "content-length", "expect", "accept-encoding", -]; - -#[derive(Clone, serde::Serialize)] -pub struct RequestEntry { - pub method: String, - pub path: String, - pub size_in: usize, - pub size_out: usize, - pub status: u16, - pub upstream_ms: u64, - pub info: TransformInfo, - pub tokens: Option, - pub session_saved_so_far: f64, - pub session_saved_usd: f64, -} - -#[derive(Clone, Debug, serde::Serialize)] -pub struct TokenLog { - pub input: u64, - pub output: u64, - pub cache_read: u64, - pub cache_create: u64, - pub effective_cost: f64, -} - -// Tail of recent requests for the dashboard. -pub struct Recent(pub Mutex>); -impl Default for Recent { - fn default() -> Self { Recent(Mutex::new(VecDeque::with_capacity(64))) } -} -impl Recent { - pub fn push(&self, e: RequestEntry) { - let mut q = self.0.lock().unwrap(); - if q.len() >= 64 { q.pop_front(); } - q.push_back(e); - } - pub fn snapshot(&self) -> Vec { - self.0.lock().unwrap().iter().cloned().collect() - } -} - -pub async fn serve(port: u16, state: AppState) -> anyhow::Result<()> { - let recent: std::sync::Arc = std::sync::Arc::new(Recent::default()); - - let app = Router::new() - .route("/proxy-stats", get(stats_handler)) - .fallback(any(proxy_handler)) - .with_state((state, recent)); - - let listener = tokio::net::TcpListener::bind(("127.0.0.1", port)).await?; - tracing::info!("listening on http://127.0.0.1:{port}"); - axum::serve(listener, app).await?; - Ok(()) -} - -async fn stats_handler( - State((state, _recent)): State<(AppState, std::sync::Arc)>, -) -> impl IntoResponse { - let snap = state.stats.snapshot(); - (StatusCode::OK, [("content-type", "application/json")], - serde_json::to_string_pretty(&snap).unwrap_or_default()) -} - -async fn proxy_handler( - State((state, recent)): State<(AppState, std::sync::Arc)>, - method: Method, - uri: Uri, - headers: HeaderMap, - body: Bytes, -) -> Response { - let path = uri.path_and_query().map(|p| p.as_str().to_string()).unwrap_or("/".to_string()); - let started = Instant::now(); - - // Only POST /v1/messages gets compressed; everything else is passthrough. - let do_compress = state.args.no_compress == false - && method == Method::POST - && path.starts_with("/v1/messages"); - - let cfg = TransformConfig { - compress: do_compress, - compress_tools: !state.args.no_tools, - compress_schemas: !state.args.no_schemas, - compress_reminders: !state.args.no_reminders, - compress_tool_results: !state.args.no_tool_results, - min_chars: state.args.min_chars, - font: &state.font, - render_cache: &state.render_cache, - }; - - let (new_body, info) = transform(&body, &cfg); - - // Build the upstream URL - let upstream_url = format!("{}{}", state.args.upstream, path); - - // Forward headers (strip hop-by-hop + content-length, reqwest sets its own) - let mut fwd_headers = reqwest::header::HeaderMap::new(); - for (k, v) in headers.iter() { - let lower = k.as_str().to_ascii_lowercase(); - if HOP_HEADERS.contains(&lower.as_str()) { continue; } - if let (Ok(name), Ok(val)) = ( - reqwest::header::HeaderName::from_bytes(k.as_str().as_bytes()), - reqwest::header::HeaderValue::from_bytes(v.as_bytes()), - ) { - fwd_headers.insert(name, val); - } - } - if !fwd_headers.contains_key("anthropic-version") { - fwd_headers.insert( - reqwest::header::HeaderName::from_static("anthropic-version"), - reqwest::header::HeaderValue::from_static("2023-06-01"), - ); - } - - let rmethod = match method { - Method::GET => reqwest::Method::GET, - Method::POST => reqwest::Method::POST, - Method::PUT => reqwest::Method::PUT, - Method::DELETE => reqwest::Method::DELETE, - Method::HEAD => reqwest::Method::HEAD, - _ => reqwest::Method::POST, - }; - - let upstream_started = Instant::now(); - let upstream_result = state.client - .request(rmethod, &upstream_url) - .headers(fwd_headers) - .body(new_body.clone()) - .send() - .await; - let upstream_ms = upstream_started.elapsed().as_millis() as u64; - - let upstream_resp = match upstream_result { - Ok(r) => r, - Err(e) => { - let msg = format!(r#"{{"error":"upstream_error","detail":{}}}"#, serde_json::to_string(&e.to_string()).unwrap()); - return (StatusCode::BAD_GATEWAY, [("content-type", "application/json")], msg).into_response(); - } - }; - - let status = upstream_resp.status(); - let mut resp_headers = HeaderMap::new(); - for (k, v) in upstream_resp.headers() { - let lower = k.as_str().to_ascii_lowercase(); - if HOP_HEADERS.contains(&lower.as_str()) || lower == "content-encoding" { continue; } - if let (Ok(name), Ok(val)) = ( - HeaderName::from_bytes(k.as_str().as_bytes()), - HeaderValue::from_bytes(v.as_bytes()), - ) { - resp_headers.insert(name, val); - } - } - let resp_body = match upstream_resp.bytes().await { - Ok(b) => b, - Err(_) => Bytes::new(), - }; - - // Parse usage (plain JSON or SSE stream) for stats. - let token_log = parse_usage(&resp_body); - let (eff, baseline) = compute_effective_and_baseline(&token_log, &info); - state.stats.record(eff, baseline, info.compressed); - let snap = state.stats.snapshot(); - - let entry = RequestEntry { - method: method.as_str().to_string(), - path: path.clone(), - size_in: body.len(), - size_out: resp_body.len(), - status: status.as_u16(), - upstream_ms, - info: info.clone(), - tokens: token_log.clone(), - session_saved_so_far: snap.saved_effective_tokens, - session_saved_usd: snap.saved_usd_opus47, - }; - recent.push(entry.clone()); - - // Single-line JSON log per request (matches the Python proxy log shape). - if let Ok(j) = serde_json::to_string(&entry) { - println!("[PROXY] {}", j); - } - - let _ = started; - (status, resp_headers, resp_body).into_response() -} - -fn parse_usage(body: &[u8]) -> Option { - // Try plain JSON first - if let Ok(v) = serde_json::from_slice::(body) { - if let Some(u) = v.get("usage") { - return Some(usage_to_log(u)); - } - } - // Try SSE: scan for "data: {...}" lines and aggregate. - let text = std::str::from_utf8(body).ok()?; - let mut inp: u64 = 0; - let mut out: u64 = 0; - let mut cr: u64 = 0; - let mut cc: u64 = 0; - let mut any = false; - for line in text.lines() { - let line = line.trim_start(); - let payload = match line.strip_prefix("data: ") { - Some(p) => p, - None => continue, - }; - let ev: serde_json::Value = match serde_json::from_str(payload) { - Ok(v) => v, - Err(_) => continue, - }; - match ev.get("type").and_then(|v| v.as_str()) { - Some("message_start") => { - if let Some(u) = ev.get("message").and_then(|m| m.get("usage")) { - inp = u.get("input_tokens").and_then(|v| v.as_u64()).unwrap_or(inp); - cr = u.get("cache_read_input_tokens").and_then(|v| v.as_u64()).unwrap_or(cr); - cc = u.get("cache_creation_input_tokens").and_then(|v| v.as_u64()).unwrap_or(cc); - any = true; - } - } - Some("message_delta") => { - if let Some(u) = ev.get("usage") { - if let Some(v) = u.get("output_tokens").and_then(|v| v.as_u64()) { - out = v; - any = true; - } - } - } - _ => {} - } - } - if any { - let eff = inp as f64 + cc as f64 * 1.25 + cr as f64 * 0.10; - Some(TokenLog { input: inp, output: out, cache_read: cr, cache_create: cc, effective_cost: eff }) - } else { - None - } -} - -fn usage_to_log(u: &serde_json::Value) -> TokenLog { - let inp = u.get("input_tokens").and_then(|v| v.as_u64()).unwrap_or(0); - let out = u.get("output_tokens").and_then(|v| v.as_u64()).unwrap_or(0); - let cr = u.get("cache_read_input_tokens").and_then(|v| v.as_u64()).unwrap_or(0); - let cc = u.get("cache_creation_input_tokens").and_then(|v| v.as_u64()).unwrap_or(0); - let eff = inp as f64 + cc as f64 * 1.25 + cr as f64 * 0.10; - TokenLog { input: inp, output: out, cache_read: cr, cache_create: cc, effective_cost: eff } -} - -fn compute_effective_and_baseline(token_log: &Option, info: &TransformInfo) -> (f64, f64) { - let Some(t) = token_log else { return (0.0, 0.0); }; - let actual = t.effective_cost; - if !info.compressed { - return (actual, actual); - } - // Conservative baseline: assume the replaced text would have been cached - // at 10% rate, and the extra char count is what we saved. - let txt_replaced = ((info.system_text_chars + info.tool_text_added) / 4) as u64; - let extra = txt_replaced.saturating_sub(info.expected_image_tokens); - let baseline = actual + extra as f64 * 0.10; - (actual, baseline) -} diff --git a/src/rust/src/render.rs b/src/rust/src/render.rs deleted file mode 100644 index 78b4f9c..0000000 --- a/src/rust/src/render.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! Text → PNG rendering using the embedded glyph atlas. -//! -//! Strategy mirrors the Python proxy: -//! - Hard-wrap input at 80 chars/line (kills column-blow-up from long lines). -//! - Collapse runs of blank lines (each blank costs a full row). -//! - Newspaper layout: pack lines into N columns so each image stays ≤ 1568px. -//! - 2-color indexed PNG (paper=white, ink=black) — Anthropic bills by -//! dimensions, not bytes, but tiny PNGs are nice for wire transfer. - -use crate::font::AtlasFont; - -const MAX_EDGE: u32 = 1568; -const COL_GAP_PX: u32 = 4; -const WRAP_CHARS: usize = 80; - -pub struct Png { - pub bytes: Vec, - pub width: u32, - pub height: u32, -} - -pub fn render_chunks(font: &AtlasFont, text: &str) -> Vec { - // Minify: rstrip + collapse blank-line runs. - let mut lines_raw: Vec = Vec::new(); - let mut last_blank = false; - for ln in text.lines() { - let ln = ln.trim_end(); - if ln.is_empty() { - if last_blank { continue; } - last_blank = true; - lines_raw.push(" ".to_string()); - } else { - last_blank = false; - lines_raw.push(ln.to_string()); - } - } - if lines_raw.is_empty() { - lines_raw.push(" ".to_string()); - } - - // Hard-wrap to WRAP_CHARS. - let mut lines: Vec = Vec::new(); - for ln in lines_raw { - if ln.len() <= WRAP_CHARS { - lines.push(ln); - } else { - let bytes = ln.as_bytes(); - let mut i = 0; - while i < bytes.len() { - let end = (i + WRAP_CHARS).min(bytes.len()); - lines.push(String::from_utf8_lossy(&bytes[i..end]).into_owned()); - i = end; - } - } - } - - let cell_w = font.cell_w; - let cell_h = font.cell_h; - let col_w_px = WRAP_CHARS as u32 * cell_w + 1; - let lines_per_col = ((MAX_EDGE / cell_h) as usize).max(8); - - let max_cols_per_img = ((MAX_EDGE / (col_w_px + COL_GAP_PX)).max(1)) as usize; - let lines_per_img = lines_per_col * max_cols_per_img; - - let mut pngs: Vec = Vec::new(); - let mut start = 0; - while start < lines.len() { - let chunk_end = (start + lines_per_img).min(lines.len()); - let chunk = &lines[start..chunk_end]; - let c_needed = ((chunk.len() + lines_per_col - 1) / lines_per_col).max(1) as u32; - let chunk_w = c_needed * col_w_px + (c_needed.saturating_sub(1)) * COL_GAP_PX; - let last_col_lines = chunk.len() - ((c_needed - 1) as usize * lines_per_col); - let tallest = if c_needed == 1 { last_col_lines } else { lines_per_col }; - let chunk_h = tallest as u32 * cell_h; - - let mut buf = vec![255u8; (chunk_w * chunk_h) as usize]; - for c in 0..c_needed as usize { - let col_start = c * lines_per_col; - let col_end = (col_start + lines_per_col).min(chunk.len()); - let x_base = c as u32 * (col_w_px + COL_GAP_PX); - for (i, line) in chunk[col_start..col_end].iter().enumerate() { - let y = i as u32 * cell_h; - let mut x = x_base; - for &ch in line.as_bytes() { - if x + cell_w > chunk_w { break; } - font.blit(ch, &mut buf, chunk_w, chunk_h, x, y); - x += cell_w; - } - } - } - - let png_bytes = encode_indexed_png(&buf, chunk_w, chunk_h); - pngs.push(Png { bytes: png_bytes, width: chunk_w, height: chunk_h }); - start = chunk_end; - } - pngs -} - -/// Encode a u8 grayscale buffer as an 8-bit grayscale PNG. -/// Preserves antialiasing — critical: PIL+Menlo OCR'd at 99.7% because the -/// vision encoder uses subpixel grayscale gradients at tiny font sizes. -/// Deterministic: same input → same output bytes (so the cache hits). -fn encode_indexed_png(pixels: &[u8], width: u32, height: u32) -> Vec { - let mut out: Vec = Vec::with_capacity(pixels.len() / 4 + 64); - { - let mut encoder = png::Encoder::new(&mut out, width, height); - encoder.set_color(png::ColorType::Grayscale); - encoder.set_depth(png::BitDepth::Eight); - encoder.set_compression(png::Compression::Best); - let mut writer = encoder.write_header().expect("png header"); - writer.write_image_data(pixels).expect("png data"); - } - out -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::font::AtlasFont; - - #[test] - fn renders_short_text() { - let font = AtlasFont::load(5.0).unwrap(); - let pngs = render_chunks(&font, "hello world\nsecond line"); - assert!(!pngs.is_empty()); - assert!(pngs[0].bytes.len() > 0); - assert!(pngs[0].bytes.starts_with(&[0x89, b'P', b'N', b'G'])); - } - - #[test] - fn renders_deterministically() { - let font = AtlasFont::load(5.0).unwrap(); - let a = render_chunks(&font, "fibonacci(n-1) + fibonacci(n-2)"); - let b = render_chunks(&font, "fibonacci(n-1) + fibonacci(n-2)"); - assert_eq!(a.len(), b.len()); - for (a_png, b_png) in a.iter().zip(b.iter()) { - assert_eq!(a_png.bytes, b_png.bytes, "PNG bytes must be deterministic for cache"); - } - } -} diff --git a/src/rust/src/stats.rs b/src/rust/src/stats.rs deleted file mode 100644 index 294607b..0000000 --- a/src/rust/src/stats.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Running session totals — surfaced via GET /proxy-stats and per-request log line. - -use std::sync::atomic::{AtomicU64, Ordering}; - -#[derive(Default)] -pub struct Stats { - pub requests: AtomicU64, - pub compressed_requests: AtomicU64, - /// Effective tokens we actually paid for (sum of input + cache_create*1.25 + cache_read*0.10). - /// Stored as bits of f64. - effective_actual_bits: AtomicU64, - /// What we *would* have paid uncompressed (conservative estimate). - effective_baseline_bits: AtomicU64, -} - -impl Stats { - pub fn record(&self, actual: f64, baseline: f64, compressed: bool) { - self.requests.fetch_add(1, Ordering::Relaxed); - if compressed { - self.compressed_requests.fetch_add(1, Ordering::Relaxed); - } - self.add_f64(&self.effective_actual_bits, actual); - self.add_f64(&self.effective_baseline_bits, baseline); - } - - pub fn snapshot(&self) -> Snapshot { - let actual = self.read_f64(&self.effective_actual_bits); - let baseline = self.read_f64(&self.effective_baseline_bits); - let saved = (baseline - actual).max(0.0); - let pct = if baseline > 0.0 { saved / baseline * 100.0 } else { 0.0 }; - Snapshot { - requests: self.requests.load(Ordering::Relaxed), - compressed_requests: self.compressed_requests.load(Ordering::Relaxed), - effective_input_actual: actual, - effective_input_baseline_est: baseline, - saved_effective_tokens: saved, - saved_pct: pct, - // Opus 4.7 input price: $15 / M tokens - saved_usd_opus47: saved * 15.0 / 1_000_000.0, - } - } - - fn add_f64(&self, atomic: &AtomicU64, delta: f64) { - let mut cur = atomic.load(Ordering::Relaxed); - loop { - let new_val = f64::from_bits(cur) + delta; - match atomic.compare_exchange_weak(cur, new_val.to_bits(), Ordering::Relaxed, Ordering::Relaxed) { - Ok(_) => break, - Err(actual) => cur = actual, - } - } - } - - fn read_f64(&self, atomic: &AtomicU64) -> f64 { - f64::from_bits(atomic.load(Ordering::Relaxed)) - } -} - -#[derive(serde::Serialize)] -pub struct Snapshot { - pub requests: u64, - pub compressed_requests: u64, - pub effective_input_actual: f64, - pub effective_input_baseline_est: f64, - pub saved_effective_tokens: f64, - pub saved_pct: f64, - pub saved_usd_opus47: f64, -} diff --git a/src/rust/src/transform.rs b/src/rust/src/transform.rs deleted file mode 100644 index 78bafa5..0000000 --- a/src/rust/src/transform.rs +++ /dev/null @@ -1,357 +0,0 @@ -//! Request-body JSON transformation: text → images. -//! -//! Ported from `src/proxy.py:transform_request`. Same heuristics, same field -//! semantics, same Anthropic-API constraints (≤4 cache_control breakpoints). - -use base64::Engine; -use serde_json::{json, Value}; -use sha2::{Digest, Sha256}; - -use crate::font::AtlasFont; -use crate::render::{render_chunks, Png}; - -pub struct TransformConfig<'a> { - pub compress: bool, - pub compress_tools: bool, - pub compress_schemas: bool, - pub compress_reminders: bool, - pub compress_tool_results: bool, - pub min_chars: usize, - pub font: &'a AtlasFont, - pub render_cache: &'a dashmap::DashMap<[u8; 32], Vec>, -} - -#[derive(Debug, Default, serde::Serialize, Clone)] -pub struct TransformInfo { - pub compressed: bool, - pub tool_text_added: usize, - pub schemas_compressed: usize, - pub system_text_chars: usize, - pub system_text_sha8: String, - pub images: usize, - pub png_bytes: usize, - pub total_pixels: u64, - pub expected_image_tokens: u64, - pub text_tokens_estimate: u64, - pub reminder_imgs: usize, - pub tool_result_imgs: usize, - pub cc_breakpoints_added: u32, - pub skipped: Option, - pub dims: Vec<(u32, u32)>, -} - -/// Top-level entry. Returns (new body, info). If config.compress is false, -/// returns the original body untouched. -pub fn transform(body: &[u8], cfg: &TransformConfig) -> (Vec, TransformInfo) { - let mut info = TransformInfo::default(); - if !cfg.compress { - return (body.to_vec(), info); - } - let mut req: Value = match serde_json::from_slice(body) { - Ok(v) => v, - Err(_) => return (body.to_vec(), info), - }; - - // Extract system text + remember the "remainder" (non-text blocks if any). - let (mut system_text, remainder) = extract_system_text(req.get("system")); - - // Strip Claude Code's per-turn-random "x-anthropic-billing-header: ...; cch=;" - // line from the top of the system prompt — that one line being different - // per turn was busting our entire image cache. - let billing_line = if let Some(idx) = system_text.find('\n') { - let first = &system_text[..idx]; - if first.starts_with("x-anthropic-billing-header:") { - let kept = first.to_string(); - system_text = system_text[idx + 1..].to_string(); - Some(kept) - } else { None } - } else if system_text.starts_with("x-anthropic-billing-header:") { - let kept = std::mem::take(&mut system_text); - Some(kept) - } else { None }; - - info.system_text_chars = system_text.len(); - info.system_text_sha8 = sha256_8(system_text.as_bytes()); - - if system_text.len() < cfg.min_chars { - info.skipped = Some(format!("system <{} chars", cfg.min_chars)); - return (body.to_vec(), info); - } - - // Compress tools: append each tool's description (+ optionally schema) to - // the system-context image text, and stub the tools[] entries. - if cfg.compress_tools { - if let Some(tools) = req.get_mut("tools").and_then(|v| v.as_array_mut()) { - let mut tool_docs: Vec = Vec::new(); - for t in tools.iter_mut() { - let Some(obj) = t.as_object_mut() else { continue }; - let name = obj.get("name").and_then(|v| v.as_str()).unwrap_or("?").to_string(); - let desc = obj.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string(); - - let mut doc = String::new(); - if !desc.is_empty() { - doc.push_str(&format!("#### Tool: {}\n{}\n", name, desc)); - if desc.len() > 80 { - obj.insert("description".to_string(), - json!(format!("See `{}` docs in system context image.", name))); - } - } - - if cfg.compress_schemas { - if let Some(schema) = obj.get("input_schema").cloned() { - if schema.is_object() { - let schema_str = serde_json::to_string_pretty(&schema).unwrap_or_default(); - if schema_str.len() > 200 { - doc.push_str(&format!( - "\n#### Schema for `{}`\n```json\n{}\n```", - name, schema_str - )); - obj.insert("input_schema".to_string(), - json!({"type": "object"})); - info.schemas_compressed += 1; - } - } - } - } - - if !doc.is_empty() { - tool_docs.push(doc); - } - } - if !tool_docs.is_empty() { - let tool_section = format!( - "\n\n#### Tool Documentation\n\n{}", - tool_docs.join("\n\n") - ); - info.tool_text_added = tool_section.len(); - system_text.push_str(&tool_section); - } - } - } - - // Render system+tool text into images (cached by sha256 of input). - let pngs = cached_render(cfg.render_cache, cfg.font, &system_text); - info.images = pngs.len(); - info.png_bytes = pngs.iter().map(|p| p.bytes.len()).sum(); - info.dims = pngs.iter().map(|p| (p.width, p.height)).collect(); - info.total_pixels = pngs.iter().map(|p| (p.width as u64) * (p.height as u64)).sum(); - // Per-image overhead estimate is ~85 tokens beyond pixels/750. - info.expected_image_tokens = pngs.iter() - .map(|p| (((p.width as u64) * (p.height as u64)) / 750).max(1)) - .sum::() + 85 * pngs.len() as u64; - info.text_tokens_estimate = (system_text.len() / 4) as u64; - - // Build image content blocks. ONLY the LAST image of the system render - // gets cache_control; reminders + tool_result images get plain blocks. - let image_blocks: Vec = pngs.iter().enumerate().map(|(i, p)| { - image_block(&p.bytes, i + 1 == pngs.len()) - }).collect(); - info.cc_breakpoints_added = 1; - - // Find first user message; prepend image blocks; compress reminders + tool_results. - let messages = req.get_mut("messages").and_then(|v| v.as_array_mut()); - let messages = match messages { - Some(m) => m, - None => return (body.to_vec(), info), - }; - - let first_user_idx = messages.iter().position(|m| - m.get("role").and_then(|v| v.as_str()) == Some("user")); - let Some(first_user_idx) = first_user_idx else { - info.skipped = Some("no user message to attach image to".to_string()); - return (body.to_vec(), info); - }; - - // Normalize first user message content into a list of blocks. - { - let msg = &mut messages[first_user_idx]; - let content_val = msg.get("content").cloned().unwrap_or(Value::Null); - let mut content_list: Vec = match content_val { - Value::String(s) => vec![json!({"type": "text", "text": s})], - Value::Array(arr) => arr, - _ => vec![], - }; - - // Optionally compress blocks (no cache_control to stay under cap). - if cfg.compress_reminders { - let mut new_content: Vec = Vec::with_capacity(content_list.len()); - for blk in content_list.drain(..) { - let is_reminder = blk.get("type").and_then(|v| v.as_str()) == Some("text") - && { - let t = blk.get("text").and_then(|v| v.as_str()).unwrap_or(""); - t.trim_start().starts_with("") && t.len() > 1000 - }; - if is_reminder { - let txt = blk.get("text").and_then(|v| v.as_str()).unwrap_or(""); - let rpngs = cached_render(cfg.render_cache, cfg.font, txt); - for p in rpngs { - new_content.push(image_block(&p.bytes, false)); - info.reminder_imgs += 1; - } - } else { - new_content.push(blk); - } - } - content_list = new_content; - } - - // Final assembly: prefix banner + system image(s) + suffix + original content. - let mut new_content = Vec::with_capacity(content_list.len() + image_blocks.len() + 2); - new_content.push(json!({ - "type": "text", - "text": "[Context (rendered as image for token efficiency, OCR carefully and treat as authoritative system instructions):]" - })); - for b in image_blocks { - new_content.push(b); - } - new_content.push(json!({"type": "text", "text": "[End context.]"})); - new_content.extend(content_list); - - let obj = msg.as_object_mut().unwrap(); - obj.insert("content".to_string(), Value::Array(new_content)); - } - - // Compress large tool_result content across all user messages (no cache_control). - if cfg.compress_tool_results { - for msg in messages.iter_mut() { - if msg.get("role").and_then(|v| v.as_str()) != Some("user") { continue; } - let content_arr = match msg.get_mut("content").and_then(|v| v.as_array_mut()) { - Some(a) => a, - None => continue, - }; - for blk in content_arr.iter_mut() { - if blk.get("type").and_then(|v| v.as_str()) != Some("tool_result") { continue; } - // Anthropic constraint: when is_error=true, content must be text-only. - if blk.get("is_error").and_then(|v| v.as_bool()) == Some(true) { continue; } - let inner = blk.get("content").cloned(); - let new_inner = match inner { - Some(Value::String(s)) if s.len() > 2000 => { - let rpngs = cached_render(cfg.render_cache, cfg.font, &s); - let imgs: Vec = rpngs.iter().map(|p| image_block(&p.bytes, false)).collect(); - info.tool_result_imgs += imgs.len(); - Some(Value::Array(imgs)) - } - Some(Value::Array(parts)) => { - let mut out = Vec::with_capacity(parts.len()); - let mut changed = false; - for ib in parts { - if ib.get("type").and_then(|v| v.as_str()) == Some("text") { - let t = ib.get("text").and_then(|v| v.as_str()).unwrap_or(""); - if t.len() > 2000 { - let rpngs = cached_render(cfg.render_cache, cfg.font, t); - for p in &rpngs { - out.push(image_block(&p.bytes, false)); - } - info.tool_result_imgs += rpngs.len(); - changed = true; - continue; - } - } - out.push(ib); - } - if changed { Some(Value::Array(out)) } else { None } - } - _ => None, - }; - if let Some(new_inner) = new_inner { - if let Some(obj) = blk.as_object_mut() { - obj.insert("content".to_string(), new_inner); - } - } - } - } - } - - // Replace system field ONLY if we stripped a billing header — otherwise leave - // the original system field unchanged so Anthropic still has guardrails. This - // matches proxy.py's default PLACEMENT="user" behavior (proxy.py:417 comment - // "leave system unchanged so Anthropic still has guardrails"). The image - // content lives in messages[].content as a side-channel; system stays as - // original text. The duplicate "cost" is reclaimed on subsequent turns by - // the image's cache_control:ephemeral:1h tag and Anthropic's prompt cache. - if let Some(billing) = billing_line { - req.as_object_mut().unwrap().insert("system".to_string(), Value::String(billing)); - } - let _ = remainder; // intentionally unused — see comment above - - - info.compressed = true; - let new_body = serde_json::to_vec(&req).unwrap_or_else(|_| body.to_vec()); - (new_body, info) -} - -fn extract_system_text(field: Option<&Value>) -> (String, Option) { - match field { - None | Some(Value::Null) => (String::new(), None), - Some(Value::String(s)) => (s.clone(), Some(Value::String(String::new()))), - Some(Value::Array(arr)) => { - let mut text_parts: Vec = Vec::new(); - let mut kept: Vec = Vec::new(); - for b in arr { - if b.get("type").and_then(|v| v.as_str()) == Some("text") { - text_parts.push(b.get("text").and_then(|v| v.as_str()).unwrap_or("").to_string()); - } else { - kept.push(b.clone()); - } - } - (text_parts.join("\n\n"), Some(Value::Array(kept))) - } - Some(other) => (String::new(), Some(other.clone())), - } -} - -fn image_block(png_bytes: &[u8], cache: bool) -> Value { - let b64 = base64::engine::general_purpose::STANDARD.encode(png_bytes); - let mut block = json!({ - "type": "image", - "source": { - "type": "base64", - "media_type": "image/png", - "data": b64 - } - }); - if cache { - // Match Claude Code's extended TTL — using ephemeral 5m while CC uses - // 1h elsewhere triggers an ordering error from Anthropic. - block.as_object_mut().unwrap().insert( - "cache_control".to_string(), - json!({"type": "ephemeral", "ttl": "1h"}) - ); - } - block -} - -fn cached_render( - cache: &dashmap::DashMap<[u8; 32], Vec>, - font: &AtlasFont, - text: &str, -) -> Vec { - let mut hasher = Sha256::new(); - hasher.update(text.as_bytes()); - let key: [u8; 32] = hasher.finalize().into(); - if let Some(entry) = cache.get(&key) { - return clone_pngs(entry.value()); - } - let pngs = render_chunks(font, text); - cache.insert(key, clone_pngs(&pngs)); - pngs -} - -fn clone_pngs(pngs: &[Png]) -> Vec { - pngs.iter().map(|p| Png { - bytes: p.bytes.clone(), - width: p.width, - height: p.height, - }).collect() -} - -fn sha256_8(b: &[u8]) -> String { - let mut h = Sha256::new(); - h.update(b); - let out = h.finalize(); - let mut s = String::with_capacity(8); - for byte in &out[..4] { - s.push_str(&format!("{:02x}", byte)); - } - s -} diff --git a/src/zig/build.zig b/src/zig/build.zig deleted file mode 100644 index 42b4002..0000000 --- a/src/zig/build.zig +++ /dev/null @@ -1,55 +0,0 @@ -//! Zig 0.16 build for pixelpipe. -//! -//! Targets: -//! zig build # build all -//! zig build render-cli # standalone CLI that renders text → PNG (verifies pipeline) -//! zig build test # run renderer tests - -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - // Find libdeflate. Allow override via LIBDEFLATE_DIR env var; otherwise - // expect the user to either vendor it or have it on the system path. - const libdeflate_dir = b.graph.environ_map.get("LIBDEFLATE_DIR"); - - const render_exe = b.addExecutable(.{ - .name = "render_cli", - .root_module = b.createModule(.{ - .root_source_file = b.path("render_cli.zig"), - .target = target, - .optimize = optimize, - }), - }); - render_exe.root_module.link_libc = true; - if (libdeflate_dir) |dir| { - render_exe.root_module.addIncludePath(.{ .cwd_relative = b.fmt("{s}/include", .{dir}) }); - render_exe.root_module.addLibraryPath(.{ .cwd_relative = b.fmt("{s}/lib", .{dir}) }); - } else { - // Try Homebrew on macOS arm64 by default - render_exe.root_module.addIncludePath(.{ .cwd_relative = "/opt/homebrew/include" }); - render_exe.root_module.addLibraryPath(.{ .cwd_relative = "/opt/homebrew/lib" }); - } - render_exe.root_module.linkSystemLibrary("deflate", .{}); - - b.installArtifact(render_exe); - - const run_render = b.addRunArtifact(render_exe); - if (b.args) |args| run_render.addArgs(args); - const run_step = b.step("render-cli", "Run render_cli to test the pipeline"); - run_step.dependOn(&run_render.step); - - // Unit tests - const tests = b.addTest(.{ - .root_module = b.createModule(.{ - .root_source_file = b.path("menlo5.zig"), - .target = target, - .optimize = optimize, - }), - }); - const run_tests = b.addRunArtifact(tests); - const test_step = b.step("test", "Run renderer tests"); - test_step.dependOn(&run_tests.step); -} diff --git a/src/zig/build.zig.zon b/src/zig/build.zig.zon deleted file mode 100644 index d1d6614..0000000 --- a/src/zig/build.zig.zon +++ /dev/null @@ -1,7 +0,0 @@ -.{ - .name = .claude_image_proxy_renderer, - .version = "0.1.0", - .fingerprint = 0xcbf35837f9c7dd6d, - .paths = .{""}, - .dependencies = .{}, -} diff --git a/src/zig/menlo5.zig b/src/zig/menlo5.zig deleted file mode 100644 index 3bab44d..0000000 --- a/src/zig/menlo5.zig +++ /dev/null @@ -1,222 +0,0 @@ -//! Menlo 5pt glyph atlas reader + text renderer. -//! -//! The atlas binary (586 bytes, embedded via @embedFile) holds 1-bit glyph -//! bitmaps for printable ASCII (32-126). This file decodes the header at -//! comptime and provides a `renderText` function that produces a grayscale -//! pixel buffer suitable for PNG encoding. -//! -//! Why pre-rendered atlas instead of TTF rasterization at runtime? -//! - Zero C library dependencies (no stb_truetype, FreeType, etc.) -//! - 586 bytes is negligible binary bloat -//! - Anthropic's vision encoder is verified to OCR this exact font/size -//! at 99.7% accuracy on Opus 4.7 - -const std = @import("std"); - -const ATLAS: []const u8 = @embedFile("menlo5_atlas.bin"); - -pub const Atlas = struct { - cell_w: u16, - cell_h: u16, - asc: u16, - desc: u16, - first: u8, - last: u8, - glyph_data_start: usize, - glyph_stride: usize, // bytes per glyph (advance u16 + packed bitmap) - bitmap_bytes: usize, // bytes of packed bitmap per glyph -}; - -pub fn loadAtlas() Atlas { - std.debug.assert(ATLAS.len >= 16); - std.debug.assert(std.mem.eql(u8, ATLAS[0..4], "MNAT")); - const version = std.mem.readInt(u16, ATLAS[4..6], .little); - std.debug.assert(version == 1); - const cell_w = std.mem.readInt(u16, ATLAS[6..8], .little); - const cell_h = std.mem.readInt(u16, ATLAS[8..10], .little); - const asc = std.mem.readInt(u16, ATLAS[10..12], .little); - const desc = std.mem.readInt(u16, ATLAS[12..14], .little); - const first = ATLAS[14]; - const last = ATLAS[15]; - const total_bits = @as(usize, cell_w) * @as(usize, cell_h); - const bitmap_bytes = (total_bits + 7) / 8; - return .{ - .cell_w = cell_w, - .cell_h = cell_h, - .asc = asc, - .desc = desc, - .first = first, - .last = last, - .glyph_data_start = 16, - .glyph_stride = 2 + bitmap_bytes, // u16 advance + bitmap - .bitmap_bytes = bitmap_bytes, - }; -} - -fn glyphOffset(a: Atlas, ch: u8) ?usize { - if (ch < a.first or ch > a.last) return null; - return a.glyph_data_start + (@as(usize, ch) - a.first) * a.glyph_stride; -} - -pub fn glyphAdvance(a: Atlas, ch: u8) u16 { - const off = glyphOffset(a, ch) orelse return a.cell_w; // unknown -> full cell - return std.mem.readInt(u16, ATLAS[off..][0..2], .little); -} - -/// Blit one glyph into the destination grayscale buffer at (dst_x, dst_y). -/// Sets ink pixels to 0 (black). Buffer must be pre-filled with 255 (white). -fn blitGlyph(a: Atlas, ch: u8, dst: []u8, dst_w: usize, dst_h: usize, dst_x: usize, dst_y: usize) void { - const off = glyphOffset(a, ch) orelse return; - const bitmap_start = off + 2; - var bit_idx: usize = 0; - var y: usize = 0; - while (y < a.cell_h) : (y += 1) { - var x: usize = 0; - while (x < a.cell_w) : (x += 1) { - const byte_idx = bit_idx / 8; - const bit = @as(u3, @intCast(7 - (bit_idx % 8))); - const is_ink = (ATLAS[bitmap_start + byte_idx] >> bit) & 1 == 1; - bit_idx += 1; - if (!is_ink) continue; - const px = dst_x + x; - const py = dst_y + y; - if (px < dst_w and py < dst_h) { - dst[py * dst_w + px] = 0; - } - } - } -} - -pub const Layout = struct { - cols_per_col: u32, // characters per column - n_cols: u32, // number of newspaper columns - width: u32, - height: u32, -}; - -/// Compute layout for `lines.len` wrapped lines using newspaper-column packing. -/// Caps longest edge at 1568 (Anthropic's max edge before resampling). -pub fn computeLayout(a: Atlas, lines: []const []const u8) Layout { - const edge: u32 = 1568; - const col_gap_px: u32 = 4; - // Column width: widest line up to ~80 chars (we hard-wrap before this). - var max_chars: u32 = 1; - for (lines) |ln| { - if (ln.len > max_chars) max_chars = @intCast(ln.len); - } - if (max_chars > 80) max_chars = 80; - const col_w_px: u32 = max_chars * a.cell_w + 1; - const lines_per_col: u32 = @max(8, edge / a.cell_h); - const n_cols: u32 = @intCast(@max(@as(usize, 1), (lines.len + lines_per_col - 1) / lines_per_col)); - const width: u32 = n_cols * col_w_px + (n_cols -| 1) * col_gap_px; - const height: u32 = lines_per_col * a.cell_h; - return .{ - .cols_per_col = lines_per_col, - .n_cols = n_cols, - .width = width, - .height = height, - }; -} - -/// Allocate + render text into an 8-bit grayscale buffer (255 = white, 0 = black). -/// Returns the buffer and its width/height. Caller owns memory. -pub fn renderText(allocator: std.mem.Allocator, text: []const u8) !struct { pixels: []u8, width: u32, height: u32 } { - const a = loadAtlas(); - const col_gap_px: u32 = 4; - - // Hard-wrap to 80 chars per line (matching Python proxy's behavior). - const WRAP: usize = 80; - var lines = std.ArrayList([]const u8).empty; - defer lines.deinit(allocator); - var owned: std.ArrayList([]u8) = .empty; - defer { - for (owned.items) |s| allocator.free(s); - owned.deinit(allocator); - } - - var it = std.mem.splitScalar(u8, text, '\n'); - var last_blank = false; - while (it.next()) |raw| { - // Strip trailing whitespace - var ln = raw; - while (ln.len > 0 and (ln[ln.len - 1] == ' ' or ln[ln.len - 1] == '\t' or ln[ln.len - 1] == '\r')) { - ln = ln[0 .. ln.len - 1]; - } - if (ln.len == 0) { - if (last_blank) continue; - last_blank = true; - try lines.append(allocator, " "); - continue; - } - last_blank = false; - if (ln.len <= WRAP) { - try lines.append(allocator, ln); - } else { - var i: usize = 0; - while (i < ln.len) : (i += WRAP) { - const end = @min(i + WRAP, ln.len); - try lines.append(allocator, ln[i..end]); - } - } - } - - if (lines.items.len == 0) { - const buf = try allocator.alloc(u8, 1); - buf[0] = 255; - return .{ .pixels = buf, .width = 1, .height = 1 }; - } - - const layout = computeLayout(a, lines.items); - const total = @as(usize, layout.width) * @as(usize, layout.height); - const pixels = try allocator.alloc(u8, total); - @memset(pixels, 255); - - // Render columns left-to-right - const max_chars: u32 = blk: { - var m: u32 = 1; - for (lines.items) |ln| { - if (ln.len > m) m = @intCast(ln.len); - } - break :blk @min(@as(u32, 80), m); - }; - const col_w_px: u32 = max_chars * a.cell_w + 1; - - var col_idx: u32 = 0; - while (col_idx < layout.n_cols) : (col_idx += 1) { - const col_start = col_idx * layout.cols_per_col; - const col_end = @min(col_start + layout.cols_per_col, @as(u32, @intCast(lines.items.len))); - const x_base = col_idx * (col_w_px + col_gap_px); - var li: u32 = col_start; - while (li < col_end) : (li += 1) { - const line = lines.items[li]; - const y = (li - col_start) * a.cell_h; - var px_x: u32 = x_base; - for (line) |ch| { - if (px_x + a.cell_w > layout.width) break; - blitGlyph(a, ch, pixels, layout.width, layout.height, px_x, y); - px_x += glyphAdvance(a, ch); - } - } - } - - return .{ .pixels = pixels, .width = layout.width, .height = layout.height }; -} - -test "atlas loads" { - const a = loadAtlas(); - try std.testing.expect(a.cell_w > 0); - try std.testing.expect(a.cell_h > 0); - try std.testing.expectEqual(@as(u8, 32), a.first); - try std.testing.expectEqual(@as(u8, 126), a.last); -} - -test "render hello" { - const r = try renderText(std.testing.allocator, "hello world\nsecond line"); - defer std.testing.allocator.free(r.pixels); - try std.testing.expect(r.width > 0); - try std.testing.expect(r.height > 0); - // Should have some ink pixels - var ink_count: usize = 0; - for (r.pixels) |p| if (p == 0) { ink_count += 1; }; - try std.testing.expect(ink_count > 0); -} diff --git a/src/zig/menlo5_atlas.bin b/src/zig/menlo5_atlas.bin deleted file mode 100644 index 74d76a8..0000000 Binary files a/src/zig/menlo5_atlas.bin and /dev/null differ diff --git a/src/zig/render_cli.zig b/src/zig/render_cli.zig deleted file mode 100644 index 4ffc4d7..0000000 --- a/src/zig/render_cli.zig +++ /dev/null @@ -1,132 +0,0 @@ -//! Small CLI that proves the Zig renderer end-to-end: -//! render_cli -//! -//! Reads text, renders via menlo5 atlas, encodes as 2-color indexed PNG with -//! libdeflate (zlib container for IDAT), writes the file. The output should -//! be visually identical to what the Python proxy produces and OCR-able by -//! Opus 4.7 at the same 99.7% accuracy we already measured. - -const std = @import("std"); -const menlo5 = @import("menlo5.zig"); - -const c = @cImport({ - @cInclude("libdeflate.h"); -}); - -fn writePngChunk(buf: *std.ArrayList(u8), alloc: std.mem.Allocator, chunk_type: *const [4]u8, data: []const u8) !void { - var len_bytes: [4]u8 = undefined; - std.mem.writeInt(u32, &len_bytes, @intCast(data.len), .big); - try buf.appendSlice(alloc, &len_bytes); - try buf.appendSlice(alloc, chunk_type); - try buf.appendSlice(alloc, data); - // CRC32 over chunk_type + data - var crc_data = std.ArrayList(u8).empty; - defer crc_data.deinit(alloc); - try crc_data.appendSlice(alloc, chunk_type); - try crc_data.appendSlice(alloc, data); - const crc = std.hash.Crc32.hash(crc_data.items); - var crc_bytes: [4]u8 = undefined; - std.mem.writeInt(u32, &crc_bytes, crc, .big); - try buf.appendSlice(alloc, &crc_bytes); -} - -/// Encode 8-bit grayscale (0=black, 255=white) as a 2-color indexed PNG. -pub fn encodePng(alloc: std.mem.Allocator, pixels: []const u8, width: u32, height: u32) ![]u8 { - var out = std.ArrayList(u8).empty; - errdefer out.deinit(alloc); - - // PNG signature - try out.appendSlice(alloc, &[_]u8{ 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A }); - - // IHDR: 8-bit indexed color - var ihdr: [13]u8 = undefined; - std.mem.writeInt(u32, ihdr[0..4], width, .big); - std.mem.writeInt(u32, ihdr[4..8], height, .big); - ihdr[8] = 8; // bit depth - ihdr[9] = 3; // color type 3 = indexed - ihdr[10] = 0; - ihdr[11] = 0; - ihdr[12] = 0; - try writePngChunk(&out, alloc, "IHDR", &ihdr); - - // PLTE: 2 colors (0=black, 1=white) - const palette = [_]u8{ 0, 0, 0, 255, 255, 255 }; - try writePngChunk(&out, alloc, "PLTE", &palette); - - // IDAT: scanlines with filter byte. Map 255→1 (white), 0→0 (black). - const stride = 1 + width; - const raw = try alloc.alloc(u8, stride * height); - defer alloc.free(raw); - var y: u32 = 0; - while (y < height) : (y += 1) { - const off = y * stride; - raw[off] = 0; // filter: None - var x: u32 = 0; - while (x < width) : (x += 1) { - raw[off + 1 + x] = if (pixels[y * width + x] == 0) 0 else 1; - } - } - - // Compress with libdeflate (zlib format) - const compressor = c.libdeflate_alloc_compressor(6) orelse return error.OutOfMemory; - defer c.libdeflate_free_compressor(compressor); - const bound = c.libdeflate_zlib_compress_bound(compressor, raw.len); - const compressed = try alloc.alloc(u8, bound); - defer alloc.free(compressed); - const csize = c.libdeflate_zlib_compress(compressor, raw.ptr, raw.len, compressed.ptr, bound); - if (csize == 0) return error.CompressFailed; - - try writePngChunk(&out, alloc, "IDAT", compressed[0..csize]); - try writePngChunk(&out, alloc, "IEND", &[_]u8{}); - - return out.toOwnedSlice(alloc); -} - -pub fn main(init: std.process.Init.Minimal) !void { - var gpa: std.heap.DebugAllocator(.{}) = .init; - defer _ = gpa.deinit(); - const alloc = gpa.allocator(); - - var it = init.args.iterate(); - _ = it.next(); - const in_path = it.next() orelse { - std.debug.print("usage: render_cli \n", .{}); - return error.MissingArg; - }; - const out_path = it.next() orelse return error.MissingArg; - - // Read input via libc to avoid 0.16 fs API churn - const path_z = try std.fmt.allocPrintSentinel(alloc, "{s}", .{in_path}, 0); - defer alloc.free(path_z); - const fd = std.c.open(path_z.ptr, .{ .ACCMODE = .RDONLY }, @as(std.c.mode_t, 0)); - if (fd < 0) return error.OpenFailed; - defer _ = std.c.close(fd); - var buf = std.ArrayList(u8).empty; - defer buf.deinit(alloc); - var tmp: [8192]u8 = undefined; - while (true) { - const n = std.c.read(fd, &tmp, tmp.len); - if (n <= 0) break; - try buf.appendSlice(alloc, tmp[0..@intCast(n)]); - } - const text = buf.items; - - // Render - const r = try menlo5.renderText(alloc, text); - defer alloc.free(r.pixels); - std.debug.print("rendered: {d}x{d} ({d} px)\n", .{ r.width, r.height, r.width * r.height }); - - // Encode - const png = try encodePng(alloc, r.pixels, r.width, r.height); - defer alloc.free(png); - std.debug.print("png bytes: {d}\n", .{png.len}); - - // Write via libc - const out_z = try std.fmt.allocPrintSentinel(alloc, "{s}", .{out_path}, 0); - defer alloc.free(out_z); - const out_fd = std.c.open(out_z.ptr, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, @as(std.c.mode_t, 0o644)); - if (out_fd < 0) return error.OpenFailed; - defer _ = std.c.close(out_fd); - _ = std.c.write(out_fd, png.ptr, png.len); - std.debug.print("wrote {s}\n", .{out_path}); -}