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
This commit is contained in:
xfy 2026-06-09 15:30:06 +08:00
parent 5ecd8a9b86
commit 2e2be7b16d
8 changed files with 284 additions and 510 deletions

View File

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

555
Cargo.lock generated
View File

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

View File

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

0
amp
View File

View File

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

View File

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

View File

@ -12,6 +12,7 @@ mod router;
mod tasks;
mod theme;
mod utils;
mod webp;
fn main() {
#[cfg(feature = "server")]

138
src/webp.rs Normal file
View File

@ -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<WebpConfig> = LazyLock::new(|| {
let quality = std::env::var("WEBP_QUALITY")
.ok()
.and_then(|s| s.parse::<f32>().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::<u8>().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<Vec<u8>, 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<image::DynamicImage, WebpError> {
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);
}
}