From 13295390596d347c20fa4449f737991ea9d89cba Mon Sep 17 00:00:00 2001 From: rm-dr <96270320+rm-dr@users.noreply.github.com> Date: Sat, 8 Nov 2025 13:03:31 -0800 Subject: [PATCH] Add `pixel-transform` --- Cargo.lock | 636 +++++++++++++++++- Cargo.toml | 2 + crates/lib/page/Cargo.toml | 2 + crates/lib/pixel-transform/Cargo.toml | 16 + crates/lib/pixel-transform/src/chain.rs | 145 ++++ crates/lib/pixel-transform/src/lib.rs | 6 + crates/lib/pixel-transform/src/pixeldim.rs | 68 ++ .../pixel-transform/src/transformers/crop.rs | 184 +++++ .../src/transformers/maxdim.rs | 82 +++ .../pixel-transform/src/transformers/mod.rs | 165 +++++ crates/lib/toolbox/src/mime.rs | 240 ++++--- 11 files changed, 1433 insertions(+), 113 deletions(-) create mode 100644 crates/lib/pixel-transform/Cargo.toml create mode 100644 crates/lib/pixel-transform/src/chain.rs create mode 100644 crates/lib/pixel-transform/src/lib.rs create mode 100644 crates/lib/pixel-transform/src/pixeldim.rs create mode 100644 crates/lib/pixel-transform/src/transformers/crop.rs create mode 100644 crates/lib/pixel-transform/src/transformers/maxdim.rs create mode 100644 crates/lib/pixel-transform/src/transformers/mod.rs diff --git a/Cargo.lock b/Cargo.lock index ae1cf44..03647ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -133,12 +142,29 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "argparse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f8ebf5827e4ac4fd5946560e6a99776ea73b596d80898f357007317a7141e47" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "ast_node" version = "5.0.0" @@ -175,6 +201,29 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +dependencies = [ + "arrayvec", +] + [[package]] name = "axum" version = "0.8.6" @@ -279,12 +328,24 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + [[package]] name = "block-buffer" version = "0.10.4" @@ -315,12 +376,30 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.10.1" @@ -381,6 +460,16 @@ dependencies = [ "shlex", ] +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -452,6 +541,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.4" @@ -557,6 +652,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.6" @@ -731,12 +857,47 @@ dependencies = [ "serde", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fancy-regex" version = "0.16.2" @@ -748,6 +909,35 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[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.4" @@ -895,6 +1085,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "glob" version = "0.3.3" @@ -945,6 +1145,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1247,6 +1458,46 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + [[package]] name = "indexmap" version = "2.12.0" @@ -1259,6 +1510,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1292,6 +1554,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -1342,12 +1613,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libc" version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libredox" version = "0.1.10" @@ -1431,6 +1718,15 @@ dependencies = [ "prost-types", ] +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru" version = "0.16.2" @@ -1517,6 +1813,16 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "md-dev" version = "0.2.0" @@ -1611,6 +1917,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "multer" version = "3.1.0" @@ -1634,6 +1950,21 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1682,6 +2013,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -1759,7 +2101,9 @@ dependencies = [ "lru", "maud", "parking_lot", + "pixel-transform", "serde", + "tokio", "toolbox", "tower-http", "tracing", @@ -1788,6 +2132,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1866,6 +2216,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pixel-transform" +version = "0.0.1" +dependencies = [ + "image", + "serde", + "strum", + "thiserror 2.0.17", + "toolbox", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -1885,6 +2246,19 @@ dependencies = [ "time", ] +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1963,6 +2337,25 @@ dependencies = [ "version_check", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.108", +] + [[package]] name = "prost" version = "0.13.5" @@ -1980,7 +2373,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.108", @@ -2021,6 +2414,30 @@ dependencies = [ "psl-types", ] +[[package]] +name = "pxfm" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +dependencies = [ + "num-traits", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.38.3" @@ -2159,6 +2576,76 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.12.1", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand 0.8.5", + "rand_chacha 0.3.1", + "simd_helpers", + "system-deps", + "thiserror 1.0.69", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "readonly" version = "0.2.13" @@ -2273,6 +2760,12 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" + [[package]] name = "ring" version = "0.17.14" @@ -2468,6 +2961,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_spanned" version = "1.0.3" @@ -2508,7 +3010,7 @@ dependencies = [ "serde", "strum", "tokio", - "toml", + "toml 0.9.8", "toolbox", "tracing", ] @@ -2554,6 +3056,15 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -2799,6 +3310,25 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml 0.8.23", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "term" version = "0.7.0" @@ -2915,6 +3445,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.44" @@ -3033,6 +3577,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit", +] + [[package]] name = "toml" version = "0.9.8" @@ -3041,13 +3597,22 @@ checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ "indexmap", "serde_core", - "serde_spanned", - "toml_datetime", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", "toml_parser", "toml_writer", "winnow", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.3" @@ -3057,6 +3622,19 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "winnow", +] + [[package]] name = "toml_parser" version = "1.0.4" @@ -3402,12 +3980,29 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d" +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + [[package]] name = "version_check" version = "0.9.5" @@ -3563,6 +4158,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009936b22a61d342859b5f0ea64681cbb35a358ab548e2a44a8cf0dac2d980b8" + [[package]] name = "winapi" version = "0.3.9" @@ -3823,6 +4424,9 @@ name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] [[package]] name = "wit-bindgen" @@ -4013,3 +4617,27 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 07457e0..9928dcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ toolbox = { path = "crates/lib/toolbox" } page = { path = "crates/lib/page" } md-footnote = { path = "crates/lib/md-footnote" } md-dev = { path = "crates/lib/md-dev" } +pixel-transform = { path = "crates/lib/pixel-transform" } service-webpage = { path = "crates/service/service-webpage" } @@ -142,6 +143,7 @@ chrono = "0.4.42" lru = "0.16.2" parking_lot = "0.12.5" lazy_static = "1.5.0" +image = "0.25.8" # md_* test utilities prettydiff = "0.9.0" diff --git a/crates/lib/page/Cargo.toml b/crates/lib/page/Cargo.toml index 4c4af4a..55bfcd8 100644 --- a/crates/lib/page/Cargo.toml +++ b/crates/lib/page/Cargo.toml @@ -10,8 +10,10 @@ workspace = true [dependencies] toolbox = { workspace = true } libservice = { workspace = true } +pixel-transform = { workspace = true } axum = { workspace = true } +tokio = { workspace = true } tracing = { workspace = true } maud = { workspace = true } chrono = { workspace = true } diff --git a/crates/lib/pixel-transform/Cargo.toml b/crates/lib/pixel-transform/Cargo.toml new file mode 100644 index 0000000..10b09f3 --- /dev/null +++ b/crates/lib/pixel-transform/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "pixel-transform" +version = { workspace = true } +rust-version = { workspace = true } +edition = { workspace = true } + +[lints] +workspace = true + +[dependencies] +toolbox = { workspace = true } + +serde = { workspace = true } +thiserror = { workspace = true } +image = { workspace = true } +strum = { workspace = true } diff --git a/crates/lib/pixel-transform/src/chain.rs b/crates/lib/pixel-transform/src/chain.rs new file mode 100644 index 0000000..328da6c --- /dev/null +++ b/crates/lib/pixel-transform/src/chain.rs @@ -0,0 +1,145 @@ +use image::{DynamicImage, ImageFormat}; +use serde::{Deserialize, Deserializer, de}; +use std::{fmt::Display, hash::Hash, io::Cursor, str::FromStr}; +use thiserror::Error; +use toolbox::mime::MimeType; + +use crate::transformers::{ImageTransformer, TransformerEnum}; + +#[derive(Debug, Error)] +pub enum TransformBytesError { + #[error("{0} is not a valid image type")] + NotAnImage(String), + + #[error("error while processing image")] + ImageError(#[from] image::ImageError), +} + +#[derive(Debug, Clone)] +pub struct TransformerChain { + pub steps: Vec, +} + +impl TransformerChain { + #[inline] + pub fn mime_is_image(mime: &MimeType) -> bool { + ImageFormat::from_mime_type(mime.to_string()).is_some() + } + + pub fn transform_image(&self, mut image: DynamicImage) -> DynamicImage { + for step in &self.steps { + match step { + TransformerEnum::Format { .. } => {} + TransformerEnum::MaxDim(t) => t.transform(&mut image), + TransformerEnum::Crop(t) => t.transform(&mut image), + } + } + + return image; + } + + pub fn transform_bytes( + &self, + image_bytes: &[u8], + image_format: Option<&MimeType>, + ) -> Result<(MimeType, Vec), TransformBytesError> { + let format: ImageFormat = match image_format { + Some(x) => ImageFormat::from_mime_type(x.to_string()) + .ok_or(TransformBytesError::NotAnImage(x.to_string()))?, + None => image::guess_format(image_bytes)?, + }; + + let out_format = self + .steps + .last() + .and_then(|x| match x { + TransformerEnum::Format { format } => Some(format), + _ => None, + }) + .unwrap_or(&format); + + let img = image::load_from_memory_with_format(image_bytes, format)?; + let img = self.transform_image(img); + + let out_mime = MimeType::from(out_format.to_mime_type()); + let mut out_bytes = Cursor::new(Vec::new()); + img.write_to(&mut out_bytes, *out_format)?; + + return Ok((out_mime, out_bytes.into_inner())); + } +} + +impl FromStr for TransformerChain { + type Err = String; + fn from_str(s: &str) -> Result { + let steps_str = s.split(";"); + + let mut steps = Vec::new(); + for s in steps_str { + let s = s.trim(); + if s.is_empty() { + continue; + } + + let step = s.parse(); + match step { + Ok(x) => steps.push(x), + Err(msg) => return Err(format!("invalid step `{s}`: {msg}")), + } + } + + let n_format = steps + .iter() + .filter(|x| matches!(x, TransformerEnum::Format { .. })) + .count(); + if n_format > 2 { + return Err("provide at most one format()".to_owned()); + } + + if n_format == 1 && !matches!(steps.last(), Some(TransformerEnum::Format { .. })) { + return Err("format() must be last".to_owned()); + } + + return Ok(Self { steps }); + } +} + +impl<'de> Deserialize<'de> for TransformerChain { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Self::from_str(&s).map_err(de::Error::custom) + } +} + +impl Display for TransformerChain { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut first = true; + for step in &self.steps { + if first { + write!(f, "{step}")?; + first = false + } else { + write!(f, ";{step}")?; + } + } + + return Ok(()); + } +} + +impl PartialEq for TransformerChain { + fn eq(&self, other: &Self) -> bool { + self.to_string() == other.to_string() + } +} + +impl Eq for TransformerChain {} + +impl Hash for TransformerChain { + fn hash(&self, state: &mut H) { + self.to_string().hash(state); + } +} diff --git a/crates/lib/pixel-transform/src/lib.rs b/crates/lib/pixel-transform/src/lib.rs new file mode 100644 index 0000000..b2cb573 --- /dev/null +++ b/crates/lib/pixel-transform/src/lib.rs @@ -0,0 +1,6 @@ +mod pixeldim; + +pub mod transformers; + +mod chain; +pub use chain::*; diff --git a/crates/lib/pixel-transform/src/pixeldim.rs b/crates/lib/pixel-transform/src/pixeldim.rs new file mode 100644 index 0000000..b742c21 --- /dev/null +++ b/crates/lib/pixel-transform/src/pixeldim.rs @@ -0,0 +1,68 @@ +use serde::{Deserialize, Deserializer}; +use std::fmt; +use std::str::FromStr; + +// TODO: parse -, + (100vw - 10px) +// TODO: parse 100vw [min] 10 +// TODO: parse 100vw [max] 10 + +#[derive(Debug, Clone, PartialEq)] +pub enum PixelDim { + Pixels(u32), + WidthPercent(f32), + HeightPercent(f32), +} + +impl FromStr for PixelDim { + type Err = String; + + fn from_str(s: &str) -> Result { + let numeric_end = s.find(|c: char| !c.is_ascii_digit() && c != '.'); + + let (quantity, unit) = numeric_end.map(|x| s.split_at(x)).unwrap_or((s, "px")); + let quantity = quantity.trim(); + let unit = unit.trim(); + + match unit { + "vw" => Ok(PixelDim::WidthPercent( + quantity + .parse() + .map_err(|_err| format!("invalid quantity {quantity}"))?, + )), + + "vh" => Ok(PixelDim::HeightPercent( + quantity + .parse() + .map_err(|_err| format!("invalid quantity {quantity}"))?, + )), + + "px" => Ok(PixelDim::Pixels( + quantity + .parse() + .map_err(|_err| format!("invalid quantity {quantity}"))?, + )), + + _ => Err(format!("invalid unit {unit}")), + } + } +} + +impl<'de> Deserialize<'de> for PixelDim { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + FromStr::from_str(&s).map_err(serde::de::Error::custom) + } +} + +impl fmt::Display for PixelDim { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PixelDim::Pixels(px) => write!(f, "{px}"), + PixelDim::WidthPercent(p) => write!(f, "{p:.2}vw"), + PixelDim::HeightPercent(p) => write!(f, "{p:.2}vh"), + } + } +} diff --git a/crates/lib/pixel-transform/src/transformers/crop.rs b/crates/lib/pixel-transform/src/transformers/crop.rs new file mode 100644 index 0000000..05943f7 --- /dev/null +++ b/crates/lib/pixel-transform/src/transformers/crop.rs @@ -0,0 +1,184 @@ +use std::{fmt::Display, str::FromStr}; + +use image::DynamicImage; +use serde::{Deserialize, Serialize}; +use strum::{Display, EnumString}; + +use crate::{pixeldim::PixelDim, transformers::ImageTransformer}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, Serialize, Deserialize, Display)] +pub enum Direction { + #[serde(rename = "n")] + #[strum(to_string = "n")] + #[strum(serialize = "north")] + North, + + #[serde(rename = "e")] + #[strum(serialize = "e")] + #[strum(serialize = "east")] + East, + + #[serde(rename = "s")] + #[strum(serialize = "s")] + #[strum(serialize = "south")] + South, + + #[serde(rename = "w")] + #[strum(to_string = "w")] + #[strum(serialize = "west")] + West, + + #[serde(rename = "c")] + #[strum(serialize = "c")] + #[strum(serialize = "center")] + Center, + + #[serde(rename = "ne")] + #[strum(serialize = "ne")] + #[strum(serialize = "northeast")] + NorthEast, + + #[serde(rename = "se")] + #[strum(serialize = "se")] + #[strum(serialize = "southeast")] + SouthEast, + + #[serde(rename = "nw")] + #[strum(serialize = "nw")] + #[strum(serialize = "northwest")] + NorthWest, + + #[serde(rename = "sw")] + #[strum(serialize = "sw")] + #[strum(serialize = "southwest")] + SouthWest, +} + +/// Crop an image to the given size. +/// - does not crop width if `w` is greater than image width +/// - does not crop height if `h` is greater than image height +/// - does nothing if `w` or `h` are less than or equal to zero. +#[derive(Debug, Clone, PartialEq)] +pub struct CropTransformer { + w: PixelDim, + h: PixelDim, + float: Direction, +} + +impl CropTransformer { + pub fn new(w: PixelDim, h: PixelDim, float: Direction) -> Self { + Self { w, h, float } + } + + fn crop_dim(&self, img_width: u32, img_height: u32) -> (u32, u32) { + let crop_width = match self.w { + PixelDim::Pixels(w) => w, + PixelDim::WidthPercent(pct) => ((img_width as f32) * pct / 100.0) as u32, + PixelDim::HeightPercent(pct) => ((img_height as f32) * pct / 100.0) as u32, + }; + + let crop_height = match self.h { + PixelDim::Pixels(h) => h, + PixelDim::WidthPercent(pct) => ((img_width as f32) * pct / 100.0) as u32, + PixelDim::HeightPercent(pct) => ((img_height as f32) * pct / 100.0) as u32, + }; + + (crop_width, crop_height) + } + + #[expect(clippy::integer_division)] + fn crop_pos( + &self, + img_width: u32, + img_height: u32, + crop_width: u32, + crop_height: u32, + ) -> (u32, u32) { + match self.float { + Direction::North => { + let x = (img_width - crop_width) / 2; + let y = 0; + (x, y) + } + Direction::East => { + let x = img_width - crop_width; + let y = (img_height - crop_height) / 2; + (x, y) + } + Direction::South => { + let x = (img_width - crop_width) / 2; + let y = img_height - crop_height; + (x, y) + } + Direction::West => { + let x = 0; + let y = (img_height - crop_height) / 2; + (x, y) + } + Direction::Center => { + let x = (img_width - crop_width) / 2; + let y = (img_height - crop_height) / 2; + (x, y) + } + Direction::NorthEast => { + let x = img_width - crop_width; + let y = 0; + (x, y) + } + Direction::SouthEast => { + let x = img_width - crop_width; + let y = img_height - crop_height; + (x, y) + } + Direction::NorthWest => { + let x = 0; + let y = 0; + (x, y) + } + Direction::SouthWest => { + let x = 0; + let y = img_height - crop_height; + (x, y) + } + } + } +} + +impl Display for CropTransformer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "crop({},{},{})", self.w, self.h, self.float) + } +} + +impl ImageTransformer for CropTransformer { + fn parse_args(args: &str) -> Result { + let args: Vec<&str> = args.split(",").collect(); + if args.len() != 3 { + return Err(format!("expected 3 args, got {}", args.len())); + } + + let w = args[0].trim().parse::()?; + let h = args[1].trim().parse::()?; + + let direction = args[2].trim(); + let direction = Direction::from_str(direction) + .map_err(|_err| format!("invalid direction {direction}"))?; + + Ok(Self { + w, + h, + float: direction, + }) + } + + fn transform(&self, input: &mut DynamicImage) { + let (img_width, img_height) = (input.width(), input.height()); + let (crop_width, crop_height) = self.crop_dim(img_width, img_height); + + if (crop_width < img_width || crop_height < img_height) && crop_width > 0 && crop_height > 0 + { + let (x, y) = self.crop_pos(img_width, img_height, crop_width, crop_height); + *input = input.crop(x, y, crop_width, crop_height); + } + } +} diff --git a/crates/lib/pixel-transform/src/transformers/maxdim.rs b/crates/lib/pixel-transform/src/transformers/maxdim.rs new file mode 100644 index 0000000..ccefca8 --- /dev/null +++ b/crates/lib/pixel-transform/src/transformers/maxdim.rs @@ -0,0 +1,82 @@ +use std::fmt::Display; + +use image::{DynamicImage, imageops::FilterType}; + +use crate::{pixeldim::PixelDim, transformers::ImageTransformer}; + +#[derive(Debug, Clone, PartialEq)] +pub struct MaxDimTransformer { + w: PixelDim, + h: PixelDim, +} + +impl MaxDimTransformer { + pub fn new(w: PixelDim, h: PixelDim) -> Self { + Self { w, h } + } + + fn target_dim(&self, img_width: u32, img_height: u32) -> (u32, u32) { + let max_width = match self.w { + PixelDim::Pixels(w) => Some(w), + PixelDim::WidthPercent(pct) => Some(((img_width as f32) * pct / 100.0) as u32), + PixelDim::HeightPercent(_) => None, + }; + + let max_height = match self.h { + PixelDim::Pixels(h) => Some(h), + PixelDim::HeightPercent(pct) => Some(((img_height as f32) * pct / 100.0) as u32), + PixelDim::WidthPercent(_) => None, + }; + + if max_width.map(|x| img_width <= x).unwrap_or(true) + && max_height.map(|x| img_height <= x).unwrap_or(true) + { + return (img_width, img_height); + } + + let width_ratio = max_width + .map(|x| x as f32 / img_width as f32) + .unwrap_or(1.0); + + let height_ratio = max_height + .map(|x| x as f32 / img_height as f32) + .unwrap_or(1.0); + + let ratio = width_ratio.min(height_ratio); + + ( + (img_width as f32 * ratio) as u32, + (img_height as f32 * ratio) as u32, + ) + } +} + +impl Display for MaxDimTransformer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "maxdim({},{})", self.w, self.h) + } +} + +impl ImageTransformer for MaxDimTransformer { + fn parse_args(args: &str) -> Result { + let args: Vec<&str> = args.split(",").collect(); + if args.len() != 2 { + return Err(format!("expected 2 args, got {}", args.len())); + } + + let w = args[0].parse::()?; + let h = args[1].parse::()?; + + Ok(Self { w, h }) + } + + fn transform(&self, input: &mut DynamicImage) { + let (img_width, img_height) = (input.width(), input.height()); + let (target_width, target_height) = self.target_dim(img_width, img_height); + + // Only resize if needed + if target_width != img_width || target_height != img_height { + *input = input.resize(target_width, target_height, FilterType::Lanczos3); + } + } +} diff --git a/crates/lib/pixel-transform/src/transformers/mod.rs b/crates/lib/pixel-transform/src/transformers/mod.rs new file mode 100644 index 0000000..95ef77d --- /dev/null +++ b/crates/lib/pixel-transform/src/transformers/mod.rs @@ -0,0 +1,165 @@ +use image::{DynamicImage, ImageFormat}; +use std::fmt; +use std::fmt::{Debug, Display}; +use std::str::FromStr; + +mod crop; +pub use crop::*; + +mod maxdim; +pub use maxdim::*; + +pub trait ImageTransformer +where + Self: PartialEq, + Self: Sized + Clone, + Self: Display + Debug, +{ + /// Transform the given image in place + fn transform(&self, input: &mut DynamicImage); + + /// Parse an arg string. + /// + /// `name({arg_string})` + fn parse_args(args: &str) -> Result; +} + +use serde::{Deserialize, Deserializer}; + +/// An enum of all [`ImageTransformer`]s +#[derive(Debug, Clone, PartialEq)] +pub enum TransformerEnum { + /// Usage: `maxdim(w, h)` + /// + /// Scale the image so its width is smaller than `w` + /// and its height is smaller than `h`. Aspect ratio is preserved. + /// + /// To only limit the size of one dimension, use `vw` or `vh`. + /// For example, `maxdim(50,100vh)` will not limit width. + MaxDim(MaxDimTransformer), + + /// Usage: `crop(w, h, float)` + /// + /// Crop the image to at most `w` by `h` pixels, + /// floating the crop area in the specified direction. + /// + /// Directions are one of: + /// - Cardinal: n,e,s,w + /// - Diagonal: ne,nw,se,sw, + /// - Centered: c + /// + /// Examples: + /// - `crop(100vw, 50)` gets the top 50 pixels of the image \ + /// (or fewer, if the image's height is smaller than 50) + /// + /// To only limit the size of one dimension, use `vw` or `vh`. + /// For example, `maxdim(50,100vh)` will not limit width. + Crop(CropTransformer), + + /// Usage: `format(format)` + /// + /// Transcode the image to the given format. + /// This step must be last, and cannot be provided + /// more than once. + /// + /// Valid formats: + /// - bmp + /// - gif + /// - ico + /// - jpeg or jpg + /// - png + /// - qoi + /// - webp + /// + /// Example: + /// - `format(png)` + /// + /// When transcoding an animated gif, the first frame is taken + /// and all others are thrown away. This happens even if we + /// transcode from a gif to a gif. + Format { format: ImageFormat }, +} + +impl FromStr for TransformerEnum { + type Err = String; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + + let (name, args) = { + let name_len = match s.find('(') { + Some(x) => x + 1, + None => { + return Err(format!( + "invalid transformation {s}. Must look like name(args)." + )); + } + }; + + let mut balance = 1; + let mut end = name_len; + for i in s[name_len..].bytes() { + match i { + b')' => balance -= 1, + b'(' => balance += 1, + _ => {} + } + + if balance == 0 { + break; + } + + end += 1; + } + + if balance != 0 { + return Err(format!("mismatched parenthesis in {s}")); + } + + let name = s[0..name_len - 1].trim(); + let args = s[name_len..end].trim(); + let trail = s[end + 1..].trim(); + if !trail.is_empty() { + return Err(format!( + "invalid transformation {s}. Must look like name(args)." + )); + } + + (name, args) + }; + + match name { + "maxdim" => Ok(Self::MaxDim(MaxDimTransformer::parse_args(args)?)), + "crop" => Ok(Self::Crop(CropTransformer::parse_args(args)?)), + + "format" => Ok(TransformerEnum::Format { + format: ImageFormat::from_extension(args) + .ok_or(format!("invalid image format {args}"))?, + }), + + _ => Err(format!("unknown transformation {name}")), + } + } +} + +impl<'de> Deserialize<'de> for TransformerEnum { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse().map_err(serde::de::Error::custom) + } +} + +impl Display for TransformerEnum { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TransformerEnum::MaxDim(x) => Display::fmt(x, f), + TransformerEnum::Crop(x) => Display::fmt(x, f), + TransformerEnum::Format { format } => { + write!(f, "format({})", format.extensions_str()[0]) + } + } + } +} diff --git a/crates/lib/toolbox/src/mime.rs b/crates/lib/toolbox/src/mime.rs index 098b1b4..2014ebd 100644 --- a/crates/lib/toolbox/src/mime.rs +++ b/crates/lib/toolbox/src/mime.rs @@ -64,6 +64,8 @@ pub enum MimeType { Jpg, /// Portable Network Graphics (image/png) Png, + /// Quite ok Image Format + Qoi, /// Scalable Vector Graphics (image/svg+xml) Svg, /// Tagged Image File Format (image/tiff) @@ -217,7 +219,9 @@ impl<'de> Deserialize<'de> for MimeType { } } +// // MARK: misc +// impl Default for MimeType { fn default() -> Self { @@ -243,6 +247,10 @@ impl From<&MimeType> for String { } } +// +// MARK: fromstr +// + impl FromStr for MimeType { type Err = std::convert::Infallible; @@ -251,7 +259,7 @@ impl FromStr for MimeType { Ok(match s { "application/octet-stream" => Self::Blob, - // MARK: Audio + // Audio "audio/aac" => Self::Aac, "audio/flac" => Self::Flac, "audio/midi" | "audio/x-midi" => Self::Midi, @@ -260,7 +268,7 @@ impl FromStr for MimeType { "audio/wav" => Self::Wav, "audio/webm" => Self::Weba, - // MARK: Video + // Video "video/x-msvideo" => Self::Avi, "video/mp4" => Self::Mp4, "video/mpeg" => Self::Mpeg, @@ -270,7 +278,7 @@ impl FromStr for MimeType { "video/3gpp" => Self::ThreeGp, "video/3gpp2" => Self::ThreeG2, - // MARK: Images + // Images "image/apng" => Self::Apng, "image/avif" => Self::Avif, "image/bmp" => Self::Bmp, @@ -281,8 +289,9 @@ impl FromStr for MimeType { "image/svg+xml" => Self::Svg, "image/tiff" => Self::Tiff, "image/webp" => Self::Webp, + "image/qoi" => Self::Qoi, - // MARK: Text + // Text "text/plain" => Self::Text, "text/css" => Self::Css, "text/csv" => Self::Csv, @@ -292,11 +301,11 @@ impl FromStr for MimeType { "application/ld+json" => Self::JsonLd, "application/xml" | "text/xml" => Self::Xml, - // MARK: Documents + // Documents "application/pdf" => Self::Pdf, "application/rtf" => Self::Rtf, - // MARK: Archives + // Archives "application/x-freearc" => Self::Arc, "application/x-bzip" => Self::Bz, "application/x-bzip2" => Self::Bz2, @@ -308,14 +317,14 @@ impl FromStr for MimeType { "application/x-tar" => Self::Tar, "application/zip" | "application/x-zip-compressed" => Self::Zip, - // MARK: Fonts + // Fonts "application/vnd.ms-fontobject" => Self::Eot, "font/otf" => Self::Otf, "font/ttf" => Self::Ttf, "font/woff" => Self::Woff, "font/woff2" => Self::Woff2, - // MARK: Applications + // Applications "application/x-abiword" => Self::Abiword, "application/vnd.amazon.ebook" => Self::Azw, "application/x-cdf" => Self::Cda, @@ -348,6 +357,10 @@ impl FromStr for MimeType { } } +// +// MARK: display +// + impl Display for MimeType { /// Get a string representation of this mimetype. /// @@ -368,7 +381,7 @@ impl Display for MimeType { match self { Self::Blob => write!(f, "application/octet-stream"), - // MARK: Audio + // Audio Self::Aac => write!(f, "audio/aac"), Self::Flac => write!(f, "audio/flac"), Self::Midi => write!(f, "audio/midi"), @@ -378,7 +391,7 @@ impl Display for MimeType { Self::Wav => write!(f, "audio/wav"), Self::Weba => write!(f, "audio/webm"), - // MARK: Video + // Video Self::Avi => write!(f, "video/x-msvideo"), Self::Mp4 => write!(f, "video/mp4"), Self::Mpeg => write!(f, "video/mpeg"), @@ -388,7 +401,7 @@ impl Display for MimeType { Self::ThreeGp => write!(f, "video/3gpp"), Self::ThreeG2 => write!(f, "video/3gpp2"), - // MARK: Images + // Images Self::Apng => write!(f, "image/apng"), Self::Avif => write!(f, "image/avif"), Self::Bmp => write!(f, "image/bmp"), @@ -399,8 +412,9 @@ impl Display for MimeType { Self::Svg => write!(f, "image/svg+xml"), Self::Tiff => write!(f, "image/tiff"), Self::Webp => write!(f, "image/webp"), + Self::Qoi => write!(f, "image/qoi"), - // MARK: Text + // Text Self::Text => write!(f, "text/plain"), Self::Css => write!(f, "text/css"), Self::Csv => write!(f, "text/csv"), @@ -410,11 +424,11 @@ impl Display for MimeType { Self::JsonLd => write!(f, "application/ld+json"), Self::Xml => write!(f, "application/xml"), - // MARK: Documents + // Documents Self::Pdf => write!(f, "application/pdf"), Self::Rtf => write!(f, "application/rtf"), - // MARK: Archives + // Archives Self::Arc => write!(f, "application/x-freearc"), Self::Bz => write!(f, "application/x-bzip"), Self::Bz2 => write!(f, "application/x-bzip2"), @@ -426,14 +440,14 @@ impl Display for MimeType { Self::Tar => write!(f, "application/x-tar"), Self::Zip => write!(f, "application/zip"), - // MARK: Fonts + // Fonts Self::Eot => write!(f, "application/vnd.ms-fontobject"), Self::Otf => write!(f, "font/otf"), Self::Ttf => write!(f, "font/ttf"), Self::Woff => write!(f, "font/woff"), Self::Woff2 => write!(f, "font/woff2"), - // MARK: Applications + // Applications Self::Abiword => write!(f, "application/x-abiword"), Self::Azw => write!(f, "application/vnd.amazon.ebook"), Self::Cda => write!(f, "application/x-cdf"), @@ -471,13 +485,15 @@ impl Display for MimeType { } impl MimeType { - // Must match `From` above + // + // MARK: from extension + // /// Try to guess a file's mime type from its extension. /// `ext` should NOT start with a dot. pub fn from_extension(ext: &str) -> Option { Some(match ext { - // MARK: Audio + // Audio "aac" => Self::Aac, "flac" => Self::Flac, "mid" | "midi" => Self::Midi, @@ -487,7 +503,7 @@ impl MimeType { "wav" => Self::Wav, "weba" => Self::Weba, - // MARK: Video + // Video "avi" => Self::Avi, "mp4" => Self::Mp4, "mpeg" => Self::Mpeg, @@ -497,7 +513,7 @@ impl MimeType { "3gp" => Self::ThreeGp, "3g2" => Self::ThreeG2, - // MARK: Images + // Images "apng" => Self::Apng, "avif" => Self::Avif, "bmp" => Self::Bmp, @@ -508,8 +524,9 @@ impl MimeType { "svg" => Self::Svg, "tif" | "tiff" => Self::Tiff, "webp" => Self::Webp, + "qoi" => Self::Qoi, - // MARK: Text + // Text "txt" => Self::Text, "css" => Self::Css, "csv" => Self::Csv, @@ -519,11 +536,11 @@ impl MimeType { "jsonld" => Self::JsonLd, "xml" => Self::Xml, - // MARK: Documents + // Documents "pdf" => Self::Pdf, "rtf" => Self::Rtf, - // MARK: Archives + // Archives "arc" => Self::Arc, "bz" => Self::Bz, "bz2" => Self::Bz2, @@ -535,14 +552,14 @@ impl MimeType { "tar" => Self::Tar, "zip" => Self::Zip, - // MARK: Fonts + // Fonts "eot" => Self::Eot, "otf" => Self::Otf, "ttf" => Self::Ttf, "woff" => Self::Woff, "woff2" => Self::Woff2, - // MARK: Applications + // Applications "abw" => Self::Abiword, "azw" => Self::Azw, "cda" => Self::Cda, @@ -569,100 +586,105 @@ impl MimeType { }) } + // + // MARK: to extension + // + /// Get the extension we use for files with this type. - /// Includes a dot. Might be the empty string. - pub fn extension(&self) -> &str { + /// Never includes a dot. + pub fn extension(&self) -> Option<&'static str> { match self { - Self::Blob => "", - Self::Other(_) => "", + Self::Blob => None, + Self::Other(_) => None, - // MARK: Audio - Self::Aac => ".aac", - Self::Flac => ".flac", - Self::Midi => ".midi", - Self::Mp3 => ".mp3", - Self::Oga => ".oga", - Self::Opus => ".opus", - Self::Wav => ".wav", - Self::Weba => ".weba", + // Audio + Self::Aac => Some("aac"), + Self::Flac => Some("flac"), + Self::Midi => Some("midi"), + Self::Mp3 => Some("mp3"), + Self::Oga => Some("oga"), + Self::Opus => Some("opus"), + Self::Wav => Some("wav"), + Self::Weba => Some("weba"), - // MARK: Video - Self::Avi => ".avi", - Self::Mp4 => ".mp4", - Self::Mpeg => ".mpeg", - Self::Ogv => ".ogv", - Self::Ts => ".ts", - Self::WebmVideo => ".webm", - Self::ThreeGp => ".3gp", - Self::ThreeG2 => ".3g2", + // Video + Self::Avi => Some("avi"), + Self::Mp4 => Some("mp4"), + Self::Mpeg => Some("mpeg"), + Self::Ogv => Some("ogv"), + Self::Ts => Some("ts"), + Self::WebmVideo => Some("webm"), + Self::ThreeGp => Some("3gp"), + Self::ThreeG2 => Some("3g2"), - // MARK: Images - Self::Apng => ".apng", - Self::Avif => ".avif", - Self::Bmp => ".bmp", - Self::Gif => ".gif", - Self::Ico => ".ico", - Self::Jpg => ".jpg", - Self::Png => ".png", - Self::Svg => ".svg", - Self::Tiff => ".tiff", - Self::Webp => ".webp", + // Images + Self::Apng => Some("apng"), + Self::Avif => Some("avif"), + Self::Bmp => Some("bmp"), + Self::Gif => Some("gif"), + Self::Ico => Some("ico"), + Self::Jpg => Some("jpg"), + Self::Png => Some("png"), + Self::Svg => Some("svg"), + Self::Tiff => Some("tiff"), + Self::Webp => Some("webp"), + Self::Qoi => Some("qoi"), - // MARK: Text - Self::Text => ".txt", - Self::Css => ".css", - Self::Csv => ".csv", - Self::Html => ".html", - Self::Javascript => ".js", - Self::Json => ".json", - Self::JsonLd => ".jsonld", - Self::Xml => ".xml", + // Text + Self::Text => Some("txt"), + Self::Css => Some("css"), + Self::Csv => Some("csv"), + Self::Html => Some("html"), + Self::Javascript => Some("js"), + Self::Json => Some("json"), + Self::JsonLd => Some("jsonld"), + Self::Xml => Some("xml"), - // MARK: Documents - Self::Pdf => ".pdf", - Self::Rtf => ".rtf", + // Documents + Self::Pdf => Some("pdf"), + Self::Rtf => Some("rtf"), - // MARK: Archives - Self::Arc => ".arc", - Self::Bz => ".bz", - Self::Bz2 => ".bz2", - Self::Gz => ".gz", - Self::Jar => ".jar", - Self::Ogg => ".ogx", - Self::Rar => ".rar", - Self::SevenZ => ".7z", - Self::Tar => ".tar", - Self::Zip => ".zip", + // Archives + Self::Arc => Some("arc"), + Self::Bz => Some("bz"), + Self::Bz2 => Some("bz2"), + Self::Gz => Some("gz"), + Self::Jar => Some("jar"), + Self::Ogg => Some("ogx"), + Self::Rar => Some("rar"), + Self::SevenZ => Some("7z"), + Self::Tar => Some("tar"), + Self::Zip => Some("zip"), - // MARK: Fonts - Self::Eot => ".eot", - Self::Otf => ".otf", - Self::Ttf => ".ttf", - Self::Woff => ".woff", - Self::Woff2 => ".woff2", + // Fonts + Self::Eot => Some("eot"), + Self::Otf => Some("otf"), + Self::Ttf => Some("ttf"), + Self::Woff => Some("woff"), + Self::Woff2 => Some("woff2"), - // MARK: Applications - Self::Abiword => ".abw", - Self::Azw => ".azw", - Self::Cda => ".cda", - Self::Csh => ".csh", - Self::Doc => ".doc", - Self::Docx => ".docx", - Self::Epub => ".epub", - Self::Ics => ".ics", - Self::Mpkg => ".mpkg", - Self::Odp => ".odp", - Self::Ods => ".ods", - Self::Odt => ".odt", - Self::Php => ".php", - Self::Ppt => ".ppt", - Self::Pptx => ".pptx", - Self::Sh => ".sh", - Self::Vsd => ".vsd", - Self::Xhtml => ".xhtml", - Self::Xls => ".xls", - Self::Xlsx => ".xlsx", - Self::Xul => ".xul", + // Applications + Self::Abiword => Some("abw"), + Self::Azw => Some("azw"), + Self::Cda => Some("cda"), + Self::Csh => Some("csh"), + Self::Doc => Some("doc"), + Self::Docx => Some("docx"), + Self::Epub => Some("epub"), + Self::Ics => Some("ics"), + Self::Mpkg => Some("mpkg"), + Self::Odp => Some("odp"), + Self::Ods => Some("ods"), + Self::Odt => Some("odt"), + Self::Php => Some("php"), + Self::Ppt => Some("ppt"), + Self::Pptx => Some("pptx"), + Self::Sh => Some("sh"), + Self::Vsd => Some("vsd"), + Self::Xhtml => Some("xhtml"), + Self::Xls => Some("xls"), + Self::Xlsx => Some("xlsx"), + Self::Xul => Some("xul"), } } }