From da1789814246758cb28a5d31eeb40c5e94ff9c19 Mon Sep 17 00:00:00 2001 From: Louis <contact@louiscap.co> Date: Sat, 1 Feb 2025 04:53:24 +0000 Subject: [PATCH] Create library module and factor out utilities into library functions --- Cargo.lock | 592 +++++++++++++++++++++++++++- Cargo.toml | 9 +- rustfmt.toml | 1 - src/actions/colours.rs | 36 ++ src/actions/mod.rs | 3 + src/cli_args.rs | 18 +- src/commands/palette.rs | 146 +------ src/commands/swap.rs | 42 +- src/lib.rs | 2 + src/main.rs | 7 +- src/{utils.rs => utils/colours.rs} | 20 +- src/utils/files.rs | 37 ++ src/{format.rs => utils/formats.rs} | 56 ++- src/utils/mapping.rs | 155 ++++++++ src/utils/mod.rs | 13 + src/utils/palette.rs | 89 +++++ 16 files changed, 988 insertions(+), 238 deletions(-) create mode 100644 src/actions/colours.rs create mode 100644 src/actions/mod.rs create mode 100644 src/lib.rs rename src/{utils.rs => utils/colours.rs} (95%) create mode 100644 src/utils/files.rs rename src/{format.rs => utils/formats.rs} (53%) create mode 100644 src/utils/mapping.rs create mode 100644 src/utils/mod.rs create mode 100644 src/utils/palette.rs diff --git a/Cargo.lock b/Cargo.lock index 61d2e7a..e68cfc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + [[package]] name = "anstream" version = "0.6.18" @@ -72,12 +78,58 @@ version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" + +[[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 = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" +dependencies = [ + "arrayvec", +] + [[package]] name = "bit_field" version = "0.10.2" @@ -90,6 +142,24 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + +[[package]] +name = "built" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + [[package]] name = "bytemuck" version = "1.20.0" @@ -102,6 +172,33 @@ 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 = "cc" +version = "1.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -212,7 +309,7 @@ dependencies = [ "rayon", "serde", "serde_json", - "thiserror", + "thiserror 2.0.11", "toml", ] @@ -316,6 +413,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gif" version = "0.13.1" @@ -368,22 +476,43 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "image" -version = "0.24.9" +version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", - "byteorder", + "byteorder-lite", "color_quant", "exr", "gif", - "jpeg-decoder", + "image-webp", "num-traits", "png", "qoi", + "ravif", + "rayon", + "rgb", "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +dependencies = [ + "byteorder-lite", + "quick-error", ] +[[package]] +name = "imgref" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" + [[package]] name = "indexmap" version = "2.7.0" @@ -394,26 +523,52 @@ dependencies = [ "hashbrown", ] +[[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "jpeg-decoder" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" -dependencies = [ - "rayon", -] [[package]] name = "lab" @@ -427,18 +582,59 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[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.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.2" @@ -449,6 +645,69 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[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" @@ -458,6 +717,24 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "png" version = "0.17.15" @@ -471,6 +748,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.92" @@ -480,6 +766,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "qoi" version = "0.4.1" @@ -489,6 +794,12 @@ 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.37" @@ -498,6 +809,86 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "system-deps", + "thiserror 1.0.69", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "rayon" version = "1.10.0" @@ -547,6 +938,18 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + [[package]] name = "ryu" version = "1.0.18" @@ -594,12 +997,27 @@ dependencies = [ "serde", ] +[[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.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -629,13 +1047,41 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] @@ -649,6 +1095,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tiff" version = "0.9.1" @@ -706,6 +1163,87 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "weezl" version = "0.1.8" @@ -794,6 +1332,33 @@ dependencies = [ "memchr", ] +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + [[package]] name = "zune-inflate" version = "0.2.54" @@ -802,3 +1367,12 @@ checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" dependencies = [ "simd-adler32", ] + +[[package]] +name = "zune-jpeg" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 87e0040..5bc2f38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,17 +16,22 @@ authors = [ name = "crunch" path = "src/main.rs" +[features] +default = ["serialise_types", "filesystem"] +serialise_types = [] +filesystem = [] + [dependencies] clap = { version = "4.5", features = ["derive"] } env_logger = "0.11" log = "0.4" anyhow = "1.0" -thiserror = "1.0" +thiserror = "2.0" deltae = "0.3" glam = "0.29" -image = "0.24" +image = "0.25" lab = "0.11" num-traits = "0.2" diff --git a/rustfmt.toml b/rustfmt.toml index d62aed7..2b6f66e 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,3 @@ hard_tabs = true -#group_imports = "StdExternalCrate" use_field_init_shorthand = true use_try_shorthand = true \ No newline at end of file diff --git a/src/actions/colours.rs b/src/actions/colours.rs new file mode 100644 index 0000000..3864f82 --- /dev/null +++ b/src/actions/colours.rs @@ -0,0 +1,36 @@ +use crate::utils::{new_image, BasicRgba, OutputFormat, PaletteMap}; +use image::{GenericImage, Pixel, Rgba}; +use num_traits::ToPrimitive; + +pub fn apply_colour_map(colour_map: &PaletteMap, image: &impl GenericImage) -> OutputFormat { + let mut output = new_image(image.width(), image.height()); + for (x, y, pixel) in image.pixels() { + let pixel = pixel.to_rgba(); + + if pixel.0[3].to_u8().unwrap() == 0 { + output.put_pixel(x, y, BasicRgba::transparent().into()); + continue; + } + + let data = Rgba::from([ + pixel.0[0].to_u8().unwrap(), + pixel.0[1].to_u8().unwrap(), + pixel.0[2].to_u8().unwrap(), + pixel.0[3].to_u8().unwrap(), + ]); + let basic = BasicRgba::from(data); + + match colour_map.get(&basic) { + Some(mapped_data) => output.put_pixel( + x, + y, + Rgba::from(BasicRgba { + a: basic.a, + ..*mapped_data + }), + ), + None => output.put_pixel(x, y, data), + }; + } + output +} diff --git a/src/actions/mod.rs b/src/actions/mod.rs new file mode 100644 index 0000000..1b6859e --- /dev/null +++ b/src/actions/mod.rs @@ -0,0 +1,3 @@ +mod colours; + +pub use colours::apply_colour_map; diff --git a/src/cli_args.rs b/src/cli_args.rs index af4ea5d..a53429e 100644 --- a/src/cli_args.rs +++ b/src/cli_args.rs @@ -1,13 +1,11 @@ -use clap::Parser; -use image::ImageFormat; -use serde::{Deserialize, Serialize}; - use crate::commands::{ Atlas, Extract, Extrude, Flip, Info, Palette, Pipeline, Reduce, Remap, Rotate, Scale, Split, Swap, }; - -use crate::load_image; +use clap::Parser; +use crunch_cli::utils::{load_image, ColourPalette, PaletteMap}; +use image::ImageFormat; +use serde::{Deserialize, Serialize}; /// Crunch is a set of utilities for quickly and easily processing a batch of files, either directly /// or by defining pipelines @@ -98,10 +96,10 @@ impl Args { let image_data = load_image(&remap.input, None)?; let palette_data = load_image(&remap.palette, None)?; - let image_palette = Palette::extract_from(&image_data)?; - let target_palette = Palette::extract_from(&palette_data)?; + let image_palette = ColourPalette::from(&image_data); + let target_palette = ColourPalette::from(&palette_data); - let mappings = Palette::calculate_mapping(&image_palette, &target_palette); + let mappings = PaletteMap::calculate_mapping(&image_palette, &target_palette); let output = Remap::remap_image(image_data, mappings)?; output @@ -111,7 +109,7 @@ impl Args { Args::Swap(swap) => { let image_data = load_image(&swap.input, None)?; let palette_data = load_image(&swap.palette, None)?; - let palette_swap = Palette::from_columns(&palette_data)?; + let palette_swap = PaletteMap::from_column_image(&palette_data)?; let output = Swap::swap_pixels(image_data, palette_swap)?; output .save_with_format(&swap.output, ImageFormat::Png) diff --git a/src/commands/palette.rs b/src/commands/palette.rs index f74461c..5be4a31 100644 --- a/src/commands/palette.rs +++ b/src/commands/palette.rs @@ -1,40 +1,11 @@ use clap::Parser; -use std::cmp::{min, Ordering}; -use std::collections::hash_map::RandomState; -use std::collections::{HashMap, HashSet}; +use std::cmp::min; -use anyhow::Error; -use deltae::{Delta, LabValue, DE2000}; +use crunch_cli::utils::{new_image, BasicRgba, ColourPalette, PaletteFormat, PaletteMap}; use image::{GenericImage, Pixel, Rgba}; -use num_traits::ToPrimitive; use serde::{Deserialize, Serialize}; use std::io::Write; -use crate::format::PaletteFormat; -use crate::utils::{new_image, BasicRgba}; - -pub type PixelPalette = Vec<BasicRgba>; -pub type ColourMapping = HashMap<BasicRgba, BasicRgba>; - -pub fn sort_by_hue(palette: &mut PixelPalette) { - palette.sort_by(|pa, pb| { - let hue_a = pa.hue(); - let hue_b = pb.hue(); - - if hue_a.is_nan() && hue_b.is_nan() { - Ordering::Equal - } else if hue_a.is_nan() { - Ordering::Less - } else if hue_b.is_nan() || hue_a > hue_b { - Ordering::Greater - } else if hue_b > hue_a { - Ordering::Less - } else { - Ordering::Equal - } - }); -} - /// Create a palette file containing every distinct colour from the input image #[derive(Parser, Clone, Serialize, Deserialize, Debug)] #[clap(author, version = "0.9.0")] @@ -55,27 +26,12 @@ pub struct Palette { impl Palette { pub fn run(&self, image: &impl GenericImage) -> anyhow::Result<()> { - let palette = Palette::extract_from(image)?; + let palette = ColourPalette::from(image); self.write_palette(palette) } - pub fn extract_from(image: &impl GenericImage) -> anyhow::Result<PixelPalette> { - let mut colours = HashSet::new(); - for (_, _, pixel) in image.pixels() { - let pixel = pixel.to_rgba(); - colours.insert(Rgba::from([ - pixel.0[0].to_u8().unwrap(), - pixel.0[1].to_u8().unwrap(), - pixel.0[2].to_u8().unwrap(), - pixel.0[3].to_u8().unwrap(), - ])); - } - - Ok(colours.iter().map(BasicRgba::from).collect()) - } - - pub fn write_palette(&self, mut colours: PixelPalette) -> anyhow::Result<()> { - sort_by_hue(&mut colours); + pub fn write_palette(&self, mut colours: ColourPalette) -> anyhow::Result<()> { + colours.sort_by_hue(); match self.format { PaletteFormat::Png => { @@ -121,96 +77,4 @@ impl Palette { Ok(()) } - - pub fn from_columns(image: &impl GenericImage) -> anyhow::Result<ColourMapping> { - if image.width() != 2 { - return Err(Error::msg("Image must have a pixel width of 2"))?; - } - - let mut mapping = ColourMapping::with_capacity(image.height() as usize); - for y in 0..image.height() { - let left = image.get_pixel(0, y); - let right = image.get_pixel(1, y); - let left = left.to_rgba(); - let right = right.to_rgba(); - let data = Rgba::from([ - left.0[0].to_u8().unwrap(), - left.0[1].to_u8().unwrap(), - left.0[2].to_u8().unwrap(), - left.0[3].to_u8().unwrap(), - ]); - let left_pixel = BasicRgba::from(data); - let data = Rgba::from([ - right.0[0].to_u8().unwrap(), - right.0[1].to_u8().unwrap(), - right.0[2].to_u8().unwrap(), - right.0[3].to_u8().unwrap(), - ]); - let right_pixel = BasicRgba::from(data); - mapping.insert(left_pixel, right_pixel); - } - - Ok(mapping) - } - - pub fn calculate_mapping(from: &PixelPalette, to: &PixelPalette) -> ColourMapping { - let colour_labs = Vec::from_iter(to.iter().map(LabValue::from)); - - let to_palette_vectors: HashMap<usize, &BasicRgba, RandomState> = - HashMap::from_iter(to.iter().enumerate()); - let mut out_map: ColourMapping = HashMap::with_capacity(from.len()); - - for colour in from { - let closest = to_palette_vectors - .keys() - .fold(None, |lowest, idx| match lowest { - Some(num) => { - let current = colour_labs[*idx]; - let previous: LabValue = colour_labs[num]; - - if colour.delta(current, DE2000) < colour.delta(previous, DE2000) { - Some(*idx) - } else { - Some(num) - } - } - None => Some(*idx), - }); - - match closest { - Some(idx) => match to_palette_vectors.get(&idx) { - Some(col) => { - out_map.insert(*colour, **col); - } - None => { - log::warn!("No matching set for {} with col {:?}", idx, &colour); - - out_map.insert( - *colour, - BasicRgba { - r: 0, - g: 0, - b: 0, - a: 0, - }, - ); - } - }, - None => { - log::warn!("No closest for {:?}", &colour); - out_map.insert( - *colour, - BasicRgba { - r: 0, - g: 0, - b: 0, - a: 0, - }, - ); - } - } - } - - out_map - } } diff --git a/src/commands/swap.rs b/src/commands/swap.rs index c8e772e..8491951 100644 --- a/src/commands/swap.rs +++ b/src/commands/swap.rs @@ -1,11 +1,9 @@ -use crate::commands::palette::ColourMapping; use clap::Parser; -use image::{GenericImage, Pixel, Rgba}; -use num_traits::ToPrimitive; +use crunch_cli::actions::apply_colour_map; +use crunch_cli::utils::{OutputFormat, PaletteMap}; +use image::GenericImage; use serde::{Deserialize, Serialize}; -use crate::utils::{new_image, BasicRgba, OutputFormat}; - /// Use an existing colour mapping file to swap colours in the input sprite #[derive(Debug, Clone, Parser, Serialize, Deserialize)] #[clap(author, version = "0.9.0")] @@ -25,38 +23,8 @@ pub struct Swap { impl Swap { pub fn swap_pixels( image: impl GenericImage, - mappings: ColourMapping, + mappings: PaletteMap, ) -> anyhow::Result<OutputFormat> { - let mut output = new_image(image.width(), image.height()); - for (x, y, pixel) in image.pixels() { - let pixel = pixel.to_rgba(); - - if pixel.0[3].to_u8().unwrap() == 0 { - output.put_pixel(x, y, Rgba::from(BasicRgba::transparent())); - continue; - } - - let data = Rgba::from([ - pixel.0[0].to_u8().unwrap(), - pixel.0[1].to_u8().unwrap(), - pixel.0[2].to_u8().unwrap(), - pixel.0[3].to_u8().unwrap(), - ]); - let basic = BasicRgba::from(data); - - match mappings.get(&basic) { - Some(mapped_data) => output.put_pixel( - x, - y, - Rgba::from(BasicRgba { - a: basic.a, - ..*mapped_data - }), - ), - None => output.put_pixel(x, y, data), - }; - } - - Ok(output) + Ok(apply_colour_map(&mappings, &image)) } } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..282555a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2 @@ +pub mod actions; +pub mod utils; diff --git a/src/main.rs b/src/main.rs index 3338e4e..665d400 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,14 @@ +#[cfg(feature = "filesystem")] mod cli_args; + mod commands; -mod format; -mod utils; use clap::Parser; use crate::cli_args::Args; -use crate::format::load_image; +// This might look silly, but we want compilation of the CLI to fail if we disable filesystem support +#[cfg(feature = "filesystem")] fn main() -> anyhow::Result<(), anyhow::Error> { env_logger::Builder::from_env("LOG_LEVEL").init(); let args: Args = Args::parse(); diff --git a/src/utils.rs b/src/utils/colours.rs similarity index 95% rename from src/utils.rs rename to src/utils/colours.rs index eb21cbe..dbc2083 100644 --- a/src/utils.rs +++ b/src/utils/colours.rs @@ -33,9 +33,8 @@ pub type RgbaOutputFormat = TypedOutputFormat<RgbaImage>; pub fn new_image(new_width: u32, new_height: u32) -> OutputFormat { let mut new_image = RgbaImage::new(new_width, new_height); - let (new_image_x, new_image_y, new_image_width, new_image_height) = new_image.bounds(); - for x in new_image_x..new_image_width { - for y in new_image_y..new_image_height { + for x in 0..new_width { + for y in 0..new_height { new_image.put_pixel(x, y, Rgba::from([0, 0, 0, 0])); } } @@ -43,6 +42,10 @@ pub fn new_image(new_width: u32, new_height: u32) -> OutputFormat { } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +#[cfg_attr( + feature = "serialise_types", + derive(serde::Serialize, serde::Deserialize) +)] pub struct BasicRgba { pub r: u8, pub g: u8, @@ -86,6 +89,17 @@ impl BasicRgba { } } +impl From<[u8; 4]> for BasicRgba { + fn from(value: [u8; 4]) -> Self { + Self { + r: value[0], + g: value[1], + b: value[2], + a: value[3], + } + } +} + impl From<Rgba<u8>> for BasicRgba { fn from(other: Rgba<u8>) -> Self { Self { diff --git a/src/utils/files.rs b/src/utils/files.rs new file mode 100644 index 0000000..eaa98b8 --- /dev/null +++ b/src/utils/files.rs @@ -0,0 +1,37 @@ +use crate::utils::Format; +use image::io::Reader; +use image::{ImageError, RgbaImage}; +use std::path::Path; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ImageLoadingError { + #[error("Failed to load image data; {0}")] + IoError(#[from] std::io::Error), + #[error("Failed to process image; {0}")] + ImageError(#[from] ImageError), +} + +pub fn load_image( + path: impl ToString, + format: Option<Format>, +) -> anyhow::Result<RgbaImage, ImageLoadingError> { + let mut image = Reader::open(path.to_string())?; + let file = match format { + Some(format) => { + image.set_format(format.as_image_format()); + image.decode()?.into_rgba8() + } + None => image.with_guessed_format()?.decode()?.into_rgba8(), + }; + + Ok(file) +} + +pub fn make_paths<T: AsRef<Path>>(path: T) -> anyhow::Result<(), std::io::Error> { + if let Some(target) = path.as_ref().parent() { + std::fs::create_dir_all(target) + } else { + Ok(()) + } +} diff --git a/src/format.rs b/src/utils/formats.rs similarity index 53% rename from src/format.rs rename to src/utils/formats.rs index 9d35924..18be803 100644 --- a/src/format.rs +++ b/src/utils/formats.rs @@ -30,6 +30,30 @@ pub enum Format { Bmp, } +impl From<Format> for ImageFormat { + fn from(value: Format) -> Self { + value.as_image_format() + } +} + +impl TryFrom<ImageFormat> for Format { + type Error = anyhow::Error; + + fn try_from(value: ImageFormat) -> Result<Self, Self::Error> { + use Format::*; + match value { + ImageFormat::Png => Ok(Png), + ImageFormat::Jpeg => Ok(Jpg), + ImageFormat::Gif => Ok(Gif), + ImageFormat::Ico => Ok(Ico), + ImageFormat::Tga => Ok(Tga), + ImageFormat::Tiff => Ok(Tiff), + ImageFormat::Bmp => Ok(Bmp), + _ => Err(anyhow::anyhow!("Unsupported image format")), + } + } +} + impl Format { pub fn as_image_format(&self) -> ImageFormat { use Format::*; @@ -44,35 +68,3 @@ impl Format { } } } - -#[derive(Error, Debug)] -pub enum ImageLoadingError { - #[error("Failed to load image data; {0}")] - IoError(#[from] std::io::Error), - #[error("Failed to process image; {0}")] - ImageError(#[from] ImageError), -} - -pub fn load_image( - path: impl ToString, - format: Option<Format>, -) -> anyhow::Result<RgbaImage, ImageLoadingError> { - let mut image = Reader::open(path.to_string())?; - let file = match format { - Some(format) => { - image.set_format(format.as_image_format()); - image.decode()?.into_rgba8() - } - None => image.with_guessed_format()?.decode()?.into_rgba8(), - }; - - Ok(file) -} - -pub fn make_paths<T: AsRef<Path>>(path: T) -> anyhow::Result<(), std::io::Error> { - if let Some(target) = path.as_ref().parent() { - std::fs::create_dir_all(target) - } else { - Ok(()) - } -} diff --git a/src/utils/mapping.rs b/src/utils/mapping.rs new file mode 100644 index 0000000..46cdb59 --- /dev/null +++ b/src/utils/mapping.rs @@ -0,0 +1,155 @@ +use crate::utils::palette::ColourPalette; +use crate::utils::BasicRgba; +use anyhow::Error; +use deltae::{Delta, LabValue, DE2000}; +use image::{GenericImage, Pixel, Rgba}; +use num_traits::ToPrimitive; +use std::collections::HashMap; +use std::hash::RandomState; +use std::ops::{Deref, DerefMut}; + +#[derive(Clone, Debug)] +#[cfg_attr( + feature = "serialise_types", + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(feature = "serialise_types", serde(transparent))] +pub struct PaletteMap(HashMap<BasicRgba, BasicRgba>); +impl Deref for PaletteMap { + type Target = HashMap<BasicRgba, BasicRgba>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for PaletteMap { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} +impl From<HashMap<BasicRgba, BasicRgba>> for PaletteMap { + fn from(other: HashMap<BasicRgba, BasicRgba>) -> Self { + PaletteMap(other) + } +} +impl FromIterator<(BasicRgba, BasicRgba)> for PaletteMap { + fn from_iter<T: IntoIterator<Item = (BasicRgba, BasicRgba)>>(iter: T) -> Self { + <PaletteMap as From<HashMap<BasicRgba, BasicRgba>>>::from(iter.into_iter().collect()) + } +} + +impl PaletteMap { + /// Create a new mapping between colours with no entries, and no space allocated. + pub fn empty() -> Self { + PaletteMap(HashMap::new()) + } + + /// Create a new, empty mapping between colours with a given amount of preallocated space. + pub fn with_capacity(capacity: usize) -> Self { + PaletteMap(HashMap::with_capacity(capacity)) + } + + pub fn from_column_image(image: &impl GenericImage) -> anyhow::Result<PaletteMap> { + if image.width() != 2 { + return Err(Error::msg("Image must have a pixel width of 2"))?; + } + + let mut mapping = PaletteMap::with_capacity(image.height() as usize); + for y in 0..image.height() { + let left = image.get_pixel(0, y).to_rgba(); + let right = image.get_pixel(1, y).to_rgba(); + + let left_pixel = BasicRgba::from([ + left[0].to_u8().unwrap(), + left[1].to_u8().unwrap(), + left[2].to_u8().unwrap(), + left[3].to_u8().unwrap(), + ]); + let right_pixel = BasicRgba::from([ + right[0].to_u8().unwrap(), + right[1].to_u8().unwrap(), + right[2].to_u8().unwrap(), + right[3].to_u8().unwrap(), + ]); + mapping.insert(left_pixel, right_pixel); + } + + Ok(mapping) + } + + /// Safe method for creating a palette map from two colour palettes. The colour palettes will be truncated to the length of the smaller palette, + /// possibly losing some colour information + pub fn from_palettes_lossy(from: &ColourPalette, to: &ColourPalette) -> PaletteMap { + let smallest = from.len().min(to.len()); + let iter = from + .iter() + .take(smallest) + .copied() + .zip(to.iter().take(smallest).copied()); + PaletteMap::from_iter(iter) + } + + pub fn calculate_mapping(from: &ColourPalette, to: &ColourPalette) -> PaletteMap { + let colour_labs = Vec::from_iter(to.iter().map(LabValue::from)); + + let to_palette_vectors: HashMap<usize, &BasicRgba> = + HashMap::from_iter(to.iter().enumerate()); + let mut out_map = PaletteMap::with_capacity(from.len()); + + for colour in from.iter() { + // Iterate all the possible output colours to find the "closest" match. Closeness is determined by the smallest Delta E2000 distance. + let closest = to_palette_vectors + .keys() + .fold(None, |lowest, idx| match lowest { + Some(num) => { + let current = colour_labs[*idx]; + let previous: LabValue = colour_labs[num]; + + if colour.delta(current, DE2000) < colour.delta(previous, DE2000) { + Some(*idx) + } else { + Some(num) + } + } + None => Some(*idx), + }); + + // We perform a check just in case the output palette is empty. As long as there is at least + // one colour in the output palette, 'closest' will be `Some`, so we output a transparent pixel + // for the `None` edge case + match closest { + Some(idx) => match to_palette_vectors.get(&idx) { + Some(col) => { + out_map.insert(*colour, **col); + } + None => { + log::warn!("No matching set for {} with col {:?}", idx, &colour); + + out_map.insert( + *colour, + BasicRgba { + r: 0, + g: 0, + b: 0, + a: 0, + }, + ); + } + }, + None => { + log::warn!("No closest for {:?}", &colour); + out_map.insert( + *colour, + BasicRgba { + r: 0, + g: 0, + b: 0, + a: 0, + }, + ); + } + } + } + + out_map + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..f458be2 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,13 @@ +mod colours; +#[cfg(feature = "filesystem")] +mod files; +mod formats; +mod mapping; +mod palette; + +pub use colours::*; +#[cfg(feature = "filesystem")] +pub use files::*; +pub use formats::*; +pub use mapping::*; +pub use palette::*; diff --git a/src/utils/palette.rs b/src/utils/palette.rs new file mode 100644 index 0000000..846a7b6 --- /dev/null +++ b/src/utils/palette.rs @@ -0,0 +1,89 @@ +use crate::utils::BasicRgba; +use image::{GenericImage, Pixel, Rgba}; +use num_traits::ToPrimitive; +use std::cmp::Ordering; +use std::collections::HashSet; +use std::ops::{Deref, DerefMut, Index}; + +#[derive(Clone, Debug)] +#[cfg_attr( + feature = "serialise_types", + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(feature = "serialise_types", serde(transparent))] +pub struct ColourPalette(Vec<BasicRgba>); +impl Deref for ColourPalette { + type Target = Vec<BasicRgba>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for ColourPalette { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} +impl From<Vec<BasicRgba>> for ColourPalette { + fn from(other: Vec<BasicRgba>) -> Self { + ColourPalette(other) + } +} +impl FromIterator<BasicRgba> for ColourPalette { + fn from_iter<T: IntoIterator<Item = BasicRgba>>(iter: T) -> Self { + <ColourPalette as From<Vec<BasicRgba>>>::from(iter.into_iter().collect()) + } +} + +impl ColourPalette { + /// Create a new colour palette with no entries, and no space allocated. + pub fn empty() -> Self { + ColourPalette(Vec::new()) + } + + /// Create a new, empty colour palette with a given amount of preallocated space. + pub fn with_capacity(capacity: usize) -> Self { + ColourPalette(Vec::with_capacity(capacity)) + } + + /// Mutate the palette in place, sorting entries by hue. Hue is sorted in ascending order by degree + pub fn sort_by_hue(&mut self) { + self.sort_by(|pa, pb| { + let hue_a = pa.hue(); + let hue_b = pb.hue(); + + if hue_a.is_nan() && hue_b.is_nan() { + Ordering::Equal + } else if hue_a.is_nan() { + Ordering::Less + } else if hue_b.is_nan() || hue_a > hue_b { + Ordering::Greater + } else if hue_b > hue_a { + Ordering::Less + } else { + Ordering::Equal + } + }); + } +} + +impl<I: GenericImage> From<&I> for ColourPalette { + fn from(image: &I) -> Self { + let mut colours = HashSet::new(); + for (_, _, pixel) in image.pixels() { + let pixel = pixel.to_rgba(); + let parts = ( + pixel[0].to_u8(), + pixel[1].to_u8(), + pixel[2].to_u8(), + pixel[3].to_u8(), + ); + match parts { + (Some(r), Some(g), Some(b), Some(a)) => { + colours.insert(BasicRgba::from([r, g, b, a])); + } + _ => continue, // Ignore pixels with missing or invalid components + } + } + ColourPalette::from_iter(colours.into_iter()) + } +} -- GitLab