From 2e2be7b16d85668327a0543cb2ce58b7fe4ef57e Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 9 Jun 2026 15:30:06 +0800 Subject: [PATCH] feat: add WebP encoding support with zenwebp - Replace image crate's WebP with zenwebp for better quality/speed - Add webp.rs module with configurable quality and method - Update .env.example with WEBP_QUALITY and WEBP_METHOD - Add WebP decode support in image serving pipeline - Add detailed timing logs for WebP conversion --- .env.example | 6 + Cargo.lock | 555 +++++++--------------------------------------- Cargo.toml | 4 +- amp | 0 src/api/image.rs | 28 ++- src/api/upload.rs | 62 ++++-- src/main.rs | 1 + src/webp.rs | 138 ++++++++++++ 8 files changed, 284 insertions(+), 510 deletions(-) delete mode 100644 amp create mode 100644 src/webp.rs diff --git a/.env.example b/.env.example index 2e896fe..b60caea 100644 --- a/.env.example +++ b/.env.example @@ -10,3 +10,9 @@ RATE_LIMIT_UPLOAD_BURST=15 # Rate Limit — 图片访问限流(/uploads/*) RATE_LIMIT_IMAGE_PER_SEC=10 RATE_LIMIT_IMAGE_BURST=50 + +# WebP encoding configuration +# Quality: 0.0 (smallest) to 100.0 (best), default 85.0 +WEBP_QUALITY=85.0 +# Method: 0 (fastest) to 6 (best quality), default 2 +WEBP_METHOD=2 diff --git a/Cargo.lock b/Cargo.lock index 8c5ee8f..f8d128c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,24 +17,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "aligned" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" -dependencies = [ - "as-slice", -] - -[[package]] -name = "aligned-vec" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" -dependencies = [ - "equator", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -70,16 +52,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] -name = "arbitrary" -version = "1.4.2" +name = "archmage" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +checksum = "2cc7afb8ae886487663ba1e0dc79df6b5f0af0d6478f8e55c50a49c3d780fcb0" +dependencies = [ + "archmage-macros", + "safe_unaligned_simd", +] [[package]] -name = "arg_enum_proc_macro" -version = "0.3.4" +name = "archmage-macros" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +checksum = "fb7505f102989d85ae6faa9ff86e4ddf41b2c25990f2e201e48944cbf776bc42" dependencies = [ "proc-macro2", "quote", @@ -98,21 +84,6 @@ dependencies = [ "password-hash", ] -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "as-slice" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" -dependencies = [ - "stable_deref_trait", -] - [[package]] name = "askama_escape" version = "0.13.0" @@ -191,49 +162,6 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "av-scenechange" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" -dependencies = [ - "aligned", - "anyhow", - "arg_enum_proc_macro", - "arrayvec", - "log", - "num-rational", - "num-traits", - "pastey", - "rayon", - "thiserror 2.0.18", - "v_frame", - "y4m", -] - -[[package]] -name = "av1-grain" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" -dependencies = [ - "anyhow", - "arrayvec", - "log", - "nom", - "num-rational", - "v_frame", -] - -[[package]] -name = "avif-serialize" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" -dependencies = [ - "arrayvec", -] - [[package]] name = "axum" version = "0.8.9" @@ -367,12 +295,6 @@ 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.12.1" @@ -382,15 +304,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "bitstream-io" -version = "4.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" -dependencies = [ - "no_std_io2", -] - [[package]] name = "blake2" version = "0.10.6" @@ -418,12 +331,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "built" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" - [[package]] name = "bumpalo" version = "3.20.3" @@ -464,8 +371,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -774,16 +679,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - [[package]] name = "crossbeam-epoch" version = "0.9.18" @@ -1641,12 +1536,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" -[[package]] -name = "either" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" - [[package]] name = "encoding_rs" version = "0.8.35" @@ -1656,6 +1545,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enough" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65641ff0923e45cf34a4cb0115080cb7fc8bf4fd419c2207670d5722bab4c581" + [[package]] name = "enumset" version = "1.1.13" @@ -1677,26 +1572,6 @@ dependencies = [ "syn", ] -[[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", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -1744,21 +1619,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "exr" -version = "1.74.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" -dependencies = [ - "bit_field", - "half", - "lebe", - "miniz_oxide", - "rayon-core", - "smallvec", - "zune-inflate", -] - [[package]] name = "fallible-iterator" version = "0.2.0" @@ -1776,12 +1636,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "fax" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" - [[package]] name = "fdeflate" version = "0.3.7" @@ -2477,37 +2331,14 @@ 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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" - [[package]] name = "indexmap" version = "2.14.0" @@ -2529,17 +2360,6 @@ dependencies = [ "cfb", ] -[[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", -] - [[package]] name = "inventory" version = "0.3.24" @@ -2555,15 +2375,6 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.18" @@ -2614,16 +2425,6 @@ dependencies = [ "syn", ] -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - [[package]] name = "js-sys" version = "0.3.99" @@ -2679,28 +2480,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" -[[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.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" -[[package]] -name = "libfuzzer-sys" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" -dependencies = [ - "arbitrary", - "cc", -] - [[package]] name = "libloading" version = "0.8.9" @@ -2711,6 +2496,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.17" @@ -2765,15 +2556,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86" -[[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.4" @@ -2806,6 +2588,15 @@ dependencies = [ "syn", ] +[[package]] +name = "magetypes" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc8b6ac14292c7e2774986a72fa7aadcf4829eeb0ae45117da6ca86aff04b53" +dependencies = [ + "archmage", +] + [[package]] name = "manganis" version = "0.7.9" @@ -2893,16 +2684,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" -[[package]] -name = "maybe-rayon" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" -dependencies = [ - "cfg-if", - "rayon", -] - [[package]] name = "md-5" version = "0.11.0" @@ -3063,36 +2844,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" -[[package]] -name = "no_std_io2" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" -dependencies = [ - "memchr", -] - -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - [[package]] name = "nonzero_ext" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" -[[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" @@ -3102,53 +2859,12 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - [[package]] name = "num-conv" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -3269,18 +2985,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pastey" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" - [[package]] name = "percent-encoding" version = "2.3.2" @@ -3516,25 +3220,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "profiling" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" -dependencies = [ - "profiling-procmacros", -] - -[[package]] -name = "profiling-procmacros" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" -dependencies = [ - "quote", - "syn", -] - [[package]] name = "psl-types" version = "2.0.11" @@ -3576,15 +3261,6 @@ version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" -[[package]] -name = "qoi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" -dependencies = [ - "bytemuck", -] - [[package]] name = "quanta" version = "0.12.6" @@ -3600,12 +3276,6 @@ dependencies = [ "winapi", ] -[[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.39.4" @@ -3767,56 +3437,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" -[[package]] -name = "rav1e" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" -dependencies = [ - "aligned-vec", - "arbitrary", - "arg_enum_proc_macro", - "arrayvec", - "av-scenechange", - "av1-grain", - "bitstream-io", - "built", - "cfg-if", - "interpolate_name", - "itertools", - "libc", - "libfuzzer-sys", - "log", - "maybe-rayon", - "new_debug_unreachable", - "noop_proc_macro", - "num-derive", - "num-traits", - "paste", - "profiling", - "rand 0.9.4", - "rand_chacha 0.9.0", - "simd_helpers", - "thiserror 2.0.18", - "v_frame", - "wasm-bindgen", -] - -[[package]] -name = "ravif" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" -dependencies = [ - "avif-serialize", - "imgref", - "loop9", - "quick-error", - "rav1e", - "rayon", - "rgb", -] - [[package]] name = "raw-cpuid" version = "11.6.0" @@ -3832,26 +3452,6 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "redox_syscall" version = "0.5.18" @@ -3939,6 +3539,9 @@ name = "rgb" version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] [[package]] name = "ring" @@ -4035,6 +3638,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "safe_unaligned_simd" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c874fb993656d484725d58fbfddb39dfcf98ebcadc1a1d09568aa6b39bafba" + [[package]] name = "same-file" version = "1.0.6" @@ -4218,15 +3827,6 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" -[[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 = "1.0.3" @@ -4524,20 +4124,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "tiff" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error", - "weezl", - "zune-jpeg", -] - [[package]] name = "time" version = "0.3.47" @@ -5030,17 +4616,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "v_frame" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" -dependencies = [ - "aligned-vec", - "num-traits", - "wasm-bindgen", -] - [[package]] name = "valuable" version = "0.1.1" @@ -5285,6 +4860,12 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "whereat" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab90f361db038ba3135da12c938c328fb43a012992101879e3d6ebccb4d7eba8" + [[package]] name = "whoami" version = "2.1.2" @@ -5745,12 +5326,6 @@ version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" -[[package]] -name = "y4m" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" - [[package]] name = "yaml-rust" version = "0.4.5" @@ -5792,6 +5367,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "zenwebp", ] [[package]] @@ -5817,6 +5393,34 @@ dependencies = [ "synstructure", ] +[[package]] +name = "yuv" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89c90da4fb561f9750984de2c5e7f0ba01035d2eb29d69a7f375b1caef37fdf4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "zenwebp" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c5707a8a64732753a92ff952b2daba1a7ccd330aab4582df342564965df4da5" +dependencies = [ + "archmage", + "byteorder-lite", + "enough", + "hashbrown 0.16.1", + "libm", + "magetypes", + "rgb", + "safe_unaligned_simd", + "thiserror 2.0.18", + "whereat", + "yuv", +] + [[package]] name = "zerocopy" version = "0.8.50" @@ -5909,15 +5513,6 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] - [[package]] name = "zune-jpeg" version = "0.5.15" diff --git a/Cargo.toml b/Cargo.toml index df7e8e7..8d6f04c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,8 @@ axum = { version = "0.8", optional = true, features = ["multipart"] } serde_json = "1.0" ammonia = { version = "4", optional = true } syntect = { version = "5", default-features = false, features = ["default-syntaxes", "default-themes", "default-fancy", "html", "parsing", "dump-load", "yaml-load"], optional = true } -image = { version = "0.25", optional = true } +image = { version = "0.25", optional = true, default-features = false, features = ["jpeg", "png", "gif"] } +zenwebp = { version = "0.3", optional = true } moka = { version = "0.12", features = ["future"], optional = true } governor = { version = "0.8", optional = true } @@ -60,6 +61,7 @@ server = [ "dep:syntect", "dep:axum", "dep:image", + "dep:zenwebp", "dep:moka", "dep:governor", ] diff --git a/amp b/amp deleted file mode 100644 index e69de29..0000000 diff --git a/src/api/image.rs b/src/api/image.rs index c875f10..cd356b2 100644 --- a/src/api/image.rs +++ b/src/api/image.rs @@ -200,6 +200,13 @@ fn process_image( img.write_with_encoder(encoder) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } + image::ImageFormat::WebP => { + let config = crate::webp::WEBP_CONFIG.clone(); + let webp_quality = params.quality.unwrap_or(config.quality as u8) as f32; + let webp_data = crate::webp::encode(&img, webp_quality, config.method) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + buf = std::io::Cursor::new(webp_data); + } _ => { img.write_to(&mut buf, output_format) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; @@ -328,11 +335,22 @@ pub async fn serve_image( }; let original_format = detect_format(&path); - let img = match image::load_from_memory_with_format(&data, original_format) { - Ok(img) => img, - Err(_) => { - let ct = content_type(original_format); - return (StatusCode::OK, [(header::CONTENT_TYPE, ct)], data).into_response(); + let img = if original_format == image::ImageFormat::WebP { + match crate::webp::decode(&data) { + Ok(img) => img, + Err(e) => { + tracing::warn!("WebP decode failed ({}), returning raw bytes", e); + let ct = content_type(original_format); + return (StatusCode::OK, [(header::CONTENT_TYPE, ct)], data).into_response(); + } + } + } else { + match image::load_from_memory_with_format(&data, original_format) { + Ok(img) => img, + Err(_) => { + let ct = content_type(original_format); + return (StatusCode::OK, [(header::CONTENT_TYPE, ct)], data).into_response(); + } } }; diff --git a/src/api/upload.rs b/src/api/upload.rs index 8d30a16..ed89307 100644 --- a/src/api/upload.rs +++ b/src/api/upload.rs @@ -159,38 +159,56 @@ pub async fn upload_image( } else { let original_data = data.to_vec(); let mime = mime_type.clone(); - let original_len = data.len(); + let config = crate::webp::WEBP_CONFIG.clone(); let result = tokio::task::spawn_blocking(move || -> (Vec, String, bool) { + let total_start = std::time::Instant::now(); match image::load_from_memory(&original_data) { Ok(img) => { - let mut buf = std::io::Cursor::new(Vec::new()); - match img.write_to(&mut buf, image::ImageFormat::WebP) { - Ok(_) => { - let webp_data = buf.into_inner(); + let decode_time = total_start.elapsed(); + let enc_start = std::time::Instant::now(); + let result = match crate::webp::encode(&img, config.quality, config.method) { + Ok(webp_data) => { + let enc_time = enc_start.elapsed(); + let total_time = total_start.elapsed(); if webp_data.len() < original_data.len() { + tracing::info!( + "WebP conversion: decode={:?} encode={:?} total={:?} {}x{} {} bytes -> {} bytes", + decode_time, enc_time, total_time, + img.width(), img.height(), + original_data.len(), webp_data.len() + ); (webp_data, "webp".to_string(), true) } else { + tracing::info!( + "WebP conversion larger, keeping original: decode={:?} encode={:?} total={:?} {}x{} original={} webp={}", + decode_time, enc_time, total_time, + img.width(), img.height(), + original_data.len(), webp_data.len() + ); (original_data, mime_to_ext(&mime).to_string(), false) } } - Err(_) => (original_data, mime_to_ext(&mime).to_string(), false), - } + Err(e) => { + tracing::warn!("WebP encode failed ({}), keeping original format", e); + (original_data, mime_to_ext(&mime).to_string(), false) + } + }; + result + } + Err(_) => { + tracing::warn!("Failed to decode image, keeping original format"); + (original_data, mime_to_ext(&mime).to_string(), false) } - Err(_) => (original_data, mime_to_ext(&mime).to_string(), false), } }) .await; match result { - Ok((converted_data, ext, was_converted)) => { - if was_converted { - tracing::info!("Converted upload to WebP: {} bytes -> {} bytes", original_len, converted_data.len()); - } else { - tracing::info!("Keeping original format (ext: {})", ext); - } - (converted_data, ext) + Ok((converted_data, ext, _was_converted)) => (converted_data, ext), + Err(_) => { + tracing::warn!("spawn_blocking task panicked, keeping original format"); + (data.to_vec(), mime_to_ext(&mime_type).to_string()) } - Err(_) => (data.to_vec(), mime_to_ext(&mime_type).to_string()), } }; @@ -235,7 +253,7 @@ pub async fn upload_image( }))) } -#[cfg(test)] +#[cfg(all(test, feature = "server"))] mod tests { #[test] fn filename_format_no_spaces() { @@ -267,19 +285,15 @@ mod tests { #[test] fn convert_to_webp_produces_bytes() { let img = image::DynamicImage::new_rgb8(10, 10); - let mut buf = std::io::Cursor::new(Vec::new()); - img.write_to(&mut buf, image::ImageFormat::WebP).unwrap(); - let result = buf.into_inner(); + let result = crate::webp::encode(&img, 85.0, 4).unwrap(); assert!(!result.is_empty()); } #[test] fn webp_roundtrip_from_rgba() { let img = image::DynamicImage::new_rgba8(2, 2); - let mut buf = std::io::Cursor::new(Vec::new()); - img.write_to(&mut buf, image::ImageFormat::WebP).unwrap(); - let webp_bytes = buf.into_inner(); - let loaded = image::load_from_memory_with_format(&webp_bytes, image::ImageFormat::WebP); + let webp_bytes = crate::webp::encode(&img, 85.0, 4).unwrap(); + let loaded = crate::webp::decode(&webp_bytes); assert!(loaded.is_ok()); } } diff --git a/src/main.rs b/src/main.rs index 9c43cb2..53a86d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ mod router; mod tasks; mod theme; mod utils; +mod webp; fn main() { #[cfg(feature = "server")] diff --git a/src/webp.rs b/src/webp.rs new file mode 100644 index 0000000..cfb0199 --- /dev/null +++ b/src/webp.rs @@ -0,0 +1,138 @@ +#[cfg(feature = "server")] +use std::sync::LazyLock; + +#[derive(Debug)] +pub enum WebpError { + Encode(String), + Decode(String), +} + +impl std::fmt::Display for WebpError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WebpError::Encode(msg) => write!(f, "WebP encode error: {}", msg), + WebpError::Decode(msg) => write!(f, "WebP decode error: {}", msg), + } + } +} + +impl std::error::Error for WebpError {} + +#[cfg(feature = "server")] +#[derive(Debug, Clone)] +pub struct WebpConfig { + pub quality: f32, + pub method: u8, +} + +#[cfg(feature = "server")] +pub static WEBP_CONFIG: LazyLock = LazyLock::new(|| { + let quality = std::env::var("WEBP_QUALITY") + .ok() + .and_then(|s| s.parse::().ok()) + .map(|q| q.clamp(0.0, 100.0)) + .unwrap_or(85.0); + + let method = std::env::var("WEBP_METHOD") + .ok() + .and_then(|s| s.parse::().ok()) + .map(|m| m.clamp(0, 6)) + .unwrap_or(2); + + tracing::info!("WebP config loaded: quality={}, method={}", quality, method); + WebpConfig { quality, method } +}); + +#[cfg(feature = "server")] +pub fn encode(img: &image::DynamicImage, quality: f32, method: u8) -> Result, WebpError> { + use zenwebp::{EncodeRequest, LossyConfig, PixelLayout}; + + let (width, height) = (img.width(), img.height()); + let config = LossyConfig::new().with_quality(quality).with_method(method); + + match img { + image::DynamicImage::ImageRgba8(rgba) => { + let pixels = rgba.as_raw(); + EncodeRequest::lossy(&config, pixels, PixelLayout::Rgba8, width, height) + .encode() + .map_err(|e| WebpError::Encode(e.to_string())) + } + image::DynamicImage::ImageRgb8(rgb) => { + let pixels = rgb.as_raw(); + EncodeRequest::lossy(&config, pixels, PixelLayout::Rgb8, width, height) + .encode() + .map_err(|e| WebpError::Encode(e.to_string())) + } + _ => { + // Convert other formats to RGBA8 + let rgba = img.to_rgba8(); + let pixels = rgba.as_raw(); + EncodeRequest::lossy(&config, pixels, PixelLayout::Rgba8, width, height) + .encode() + .map_err(|e| WebpError::Encode(e.to_string())) + } + } +} + +#[cfg(feature = "server")] +pub fn decode(data: &[u8]) -> Result { + use zenwebp::WebPDecoder; + + let mut decoder = WebPDecoder::build(data) + .map_err(|e| WebpError::Decode(format!("Failed to build decoder: {}", e)))?; + + let info = decoder.info(); + let width = info.width; + let height = info.height; + let has_alpha = info.has_alpha; + + let buf_size = decoder + .output_buffer_size() + .ok_or_else(|| WebpError::Decode("Image too large".to_string()))?; + + let mut output = vec![0u8; buf_size]; + decoder + .read_image(&mut output) + .map_err(|e| WebpError::Decode(format!("Failed to decode: {}", e)))?; + + if has_alpha { + image::RgbaImage::from_raw(width, height, output) + .map(image::DynamicImage::ImageRgba8) + .ok_or_else(|| WebpError::Decode("Invalid RGBA dimensions".to_string())) + } else { + // For RGB output, the buffer is width * height * 3 + image::RgbImage::from_raw(width, height, output) + .map(image::DynamicImage::ImageRgb8) + .ok_or_else(|| WebpError::Decode("Invalid RGB dimensions".to_string())) + } +} + +#[cfg(all(test, feature = "server"))] +mod tests { + use super::*; + + #[test] + fn encode_produces_non_empty_bytes() { + let img = image::DynamicImage::new_rgba8(10, 10); + let result = encode(&img, 85.0, 4).unwrap(); + assert!(!result.is_empty()); + } + + #[test] + fn decode_roundtrip_rgba() { + let original = image::DynamicImage::new_rgba8(5, 5); + let encoded = encode(&original, 85.0, 4).unwrap(); + let decoded = decode(&encoded).unwrap(); + assert_eq!(decoded.width(), 5); + assert_eq!(decoded.height(), 5); + } + + #[test] + fn decode_roundtrip_rgb() { + let original = image::DynamicImage::new_rgb8(5, 5); + let encoded = encode(&original, 85.0, 4).unwrap(); + let decoded = decode(&encoded).unwrap(); + assert_eq!(decoded.width(), 5); + assert_eq!(decoded.height(), 5); + } +}