diff --git a/Cargo.lock b/Cargo.lock index 1289ba0..ee61680 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "adapters-thumbnail" +version = "0.1.0" +dependencies = [ + "bytes", + "domain", + "image", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -77,6 +92,24 @@ 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" @@ -137,6 +170,38 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[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", +] + +[[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 = "async-nats" version = "0.48.0" @@ -205,6 +270,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[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", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + [[package]] name = "axum" version = "0.8.9" @@ -295,6 +403,12 @@ dependencies = [ "zeroize", ] +[[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.11.1" @@ -304,6 +418,15 @@ 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 = "block-buffer" version = "0.10.4" @@ -346,18 +469,36 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + [[package]] name = "bumpalo" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[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.11.1" @@ -374,6 +515,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -424,6 +567,12 @@ dependencies = [ "inout", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -488,6 +637,34 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.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-queue" version = "0.3.12" @@ -503,6 +680,12 @@ 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.7" @@ -649,6 +832,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -701,6 +904,36 @@ dependencies = [ "tracing", ] +[[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 = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -713,6 +946,16 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flume" version = "0.11.1" @@ -906,6 +1149,16 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "h2" version = "0.4.14" @@ -925,6 +1178,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.15.5" @@ -1234,6 +1498,46 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" + [[package]] name = "indexmap" version = "2.14.0" @@ -1255,6 +1559,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1268,7 +1583,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5090db9c6a716d1f4eeb729957e889e9c28156061c825cbccd44950cf0f3c66" dependencies = [ "geo-types", - "nom", + "nom 7.1.3", ] [[package]] @@ -1280,12 +1595,31 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "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.83" @@ -1326,12 +1660,28 @@ 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.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libm" version = "0.2.16" @@ -1381,6 +1731,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -1402,6 +1761,16 @@ 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.10.6" @@ -1430,6 +1799,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.1.1" @@ -1441,6 +1820,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "multer" version = "3.1.0" @@ -1458,6 +1847,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nkeys" version = "0.4.5" @@ -1473,6 +1868,15 @@ dependencies = [ "signatory", ] +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + [[package]] name = "nom" version = "7.1.3" @@ -1483,6 +1887,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nom-exif" version = "2.8.0" @@ -1492,13 +1905,19 @@ dependencies = [ "bytes", "chrono", "iso6709parse", - "nom", + "nom 7.1.3", "regex", "serde", "thiserror", "tracing", ] +[[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" @@ -1549,6 +1968,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[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" @@ -1569,6 +1999,17 @@ 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" @@ -1592,7 +2033,7 @@ dependencies = [ "futures", "humantime", "hyper", - "itertools", + "itertools 0.13.0", "md-5", "parking_lot", "percent-encoding", @@ -1651,6 +2092,18 @@ dependencies = [ "windows-link", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "pem" version = "3.0.6" @@ -1735,6 +2188,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -1804,6 +2270,46 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "pxfm" +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 = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.37.5" @@ -1966,6 +2472,76 @@ 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 0.14.0", + "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", + "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 = "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" @@ -2055,6 +2631,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + [[package]] name = "ring" version = "0.17.14" @@ -2370,6 +2952,21 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +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 = "simple_asn1" version = "0.6.4" @@ -2726,6 +3323,20 @@ 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" @@ -3100,6 +3711,17 @@ 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" @@ -3310,6 +3932,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "whoami" version = "1.6.1" @@ -3638,6 +4266,7 @@ dependencies = [ "adapters-nats", "adapters-postgres", "adapters-storage", + "adapters-thumbnail", "anyhow", "application", "async-nats", @@ -3657,6 +4286,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yoke" version = "0.8.1" @@ -3765,3 +4400,27 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index e12b254..f51e568 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/adapters/event-transport", "crates/adapters/nats", "crates/adapters/exif", + "crates/adapters/thumbnail", "crates/presentation", "crates/bootstrap", "crates/worker", @@ -47,7 +48,8 @@ adapters-storage = { path = "crates/adapters/storage" } event-payload = { path = "crates/adapters/event-payload" } event-transport = { path = "crates/adapters/event-transport" } adapters-nats = { path = "crates/adapters/nats" } -adapters-exif = { path = "crates/adapters/exif" } +adapters-exif = { path = "crates/adapters/exif" } +adapters-thumbnail = { path = "crates/adapters/thumbnail" } async-nats = "0.48" async-stream = "0.3" presentation = { path = "crates/presentation" } diff --git a/crates/adapters/postgres/migrations/011_seed_default_plugins.sql b/crates/adapters/postgres/migrations/011_seed_default_plugins.sql index 72cee7a..a3c30e3 100644 --- a/crates/adapters/postgres/migrations/011_seed_default_plugins.sql +++ b/crates/adapters/postgres/migrations/011_seed_default_plugins.sql @@ -1,17 +1,28 @@ -- Default plugins matching worker's InMemoryPluginRegistry INSERT INTO plugins (plugin_id, name, plugin_type, is_enabled, configuration) VALUES - ('a0000000-0000-4000-8000-000000000001', 'metadata_extractor', 'media_processor', true, '{}'), - ('a0000000-0000-4000-8000-000000000002', 'sidecar_sync', 'sidecar_writer', true, '{}'), - ('a0000000-0000-4000-8000-000000000003', 'no_op', 'scheduled_task', true, '{}') + ('a0000000-0000-4000-8000-000000000001', 'metadata_extractor', 'media_processor', true, '{}'), + ('a0000000-0000-4000-8000-000000000002', 'sidecar_sync', 'sidecar_writer', true, '{}'), + ('a0000000-0000-4000-8000-000000000003', 'no_op', 'scheduled_task', true, '{}'), + ('a0000000-0000-4000-8000-000000000004', 'thumbnail_generator', 'media_processor', true, '{}') ON CONFLICT (plugin_id) DO NOTHING; --- Pipeline: extract_metadata → metadata_extractor +-- Pipeline: extract_metadata → metadata_extractor, then thumbnail_generator INSERT INTO processing_pipelines (pipeline_id, trigger_event, steps) VALUES ( 'b0000000-0000-4000-8000-000000000001', 'extract_metadata', - '[{"plugin_id": "a0000000-0000-4000-8000-000000000001", "step_order": 0, "configuration": {}}]' + '[{"plugin_id": "a0000000-0000-4000-8000-000000000001", "step_order": 0, "configuration": {}}, + {"plugin_id": "a0000000-0000-4000-8000-000000000004", "step_order": 1, "configuration": {"width": "300", "height": "300", "format": "webp", "profile": "ThumbnailSquare"}}]' +) +ON CONFLICT (pipeline_id) DO NOTHING; + +-- Pipeline: generate_derivative (standalone, configurable per-step) +INSERT INTO processing_pipelines (pipeline_id, trigger_event, steps) +VALUES ( + 'b0000000-0000-4000-8000-000000000003', + 'generate_derivative', + '[{"plugin_id": "a0000000-0000-4000-8000-000000000004", "step_order": 0, "configuration": {"width": "300", "height": "300", "format": "webp", "profile": "ThumbnailSquare"}}]' ) ON CONFLICT (pipeline_id) DO NOTHING; diff --git a/crates/adapters/postgres/migrations/012_derivatives.sql b/crates/adapters/postgres/migrations/012_derivatives.sql new file mode 100644 index 0000000..da22ec1 --- /dev/null +++ b/crates/adapters/postgres/migrations/012_derivatives.sql @@ -0,0 +1,14 @@ +CREATE TABLE derivatives ( + derivative_id UUID PRIMARY KEY, + parent_asset_id UUID NOT NULL REFERENCES assets(asset_id), + profile_type TEXT NOT NULL, + storage_path TEXT NOT NULL, + mime_type TEXT NOT NULL DEFAULT '', + file_size BIGINT NOT NULL DEFAULT 0, + width INTEGER NOT NULL DEFAULT 0, + height INTEGER NOT NULL DEFAULT 0, + generation_status TEXT NOT NULL DEFAULT 'pending' +); + +CREATE INDEX idx_derivatives_parent ON derivatives(parent_asset_id); +CREATE INDEX idx_derivatives_parent_profile ON derivatives(parent_asset_id, profile_type); diff --git a/crates/adapters/postgres/src/catalog/mod.rs b/crates/adapters/postgres/src/catalog/mod.rs index a306d4d..e46f6c6 100644 --- a/crates/adapters/postgres/src/catalog/mod.rs +++ b/crates/adapters/postgres/src/catalog/mod.rs @@ -3,11 +3,12 @@ use async_trait::async_trait; use chrono::{DateTime, Utc}; use domain::{ entities::{ - Asset, AssetMetadata, AssetType, DetectionMethod, DuplicateCandidate, DuplicateGroup, - DuplicateStatus, MetadataSource, SourceReference, + Asset, AssetMetadata, AssetType, DerivativeAsset, DerivativeProfile, DetectionMethod, + DuplicateCandidate, DuplicateGroup, DuplicateStatus, GenerationStatus, MetadataSource, + SourceReference, }, errors::DomainError, - ports::{AssetMetadataRepository, AssetRepository, DuplicateRepository}, + ports::{AssetMetadataRepository, AssetRepository, DerivativeRepository, DuplicateRepository}, value_objects::{Checksum, DateTimeStamp, MetadataValue, StructuredData, SystemId}, }; use uuid::Uuid; @@ -452,3 +453,147 @@ impl DuplicateRepository for PostgresDuplicateRepository { Ok(()) } } + +// ── DerivativeRepository ────────────────────────────────────────────── + +#[derive(sqlx::FromRow)] +struct DerivativeRow { + derivative_id: Uuid, + parent_asset_id: Uuid, + profile_type: String, + storage_path: String, + mime_type: String, + file_size: i64, + width: i32, + height: i32, + generation_status: String, +} + +fn profile_from_str(s: &str) -> DerivativeProfile { + match s { + "thumbnail_square" => DerivativeProfile::ThumbnailSquare, + "thumbnail_large" => DerivativeProfile::ThumbnailLarge, + "web_optimized" => DerivativeProfile::WebOptimized, + "video_sd" => DerivativeProfile::VideoSd, + _ => DerivativeProfile::ThumbnailSquare, + } +} + +fn profile_to_str(p: &DerivativeProfile) -> &'static str { + match p { + DerivativeProfile::ThumbnailSquare => "thumbnail_square", + DerivativeProfile::ThumbnailLarge => "thumbnail_large", + DerivativeProfile::WebOptimized => "web_optimized", + DerivativeProfile::VideoSd => "video_sd", + } +} + +fn gen_status_from_str(s: &str) -> GenerationStatus { + match s { + "pending" => GenerationStatus::Pending, + "ready" => GenerationStatus::Ready, + "failed" => GenerationStatus::Failed, + _ => GenerationStatus::Pending, + } +} + +fn gen_status_to_str(s: &GenerationStatus) -> &'static str { + match s { + GenerationStatus::Pending => "pending", + GenerationStatus::Ready => "ready", + GenerationStatus::Failed => "failed", + } +} + +impl From for DerivativeAsset { + fn from(r: DerivativeRow) -> Self { + Self { + derivative_id: SystemId::from_uuid(r.derivative_id), + parent_asset_id: SystemId::from_uuid(r.parent_asset_id), + profile_type: profile_from_str(&r.profile_type), + storage_path: r.storage_path, + mime_type: r.mime_type, + file_size: r.file_size as u64, + dimensions: (r.width as u32, r.height as u32), + generation_status: gen_status_from_str(&r.generation_status), + } + } +} + +pg_repo!(PostgresDerivativeRepository); + +#[async_trait] +impl DerivativeRepository for PostgresDerivativeRepository { + async fn find_by_asset( + &self, + asset_id: &SystemId, + ) -> Result, DomainError> { + let rows = sqlx::query_as::<_, DerivativeRow>( + "SELECT derivative_id, parent_asset_id, profile_type, storage_path, + mime_type, file_size, width, height, generation_status + FROM derivatives WHERE parent_asset_id = $1", + ) + .bind(*asset_id.as_uuid()) + .fetch_all(&self.pool) + .await + .map_pg()?; + + Ok(rows.into_iter().map(Into::into).collect()) + } + + async fn find_by_asset_and_profile( + &self, + asset_id: &SystemId, + profile: DerivativeProfile, + ) -> Result, DomainError> { + let row = sqlx::query_as::<_, DerivativeRow>( + "SELECT derivative_id, parent_asset_id, profile_type, storage_path, + mime_type, file_size, width, height, generation_status + FROM derivatives WHERE parent_asset_id = $1 AND profile_type = $2", + ) + .bind(*asset_id.as_uuid()) + .bind(profile_to_str(&profile)) + .fetch_optional(&self.pool) + .await + .map_pg()?; + + Ok(row.map(Into::into)) + } + + async fn save(&self, d: &DerivativeAsset) -> Result<(), DomainError> { + sqlx::query( + "INSERT INTO derivatives (derivative_id, parent_asset_id, profile_type, storage_path, + mime_type, file_size, width, height, generation_status) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ON CONFLICT (derivative_id) DO UPDATE SET + storage_path = EXCLUDED.storage_path, + mime_type = EXCLUDED.mime_type, + file_size = EXCLUDED.file_size, + width = EXCLUDED.width, + height = EXCLUDED.height, + generation_status = EXCLUDED.generation_status", + ) + .bind(*d.derivative_id.as_uuid()) + .bind(*d.parent_asset_id.as_uuid()) + .bind(profile_to_str(&d.profile_type)) + .bind(&d.storage_path) + .bind(&d.mime_type) + .bind(d.file_size as i64) + .bind(d.dimensions.0 as i32) + .bind(d.dimensions.1 as i32) + .bind(gen_status_to_str(&d.generation_status)) + .execute(&self.pool) + .await + .map_pg()?; + Ok(()) + } + + async fn delete(&self, id: &SystemId) -> Result<(), DomainError> { + sqlx::query("DELETE FROM derivatives WHERE derivative_id = $1") + .bind(*id.as_uuid()) + .execute(&self.pool) + .await + .map_pg()?; + Ok(()) + } +} diff --git a/crates/adapters/thumbnail/Cargo.toml b/crates/adapters/thumbnail/Cargo.toml new file mode 100644 index 0000000..593a139 --- /dev/null +++ b/crates/adapters/thumbnail/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "adapters-thumbnail" +version = "0.1.0" +edition = "2024" + +[dependencies] +domain = { workspace = true } +bytes = { workspace = true } +image = "0.25" diff --git a/crates/adapters/thumbnail/src/lib.rs b/crates/adapters/thumbnail/src/lib.rs new file mode 100644 index 0000000..0813ab1 --- /dev/null +++ b/crates/adapters/thumbnail/src/lib.rs @@ -0,0 +1,57 @@ +use bytes::Bytes; +use domain::{ + errors::DomainError, + ports::{ThumbnailGeneratorPort, ThumbnailOutput}, +}; +use image::{DynamicImage, ImageFormat, load_from_memory}; +use std::io::Cursor; + +pub struct ImageThumbnailGenerator; + +impl ThumbnailGeneratorPort for ImageThumbnailGenerator { + fn generate( + &self, + source: &Bytes, + width: u32, + height: u32, + format: &str, + ) -> Result { + let img = load_from_memory(source) + .map_err(|e| DomainError::Internal(format!("failed to decode image: {e}")))?; + + let thumb = img.thumbnail(width, height); + let (img_format, mime) = parse_format(format)?; + + let encoded = encode(&thumb, img_format)?; + let actual_width = thumb.width(); + let actual_height = thumb.height(); + + Ok(ThumbnailOutput { + bytes: Bytes::from(encoded), + width: actual_width, + height: actual_height, + mime_type: mime.to_string(), + }) + } +} + +fn parse_format(s: &str) -> Result<(ImageFormat, &'static str), DomainError> { + match s { + "jpeg" | "jpg" => Ok((ImageFormat::Jpeg, "image/jpeg")), + "webp" => Ok((ImageFormat::WebP, "image/webp")), + "png" => Ok((ImageFormat::Png, "image/png")), + other => Err(DomainError::Validation(format!( + "unsupported thumbnail format: {other}" + ))), + } +} + +fn encode(img: &DynamicImage, format: ImageFormat) -> Result, DomainError> { + let mut buf = Cursor::new(Vec::new()); + img.write_to(&mut buf, format) + .map_err(|e| DomainError::Internal(format!("failed to encode thumbnail: {e}")))?; + Ok(buf.into_inner()) +} + +#[cfg(test)] +mod tests; diff --git a/crates/adapters/thumbnail/src/tests.rs b/crates/adapters/thumbnail/src/tests.rs new file mode 100644 index 0000000..c90515e --- /dev/null +++ b/crates/adapters/thumbnail/src/tests.rs @@ -0,0 +1,66 @@ +use crate::ImageThumbnailGenerator; +use bytes::Bytes; +use domain::ports::ThumbnailGeneratorPort as _; + +fn make_test_png() -> Bytes { + use image::{ImageFormat, RgbImage}; + use std::io::Cursor; + + let img = RgbImage::new(100, 200); + let mut buf = Cursor::new(Vec::new()); + img.write_to(&mut buf, ImageFormat::Png).unwrap(); + Bytes::from(buf.into_inner()) +} + +#[test] +fn generates_jpeg_thumbnail() { + let generator = ImageThumbnailGenerator; + let source = make_test_png(); + + let out = generator.generate(&source, 50, 50, "jpeg").unwrap(); + + assert!(out.width <= 50); + assert!(out.height <= 50); + assert_eq!(out.mime_type, "image/jpeg"); + assert!(!out.bytes.is_empty()); +} + +#[test] +fn generates_webp_thumbnail() { + let generator = ImageThumbnailGenerator; + let source = make_test_png(); + + let out = generator.generate(&source, 30, 30, "webp").unwrap(); + + assert!(out.width <= 30); + assert!(out.height <= 30); + assert_eq!(out.mime_type, "image/webp"); +} + +#[test] +fn preserves_aspect_ratio() { + let generator = ImageThumbnailGenerator; + let source = make_test_png(); // 100x200 + + let out = generator.generate(&source, 50, 50, "png").unwrap(); + + // 100x200 → fits in 50x50 → 25x50 + assert_eq!(out.width, 25); + assert_eq!(out.height, 50); +} + +#[test] +fn rejects_unsupported_format() { + let generator = ImageThumbnailGenerator; + let source = make_test_png(); + + let result = generator.generate(&source, 50, 50, "bmp"); + assert!(result.is_err()); +} + +#[test] +fn rejects_garbage_input() { + let generator = ImageThumbnailGenerator; + let result = generator.generate(&Bytes::from_static(b"not an image"), 50, 50, "jpeg"); + assert!(result.is_err()); +} diff --git a/crates/domain/src/catalog/ports.rs b/crates/domain/src/catalog/ports.rs index dd37720..3ba913b 100644 --- a/crates/domain/src/catalog/ports.rs +++ b/crates/domain/src/catalog/ports.rs @@ -81,3 +81,22 @@ pub trait DuplicateRepository: Send + Sync { pub trait MetadataExtractorPort: Send + Sync { fn extract(&self, bytes: &Bytes) -> Result; } + +// --- ThumbnailGeneratorPort --- + +pub struct ThumbnailOutput { + pub bytes: Bytes, + pub width: u32, + pub height: u32, + pub mime_type: String, +} + +pub trait ThumbnailGeneratorPort: Send + Sync { + fn generate( + &self, + source: &Bytes, + width: u32, + height: u32, + format: &str, + ) -> Result; +} diff --git a/crates/worker/Cargo.toml b/crates/worker/Cargo.toml index 5d49b1e..f1ede4f 100644 --- a/crates/worker/Cargo.toml +++ b/crates/worker/Cargo.toml @@ -16,6 +16,7 @@ adapters-storage = { workspace = true } adapters-nats = { workspace = true } event-transport = { workspace = true } adapters-exif = { workspace = true } +adapters-thumbnail = { workspace = true } async-nats = { workspace = true } futures = { workspace = true } diff --git a/crates/worker/src/factories/infra.rs b/crates/worker/src/factories/infra.rs index 73a956e..b3477ee 100644 --- a/crates/worker/src/factories/infra.rs +++ b/crates/worker/src/factories/infra.rs @@ -1,7 +1,7 @@ use adapters_postgres::{ - PostgresAssetMetadataRepository, PostgresAssetRepository, PostgresJobBatchRepository, - PostgresJobRepository, PostgresPipelineRepository, PostgresPluginRepository, - PostgresSidecarRepository, + PostgresAssetMetadataRepository, PostgresAssetRepository, PostgresDerivativeRepository, + PostgresJobBatchRepository, PostgresJobRepository, PostgresPipelineRepository, + PostgresPluginRepository, PostgresSidecarRepository, }; use std::sync::Arc; @@ -12,6 +12,7 @@ pub struct Repos { pub plugin: Arc, pub asset: Arc, pub metadata: Arc, + pub derivative: Arc, pub sidecar: Arc, } @@ -24,6 +25,7 @@ impl Repos { plugin: Arc::new(PostgresPluginRepository::new(pool.clone())), asset: Arc::new(PostgresAssetRepository::new(pool.clone())), metadata: Arc::new(PostgresAssetMetadataRepository::new(pool.clone())), + derivative: Arc::new(PostgresDerivativeRepository::new(pool.clone())), sidecar: Arc::new(PostgresSidecarRepository::new(pool)), } } diff --git a/crates/worker/src/factories/plugins.rs b/crates/worker/src/factories/plugins.rs index 1e50772..79bb1e3 100644 --- a/crates/worker/src/factories/plugins.rs +++ b/crates/worker/src/factories/plugins.rs @@ -1,6 +1,8 @@ use crate::plugin_registry::InMemoryPluginRegistry; -use crate::plugins::{MetadataExtractorPlugin, NoOpPlugin, SidecarSyncPlugin}; -use domain::ports::{MetadataExtractorPort, SidecarWriterPort}; +use crate::plugins::{ + MetadataExtractorPlugin, NoOpPlugin, SidecarSyncPlugin, ThumbnailGeneratorPlugin, +}; +use domain::ports::{MetadataExtractorPort, SidecarWriterPort, ThumbnailGeneratorPort}; use std::sync::Arc; use super::Repos; @@ -10,16 +12,23 @@ pub fn build_plugin_registry( file_storage: Arc, sidecar_writer: Arc, extractor: Arc, + thumbnail_gen: Arc, ) -> InMemoryPluginRegistry { let mut registry = InMemoryPluginRegistry::new(); registry.register(Arc::new(NoOpPlugin)); registry.register(Arc::new(MetadataExtractorPlugin::new( repos.asset.clone(), - file_storage, + file_storage.clone(), repos.metadata.clone(), extractor, ))); + registry.register(Arc::new(ThumbnailGeneratorPlugin::new( + repos.asset.clone(), + file_storage, + repos.derivative.clone(), + thumbnail_gen, + ))); let export_handler = Arc::new(application::sidecar::ExportSidecarHandler::new( repos.metadata.clone(), diff --git a/crates/worker/src/main.rs b/crates/worker/src/main.rs index 81efd29..30ab958 100644 --- a/crates/worker/src/main.rs +++ b/crates/worker/src/main.rs @@ -53,11 +53,14 @@ async fn main() -> anyhow::Result<()> { let extractor: Arc = Arc::new(adapters_exif::NomExifExtractor); + let thumbnail_gen: Arc = + Arc::new(adapters_thumbnail::ImageThumbnailGenerator); let registry = Arc::new(build_plugin_registry( &repos, file_storage, sidecar_writer, extractor, + thumbnail_gen, )); let process_next = Arc::new(build_process_next_handler( &repos, diff --git a/crates/worker/src/plugins/mod.rs b/crates/worker/src/plugins/mod.rs index 047144f..3541655 100644 --- a/crates/worker/src/plugins/mod.rs +++ b/crates/worker/src/plugins/mod.rs @@ -1,7 +1,9 @@ pub mod metadata_extractor; pub mod no_op; pub mod sidecar_sync; +pub mod thumbnail_generator; pub use metadata_extractor::MetadataExtractorPlugin; pub use no_op::NoOpPlugin; pub use sidecar_sync::SidecarSyncPlugin; +pub use thumbnail_generator::ThumbnailGeneratorPlugin; diff --git a/crates/worker/src/plugins/thumbnail_generator.rs b/crates/worker/src/plugins/thumbnail_generator.rs new file mode 100644 index 0000000..7b32919 --- /dev/null +++ b/crates/worker/src/plugins/thumbnail_generator.rs @@ -0,0 +1,138 @@ +use async_trait::async_trait; +use domain::{ + entities::{DerivativeAsset, DerivativeProfile}, + errors::DomainError, + ports::{ + AssetRepository, DerivativeRepository, FileStoragePort, PluginExecutor, + ThumbnailGeneratorPort, + }, + value_objects::{MetadataValue, StructuredData, SystemId}, +}; +use std::sync::Arc; +use tracing::info; + +pub struct ThumbnailGeneratorPlugin { + asset_repo: Arc, + file_storage: Arc, + derivative_repo: Arc, + thumbnail_gen: Arc, +} + +impl ThumbnailGeneratorPlugin { + pub fn new( + asset_repo: Arc, + file_storage: Arc, + derivative_repo: Arc, + thumbnail_gen: Arc, + ) -> Self { + Self { + asset_repo, + file_storage, + derivative_repo, + thumbnail_gen, + } + } +} + +fn parse_profile(s: &str) -> DerivativeProfile { + match s { + "ThumbnailLarge" => DerivativeProfile::ThumbnailLarge, + "WebOptimized" => DerivativeProfile::WebOptimized, + "VideoSd" => DerivativeProfile::VideoSd, + _ => DerivativeProfile::ThumbnailSquare, + } +} + +fn format_extension(format: &str) -> &str { + match format { + "jpeg" | "jpg" => "jpg", + "png" => "png", + _ => "webp", + } +} + +#[async_trait] +impl PluginExecutor for ThumbnailGeneratorPlugin { + fn plugin_name(&self) -> &str { + "thumbnail_generator" + } + + async fn execute( + &self, + asset_id: Option, + _payload: &StructuredData, + config: &StructuredData, + ) -> Result { + let asset_id = asset_id.ok_or_else(|| { + DomainError::Validation("thumbnail_generator requires asset_id".into()) + })?; + + let width = config + .get_string("width") + .and_then(|s| s.parse().ok()) + .unwrap_or(300u32); + let height = config + .get_string("height") + .and_then(|s| s.parse().ok()) + .unwrap_or(300u32); + let format = config.get_string("format").unwrap_or("webp"); + let profile = config + .get_string("profile") + .map(parse_profile) + .unwrap_or(DerivativeProfile::ThumbnailSquare); + + let asset = self + .asset_repo + .find_by_id(&asset_id) + .await? + .ok_or_else(|| DomainError::NotFound(format!("Asset {} not found", asset_id)))?; + + if !asset.mime_type.starts_with("image/") { + return Ok(StructuredData::new()); + } + + let source_bytes = self + .file_storage + .read_file(&asset.source_reference.relative_path) + .await?; + + let output = self + .thumbnail_gen + .generate(&source_bytes, width, height, format)?; + + let ext = format_extension(format); + let storage_path = format!("derivatives/{asset_id}_{profile:?}.{ext}"); + + let byte_len = output.bytes.len() as u64; + self.file_storage + .store_file(&storage_path, output.bytes) + .await?; + + let mut derivative = match self + .derivative_repo + .find_by_asset_and_profile(&asset_id, profile) + .await? + { + Some(d) => d, + None => DerivativeAsset::new_pending(asset_id, profile, &storage_path), + }; + + derivative.storage_path = storage_path.clone(); + derivative.mark_ready(&output.mime_type, byte_len, (output.width, output.height)); + self.derivative_repo.save(&derivative).await?; + + let mut result = StructuredData::new(); + result.insert("thumbnail_path", MetadataValue::String(storage_path)); + result.insert( + "thumbnail_width", + MetadataValue::Integer(output.width as i64), + ); + result.insert( + "thumbnail_height", + MetadataValue::Integer(output.height as i64), + ); + + info!(asset_id = %asset_id, w = output.width, h = output.height, "thumbnail generated"); + Ok(result) + } +}