summary history branches tags files
commit:6bcb534854b504cd4198c5ecf9b1ebd141a5ffac
author:Trevor Bentley
committer:Trevor Bentley
date:Mon Jan 13 02:21:33 2025 +0100
parents:
initial feature-complete PoC
diff --git a/.gitignore b/.gitignore
line changes: +2/-0
index 0000000..e2a3069
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+*~

diff --git a/Cargo.lock b/Cargo.lock
line changes: +1547/-0
index 0000000..e51206c
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1547 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
+
+[[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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
+
+[[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.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
+[[package]]
+name = "bytemuck"
+version = "1.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
+[[package]]
+name = "cc"
+version = "1.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7"
+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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "4.5.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+ "terminal_size",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+
+[[package]]
+name = "confy"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45b1f4c00870f07dc34adcac82bb6a72cc5aabca8536ba1797e01df51d2ce9a0"
+dependencies = [
+ "directories",
+ "serde",
+ "thiserror",
+ "toml",
+]
+
+[[package]]
+name = "cpp"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f36bcac3d8234c1fb813358e83d1bb6b0290a3d2b3b5efc6b88bfeaf9d8eec17"
+dependencies = [
+ "cpp_macros",
+]
+
+[[package]]
+name = "cpp_build"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27f8638c97fbd79cc6fc80b616e0e74b49bac21014faed590bbc89b7e2676c90"
+dependencies = [
+ "cc",
+ "cpp_common",
+ "lazy_static",
+ "proc-macro2",
+ "regex",
+ "syn",
+ "unicode-xid",
+]
+
+[[package]]
+name = "cpp_common"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25fcfea2ee05889597d35e986c2ad0169694320ae5cc8f6d2640a4bb8a884560"
+dependencies = [
+ "lazy_static",
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "cpp_macros"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d156158fe86e274820f5a53bc9edb0885a6e7113909497aa8d883b69dd171871"
+dependencies = [
+ "aho-corasick",
+ "byteorder",
+ "cpp_common",
+ "lazy_static",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crunchy"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+
+[[package]]
+name = "directories"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "either"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "euclid"
+version = "0.22.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "exr"
+version = "1.73.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0"
+dependencies = [
+ "bit_field",
+ "half",
+ "lebe",
+ "miniz_oxide",
+ "rayon-core",
+ "smallvec",
+ "zune-inflate",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
+dependencies = [
+ "crc32fast",
+ "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 = "gexiv2-sys"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4edf7a47be383873c52eb34426723c7c9b040f9e58cf5088f2253cef149daf1"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "gif"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
+[[package]]
+name = "half"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "image"
+version = "0.25.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "color_quant",
+ "exr",
+ "gif",
+ "image-webp",
+ "num-traits",
+ "png",
+ "qoi",
+ "ravif",
+ "rayon",
+ "rgb",
+ "tiff",
+ "zune-core",
+ "zune-jpeg",
+]
+
+[[package]]
+name = "image-webp"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f"
+dependencies = [
+ "byteorder-lite",
+ "quick-error",
+]
+
+[[package]]
+name = "image2"
+version = "1.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "341b08fa47d58e19105a80c36b2d31f2cde35ee5020ffc2c4c1f6e9bcfa9624e"
+dependencies = [
+ "cpp",
+ "cpp_build",
+ "euclid",
+ "half",
+ "memmap2",
+ "rayon",
+ "thiserror",
+]
+
+[[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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
+dependencies = [
+ "equivalent",
+ "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 = "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"
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "lebe"
+version = "0.5.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.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa"
+dependencies = [
+ "arbitrary",
+ "cc",
+]
+
+[[package]]
+name = "libredox"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+dependencies = [
+ "bitflags 2.7.0",
+ "libc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[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 = "memmap2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
+dependencies = [
+ "libc",
+]
+
+[[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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
+dependencies = [
+ "adler2",
+ "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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "photo-what-what"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "clap_derive",
+ "confy",
+ "image",
+ "image2",
+ "regex",
+ "rexiv2",
+ "serde",
+ "tempfile",
+]
+
+[[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.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
+dependencies = [
+ "bitflags 1.3.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
+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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "quick-error"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
+[[package]]
+name = "quote"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
+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",
+ "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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "rexiv2"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11ecb9dccad43fea1bf58ea7bb8e34614446bea8aa00f78add6624d7ee26fb87"
+dependencies = [
+ "gexiv2-sys",
+ "libc",
+ "num-rational",
+]
+
+[[package]]
+name = "rgb"
+version = "0.8.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
+
+[[package]]
+name = "rustix"
+version = "0.38.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
+dependencies = [
+ "bitflags 2.7.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.217"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.217"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
+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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.96"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "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 = "tempfile"
+version = "3.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "getrandom",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "terminal_size"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9"
+dependencies = [
+ "rustix",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tiff"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e"
+dependencies = [
+ "flate2",
+ "jpeg-decoder",
+ "weezl",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "v_frame"
+version = "0.3.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.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
+
+[[package]]
+name = "weezl"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winnow"
+version = "0.6.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980"
+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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+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
line changes: +18/-0
index 0000000..ec7fdc7
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "photo-what-what"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+confy = "0.6.1"
+# for determining input image type (doesn't support RAW)
+image = "0.25.5"
+# for converting image to other type (supports RAW inputs)
+image2 = { version = "1.9.2", default_features = false, features = ["parallel", "mmap", "oiio"] }
+regex = "1.11.1"
+rexiv2 = "0.10.0"
+tempfile = "3.15.0"
+serde = { version = "1.0", features = ["derive"] }
+# CLI
+clap = { version = "4.5", features = ["derive", "wrap_help"] }
+clap_derive = "4.5"

diff --git a/src/main.rs b/src/main.rs
line changes: +677/-0
index 0000000..1413bbd
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,677 @@
+use regex::Regex;
+use std::collections::HashSet;
+use std::io::Read;
+use std::path::{Path, PathBuf};
+use std::process::Stdio;
+use image2::Image;
+use tempfile::NamedTempFile;
+use clap::{Parser, ValueEnum};
+use serde::{Serialize, Deserialize};
+
+#[derive(Debug)]
+enum PwwError {
+    Unknown(String),
+    MissingSidecar(String),
+}
+impl std::fmt::Display for PwwError {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        match self {
+            PwwError::Unknown(s) => write!(f, "PWW unknown error: {}", s),
+            PwwError::MissingSidecar(s) => write!(f, "PWW missing XMP sidecar error: {}", s),
+        }
+    }
+}
+
+#[derive(Parser, Debug)]
+#[clap(trailing_var_arg=true, term_width=100)]
+struct PwwArgs {
+    /// Path to program that classifies images
+    #[arg(short = 'b', long)]
+    identifier_bin: Option<PathBuf>,
+
+    /// Print information about which tags are written
+    #[arg(short = 'v', long)]
+    verbose: bool,
+
+    /// Print extra trace information about program flow, for debugging.
+    #[arg(short = 'd', long)]
+    debug: bool,
+
+    /// Keep converted image files in temp directory instead of
+    /// removing them.
+    #[arg(long)]
+    keep_converted: bool,
+
+    /// Image files to analyze and tag
+    #[arg(trailing_var_arg = true, allow_hyphen_values = true, value_name = "IMAGE", required = true)]
+    image_paths: Vec<PathBuf>,
+}
+
+#[derive(Default, Clone, Serialize, Deserialize, ValueEnum)]
+enum TagUpdatePolicy {
+    // Replace all existing contents (if any) with new items
+    Replace,
+    // Append new items to existing tag contents
+    Append,
+    // Remove existing items with the same prefix and then append new ones.
+    #[default]
+    ReplacePrefixed,
+}
+
+#[derive(Default, Clone, Serialize, Deserialize, ValueEnum, PartialEq)]
+enum FileUpdatePolicy {
+    /// Just print the tags, do not update any files
+    DisplayOnly,
+
+    /// Only update XMP sidecar (error if XMP file missing).  Only
+    /// sidecar tags are used.
+    SidecarOnly,
+
+    /// Only update image itself.  Only image tags are used.
+    ImageOnly,
+
+    /// Update both XMP sidecar and image itself.  Both sidecar and
+    /// image tags are used.
+    SidecarAndImage,
+
+    /// Only update XMP sidecar if present, otherwise update image
+    /// itself.  Either sidecar or image tags are used.
+    SidecarIfPresent,
+
+    /// Only update XMP sidecar unless image is one of a few common
+    /// known types with good EXIF support (JPG, PNG, TIFF, WebP).
+    /// Either sidecar or image tags are used.  Error if XMP file
+    /// missing.
+    #[default]
+    SidecarUnlessCommonImage,
+}
+
+#[derive(Default, Clone, Serialize, Deserialize, ValueEnum)]
+enum DownscalePolicy {
+    /// Images that require conversion due to unsupported input format
+    /// or images that exceed the maximum dimension are downscaled.
+    #[default]
+    LargeOrConverted,
+
+    /// Only images that require conversion due to unsupported input
+    /// format are downscaled.
+    ConvertedOnly,
+
+    /// Images that exceed the maximum dimensions are downscaled.
+    Large,
+
+    /// Images are never downscaled.  They are processed at the original size.
+    Never,
+
+    /// Images are always downscaled.  This means that all images
+    /// larger than the minimum dimension will be converted.
+    Always,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, ValueEnum, PartialEq)]
+enum ValidImageFormat {
+    Png,
+    Jpg,
+    Tiff,
+    Unknown,
+}
+
+impl From<image::ImageFormat> for ValidImageFormat {
+    fn from(fmt: image::ImageFormat) -> Self {
+        match fmt {
+            image::ImageFormat::Jpeg => ValidImageFormat::Jpg,
+            image::ImageFormat::Png => ValidImageFormat::Png,
+            image::ImageFormat::Tiff => ValidImageFormat::Tiff,
+            _ => ValidImageFormat::Unknown,
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, ValueEnum, PartialEq)]
+enum ValidImageColor {
+    Rgb8,
+    Rgba8,
+    Rgb16,
+    Rgba16,
+    Unknown,
+}
+
+impl From<image::ColorType> for ValidImageColor {
+    fn from(fmt: image::ColorType) -> Self {
+        match fmt {
+            image::ColorType::Rgb8 => ValidImageColor::Rgb8,
+            image::ColorType::Rgba8 => ValidImageColor::Rgba8,
+            image::ColorType::Rgb16 => ValidImageColor::Rgb16,
+            image::ColorType::Rgba16 => ValidImageColor::Rgba16,
+            _ => ValidImageColor::Unknown,
+        }
+    }
+}
+
+#[derive(Serialize, Deserialize)]
+struct PwwConfig {
+    /// Path to program that classifies images
+    identifier_bin: Option<PathBuf>,
+
+    /// Extra arguments passed to identifier binary before the image
+    /// filename.
+    identifier_bin_args: Vec<String>,
+
+    /// Directory to store temporary files
+    temp_dir: Option<PathBuf>,
+
+    /// Policy for which metadata should be updated
+    file_update_policy: FileUpdatePolicy,
+
+    /// Policy for how changes should be made to the specified tags.
+    tag_update_policy: TagUpdatePolicy,
+
+    /// Prefix that should be appended to all tags
+    tag_prefix: Option<String>,
+
+    /// Full hierarchical XMP, EXIF, or IPTC tags to write to sidecar files.
+    ///
+    /// examples:
+    ///  - Xmp.dc.subject
+    ///  - Xmp.lr.hierarchicalSubject
+    ///  - Xmp.digiKam.TagsList
+    sidecar_tags: Vec<String>,
+
+    /// Full hierarchical XMP, EXIF, or IPTC tags to write to image files.
+    ///
+    /// examples:
+    ///  - Iptc.Application2.Keywords
+    ///  - Xmp.dc.subject
+    ///  - Xmp.lr.hierarchicalSubject
+    ///  - Xmp.digiKam.TagsList
+    image_tags: Vec<String>,
+
+    /// Maximum input image dimension (in pixels).  If height or width
+    /// exceeds this, image will be downscaled before analysis.
+    max_dimension: u32,
+
+    /// Minimum image dimension when converting or downscaling.
+    /// Neither height nor width will shrink below this dimension (in
+    /// pixels).
+    min_dimension: u32,
+
+    /// Policy for when images should be downscaled
+    downscale_policy: DownscalePolicy,
+
+    /// Image formats that can be analyzed without conversion
+    valid_image_formats: Vec<ValidImageFormat>,
+
+    /// Image color formats that can be analyzed without conversion.
+    valid_image_colors: Vec<ValidImageColor>,
+
+    /// Whether to stop processing after first error, or continue.
+    halt_on_error: bool,
+
+    /// List of tags that are allowed to be added to image metadata.
+    /// Any other tags in output will be ignored.  If not provided,
+    /// all tags permitted.
+    permitted_tags: Vec<String>,
+
+    /// List of tags that are forbidden from being added to image
+    /// metadata.
+    forbidden_tags: Vec<String>,
+
+    #[serde(skip_serializing, skip_deserializing)]
+    cli: Option<PwwArgs>,
+}
+
+impl PwwConfig {
+    fn identifier_bin(&self) -> Option<PathBuf> {
+        match self.cli.as_ref().map(|c| c.identifier_bin.clone()) {
+            Some(ref b) => b.clone(),
+            _ => self.identifier_bin.clone()
+        }
+    }
+
+    fn image_paths(&self) -> Vec<PathBuf> {
+        match &self.cli {
+            Some(c) => c.image_paths.clone(),
+            _ => Default::default(),
+        }
+    }
+
+    fn verbose(&self) -> bool {
+        self.cli.as_ref().map(|c| c.verbose).unwrap_or_default()
+    }
+
+    #[allow(dead_code)]
+    fn debug(&self) -> bool {
+        self.cli.as_ref().map(|c| c.debug).unwrap_or_default()
+    }
+
+    fn keep_converted(&self) -> bool {
+        self.cli.as_ref().map(|c| c.keep_converted).unwrap_or_default()
+    }
+}
+
+impl ::std::default::Default for PwwConfig {
+    fn default() -> Self {
+        Self {
+            identifier_bin: None,
+            identifier_bin_args: vec!(),
+            temp_dir: None,
+            file_update_policy: FileUpdatePolicy::SidecarOnly,
+            tag_update_policy: TagUpdatePolicy::ReplacePrefixed,
+            tag_prefix: Some("[ML] ".to_owned()),
+
+            sidecar_tags: vec!(
+                // note: capitalization is critical: "subject" makes
+                // an unordered list read by standard tools, "Subject"
+                // makes a single string read by nothing.
+                "Xmp.dc.subject".into(),
+                "Xmp.lr.hierarchicalSubject".into(),
+            ),
+            image_tags: vec!(
+                "Iptc.Application2.Keywords".into(),
+            ),
+
+            max_dimension: 8000,
+            min_dimension: 768,
+            downscale_policy: DownscalePolicy::LargeOrConverted,
+            valid_image_formats: vec!(ValidImageFormat::Jpg,
+                                      ValidImageFormat::Png
+            ),
+            valid_image_colors: vec!(ValidImageColor::Rgb8,
+                                     ValidImageColor::Rgba8,
+                                     ValidImageColor::Rgb16,
+                                     ValidImageColor::Rgba16
+            ),
+            halt_on_error: false,
+            permitted_tags: vec!(),
+            forbidden_tags: vec!(),
+            cli: None,
+        }
+    }
+}
+
+fn exec_identifier<P: AsRef<Path>>(config: &PwwConfig, file_path: P) -> Result<String, PwwError> {
+    let os_path = file_path.as_ref().as_os_str();
+    let mut args: Vec<std::ffi::OsString> = config.identifier_bin_args.iter().map(|x| x.into()).collect();
+    args.push(os_path.to_os_string());
+    let mut p = std::process::Command::new(&config.identifier_bin().expect("No identifier binary provided."))
+        .args(args)
+        .stdout(Stdio::piped())
+        .spawn()
+        .map_err(|e| PwwError::Unknown(format!("Failed to launch identifier binary: {}", e)))?;
+    p.wait()
+        .map_err(|e| PwwError::Unknown(format!("Failed to wait for identifier binary: {}", e)))?;
+    let mut s: String = String::new();
+    let mut stdout = p.stdout.ok_or_else(|| PwwError::Unknown(format!("Failed to get output from identifier binary")))?;
+    stdout.read_to_string(&mut s).map_err(|x| PwwError::Unknown(x.to_string()))?;
+    Ok(s)
+}
+
+fn normalized_tags(config: &PwwConfig, output: String) -> Vec<String> {
+    let re_num: Regex = Regex::new(r"^(\s*\d+[.]\s+)")
+        .expect("Couldn't compile regex.");
+    let re_bul: Regex = Regex::new(r"^(\s*[*]\s+)")
+        .expect("Couldn't compile regex.");
+    let re_lst: Regex = Regex::new(r"^(\s*[-]\s+)")
+        .expect("Couldn't compile regex.");
+
+    // trim line spacing
+    let lines: Vec<String> = output.lines().map(|l| {
+        // remove quotes
+        let l = l.chars().filter(|c| !['"', '\'', '`'].contains(c)).collect::<String>()
+        // make lowercase
+            .to_lowercase()
+        // trim line spacing
+            .trim().to_owned();
+        // remove numbering
+        let l = re_num.replace_all(&l, "").trim().to_owned();
+        // remove star bullets
+        let l = re_bul.replace_all(&l, "").trim().to_owned();
+        // remove dash bullets
+        let l = re_lst.replace_all(&l, "").trim().to_owned();
+        // change underscores and dashes to spaces
+        l.replace("_", " ")
+            .replace("-", " ")
+        // remove space after commas
+            .replace(", ", ",")
+    }).collect();
+
+    let set: HashSet<String> = lines.join(",")
+        .split(",")
+        .map(|l| l.to_owned())
+        .filter(|l| l.len() > 0)
+        .collect();
+    let mut tags = set.iter()
+        .filter(|t| config.permitted_tags.is_empty() || config.permitted_tags.contains(t))
+        .filter(|t| !config.forbidden_tags.contains(t))
+        .map(|l| {
+            config.tag_prefix.as_ref().map(|p| p.to_owned()).unwrap_or_default() + &l
+        })
+        .collect::<Vec<String>>();
+    tags.sort();
+    tags
+}
+
+// Assume that scripts can handle 8- or 16-bit JPG or PNG files.  All
+// others require conversion.  Resize policy may also trigger
+// conversion.
+fn image_needs_conversion<P: AsRef<Path>>(config: &PwwConfig, filepath: P) -> Result<bool, PwwError> {
+    let img = image::ImageReader::open(filepath)
+        .map_err(|e| PwwError::Unknown(e.to_string()))?;
+    let valid_format = match img.format() {
+        Some(fmt) => {
+            let local_fmt: ValidImageFormat = fmt.into();
+            local_fmt != ValidImageFormat::Unknown &&
+                config.valid_image_formats.contains(&local_fmt)
+        },
+        _ => false,
+    };
+
+    let (valid_color, valid_dimension) = match valid_format {
+        true => {
+            let img = img.decode().map_err(|e| PwwError::Unknown(e.to_string()))?;
+            let local_color: ValidImageColor = img.color().into();
+            match local_color != ValidImageColor::Unknown &&
+                config.valid_image_colors.contains(&local_color) {
+                    true => {
+                        let max_dimension = std::cmp::max(img.width(), img.height());
+                        match config.downscale_policy {
+                            DownscalePolicy::Never |
+                            DownscalePolicy::ConvertedOnly => {
+                                (true, true)
+                            },
+                            DownscalePolicy::Always |
+                            DownscalePolicy::Large |
+                            DownscalePolicy::LargeOrConverted => {
+                                (true, max_dimension <= config.max_dimension)
+                            },
+                        }
+                    },
+                    _ => (false, false),
+                }
+        },
+        _ => (false, false),
+    };
+
+    let conversion_required = !valid_format || !valid_color || !valid_dimension;
+    if config.debug() && conversion_required {
+        if !valid_format {
+            println!(" - conversion required: {}",
+                     match (valid_format, valid_color, valid_dimension) {
+                         (false, _, _) => "invalid format",
+                         (true, false, _) => "invalid color space",
+                         (true, true, false) => "invalid dimensions",
+                         _ => "not required",
+                     })
+        }
+    }
+    Ok(conversion_required)
+}
+
+fn xmp_file_path<P: AsRef<Path>>(filepath: P) -> Result<Option<PathBuf>, PwwError> {
+    // Look for XMP files, which must have the same filename plus
+    // either .xmp or .XMP extension.
+    let xmp_path: PathBuf = filepath.as_ref().to_path_buf();
+    let ext = xmp_path.extension().unwrap_or_default().to_string_lossy().to_string();
+    let xmp_ext_lower: String = ext.clone() + ".xmp";
+    let xmp_ext_upper: String = ext + ".XMP";
+    let xmp_path_lower = xmp_path.with_extension(xmp_ext_lower);
+    let xmp_path_upper = xmp_path.with_extension(xmp_ext_upper);
+    let lower_exists = std::path::Path::exists(&xmp_path_lower);
+    let upper_exists = std::path::Path::exists(&xmp_path_upper);
+    let xmp_path: Option<PathBuf> = match (lower_exists, upper_exists) {
+        (false, false) => None,
+        (false, true) => Some(xmp_path_upper),
+        _ => Some(xmp_path_lower),
+    };
+    Ok(xmp_path)
+}
+
+fn should_update_sidecar<P: AsRef<Path>>(config: &PwwConfig, filepath: P) -> Result<Option<PathBuf>, PwwError> {
+    let xmp_path: Option<PathBuf> = xmp_file_path(&filepath).unwrap_or_default();
+
+    // Return path to XMP file if it exists and we're in a mode that
+    // updates it, None if we shouldn't update it, or error if it's
+    // required but missing.
+    match config.file_update_policy {
+        FileUpdatePolicy::SidecarOnly | FileUpdatePolicy::SidecarAndImage => {
+            match xmp_path {
+                Some(_) => Ok(xmp_path),
+                _ => Err(PwwError::MissingSidecar("XMP sidecar file not found.".into())),
+            }
+        },
+        FileUpdatePolicy::SidecarUnlessCommonImage => {
+            let img = image::ImageReader::open(filepath)
+                .map_err(|e| PwwError::Unknown(e.to_string()))?;
+            match img.format() {
+                Some(image::ImageFormat::Jpeg) |
+                Some(image::ImageFormat::Png) |
+                Some(image::ImageFormat::Tiff) |
+                Some(image::ImageFormat::WebP) => {
+                    Ok(None)
+                },
+                _ => {
+                    match xmp_path {
+                        Some(_) => Ok(xmp_path),
+                        _ => Err(PwwError::MissingSidecar("XMP sidecar file not found.".into())),
+                    }
+                }
+            }
+        }
+        FileUpdatePolicy::SidecarIfPresent => {
+            Ok(xmp_path)
+        },
+        FileUpdatePolicy::DisplayOnly | FileUpdatePolicy::ImageOnly => {
+            Ok(None)
+        },
+    }
+}
+
+fn should_update_image<P: AsRef<Path>>(config: &PwwConfig, filepath: P) -> Result<bool, PwwError> {
+    match config.file_update_policy {
+        FileUpdatePolicy::ImageOnly | FileUpdatePolicy::SidecarAndImage => Ok(true),
+        FileUpdatePolicy::SidecarIfPresent => {
+            match xmp_file_path(&filepath) {
+                Ok(Some(_)) => Ok(false),
+                _ => Ok(true),
+            }
+        }
+        FileUpdatePolicy::SidecarUnlessCommonImage => {
+            let img = image::ImageReader::open(filepath)
+                .map_err(|e| PwwError::Unknown(e.to_string()))?;
+            match img.format() {
+                Some(image::ImageFormat::Jpeg) |
+                Some(image::ImageFormat::Png) |
+                Some(image::ImageFormat::Tiff) |
+                Some(image::ImageFormat::WebP) => {
+                    Ok(true)
+                },
+                _ => Ok(false),
+            }
+        }
+        _ => Ok(false),
+    }
+}
+
+fn write_tags_to_file<P: AsRef<std::ffi::OsStr>>(config: &PwwConfig, metatags: &[String], tags: &[String], filepath: P) -> Result<(), PwwError> {
+    let meta = rexiv2::Metadata::new_from_path(&filepath)
+        .map_err(|e| PwwError::Unknown(format!("Unable to read metadata from image file: {}", e)))?;
+    if config.debug() {
+        println!(" - updating file: {}", filepath.as_ref().to_string_lossy());
+    }
+    for metatag in metatags {
+        let new_values: Vec<String> = match config.tag_update_policy {
+            TagUpdatePolicy::Append => {
+                meta.get_tag_multiple_strings(&metatag).unwrap_or_default().into_iter().chain(tags.iter().cloned()).collect()
+            },
+            TagUpdatePolicy::Replace => {
+                tags.to_vec()
+            },
+            TagUpdatePolicy::ReplacePrefixed => {
+                let new_tags: Vec<String> = meta.get_tag_multiple_strings(&metatag)
+                    .unwrap_or_default()
+                    .iter()
+                    .map(|t| t.trim().to_string())
+                    .filter(|t| t.len() > 0 && !t.starts_with(&config.tag_prefix.clone().unwrap_or_default()))
+                    .collect();
+                new_tags.into_iter().chain(tags.iter().cloned()).collect()
+            },
+        };
+        let ref_values: Vec<&str> = new_values.iter().map(|s| s.as_ref()).collect();
+
+        meta.clear_tag(&metatag);
+        meta.set_tag_multiple_strings(&metatag, &ref_values)
+            .map_err(|e| PwwError::Unknown(format!("Failed to update metadata tag in file: {}", e)))?;
+        if config.debug() {
+            println!(" - updated tag: {}", metatag);
+        }
+    }
+    meta.save_to_file(&filepath)
+        .map_err(|e| PwwError::Unknown(format!("Failed to write metadata to image file: {}", e)))?;
+    if config.verbose() {
+        println!(" - saved: {}\n", filepath.as_ref().to_string_lossy());
+    }
+    Ok(())
+}
+
+fn process_image(config: &PwwConfig, input_path: &Path) -> Result<(), PwwError> {
+    // If a tempfile is generated, it must live longer than all path
+    // references.  It is deleted automatically when dropped.
+    let tmpfile: Option<NamedTempFile>;
+
+    // Path of file to analyze: either the input path if not
+    // converted, or the tempfile path if converted.
+    let mut analyze_path: PathBuf = input_path.to_path_buf();
+
+    if config.verbose() {
+        println!("Analyzing: {}", analyze_path.to_string_lossy());
+    }
+
+    // Transcode to a temporary file if necessary
+    match image_needs_conversion(&config, &input_path) {
+        Ok(true) => {
+            if config.debug() {
+                println!(" - converting to rgb8 jpg");
+            }
+            let temp_dir = config.temp_dir.clone()
+                .unwrap_or(PathBuf::from(std::env::temp_dir()));
+            tmpfile = Some(tempfile::Builder::new()
+                           .prefix("pww-")
+                           .suffix(".jpg")
+                           .rand_bytes(8)
+                           .keep(config.keep_converted())
+                           .tempfile_in(temp_dir)
+                           .map_err(|e| PwwError::Unknown(format!("Failed to create temp file for image conversion: {}", e)))?);
+            let outpath = tmpfile.as_ref().map(|x| x.path().to_path_buf())
+                .ok_or_else(|| PwwError::Unknown(format!("Unable to open temp file for image conversion.")))?;
+            let i = Image::<u8, image2::Rgb>::open(&input_path)
+                .map_err(|e| PwwError::Unknown(format!("Unable to open source image file: {}", e)))?;
+            // convert to Rgb8
+            let conv = image2::filter::convert();
+            let i: Image<u8, image2::Rgb> = i.run(conv, None);
+            let max_dimension = std::cmp::max(i.width(), i.height());
+            let min_dimension = std::cmp::min(i.width(), i.height());
+            let should_scale = match config.downscale_policy {
+                DownscalePolicy::Always => {
+                    true
+                },
+                DownscalePolicy::ConvertedOnly => {
+                    true
+                },
+                DownscalePolicy::LargeOrConverted => {
+                    true
+                }
+                DownscalePolicy::Large => {
+                    max_dimension as u32 > config.max_dimension
+                }
+                DownscalePolicy::Never => {
+                    false
+                }
+            };
+            if should_scale && min_dimension as u32 > config.min_dimension {
+                let scale = config.min_dimension as f64 / min_dimension as f64;
+                if config.debug() {
+                    println!(" - scaling by {:.2} ({} to {})", scale, min_dimension, config.min_dimension);
+                }
+                i.scale(scale, scale);
+            }
+            i.save(&outpath)
+                .map_err(|e| PwwError::Unknown(format!("Unable to save converted image file: {}", e)))?;
+            analyze_path = outpath;
+        },
+        Err(e) => {
+            eprintln!("Error parsing input image: {}", e.to_string());
+        }
+        _ => {},
+    }
+
+    // Check sidecar file before analysis because it's a common error
+    // and we might as well not waste time analyzing when the tags
+    // won't be writeable.
+    let _ = should_update_sidecar(&config, &input_path)?;
+
+    // run ML engine on file
+    if config.debug() {
+        if analyze_path != input_path {
+            println!(" - converted to: {}", analyze_path.to_string_lossy())
+        }
+    }
+    let raw_output = exec_identifier(&config, &analyze_path)
+        .map_err(|e| PwwError::Unknown(format!("Identifier binary failed to execute: {}", e)))?;
+
+    // convert string output to list of tags
+    let tags = normalized_tags(&config, raw_output);
+    if config.verbose() {
+        println!(" - tags: {}", tags.join(", "));
+    }
+
+    match should_update_sidecar(&config, &input_path) {
+        Ok(Some(sidecar_path)) => {
+            write_tags_to_file(&config, &config.sidecar_tags, &tags, &sidecar_path)?;
+        },
+        Err(e) => {
+            return Err(PwwError::Unknown(format!("Error: XMP file required but not found: {}", e)));
+        },
+        _ => {
+        }
+    }
+
+    match should_update_image(&config, &input_path) {
+        Ok(true) => {
+            write_tags_to_file(&config, &config.image_tags, &tags, &input_path)?;
+        },
+        Err(e) => {
+            return Err(PwwError::Unknown(format!("Error: Unable to determine if EXIF should be written: {}", e)));
+        },
+        _ => {
+        }
+    }
+
+    if config.file_update_policy == FileUpdatePolicy::DisplayOnly {
+        println!("{}", tags.join(", "));
+    }
+
+    Ok(())
+}
+
+fn main() -> Result<(), PwwError> {
+    let mut config: PwwConfig = confy::load("photo-what-what", "pww_config")
+        .map_err(|e| PwwError::Unknown(format!("Error loading config file: {}", e)))?;
+    config.cli = Some(PwwArgs::parse());
+
+    for input_path in config.image_paths() {
+        match process_image(&config, &input_path) {
+            Err(e) => {
+                println!("Error processing image ({}): {}", input_path.to_string_lossy(), e);
+                if config.halt_on_error {
+                    break;
+                }
+                if config.verbose() || config.debug() {
+                    println!();
+                }
+            },
+            _ => {}
+        }
+    }
+
+    Ok(())
+}