From d98075562143352a36b3bd317baa6e2c7469798b Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Fri, 24 Apr 2026 04:54:33 +0200 Subject: [PATCH] init --- .gitignore | 4 + Cargo.lock | 1504 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 19 + LICENSE | 21 + README.md | 8 + src/cli.rs | 294 +++++++++ src/color_space.rs | 145 +++++ src/lib.rs | 6 + src/main.rs | 7 + src/palette.rs | 175 ++++++ src/processor.rs | 130 ++++ 11 files changed, 2313 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/cli.rs create mode 100644 src/color_space.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/palette.rs create mode 100644 src/processor.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a800b31 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +*.png +*.jpg +output/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8c6f178 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1504 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[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 = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +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 = "autocfg" +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", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +dependencies = [ + "arrayvec", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[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 = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys", +] + +[[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-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[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 = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[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 = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[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 = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", +] + +[[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 = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +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 = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[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 = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "no_std_io2" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b51ed7824b6e07d354605f4abb3d9d300350701299da96642ee084f5ce631550" +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 = "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[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-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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "palette_derive", + "phf", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn", +] + +[[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 = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.6", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pixel-palette-colorizer" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "image", + "indicatif", + "palette", + "rayon", + "tempfile", + "thiserror", + "tracing", + "tracing-subscriber", + "walkdir", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn", +] + +[[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 = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom", +] + +[[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", + "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 = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "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 = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "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 = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "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 new file mode 100644 index 0000000..a0725b0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "pixel-palette-colorizer" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.102" +clap = { version = "4.6.1", features = ["derive"] } +image = "0.25.10" +indicatif = "0.18.4" +palette = "0.7" +rayon = "1.12.0" +thiserror = "2.0.18" +tracing = "0.1.44" +tracing-subscriber = "0.3.23" +walkdir = "2" + +[dev-dependencies] +tempfile = "3" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dd83b9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Gabriel Kaszewski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..945a496 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# Pixel palette colorizer + +This tool was created because I was lazy to change palettes of pixel art to match the style of my game. +Usage is pretty straightforward, you give the tool a palette and images you want to colorize, and it will output the colorized images, you can also specify the color space to use for color matching. + +# License + +MIT License, see [LICENSE](LICENSE) for details. I don't care what you do with this code or software. diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..440c6a6 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,294 @@ +use std::path::PathBuf; + +use clap::Parser; +use rayon::prelude::*; +use tracing::info; + +use crate::{ + color_space::ColorSpace, + palette::{FilePaletteSource, Palette}, + processor::{process_image, ProcessResult}, +}; + +#[derive(Parser, Debug)] +#[command(author, version, about = "A pixel palette colorizer tool.")] +struct Cli { + #[arg(required = true)] + inputs: Vec, + + #[arg(short, long)] + palette: PathBuf, + + #[arg(short, long, default_value = "output")] + out_dir: PathBuf, + + #[arg(long, default_value_t = false)] + dry_run: bool, + + #[arg( + short = 'e', + long, + default_value = "png,jpg", + value_delimiter = ',' + )] + extensions: Vec, + + #[arg( + short, + long, + default_value = "rgb", + value_parser = clap::builder::PossibleValuesParser::new(crate::color_space::available_names()) + )] + color_space: String, +} + +pub fn expand_inputs(inputs: &[PathBuf], extensions: &[String]) -> Vec { + let exts: Vec = extensions.iter().map(|e| e.to_lowercase()).collect(); + let mut result = Vec::new(); + for path in inputs { + if path.is_dir() { + for entry in walkdir::WalkDir::new(path) + .follow_links(false) + .into_iter() + .filter_map(|e| match e { + Ok(entry) => Some(entry), + Err(err) => { + tracing::warn!("Skipping unreadable entry: {}", err); + None + } + }) + { + let p = entry.path(); + if p.is_file() { + let matches = p + .extension() + .and_then(|e| e.to_str()) + .map(|e| exts.contains(&e.to_lowercase())) + .unwrap_or(false); + if matches { + result.push(p.to_path_buf()); + } + } + } + } else { + result.push(path.clone()); + } + } + result +} + +pub trait Reporter: Send + Sync { + fn on_complete(&self, outcome: &FileOutcome); + fn summarize(&self, outcomes: &[FileOutcome]); +} + +pub struct FileOutcome { + pub path: PathBuf, + pub result: anyhow::Result, +} + +pub fn run_batch( + inputs: &[PathBuf], + out_dir: &std::path::Path, + palette: &[[u8; 4]], + space: &dyn ColorSpace, + dry_run: bool, + reporter: &dyn Reporter, +) -> Vec { + inputs + .par_iter() + .map(|path| { + let result = process_image(path, out_dir, palette, space, dry_run); + let outcome = FileOutcome { path: path.clone(), result }; + reporter.on_complete(&outcome); + outcome + }) + .collect() +} + +pub struct DefaultReporter { + bar: indicatif::ProgressBar, +} + +impl DefaultReporter { + pub fn new(total: u64) -> Self { + let bar = indicatif::ProgressBar::new(total); + bar.set_style( + indicatif::ProgressStyle::with_template("{pos}/{len} [{bar:40}] {msg}") + .unwrap() + .progress_chars("=> "), + ); + Self { bar } + } +} + +impl Reporter for DefaultReporter { + fn on_complete(&self, outcome: &FileOutcome) { + self.bar.inc(1); + match &outcome.result { + Ok(r) => info!( + "Processed {:?} ({} pixels changed)", + outcome.path.file_name().unwrap_or_default(), + r.pixels_changed + ), + Err(e) => tracing::error!( + "Failed {:?}: {:#}", + outcome.path.file_name().unwrap_or_default(), + e + ), + } + } + + fn summarize(&self, outcomes: &[FileOutcome]) { + self.bar.finish_and_clear(); + let succeeded = outcomes.iter().filter(|o| o.result.is_ok()).count(); + let total_pixels: u64 = outcomes + .iter() + .filter_map(|o| o.result.as_ref().ok()) + .map(|r| r.pixels_changed) + .sum(); + info!( + "Done: {}/{} files succeeded, {} pixels remapped.", + succeeded, + outcomes.len(), + total_pixels + ); + } +} + +pub fn run() -> anyhow::Result<()> { + info!("Running pixel palette colorizer..."); + + let cli = Cli::parse(); + + let space = crate::color_space::from_name(&cli.color_space)?; + info!("Color space: {}", cli.color_space); + + info!("Loading palette from {:?}", cli.palette); + let palette = Palette::load(&FilePaletteSource(cli.palette.clone()))?; + info!("Loaded {} colors.", palette.len()); + + let inputs = expand_inputs(&cli.inputs, &cli.extensions); + info!("Processing {} files...", inputs.len()); + let reporter = DefaultReporter::new(inputs.len() as u64); + let outcomes = run_batch(&inputs, &cli.out_dir, palette.colors(), &*space, cli.dry_run, &reporter); + reporter.summarize(&outcomes); + + let failed = outcomes.iter().filter(|o| o.result.is_err()).count(); + if failed > 0 { + anyhow::bail!("{}/{} files failed", failed, outcomes.len()); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::color_space; + + struct NoopReporter; + impl Reporter for NoopReporter { + fn on_complete(&self, _: &FileOutcome) {} + fn summarize(&self, _: &[FileOutcome]) {} + } + + #[test] + fn run_batch_collects_all_errors_without_aborting() { + let space = color_space::from_name("rgb").unwrap(); + let palette = vec![[255u8, 0, 0, 255]]; + let inputs = vec![ + std::path::PathBuf::from("/nonexistent/a.png"), + std::path::PathBuf::from("/nonexistent/b.png"), + ]; + let outcomes = run_batch( + &inputs, + std::path::Path::new("/tmp"), + &palette, + &*space, + false, + &NoopReporter, + ); + assert_eq!(outcomes.len(), 2); + assert!(outcomes.iter().all(|o| o.result.is_err())); + } + + #[test] + fn run_batch_returns_one_outcome_per_input() { + let space = color_space::from_name("rgb").unwrap(); + let palette = vec![[0u8, 0, 0, 255]]; + let inputs: Vec = (0..5) + .map(|i| std::path::PathBuf::from(format!("/nonexistent/{i}.png"))) + .collect(); + let outcomes = run_batch( + &inputs, + std::path::Path::new("/tmp"), + &palette, + &*space, + false, + &NoopReporter, + ); + assert_eq!(outcomes.len(), 5); + } + + #[test] + fn expand_inputs_passes_through_explicit_file() { + let path = PathBuf::from("/nonexistent/file.png"); + let result = expand_inputs(&[path.clone()], &["png".to_string()]); + assert_eq!(result, vec![path]); + } + + #[test] + fn expand_inputs_passes_through_nonexistent_path() { + let path = PathBuf::from("/nonexistent/missing.xyz"); + let result = expand_inputs(&[path.clone()], &["png".to_string()]); + assert_eq!(result, vec![path]); + } + + #[test] + fn expand_inputs_recurses_directory() { + let dir = tempfile::tempdir().unwrap(); + let sub = dir.path().join("sub"); + std::fs::create_dir(&sub).unwrap(); + std::fs::write(sub.join("nested.png"), b"").unwrap(); + std::fs::write(dir.path().join("top.png"), b"").unwrap(); + + let result = expand_inputs(&[dir.path().to_path_buf()], &["png".to_string()]); + assert_eq!(result.len(), 2); + } + + #[test] + fn expand_inputs_filters_by_extension() { + let dir = tempfile::tempdir().unwrap(); + std::fs::write(dir.path().join("keep.png"), b"").unwrap(); + std::fs::write(dir.path().join("skip.txt"), b"").unwrap(); + + let result = expand_inputs(&[dir.path().to_path_buf()], &["png".to_string()]); + assert_eq!(result.len(), 1); + assert_eq!(result[0].file_name().unwrap(), "keep.png"); + } + + #[test] + fn expand_inputs_extension_match_is_case_insensitive() { + let dir = tempfile::tempdir().unwrap(); + std::fs::write(dir.path().join("image.PNG"), b"").unwrap(); + + let result = expand_inputs(&[dir.path().to_path_buf()], &["png".to_string()]); + assert_eq!(result.len(), 1); + } + + #[test] + fn expand_inputs_mixed_file_and_directory() { + let dir = tempfile::tempdir().unwrap(); + std::fs::write(dir.path().join("a.png"), b"").unwrap(); + let explicit = PathBuf::from("/nonexistent/b.png"); + + let mut result = expand_inputs( + &[dir.path().to_path_buf(), explicit.clone()], + &["png".to_string()], + ); + result.sort(); + assert_eq!(result.len(), 2); + assert!(result.contains(&explicit)); + } +} diff --git a/src/color_space.rs b/src/color_space.rs new file mode 100644 index 0000000..d079919 --- /dev/null +++ b/src/color_space.rs @@ -0,0 +1,145 @@ +use palette::{Hsl, IntoColor, Lab, Oklab, Srgb}; + +pub trait ColorSpace: Send + Sync { + fn distance(&self, p1: &[u8; 4], p2: &[u8; 4]) -> f64; +} + +pub struct RgbSpace; +impl ColorSpace for RgbSpace { + fn distance(&self, p1: &[u8; 4], p2: &[u8; 4]) -> f64 { + let dr = (p1[0] as f64) - (p2[0] as f64); + let dg = (p1[1] as f64) - (p2[1] as f64); + let db = (p1[2] as f64) - (p2[2] as f64); + (dr * dr + dg * dg + db * db).sqrt() + } +} + +pub struct LabSpace; +impl ColorSpace for LabSpace { + fn distance(&self, p1: &[u8; 4], p2: &[u8; 4]) -> f64 { + let to_lab = |p: &[u8; 4]| -> Lab { + Srgb::new(p[0], p[1], p[2]).into_format::().into_color() + }; + let l1 = to_lab(p1); + let l2 = to_lab(p2); + let dl = (l1.l - l2.l) as f64; + let da = (l1.a - l2.a) as f64; + let db = (l1.b - l2.b) as f64; + (dl * dl + da * da + db * db).sqrt() + } +} + +pub struct HslSpace; +impl ColorSpace for HslSpace { + fn distance(&self, p1: &[u8; 4], p2: &[u8; 4]) -> f64 { + let to_cart = |p: &[u8; 4]| -> (f64, f64, f64) { + let hsl: Hsl = Srgb::new(p[0], p[1], p[2]).into_format::().into_color(); + let h = hsl.hue.into_radians() as f64; + let s = hsl.saturation as f64; + let l = hsl.lightness as f64; + (s * h.cos(), s * h.sin(), l) + }; + let (x1, y1, z1) = to_cart(p1); + let (x2, y2, z2) = to_cart(p2); + let dx = x1 - x2; + let dy = y1 - y2; + let dz = z1 - z2; + (dx * dx + dy * dy + dz * dz).sqrt() + } +} + +pub struct OklabSpace; +impl ColorSpace for OklabSpace { + fn distance(&self, p1: &[u8; 4], p2: &[u8; 4]) -> f64 { + let to_oklab = |p: &[u8; 4]| -> Oklab { + Srgb::new(p[0], p[1], p[2]).into_format::().into_color() + }; + let o1 = to_oklab(p1); + let o2 = to_oklab(p2); + let dl = (o1.l - o2.l) as f64; + let da = (o1.a - o2.a) as f64; + let db = (o1.b - o2.b) as f64; + (dl * dl + da * da + db * db).sqrt() + } +} + +struct ColorSpaceEntry { + name: &'static str, + build: fn() -> Box, +} + +static REGISTRY: &[ColorSpaceEntry] = &[ + ColorSpaceEntry { name: "rgb", build: || Box::new(RgbSpace) }, + ColorSpaceEntry { name: "hsl", build: || Box::new(HslSpace) }, + ColorSpaceEntry { name: "lab", build: || Box::new(LabSpace) }, + ColorSpaceEntry { name: "oklab", build: || Box::new(OklabSpace) }, +]; + +pub fn available_names() -> impl Iterator { + REGISTRY.iter().map(|e| e.name) +} + +pub fn from_name(name: &str) -> anyhow::Result> { + REGISTRY + .iter() + .find(|e| e.name == name) + .map(|e| (e.build)()) + .ok_or_else(|| anyhow::anyhow!("Unknown color space: {name}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_name_rgb_returns_working_space() { + let space = from_name("rgb").unwrap(); + let white = [255u8, 255, 255, 255]; + let black = [0u8, 0, 0, 255]; + assert!(space.distance(&white, &black) > 0.0); + assert_eq!(space.distance(&white, &white), 0.0); + } + + #[test] + fn from_name_unknown_is_err() { + assert!(from_name("xyz").is_err()); + } + + #[test] + fn registry_is_coherent() { + let names: Vec<_> = available_names().collect(); + assert_eq!(names.len(), 4, "expected 4 registered color spaces"); + for name in &names { + assert!(from_name(name).is_ok(), "from_name failed for {name}"); + } + } + + #[test] + fn lab_distance_positive_for_different_colors() { + let space = LabSpace; + let white = [255u8, 255, 255, 255]; + let black = [0u8, 0, 0, 255]; + assert!(space.distance(&white, &black) > 0.0); + assert!(space.distance(&white, &white) < 1e-10); + } + + #[test] + fn hsl_cylindrical_distance_works() { + let space = HslSpace; + let red = [255u8, 0, 0, 255]; + assert!(space.distance(&red, &red) < 1e-10); + let blue = [0u8, 0, 255, 255]; + assert!(space.distance(&red, &blue) > 0.0); + let grey = [128u8, 128, 128, 255]; + assert!(space.distance(&grey, &grey) < 1e-10); + } + + #[test] + fn oklab_distance_positive_for_different_colors() { + let space = OklabSpace; + let white = [255u8, 255, 255, 255]; + let black = [0u8, 0, 0, 255]; + assert!(space.distance(&white, &black) > 0.0); + assert!(space.distance(&white, &white) < 1e-10); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f5a8776 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +mod cli; +mod color_space; +mod palette; +mod processor; + +pub use cli::run; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6dc9a9d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,7 @@ +use pixel_palette_colorizer::run; + +fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + + run() +} diff --git a/src/palette.rs b/src/palette.rs new file mode 100644 index 0000000..f269438 --- /dev/null +++ b/src/palette.rs @@ -0,0 +1,175 @@ +use anyhow::Context; + +pub trait PaletteSource { + fn extension(&self) -> &str; + fn read_bytes(&self) -> anyhow::Result>; +} + +pub struct FilePaletteSource(pub std::path::PathBuf); + +impl PaletteSource for FilePaletteSource { + fn extension(&self) -> &str { + self.0 + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + } + + fn read_bytes(&self) -> anyhow::Result> { + std::fs::read(&self.0).context("Failed to read palette file") + } +} + +#[derive(Debug)] +pub struct Palette(Vec<[u8; 4]>); + +fn parse_hex_text(bytes: &[u8]) -> anyhow::Result> { + let content = std::str::from_utf8(bytes).context("Palette text is not valid UTF-8")?; + let mut colors = Vec::new(); + for line in content.lines() { + let hex = line.trim().trim_start_matches('#'); + if hex.is_empty() { + continue; + } + if hex.len() == 6 || hex.len() == 8 { + let r = u8::from_str_radix(&hex[0..2], 16).context("Invalid hex color")?; + let g = u8::from_str_radix(&hex[2..4], 16).context("Invalid hex color")?; + let b = u8::from_str_radix(&hex[4..6], 16).context("Invalid hex color")?; + let a = if hex.len() == 8 { + u8::from_str_radix(&hex[6..8], 16).context("Invalid hex color")? + } else { + 255 + }; + colors.push([r, g, b, a]); + } else { + tracing::warn!("Skipping invalid hex color: {}", line.trim()); + } + } + Ok(colors) +} + +fn parse_image_bytes(bytes: &[u8]) -> anyhow::Result> { + use image::GenericImageView; + let img = image::load_from_memory(bytes).context("Failed to decode palette image")?; + let mut seen = std::collections::HashSet::new(); + let mut colors = Vec::new(); + for (_, _, pixel) in img.pixels() { + let key = [pixel[0], pixel[1], pixel[2], pixel[3]]; + if seen.insert(key) { + colors.push(key); + } + } + Ok(colors) +} + +impl Palette { + pub fn load(source: &dyn PaletteSource) -> anyhow::Result { + let bytes = source.read_bytes()?; + let colors = match source.extension() { + "txt" | "hex" => parse_hex_text(&bytes)?, + "png" | "jpg" | "jpeg" | "bmp" | "gif" => parse_image_bytes(&bytes)?, + ext => anyhow::bail!("Unsupported palette file format: {ext}"), + }; + anyhow::ensure!(!colors.is_empty(), "Palette contains no colors"); + Ok(Palette(colors)) + } + + pub fn colors(&self) -> &[[u8; 4]] { + &self.0 + } + + pub fn len(&self) -> usize { + self.0.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct InMemorySource { + ext: &'static str, + data: Vec, + } + + impl PaletteSource for InMemorySource { + fn extension(&self) -> &str { self.ext } + fn read_bytes(&self) -> anyhow::Result> { Ok(self.data.clone()) } + } + + fn src(ext: &'static str, data: &str) -> InMemorySource { + InMemorySource { ext, data: data.as_bytes().to_vec() } + } + + #[test] + fn hex_parses_six_char_rgb() { + let p = Palette::load(&src("hex", "#ff0000\n")).unwrap(); + assert_eq!(p.colors(), &[[255u8, 0, 0, 255]]); + } + + #[test] + fn hex_parses_eight_char_rgba() { + let p = Palette::load(&src("hex", "ff000080\n")).unwrap(); + assert_eq!(p.colors(), &[[255u8, 0, 0, 128]]); + } + + #[test] + fn hex_skips_blank_lines() { + let p = Palette::load(&src("hex", "\n#ff0000\n\n#00ff00\n\n")).unwrap(); + assert_eq!(p.colors().len(), 2); + } + + #[test] + fn hex_parses_six_char_rgb_without_prefix() { + let p = Palette::load(&src("txt", "ff0000\n")).unwrap(); + assert_eq!(p.colors(), &[[255u8, 0, 0, 255]]); + } + + #[test] + fn hex_skips_wrong_length_lines() { + let p = Palette::load(&src("hex", "#fff\n#ff0000\n")).unwrap(); + assert_eq!(p.colors().len(), 1); + } + + #[test] + fn hex_rejects_invalid_hex_digits() { + let result = Palette::load(&src("hex", "gggggg\n")); + assert!(result.is_err()); + } + + fn make_png_bytes(colors: &[[u8; 4]]) -> Vec { + let mut img = image::RgbaImage::new(colors.len() as u32, 1); + for (i, &c) in colors.iter().enumerate() { + img.put_pixel(i as u32, 0, image::Rgba(c)); + } + let mut buf = std::io::Cursor::new(Vec::new()); + img.write_to(&mut buf, image::ImageFormat::Png).unwrap(); + buf.into_inner() + } + + #[test] + fn image_extracts_unique_colors() { + let bytes = make_png_bytes(&[[255u8, 0, 0, 255], [0, 255, 0, 255], [255, 0, 0, 255]]); + let source = InMemorySource { ext: "png", data: bytes }; + let p = Palette::load(&source).unwrap(); + assert_eq!(p.colors().len(), 2); // [255,0,0,255] deduped + } + + #[test] + fn image_jpeg_extension_also_works() { + let bytes = make_png_bytes(&[[0u8, 0, 255, 255]]); + let source = InMemorySource { ext: "jpg", data: bytes }; + assert!(Palette::load(&source).is_ok()); + } + + #[test] + fn empty_text_palette_is_rejected() { + assert!(Palette::load(&src("hex", "\n\n")).is_err()); + } + + #[test] + fn unknown_extension_is_rejected() { + let err = Palette::load(&src("csv", "#ff0000\n")).unwrap_err(); + assert!(err.to_string().contains("Unsupported"), "{err}"); + } +} diff --git a/src/processor.rs b/src/processor.rs new file mode 100644 index 0000000..03b6367 --- /dev/null +++ b/src/processor.rs @@ -0,0 +1,130 @@ +use std::path::Path; + +use anyhow::Context; + +use crate::color_space::ColorSpace; + +pub struct ProcessResult { + pub pixels_changed: u64, +} + +pub struct RemapStats { + pub pixels_changed: u64, +} + +// Palette alpha is ignored; each pixel's original alpha is preserved. +pub fn remap_pixels( + img: &mut image::RgbaImage, + palette: &[[u8; 4]], + color_space: &dyn ColorSpace, +) -> RemapStats { + debug_assert!(!palette.is_empty(), "remap_pixels called with empty palette"); + let mut pixels_changed: u64 = 0; + for pixel in img.pixels_mut() { + if pixel[3] == 0 { + continue; + } + let mut min_distance = f64::MAX; + let mut best_match: [u8; 4] = pixel.0; + for palette_color in palette { + let dist = color_space.distance(&pixel.0, palette_color); + if dist < min_distance { + min_distance = dist; + best_match = *palette_color; + } + } + best_match[3] = pixel[3]; + if pixel.0 != best_match { + pixels_changed += 1; + } + pixel.0 = best_match; + } + RemapStats { pixels_changed } +} + +pub fn process_image( + input_path: &Path, + out_dir: &Path, + palette: &[[u8; 4]], + color_space: &dyn ColorSpace, + dry_run: bool, +) -> anyhow::Result { + let img = image::open(input_path) + .with_context(|| format!("Failed to open input image {:?}", input_path))?; + + let mut out_img = img.to_rgba8(); + let stats = remap_pixels(&mut out_img, palette, color_space); + + let file_name = input_path.file_name().context("Invalid input file name")?; + let out_path = out_dir.join(file_name); + + if !dry_run { + std::fs::create_dir_all(out_dir)?; + out_img + .save(&out_path) + .with_context(|| format!("Failed to save output image to {:?}", out_path))?; + } + + Ok(ProcessResult { + pixels_changed: stats.pixels_changed, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::color_space::RgbSpace; + + #[test] + fn transparent_pixels_are_not_remapped() { + let mut img = image::RgbaImage::from_pixel(1, 1, image::Rgba([100u8, 150, 200, 0])); + let palette = vec![[255u8, 0, 0, 255]]; + let stats = remap_pixels(&mut img, &palette, &RgbSpace); + assert_eq!(stats.pixels_changed, 0); + assert_eq!(img.get_pixel(0, 0).0, [100, 150, 200, 0]); + } + + #[test] + fn nearest_color_replaces_pixel() { + let mut img = image::RgbaImage::from_pixel(1, 1, image::Rgba([100u8, 150, 200, 255])); + let palette = vec![[255u8, 0, 0, 255], [0u8, 0, 255, 255]]; + let stats = remap_pixels(&mut img, &palette, &RgbSpace); + assert_eq!(stats.pixels_changed, 1); + assert_eq!(img.get_pixel(0, 0).0, [0, 0, 255, 255]); + } + + #[test] + fn alpha_is_preserved_after_remap() { + let mut img = image::RgbaImage::from_pixel(1, 1, image::Rgba([200u8, 200, 200, 128])); + let palette = vec![[0u8, 0, 0, 255]]; + let stats = remap_pixels(&mut img, &palette, &RgbSpace); + assert_eq!(img.get_pixel(0, 0).0[3], 128); + assert_eq!(stats.pixels_changed, 1); + } + + #[test] + fn identical_pixel_is_not_counted_as_changed() { + let mut img = image::RgbaImage::from_pixel(1, 1, image::Rgba([255u8, 0, 0, 255])); + let palette = vec![[255u8, 0, 0, 255]]; + let stats = remap_pixels(&mut img, &palette, &RgbSpace); + assert_eq!(stats.pixels_changed, 0); + } + + #[test] + fn process_image_dry_run_does_not_write() { + let tmp = std::env::temp_dir().join("ppc_proc_test_input.png"); + let img = image::RgbaImage::from_pixel(1, 1, image::Rgba([100u8, 150, 200, 255])); + img.save(&tmp).unwrap(); + + let out_dir = std::env::temp_dir().join("ppc_proc_test_dryrun_out"); + let _ = std::fs::remove_dir_all(&out_dir); + + let palette = vec![[255u8, 0, 0, 255]]; + let result = process_image(&tmp, &out_dir, &palette, &RgbSpace, true).unwrap(); + + assert_eq!(result.pixels_changed, 1); + assert!(!out_dir.exists(), "dry_run must not create output directory"); + + std::fs::remove_file(&tmp).ok(); + } +}