diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c10a125360107b464dcf1743b216a387e4536f4..348a34dba88bbfffeaf72d9fc4d0d0d2cdcc856b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Changed +- Moved all arguments into subcommands, for a less confusing end user experience + ## [0.4.0] - 2022-08-08 ### Added - Support for GlobRef definitions in a `pipeline.toml` file diff --git a/Cargo.lock b/Cargo.lock index 9b2200f444326575a8df0ede0fac5239ef54094f..9285e542d3c79017d981b10a52dd3d6bb9f4e081 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,20 +25,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.58" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] +checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" [[package]] name = "autocfg" @@ -76,6 +65,12 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + [[package]] name = "cfg-if" version = "1.0.0" @@ -84,26 +79,24 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.2.8" +version = "4.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83" +checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76" dependencies = [ - "atty", "bitflags", "clap_derive", "clap_lex", - "indexmap", + "is-terminal", "once_cell", "strsim", "termcolor", - "textwrap", ] [[package]] name = "clap_derive" -version = "3.2.7" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902" +checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" dependencies = [ "heck", "proc-macro-error", @@ -114,9 +107,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade" dependencies = [ "os_str_bytes", ] @@ -138,9 +131,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" dependencies = [ "cfg-if", "crossbeam-utils", @@ -148,9 +141,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -159,26 +152,24 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.9" +version = "0.9.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset", - "once_cell", "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.10" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -219,23 +210,44 @@ checksum = "e412cd91a4ec62fcc739ea50c40babe21e3de60d69f36393cce377c7c04ead5a" [[package]] name = "either" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "env_logger" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" dependencies = [ - "atty", "humantime", + "is-terminal", "log", "regex", "termcolor", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "exr" version = "1.4.2" @@ -312,15 +324,15 @@ dependencies = [ [[package]] name = "glam" -version = "0.20.5" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43e957e744be03f5801a55472f593d43fabdebf25a4585db250f04d86b1675f" +checksum = "12f597d56c1bd55a811a1be189459e8fad2bbc272616375602443bdfb37fa774" [[package]] name = "glob" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "half" @@ -342,13 +354,19 @@ checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "856b5cb0902c2b6d65d5fd97dfa30f9b70c7538e770b98eab5ed52d8db923e01" + [[package]] name = "humantime" version = "2.1.0" @@ -394,11 +412,33 @@ dependencies = [ "adler32", ] +[[package]] +name = "io-lifetimes" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef" +dependencies = [ + "hermit-abi 0.3.0", + "io-lifetimes", + "rustix", + "windows-sys", +] + [[package]] name = "itoa" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" [[package]] name = "jpeg-decoder" @@ -438,9 +478,15 @@ checksum = "7efd1d698db0759e6ef11a7cd44407407399a910c774dd804c64c032da7826ff" [[package]] name = "libc" -version = "0.2.126" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] name = "lock_api" @@ -469,9 +515,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" -version = "0.6.5" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] @@ -494,6 +540,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "nom8" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" +dependencies = [ + "memchr", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -537,19 +592,19 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi", + "hermit-abi 0.2.6", "libc", ] [[package]] name = "once_cell" -version = "1.13.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "os_str_bytes" @@ -615,39 +670,37 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.40" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] [[package]] name = "rayon" -version = "1.5.3" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" dependencies = [ - "autocfg", - "crossbeam-deque", "either", "rayon-core", ] [[package]] name = "rayon-core" -version = "1.9.3" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -672,11 +725,25 @@ version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +[[package]] +name = "rustix" +version = "0.36.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "ryu" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" [[package]] name = "scoped_threadpool" @@ -692,18 +759,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.138" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.138" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", @@ -712,15 +779,24 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.82" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +checksum = "7434af0dc1cbd59268aa98b4c22c131c0584d2232f6fb166efb993e2832e896a" dependencies = [ "itoa", "ryu", "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +dependencies = [ + "serde", +] + [[package]] name = "smallvec" version = "1.9.0" @@ -744,9 +820,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.98" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", "quote", @@ -762,26 +838,20 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" - [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", @@ -810,18 +880,43 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.9" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "772c1426ab886e7362aedf4abc9c0d1348a979517efedfc25862944d10137af0" dependencies = [ "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f35c303ea3e062b6131be4de16debe23860b9d3f2396658f13b7af1987fb473" +dependencies = [ + "indexmap", + "nom8", + "serde", + "serde_spanned", + "toml_datetime", ] [[package]] name = "unicode-ident" -version = "1.0.1" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] name = "version_check" @@ -925,3 +1020,69 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" diff --git a/Cargo.toml b/Cargo.toml index a9b05e19cb0a761cc14a08dcbaab759dd42cc72d..c03ac1044846e142e9257d156cfd45e6ec2dea07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,19 +3,28 @@ name = "crunch" version = "0.4.0" edition = "2021" +license = "GPL-3" +description = "Command line asset manipulation, set up a pipeline once and run it against all of your files" +authors = [ + "Louis Capitanchik <louis@microhacks.co.uk>" +] + [dependencies] -anyhow = "1.0.53" -clap = { version = "3.0.14", features = ["derive"] } +clap = { version = "4.1.4", features = ["derive"] } +env_logger = "0.10.0" +log = "0.4.17" + +anyhow = "1.0.69" +thiserror = "1.0.38" + deltae = "0.3.0" -env_logger = "0.9.0" -glam = "0.20.5" +glam = "0.22.0" image = "0.24" lab = "0.11.0" -log = "0.4.14" -num-traits = "0.2.14" -rayon = "1.5.1" -thiserror = "1.0.30" -serde = { version = "1.0.131", features = ["derive"] } -toml = "0.5.9" -serde_json = "1.0.81" -glob = "0.3.0" + +num-traits = "0.2.15" +rayon = "1.6.1" +toml = "0.7.1" +serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0.92" +glob = "0.3.1" diff --git a/src/cli_args.rs b/src/cli_args.rs index f178bbbfd11564a5fc6ae8d5772dc7398d5f1cef..ddc5234a332d068443d38bb0dbe1fe0c0d00377a 100644 --- a/src/cli_args.rs +++ b/src/cli_args.rs @@ -1,165 +1,91 @@ use clap::{Parser, Subcommand}; use serde::{Deserialize, Serialize}; -use crate::commands::{ - calculate_mapping, execute_pipeline, extrude, flip, palette, remap_image, rescale, rotate, - write_palette, FlipDirection, RotateDegree, -}; +// use crate::commands::{calculate_mapping, execute_pipeline, extrude, flip, palette, remap_image, rescale, rotate, write_palette, FlipDirection, RotateDegree, Rotate}; +use crate::commands::{Extrude, Flip, Palette, Pipeline, Remap, Rotate, Scale}; use crate::format::PaletteFormat; use crate::{load_image, Format}; -/// Image utilities for Advent -#[derive(Parser, Debug, Clone)] +/// Crunch is a set of utilities for quickly and easily processing a batch of files, either directly +/// or by defining pipelines +#[derive(Parser, Debug, Clone, Serialize, Deserialize)] #[clap(name = "Crunch")] #[clap(author = "Louis Capitanchik <louis@microhacks.co.uk>")] -#[clap(version = "0.4.0")] +#[clap(version = "0.5.0-beta.1")] #[clap(about, long_about = None)] -pub struct Args { - /// The path to the spritesheet file - in_path: String, - /// The path to the output file - out_path: String, - - /// Force Crunch to read the input file as a specific format - #[clap(short, long, arg_enum)] - format: Option<Format>, - - #[clap(subcommand)] - command: CrunchCommand, -} - -#[inline] -pub fn u32_zero() -> u32 { - 0 -} -#[inline] -pub fn u32_32() -> u32 { - 32 -} -#[inline] -pub fn f32_one() -> f32 { - 1.0 -} - -#[derive(Debug, Clone, Subcommand, Serialize, Deserialize)] #[serde(tag = "command", content = "params")] -pub enum CrunchCommand { - /// Take each tile in an image and expand its borders by a given amount - #[clap(version = "0.3.0")] - Extrude { - /// The amount of horizontal padding to add between each sprite in the image - #[clap(long, default_value_t = 0)] - #[serde(default = "u32_zero")] - space_x: u32, - /// The amount of vertical padding to add between each sprite in the image - #[clap(long, default_value_t = 0)] - #[serde(default = "u32_zero")] - space_y: u32, - /// The amount of horizontal padding to add between each sprite in the image - #[clap(long, default_value_t = 0)] - #[serde(default = "u32_zero")] - pad_x: u32, - /// The amount of vertical padding to add between each sprite in the image - #[clap(long, default_value_t = 0)] - #[serde(default = "u32_zero")] - pad_y: u32, - /// The size of each tile in the spritesheet. Assumed to be square tiles - #[clap(short, long, default_value_t = 32)] - #[serde(default = "u32_32")] - tile_size: u32, - /// Use nearby pixels for padding - #[clap(short, long)] - #[serde(default)] - extrude: bool, - }, - /// Create a palette file containing every distinct colour from the input image - #[clap(version = "0.3.0")] - Palette { - #[clap(short, long, arg_enum, default_value_t)] - #[serde(default)] - format: PaletteFormat, - }, - /// Convert the colour space of an input image to the given palette - #[clap(version = "0.3.0")] - Remap { - /// The path to the palette file containing the output colours - palette_file: String, - }, - /// Make an image larger or smaller - #[clap(version = "0.3.0")] - Scale { - /// The scale factor to use; numbers between 0-1 shrink the image; numbers > 1 enlarge - #[clap(long, default_value_t = 1.0)] - #[serde(default = "f32_one")] - factor: f32, - }, - /// Apply a clockwise rotation to the image - #[clap(version = "0.3.0")] - Rotate { - /// How many 90 degree steps should this image be rotated by - #[clap(long, arg_enum)] - amount: RotateDegree, - }, - /// Flip an image along one or both axis - #[clap(version = "0.3.0")] - Flip { - /// The axis along which the image should be flipped - #[clap(long, arg_enum)] - direction: FlipDirection, - }, - /// Execute a pipeline file to run multiple commands on one or more images - #[clap(version = "0.3.0")] - Pipeline, +pub enum Args { + #[clap(name = "rotate")] + #[serde(alias = "rotate")] + Rotate(Rotate), + #[clap(name = "extrude")] + #[serde(alias = "extrude")] + Extrude(Extrude), + #[clap(name = "palette")] + #[serde(alias = "palette")] + Palette(Palette), + #[clap(name = "scale")] + #[serde(alias = "scale")] + Scale(Scale), + #[clap(name = "flip")] + #[serde(alias = "flip")] + Flip(Flip), + #[clap(name = "remap")] + #[serde(alias = "remap")] + Remap(Remap), + #[clap(name = "remap")] + #[serde(alias = "remap")] + Pipeline(Pipeline), } impl Args { pub fn run(&self) -> anyhow::Result<()> { - match &self.command { - CrunchCommand::Extrude { - extrude: ext, - pad_x, - pad_y, - space_x, - space_y, - tile_size, - } => { - let image = load_image(&self.in_path, self.format)?; - let output = extrude(image, *tile_size, *pad_x, *pad_y, *space_x, *space_y, *ext)?; - output.save(&self.out_path).map_err(anyhow::Error::from) + match &self { + Args::Rotate(rotate) => { + let image = load_image(&rotate.input, None)?; + let output = rotate.run(&image)?; + output + .save(rotate.output.as_str()) + .map_err(anyhow::Error::from) } - CrunchCommand::Palette { format } => { - let image = load_image(&self.in_path, self.format)?; - let output = palette(&image)?; - write_palette(output, *format, &self.out_path) + Args::Extrude(extrude) => { + let image = load_image(&extrude.input, None)?; + let output = extrude.run(&image)?; + output + .save(extrude.output.as_str()) + .map_err(anyhow::Error::from) } - CrunchCommand::Remap { palette_file } => { - let image_data = load_image(&self.in_path, self.format)?; - let palette_data = load_image(&palette_file, self.format)?; - - let image_palette = palette(&image_data)?; - let target_palette = palette(&palette_data)?; - - let mappings = calculate_mapping(&image_palette, &target_palette); - let output = remap_image(image_data, mappings)?; - - output.save(&self.out_path).map_err(anyhow::Error::from) + Args::Palette(palette) => { + let image = load_image(&palette.input, None)?; + palette.run(&image) } - CrunchCommand::Scale { factor } => { - let image = load_image(self.in_path.clone(), self.format)?; - let output = rescale(&image, *factor)?; - output.save(&self.out_path).map_err(anyhow::Error::from) + Args::Scale(scale) => { + let image = load_image(&scale.input, None)?; + let output = scale.run(&image)?; + output + .save(scale.output.as_str()) + .map_err(anyhow::Error::from) } - CrunchCommand::Rotate { amount } => { - let image = load_image(self.in_path.clone(), self.format)?; - let output = rotate(&image, *amount)?; - output.save(&self.out_path).map_err(anyhow::Error::from) + Args::Flip(flip) => { + let image = load_image(&flip.input, None)?; + let output = flip.run(&image)?; + output + .save(flip.output.as_str()) + .map_err(anyhow::Error::from) } - CrunchCommand::Flip { direction } => { - let image = load_image(self.in_path.clone(), self.format)?; - let output = flip(&image, *direction)?; - output.save(&self.out_path).map_err(anyhow::Error::from) + Args::Remap(remap) => { + 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 mappings = Palette::calculate_mapping(&image_palette, &target_palette); + let output = Remap::remap_image(image_data, mappings)?; + + output.save(&remap.output).map_err(anyhow::Error::from) } - CrunchCommand::Pipeline => execute_pipeline(&self.in_path, &self.out_path), + Args::Pipeline(pipeline) => pipeline.execute(), } } } diff --git a/src/commands/extrude.rs b/src/commands/extrude.rs index 168e5057e48d0883a414787ee657576d7f2343de..4aa0a4776fcb5df3b82e5d005f50f7e9d77461c0 100644 --- a/src/commands/extrude.rs +++ b/src/commands/extrude.rs @@ -1,84 +1,122 @@ +use crate::utils::{OutputFormat, RgbaOutputFormat, SpriteData}; +use clap::Parser; use image::{GenericImage, GenericImageView, Pixel, Rgba, RgbaImage}; use num_traits::cast::ToPrimitive; +use serde::{Deserialize, Serialize}; -use crate::{OutputFormat, SpriteData}; +#[inline(always)] +fn tile_size() -> u32 { + 32 +} -pub fn extrude( - image: impl GenericImage, - tile_size: u32, - pad_x: u32, - pad_y: u32, +/// Take each tile in an image and expand its borders by a given amount. Optionally fill with +/// nearby pixels instead of empty space +#[derive(Parser, Serialize, Deserialize, Clone, Debug)] +#[clap(author, version = "0.3.0")] +pub struct Extrude { + /// The path to the spritesheet file + #[serde(default)] + pub input: String, + /// The path to write the extruded spritesheet + #[serde(default)] + pub output: String, + + /// The amount of horizontal padding to add between each sprite in the image + #[clap(long, default_value_t = 0)] + #[serde(default)] space_x: u32, + /// The amount of vertical padding to add between each sprite in the image + #[clap(long, default_value_t = 0)] + #[serde(default)] space_y: u32, - _extrude: bool, -) -> anyhow::Result<OutputFormat> { - log::info!( - "Image loaded with size {} x {}", - image.width(), - image.height() - ); + /// The amount of horizontal padding to add between each sprite in the image + #[clap(long, default_value_t = 0)] + #[serde(default)] + pad_x: u32, + /// The amount of vertical padding to add between each sprite in the image + #[clap(long, default_value_t = 0)] + #[serde(default)] + pad_y: u32, + /// The size of each tile in the spritesheet. Assumed to be square tiles + #[clap(short, long, default_value_t = 32)] + #[serde(default = "tile_size")] + tile_size: u32, + /// Use nearby pixels for padding + #[clap(short, long)] + #[serde(default)] + extrude: bool, +} + +impl Extrude { + pub fn run(&self, image: &impl GenericImage) -> anyhow::Result<RgbaOutputFormat> { + log::info!( + "Image loaded with size {} x {}", + image.width(), + image.height() + ); - let columns = image.width() / tile_size; - let rows = image.height() / tile_size; - log::info!("Inferred sheet contains {} columns", columns); - log::info!("Inferred sheet contains {} rows", rows); + let columns = image.width() / self.tile_size; + let rows = image.height() / self.tile_size; + log::info!("Inferred sheet contains {} columns", columns); + log::info!("Inferred sheet contains {} rows", rows); - let mut views = Vec::with_capacity((columns * rows) as usize); - for x in 0..columns { - for y in 0..rows { - let img_x = x * tile_size; - let img_y = y * tile_size; + let mut views = Vec::with_capacity((columns * rows) as usize); + for x in 0..columns { + for y in 0..rows { + let img_x = x * self.tile_size; + let img_y = y * self.tile_size; - let payload = SpriteData { - data: image.view(img_x, img_y, tile_size, tile_size), - x: img_x, - y: img_y, - tx: x, - ty: y, - width: tile_size, - height: tile_size, - }; + let payload = SpriteData { + data: image.view(img_x, img_y, self.tile_size, self.tile_size), + x: img_x, + y: img_y, + tx: x, + ty: y, + width: self.tile_size, + height: self.tile_size, + }; - views.push(payload); + views.push(payload); + } } - } - let new_width = (pad_x * 2 + space_x * columns) + image.width(); - let new_height = (pad_y * 2 + space_y * rows) + image.height(); + let new_width = (self.pad_x * 2 + self.space_x * columns) + image.width(); + let new_height = (self.pad_y * 2 + self.space_y * rows) + image.height(); - log::info!( - "Using new image width {} / height {}", - new_width, - new_height - ); - 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 { - new_image.put_pixel(x, y, Rgba::from([0, 0, 0, 0])); + log::info!( + "Using new image width {} / height {}", + new_width, + new_height + ); + 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 { + new_image.put_pixel(x, y, Rgba::from([0, 0, 0, 0])); + } } - } - for sprite in views.iter() { - let (img_x, img_y, width, height) = sprite.data.bounds(); - for x in 0..width { - for y in 0..height { - let pix = sprite.data.get_pixel(x, y).to_rgba(); - let p = Rgba::from([ - pix.0[0].to_u8().unwrap(), - pix.0[1].to_u8().unwrap(), - pix.0[2].to_u8().unwrap(), - pix.0[3].to_u8().unwrap(), - ]); + for sprite in views.iter() { + let (img_x, img_y, width, height) = sprite.data.bounds(); + for x in 0..width { + for y in 0..height { + let pix = sprite.data.get_pixel(x, y).to_rgba(); + let p = Rgba::from([ + pix.0[0].to_u8().unwrap(), + pix.0[1].to_u8().unwrap(), + pix.0[2].to_u8().unwrap(), + pix.0[3].to_u8().unwrap(), + ]); - new_image.put_pixel( - pad_x + (sprite.tx * space_x) + img_x + x, - pad_y + (sprite.ty * space_y) + img_y + y, - p, - ); + new_image.put_pixel( + self.pad_x + (sprite.tx * self.space_x) + img_x + x, + self.pad_y + (sprite.ty * self.space_y) + img_y + y, + p, + ); + } } } - } - Ok(new_image) + Ok(new_image) + } } diff --git a/src/commands/flip.rs b/src/commands/flip.rs index 6343cca356401ff2c24d12b28a307078dc196a0d..787c1d72378fe5e93ccd39d22ef13e5c1c12087f 100644 --- a/src/commands/flip.rs +++ b/src/commands/flip.rs @@ -1,10 +1,9 @@ -use clap::ArgEnum; +use crate::utils::TypedOutputFormat; +use clap::{Parser, ValueEnum}; use image::{imageops, GenericImage, Pixel}; use serde::{Deserialize, Serialize}; -use crate::TypedOutputFormat; - -#[derive(Copy, Clone, Serialize, Deserialize, ArgEnum, Debug)] +#[derive(Copy, Clone, Serialize, Deserialize, ValueEnum, Debug)] pub enum FlipDirection { #[serde(rename = "vertical")] Vertical, @@ -14,19 +13,37 @@ pub enum FlipDirection { Both, } -pub fn flip<T: GenericImage>(image: &T, dir: FlipDirection) -> anyhow::Result<TypedOutputFormat<T>> -where - T::Pixel: 'static, - <T::Pixel as Pixel>::Subpixel: 'static, -{ - use FlipDirection::*; - match dir { - Vertical => Ok(imageops::flip_vertical(image)), - Horizontal => Ok(imageops::flip_horizontal(image)), - Both => { - let mut image = imageops::flip_horizontal(image); - imageops::flip_vertical_in_place(&mut image); - Ok(image) +/// Rotate an image clockwise by the given degree +#[derive(Debug, Clone, Parser, Serialize, Deserialize)] +#[clap(author, version = "0.3.0")] +pub struct Flip { + /// The path to the spritesheet file + #[serde(default)] + pub input: String, + /// The path to write the extruded spritesheet + #[serde(default)] + pub output: String, + + /// The axis along which the image should be flipped + #[clap(long, value_enum)] + direction: FlipDirection, +} + +impl Flip { + pub fn run<T: GenericImage>(&self, image: &T) -> anyhow::Result<TypedOutputFormat<T>> + where + T::Pixel: 'static, + <T::Pixel as Pixel>::Subpixel: 'static, + { + use FlipDirection::*; + match self.direction { + Vertical => Ok(imageops::flip_vertical(image)), + Horizontal => Ok(imageops::flip_horizontal(image)), + Both => { + let mut image = imageops::flip_horizontal(image); + imageops::flip_vertical_in_place(&mut image); + Ok(image) + } } } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index be37ccd3cea104aa778091a5b261140f0f252dda..f54759816d033750adb8144beb52bbb147f72e7c 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,10 +6,10 @@ mod remap; mod rotate; mod scale; -pub use extrude::extrude; -pub use flip::{flip, FlipDirection}; -pub use palette::{calculate_mapping, palette, write_palette, ColourMapping}; -pub use pipeline::execute_pipeline; -pub use remap::remap_image; -pub use rotate::{rotate, RotateDegree}; -pub use scale::rescale; +pub use extrude::Extrude; +pub use flip::Flip; +pub use palette::Palette; +pub use pipeline::Pipeline; +pub use remap::Remap; +pub use rotate::Rotate; +pub use scale::Scale; diff --git a/src/commands/palette.rs b/src/commands/palette.rs index 201f0cf307984a8482e6bb124d2c574b69402104..0c489a8eb710d2ba516032f5665beab7510d5025 100644 --- a/src/commands/palette.rs +++ b/src/commands/palette.rs @@ -1,3 +1,4 @@ +use clap::Parser; use std::cmp::{min, Ordering}; use std::collections::hash_map::RandomState; use std::collections::{HashMap, HashSet}; @@ -8,63 +9,16 @@ use std::path::Path; use deltae::{Delta, LabValue, DE2000}; use image::{GenericImage, Pixel, Rgba}; use num_traits::ToPrimitive; +use serde::{Deserialize, Serialize}; use crate::format::PaletteFormat; use crate::utils::{new_image, BasicRgba}; -pub type Palette = Vec<BasicRgba>; - -pub fn palette(image: &impl GenericImage) -> anyhow::Result<Palette> { - 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()) -} - -struct HexStringValue(String); -trait HexString { - fn as_hex_string(&self) -> HexStringValue; -} -impl HexString for Rgba<u8> { - fn as_hex_string(&self) -> HexStringValue { - HexStringValue(format!( - "{:02X}{:02X}{:02X}{:02X}", - self.0[0], self.0[1], self.0[2], self.0[3] - )) - } -} -impl<T: HexString + Clone> HexString for &T { - fn as_hex_string(&self) -> HexStringValue { - (*self).clone().as_hex_string() - } -} -impl UpperHex for HexStringValue { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.0.to_uppercase()) - } -} -impl LowerHex for HexStringValue { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.0.to_lowercase()) - } -} +pub type PixelPalette = Vec<BasicRgba>; +pub type ColourMapping = HashMap<BasicRgba, BasicRgba>; -pub fn write_palette<T: AsRef<Path>>( - colours: Palette, - format: PaletteFormat, - outpath: T, -) -> anyhow::Result<()> { - #[allow(clippy::redundant_clone)] - let mut sorted = colours.clone(); - sorted.sort_by(|pa, pb| { +pub fn sort_by_hue(palette: &mut PixelPalette) { + palette.sort_by(|pa, pb| { let hue_a = pa.hue(); let hue_b = pb.hue(); @@ -78,74 +32,128 @@ pub fn write_palette<T: AsRef<Path>>( Ordering::Equal } }); +} - match format { - PaletteFormat::Png => { - let num_colours = sorted.len(); - let image_width = min(16, num_colours); - let image_height = if num_colours % 16 > 0 { - num_colours / image_width + 1 - } else { - num_colours / image_width - }; - - let mut out_image = new_image(image_width as u32, image_height as u32); - for (idx, colour) in sorted.iter().enumerate() { - out_image.put_pixel( - (idx % image_width) as u32, - (idx / image_width) as u32, - Rgba::from(colour), - ); - } +/// Create a palette file containing every distinct colour from the input image +#[derive(Parser, Clone, Serialize, Deserialize, Debug)] +#[clap(author, version = "0.3.0")] +pub struct Palette { + /// The path to the spritesheet file + #[serde(default)] + pub input: String, + /// The path to write the extruded spritesheet + #[serde(default)] + pub output: String, + + /// The format for the palette file. PNG will output an image with colours, while TXT will + /// output a text file with one hex value per line + #[clap(short, long, value_enum, default_value_t)] + #[serde(default)] + format: PaletteFormat, +} - out_image.save(outpath)?; - } - PaletteFormat::Txt => { - let mut file = std::fs::File::create(outpath)?; - for colour in sorted.iter() { - let line = format!("#{:X}\n", colour); - file.write_all(line.as_bytes())?; - } - } +impl Palette { + pub fn run(&self, image: &impl GenericImage) -> anyhow::Result<()> { + let palette = Palette::extract_from(image)?; + self.write_palette(palette) } - Ok(()) -} + 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(), + ])); + } -pub type ColourMapping = HashMap<BasicRgba, BasicRgba>; + Ok(colours.iter().map(BasicRgba::from).collect()) + } -pub fn calculate_mapping(from: &Palette, to: &Palette) -> 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) - } + pub fn write_palette(&self, mut colours: PixelPalette) -> anyhow::Result<()> { + sort_by_hue(&mut colours); + + match self.format { + PaletteFormat::Png => { + let num_colours = colours.len(); + let image_width = min(16, num_colours); + let image_height = if num_colours % 16 > 0 { + num_colours / image_width + 1 + } else { + num_colours / image_width + }; + + let mut out_image = new_image(image_width as u32, image_height as u32); + for (idx, colour) in colours.iter().enumerate() { + out_image.put_pixel( + (idx % image_width) as u32, + (idx / image_width) as u32, + Rgba::from(colour), + ); } - None => Some(*idx), - }); - match closest { - Some(idx) => match to_palette_vectors.get(&idx) { - Some(col) => { - out_map.insert(*colour, **col); + out_image.save(self.output.as_str())?; + } + PaletteFormat::Txt => { + let mut file = std::fs::File::create(self.output.as_str())?; + for colour in colours.iter() { + let line = format!("#{:X}\n", colour); + file.write_all(line.as_bytes())?; } - None => { - println!("No matching vec for {} with col {:?}", idx, &colour); + } + } + + Ok(()) + } + + 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 { @@ -156,21 +164,9 @@ pub fn calculate_mapping(from: &Palette, to: &Palette) -> ColourMapping { }, ); } - }, - None => { - println!("No closest for {:?}", &colour); - out_map.insert( - *colour, - BasicRgba { - r: 0, - g: 0, - b: 0, - a: 0, - }, - ); } } - } - out_map + out_map + } } diff --git a/src/commands/pipeline.rs b/src/commands/pipeline.rs index bf86883d9e24491bb658efc41a1c4923712ff274..724e336653ddfa1cbf0065978fee300dc7926729 100644 --- a/src/commands/pipeline.rs +++ b/src/commands/pipeline.rs @@ -1,3 +1,4 @@ +use clap::Parser; use std::collections::HashMap; use std::path::{Path, PathBuf}; @@ -5,10 +6,11 @@ use rayon::prelude::*; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::cli_args::CrunchCommand; +use crate::cli_args::Args; +use crate::commands::{Palette, Remap}; use crate::format::make_paths; +use crate::load_image; use crate::utils::normalize_path; -use crate::{commands, load_image}; #[derive(Error, Debug)] pub enum PipelineError { @@ -22,7 +24,7 @@ pub enum PipelineType { Pipeline { input_path: String, output_path: String, - actions: Vec<CrunchCommand>, + actions: Vec<Args>, }, Ref { input_path: String, @@ -38,7 +40,7 @@ pub enum PipelineType { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PipelineRef { - pub actions: Vec<CrunchCommand>, + pub actions: Vec<Args>, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -47,215 +49,132 @@ pub struct PipelineFile { pub pipelines: Vec<PipelineType>, } -pub fn execute_pipeline<IN: ToString, OUT: ToString>( - file_path: IN, - _outpath: OUT, -) -> anyhow::Result<()> { - let path = std::env::current_dir().map(|path| path.join(file_path.to_string()))?; - let path_string = format!("{}", &path.display()); +/// Rotate an image clockwise by the given degree +#[derive(Debug, Clone, Parser, Serialize, Deserialize)] +#[clap(author, version = "0.3.0")] +pub struct Pipeline { + /// The path to the pipeline definition file + #[serde(default)] + pub config: String, +} - log::debug!("Trying to read from input file: {}", &path.display()); +macro_rules! result { + ($value: expr) => { + match $value { + Ok(val) => val, + Err(e) => { + log::error!("{}", e); + return; + } + } + }; +} - if !&path_string.ends_with(".toml") && !&path_string.ends_with(".json") { - Err(PipelineError::FormatDetection)?; - } +impl Pipeline { + pub fn execute(&self) -> anyhow::Result<()> { + let path = std::env::current_dir().map(|path| path.join(&self.config))?; + let path_string = format!("{}", &path.display()); - let file_contents = std::fs::read(&path)?; + log::debug!("Trying to read from input file: {}", &path.display()); - log::debug!("Found correct file type and read bytes, trying to parse"); - let pipeline_data: PipelineFile = if path_string.ends_with(".toml") { - toml::from_slice(&file_contents)? - } else { - serde_json::from_slice(&file_contents)? - }; + if !&path_string.ends_with(".toml") && !&path_string.ends_with(".json") { + Err(PipelineError::FormatDetection)?; + } - log::debug!("Expanding pipeline file into targets"); - let base_path = PathBuf::from(path.parent().unwrap()); - get_targets(base_path.clone(), &pipeline_data).for_each( - |(input_path, output_path, actions)| { - match make_paths(&output_path) { - Ok(_) => {} - Err(e) => { - log::error!("Failed to create target directory {}; {}", &output_path, e); - return; - } - } + let file_contents = std::fs::read(&path)?; - if actions.is_empty() { - match std::fs::copy(&input_path, &output_path) { + log::debug!("Found correct file type and read bytes, trying to parse"); + let pipeline_data: PipelineFile = if path_string.ends_with(".toml") { + toml::from_str(String::from_utf8(file_contents)?.as_str())? + } else { + serde_json::from_slice(&file_contents)? + }; + + log::debug!("Expanding pipeline file into targets"); + let base_path = PathBuf::from(path.parent().unwrap()); + get_targets(base_path.clone(), &pipeline_data).for_each( + |(input_path, output_path, actions)| { + match make_paths(&output_path) { Ok(_) => {} Err(e) => { - log::error!("Failed to copy {} to {}; {}", input_path, output_path, e); + log::error!("Failed to create target directory {}; {}", &output_path, e); + return; } - }; - return; - } + } - let mut file = match load_image(&input_path, None) { - Ok(image) => image, - Err(e) => { - log::error!("Error loading {}; {:?}", &input_path, e); + if actions.is_empty() { + match std::fs::copy(&input_path, &output_path) { + Ok(_) => {} + Err(e) => { + log::error!("Failed to copy {} to {}; {}", input_path, output_path, e); + } + }; return; } - }; - log::debug!( - "Loaded {}, Executing {} actions", - &input_path, - actions.len() - ); - let mut count = 1; - for step in actions { - match step { - CrunchCommand::Extrude { - tile_size, - space_y, - space_x, - pad_y, - pad_x, - extrude, - } => { - file = match commands::extrude( - file, tile_size, pad_x, pad_y, space_x, space_y, extrude, - ) { - Ok(f) => f, - Err(e) => { - log::error!( - "Failed to extrude {} at step {}; {}", - input_path, - count, - e - ); - return; - } - }; + let mut file = result!(load_image(&input_path, None)); + + log::debug!( + "Loaded {}, Executing {} actions", + &input_path, + actions.len() + ); + + let mut count = 1; + for step in actions { + match step { + Args::Rotate(rotate) => { + file = result!(rotate.run(&file)); + } + Args::Extrude(extrude) => { + file = result!(extrude.run(&file)); + } + Args::Scale(scale) => { + file = result!(scale.run(&file)); + } + Args::Flip(flip) => { + file = result!(flip.run(&file)); + } + Args::Remap(remap) => { + let palette = result!(load_image(&remap.palette, None)); + let image_palette = result!(Palette::extract_from(&file)); + let target_palette = result!(Palette::extract_from(&palette)); + + let mappings = + Palette::calculate_mapping(&image_palette, &target_palette); + file = result!(Remap::remap_image(file, mappings)); + } + _ => {} } - CrunchCommand::Remap { palette_file } => { - let palette_data = match load_image(join(&base_path, &palette_file), None) { - Ok(p) => p, - Err(e) => { - log::error!( - "Failed to load palette {} at step {}; {:?}", - &palette_file, - count, - e - ); - return; - } - }; - - let image_palette = match commands::palette(&file) { - Ok(ip) => ip, - Err(e) => { - log::error!( - "Failed to extract palette from {} at step {}; {}", - input_path, - count, - e - ); - return; - } - }; - let target_palette = match commands::palette(&palette_data) { - Ok(tp) => tp, - Err(e) => { - log::error!( - "Failed to extract palette from {} at step {}; {}", - &palette_file, - count, - e - ); - return; - } - }; - - let mappings = commands::calculate_mapping(&image_palette, &target_palette); - file = match commands::remap_image(file, mappings) { - Ok(f) => f, - Err(e) => { - log::error!( - "Failed to remap {} at step {}; {}", - input_path, - count, - e - ); - return; - } - }; - } - CrunchCommand::Scale { factor } => { - file = match commands::rescale(&file, factor) { - Ok(f) => f, - Err(e) => { - log::error!( - "Failed to scale {} at step {}; {}", - input_path, - count, - e - ); - return; - } - }; - } - CrunchCommand::Rotate { amount } => { - file = match commands::rotate(&file, amount) { - Ok(f) => f, - Err(e) => { - log::error!( - "Failed to rotate {} by {:?} step(s); {}", - input_path, - amount, - e - ); - return; - } - }; - } - CrunchCommand::Flip { direction } => { - file = match commands::flip(&file, direction) { - Ok(f) => f, - Err(e) => { - log::error!( - "Failed to flip {} in the following direction: {:?}; {}", - input_path, - direction, - e - ); - return; - } - }; - } - CrunchCommand::Palette { .. } | CrunchCommand::Pipeline => continue, + count += 1; } - count += 1; - } - - let mut outer_target_path = PathBuf::from(&output_path); - outer_target_path.pop(); - - if let Err(e) = std::fs::create_dir(&outer_target_path) { - match e.kind() { - std::io::ErrorKind::AlreadyExists => { /* This is fine */ } - _ => log::error!( - "Failed to create containing directory {}; {}", - outer_target_path.to_string_lossy(), - e - ), + let mut outer_target_path = PathBuf::from(&output_path); + outer_target_path.pop(); + + if let Err(e) = std::fs::create_dir(&outer_target_path) { + match e.kind() { + std::io::ErrorKind::AlreadyExists => { /* This is fine */ } + _ => log::error!( + "Failed to create containing directory {}; {}", + outer_target_path.to_string_lossy(), + e + ), + } } - } - match file.save(&output_path) { - Ok(_) => {} - Err(e) => { - log::error!("Failed to save to {}; {}", output_path, e); + match file.save(&output_path) { + Ok(_) => {} + Err(e) => { + log::error!("Failed to save to {}; {}", output_path, e); + } } - } - }, - ); + }, + ); - Ok(()) + Ok(()) + } } fn join<T: AsRef<Path>>(root: &Path, rest: &T) -> String { @@ -266,7 +185,7 @@ fn join<T: AsRef<Path>>(root: &Path, rest: &T) -> String { fn get_targets( base_path: PathBuf, pipeline_data: &PipelineFile, -) -> impl ParallelIterator<Item = (String, String, Vec<CrunchCommand>)> + '_ { +) -> impl ParallelIterator<Item = (String, String, Vec<Args>)> + '_ { pipeline_data .pipelines .par_iter() diff --git a/src/commands/remap.rs b/src/commands/remap.rs index d1b554541cb73bfd9a7bacd7c6091642d55c9cff..8e34444dca8ab63bd79dd576593726c7e8adc6c7 100644 --- a/src/commands/remap.rs +++ b/src/commands/remap.rs @@ -1,43 +1,62 @@ +use crate::commands::palette::ColourMapping; +use clap::Parser; use image::{GenericImage, Pixel, Rgba}; use num_traits::ToPrimitive; +use serde::{Deserialize, Serialize}; -use crate::commands::ColourMapping; -use crate::utils::{new_image, BasicRgba}; -use crate::OutputFormat; - -pub fn remap_image( - image: impl GenericImage, - mappings: ColourMapping, -) -> 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; +use crate::utils::{new_image, BasicRgba, OutputFormat, TypedOutputFormat}; + +/// Rotate an image clockwise by the given degree +#[derive(Debug, Clone, Parser, Serialize, Deserialize)] +#[clap(author, version = "0.3.0")] +pub struct Remap { + /// The path to the spritesheet file + #[serde(default)] + pub input: String, + /// The path to write the extruded spritesheet + #[serde(default)] + pub output: String, + + /// The path to the palette file containing the output colours + #[clap(short, long)] + pub palette: String, +} + +impl Remap { + pub fn remap_image( + image: impl GenericImage, + mappings: ColourMapping, + ) -> 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, Rgba::from(BasicRgba::transparent())), + }; } - 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, Rgba::from(BasicRgba::transparent())), - }; + Ok(output) } - - Ok(output) } diff --git a/src/commands/rotate.rs b/src/commands/rotate.rs index bcb1c7e1dcd5b7734e84f12604808e8af5cbb43f..3531cb211e2c3d9625975aeda8c538ad22e86969 100644 --- a/src/commands/rotate.rs +++ b/src/commands/rotate.rs @@ -1,10 +1,9 @@ -use clap::ArgEnum; +use crate::utils::TypedOutputFormat; +use clap::{Parser, ValueEnum}; use image::{imageops, GenericImage, Pixel}; use serde::{Deserialize, Serialize}; -use crate::TypedOutputFormat; - -#[derive(Copy, Clone, Serialize, Deserialize, ArgEnum, Debug)] +#[derive(Copy, Clone, Serialize, Deserialize, ValueEnum, Debug)] pub enum RotateDegree { #[serde(rename = "90")] One, @@ -14,18 +13,33 @@ pub enum RotateDegree { Three, } -pub fn rotate<T: GenericImage>( - image: &T, - degree: RotateDegree, -) -> anyhow::Result<TypedOutputFormat<T>> -where - T::Pixel: 'static, - <T::Pixel as Pixel>::Subpixel: 'static, -{ - use RotateDegree::*; - match degree { - One => Ok(imageops::rotate90(image)), - Two => Ok(imageops::rotate180(image)), - Three => Ok(imageops::rotate270(image)), +/// Rotate an image clockwise by the given degree +#[derive(Debug, Clone, Parser, Serialize, Deserialize)] +#[clap(author, version = "0.3.0")] +pub struct Rotate { + /// The path to the spritesheet file + #[serde(default)] + pub input: String, + /// The path to write the extruded spritesheet + #[serde(default)] + pub output: String, + + /// How many 90 degree steps should this image be rotated by + #[clap(short, long, value_enum)] + pub amount: RotateDegree, +} + +impl Rotate { + pub fn run<T: GenericImage>(&self, image: &T) -> anyhow::Result<TypedOutputFormat<T>> + where + T::Pixel: 'static, + <T::Pixel as Pixel>::Subpixel: 'static, + { + use RotateDegree::*; + match self.amount { + One => Ok(imageops::rotate90(image)), + Two => Ok(imageops::rotate180(image)), + Three => Ok(imageops::rotate270(image)), + } } } diff --git a/src/commands/scale.rs b/src/commands/scale.rs index 66b3ad69c04dd340fbfcbbfb26722052f5e79633..f9933fff342b7fc4b2f87d9ebaddd59f7a6f2262 100644 --- a/src/commands/scale.rs +++ b/src/commands/scale.rs @@ -1,20 +1,40 @@ +use crate::utils::TypedOutputFormat; +use clap::Parser; use image::imageops::FilterType; use image::{imageops, GenericImage, Pixel}; +use serde::{Deserialize, Serialize}; -use crate::TypedOutputFormat; +#[inline(always)] +fn one() -> f32 { + 1.0 +} + +/// Rotate an image clockwise by the given degree +#[derive(Debug, Clone, Parser, Serialize, Deserialize)] +#[clap(author, version = "0.3.0")] +pub struct Scale { + /// The path to the spritesheet file + #[serde(default)] + pub input: String, + /// The path to write the extruded spritesheet + #[serde(default)] + pub output: String, + + /// The scale factor to use; numbers between 0-1 shrink the image; numbers > 1 enlarge + #[clap(long, default_value_t = 1.0)] + #[serde(default = "one")] + factor: f32, +} -pub fn rescale<T: GenericImage>(image: &T, factor: f32) -> anyhow::Result<TypedOutputFormat<T>> -where - T::Pixel: 'static, - <T::Pixel as Pixel>::Subpixel: 'static, -{ - let nwidth = (image.width() as f32 * factor) as u32; - let nheight = (image.height() as f32 * factor) as u32; +impl Scale { + pub fn run<T: GenericImage>(&self, image: &T) -> anyhow::Result<TypedOutputFormat<T>> + where + T::Pixel: 'static, + <T::Pixel as Pixel>::Subpixel: 'static, + { + let width = (image.width() as f32 * self.factor) as u32; + let height = (image.height() as f32 * self.factor) as u32; - Ok(imageops::resize( - image, - nwidth, - nheight, - FilterType::Nearest, - )) + Ok(imageops::resize(image, width, height, FilterType::Nearest)) + } } diff --git a/src/format.rs b/src/format.rs index 82ee1a40dd2b46d5cdc08812e166dffb725fbaf9..eb70c53baf50de3efb046f2ccc43b33e37e40c27 100644 --- a/src/format.rs +++ b/src/format.rs @@ -1,21 +1,23 @@ use std::path::Path; -use clap::ArgEnum; +use clap::ValueEnum; use image::io::Reader; use image::{ImageError, ImageFormat, RgbaImage}; use serde::{Deserialize, Serialize}; use thiserror::Error; #[derive( - Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ArgEnum, Debug, Serialize, Deserialize, Default, + Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize, Default, )] pub enum PaletteFormat { + #[serde(alias = "txt")] Txt, #[default] + #[serde(alias = "png")] Png, } -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ArgEnum, Debug)] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] pub enum Format { Png, Jpg, @@ -49,11 +51,11 @@ pub enum ImageLoadingError { ImageError(#[from] ImageError), } -pub fn load_image<T: AsRef<Path>>( - path: T, +pub fn load_image( + path: impl ToString, format: Option<Format>, ) -> anyhow::Result<RgbaImage, ImageLoadingError> { - let mut image = Reader::open(path)?; + let mut image = Reader::open(path.to_string())?; let file = match format { Some(format) => { image.set_format(format.as_image_format()); diff --git a/src/main.rs b/src/main.rs index 71f8d0141448d571881ea45d2dc0badb402fe386..84689c5a8a19941421cc6eed9ac5c68a0678ffe3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,26 +9,6 @@ use image::{GenericImage, Rgba, SubImage}; use crate::cli_args::Args; use crate::format::{load_image, Format}; -#[derive(Clone, Copy)] -struct SpriteData<'a, T: GenericImage> { - pub data: SubImage<&'a T>, - #[allow(dead_code)] - pub x: u32, - #[allow(dead_code)] - pub y: u32, - pub tx: u32, - pub ty: u32, - #[allow(dead_code)] - pub width: u32, - #[allow(dead_code)] - pub height: u32, -} - -pub type OutputFormat = image::ImageBuffer<Rgba<u8>, Vec<u8>>; -#[allow(type_alias_bounds)] -pub type TypedOutputFormat<T: image::GenericImage> = - image::ImageBuffer<T::Pixel, Vec<<T::Pixel as image::Pixel>::Subpixel>>; - 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.rs index deff07c28e32d587d03e3bd9466699afb73ba3b1..e5c952b57a2491e26586a01729a2db514d157097 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,11 +3,30 @@ use std::path::{Component, Path, PathBuf}; use deltae::LabValue; use glam::Vec3; -use image::{GenericImageView, Rgb, Rgba, RgbaImage}; +use image::{GenericImage, GenericImageView, Rgb, Rgba, RgbaImage, SubImage}; use lab::Lab; use serde::{Deserialize, Serialize}; -use crate::OutputFormat; +#[derive(Clone, Copy)] +pub struct SpriteData<'a, T: GenericImage> { + pub data: SubImage<&'a T>, + #[allow(dead_code)] + pub x: u32, + #[allow(dead_code)] + pub y: u32, + pub tx: u32, + pub ty: u32, + #[allow(dead_code)] + pub width: u32, + #[allow(dead_code)] + pub height: u32, +} + +pub type OutputFormat = image::ImageBuffer<Rgba<u8>, Vec<u8>>; +#[allow(type_alias_bounds)] +pub type TypedOutputFormat<T: GenericImage> = + image::ImageBuffer<T::Pixel, Vec<<T::Pixel as image::Pixel>::Subpixel>>; +pub type RgbaOutputFormat = TypedOutputFormat<RgbaImage>; pub fn max_f32(a: f32, b: f32) -> f32 { if a > b { @@ -244,11 +263,32 @@ impl LowerHex for BasicRgba { } } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Pipeline { - pub input_path: String, - pub output_path: String, - pub actions: Vec<crate::cli_args::CrunchCommand>, +pub struct HexStringValue(String); +pub trait HexString { + fn as_hex_string(&self) -> HexStringValue; +} +impl HexString for Rgba<u8> { + fn as_hex_string(&self) -> HexStringValue { + HexStringValue(format!( + "{:02X}{:02X}{:02X}{:02X}", + self.0[0], self.0[1], self.0[2], self.0[3] + )) + } +} +impl<T: HexString + Clone> HexString for &T { + fn as_hex_string(&self) -> HexStringValue { + (*self).clone().as_hex_string() + } +} +impl UpperHex for HexStringValue { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0.to_uppercase()) + } +} +impl LowerHex for HexStringValue { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0.to_lowercase()) + } } pub fn normalize_path<T: AsRef<Path>>(path: T) -> PathBuf {