--- /dev/null
+CHANGELOG.md
+/target
+flow-texpack.log
+out
+test_data/cli_*
+test_data/atlas_*
--- /dev/null
+flow-texpack: A program that will allow you to generate texture atlas.
+Maintainer: Andreas Widen <aw@luflow.net>
+License: zlib
+URL: https://www.luflow.net
+
+Authors
+=======
+
+Andreas Widen <aw@luflow.net>
--- /dev/null
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "aligned"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685"
+dependencies = [
+ "as-slice",
+]
+
+[[package]]
+name = "aligned-vec"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
+dependencies = [
+ "equator",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+
+[[package]]
+name = "arg_enum_proc_macro"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "as-slice"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
+dependencies = [
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "assert_cmd"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6"
+dependencies = [
+ "anstyle",
+ "bstr",
+ "libc",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "wait-timeout",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "av-scenechange"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394"
+dependencies = [
+ "aligned",
+ "anyhow",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "log",
+ "num-rational",
+ "num-traits",
+ "pastey",
+ "rayon",
+ "thiserror",
+ "v_frame",
+ "y4m",
+]
+
+[[package]]
+name = "av1-grain"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8"
+dependencies = [
+ "anyhow",
+ "arrayvec",
+ "log",
+ "nom",
+ "num-rational",
+ "v_frame",
+]
+
+[[package]]
+name = "avif-serialize"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bit_field"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "bitstream-io"
+version = "4.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f"
+dependencies = [
+ "no_std_io2",
+]
+
+[[package]]
+name = "bstr"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
+dependencies = [
+ "memchr",
+ "regex-automata",
+ "serde",
+]
+
+[[package]]
+name = "built"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64"
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "cc"
+version = "1.2.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "convert_case"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crossterm"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "derive_more",
+ "document-features",
+ "mio",
+ "parking_lot",
+ "rustix",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derive_more"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn",
+]
+
+[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
+[[package]]
+name = "document-features"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "equator"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
+dependencies = [
+ "equator-macro",
+]
+
+[[package]]
+name = "equator-macro"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "exr"
+version = "1.74.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
+dependencies = [
+ "bit_field",
+ "half",
+ "lebe",
+ "miniz_oxide",
+ "rayon-core",
+ "smallvec",
+ "zune-inflate",
+]
+
+[[package]]
+name = "fancy-regex"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8"
+dependencies = [
+ "bit-set",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "fax"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a"
+
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "fern"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "flow-rbp"
+version = "0.1.0"
+source = "git+https://luflow.net/git/flow-rbp.git?tag=v0.1.0#da55a684439d3c39455e6ec21f348ac19e6eb96e"
+
+[[package]]
+name = "flow-texpack"
+version = "0.1.0"
+dependencies = [
+ "assert_cmd",
+ "async-recursion",
+ "clap",
+ "fern",
+ "flow-rbp",
+ "humantime",
+ "image",
+ "log",
+ "rich_rust",
+ "tokio",
+]
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+]
+
+[[package]]
+name = "gif"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+ "zerocopy",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "humantime"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
+
+[[package]]
+name = "image"
+version = "0.25.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "color_quant",
+ "exr",
+ "gif",
+ "image-webp",
+ "moxcms",
+ "num-traits",
+ "png",
+ "qoi",
+ "ravif",
+ "rayon",
+ "rgb",
+ "tiff",
+ "zune-core",
+ "zune-jpeg",
+]
+
+[[package]]
+name = "image-webp"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
+dependencies = [
+ "byteorder-lite",
+ "quick-error",
+]
+
+[[package]]
+name = "imgref"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2"
+
+[[package]]
+name = "interpolate_name"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "jobserver"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+dependencies = [
+ "getrandom",
+ "libc",
+]
+
+[[package]]
+name = "lebe"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
+dependencies = [
+ "arbitrary",
+ "cc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litrs"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
+
+[[package]]
+name = "loop9"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
+dependencies = [
+ "imgref",
+]
+
+[[package]]
+name = "lru"
+version = "0.16.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39"
+dependencies = [
+ "hashbrown",
+]
+
+[[package]]
+name = "maybe-rayon"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
+dependencies = [
+ "cfg-if",
+ "rayon",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys",
+]
+
+[[package]]
+name = "moxcms"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
+dependencies = [
+ "num-traits",
+ "pxfm",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
+name = "no_std_io2"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "nom"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "noop_proc_macro"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
+
+[[package]]
+name = "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-conv"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
+
+[[package]]
+name = "num-derive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+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 = "num_threads"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "os_pipe"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pastey"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "png"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
+dependencies = [
+ "bitflags",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "predicates"
+version = "3.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe"
+dependencies = [
+ "anstyle",
+ "difflib",
+ "predicates-core",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2"
+dependencies = [
+ "predicates-core",
+ "termtree",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "profiling"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
+dependencies = [
+ "profiling-procmacros",
+]
+
+[[package]]
+name = "profiling-procmacros"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pxfm"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
+
+[[package]]
+name = "qoi"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "quick-error"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "rand"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+dependencies = [
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rav1e"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b"
+dependencies = [
+ "aligned-vec",
+ "arbitrary",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "av-scenechange",
+ "av1-grain",
+ "bitstream-io",
+ "built",
+ "cfg-if",
+ "interpolate_name",
+ "itertools",
+ "libc",
+ "libfuzzer-sys",
+ "log",
+ "maybe-rayon",
+ "new_debug_unreachable",
+ "noop_proc_macro",
+ "num-derive",
+ "num-traits",
+ "paste",
+ "profiling",
+ "rand",
+ "rand_chacha",
+ "simd_helpers",
+ "thiserror",
+ "v_frame",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "ravif"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
+dependencies = [
+ "avif-serialize",
+ "imgref",
+ "loop9",
+ "quick-error",
+ "rav1e",
+ "rayon",
+ "rgb",
+]
+
+[[package]]
+name = "rayon"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "rgb"
+version = "0.8.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
+
+[[package]]
+name = "rich_rust"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28b19f5711867dc33a82cdbfd437c03b4089308f63a7ec3ee6ab34a9d74ff519"
+dependencies = [
+ "bitflags",
+ "crossterm",
+ "fancy-regex",
+ "log",
+ "lru",
+ "num-rational",
+ "once_cell",
+ "os_pipe",
+ "regex",
+ "smallvec",
+ "stdio-override",
+ "time",
+ "unicode-width",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+
+[[package]]
+name = "simd_helpers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
+dependencies = [
+ "quote",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "stdio-override"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cffa8a2e517b4e9f270c47e1c4120df90506d9451c1efa67e3698d66446d30ce"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "termtree"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tiff"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
+dependencies = [
+ "fax",
+ "flate2",
+ "half",
+ "quick-error",
+ "weezl",
+ "zune-jpeg",
+]
+
+[[package]]
+name = "time"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
+dependencies = [
+ "deranged",
+ "itoa",
+ "libc",
+ "num-conv",
+ "num_threads",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
+
+[[package]]
+name = "time-macros"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tokio"
+version = "1.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+dependencies = [
+ "bytes",
+ "pin-project-lite",
+ "tokio-macros",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "v_frame"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2"
+dependencies = [
+ "aligned-vec",
+ "num-traits",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wait-timeout"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.3+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.121"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.121"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.121"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.121"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "weezl"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+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-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "y4m"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
+
+[[package]]
+name = "zerocopy"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zune-core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
+
+[[package]]
+name = "zune-inflate"
+version = "0.2.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "zune-jpeg"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
+dependencies = [
+ "zune-core",
+]
--- /dev/null
+[package]
+name = "flow-texpack"
+version = "0.1.0"
+edition = "2024"
+authors = ["Andreas Widen <aw@luflow.net>"]
+description = "flow-texpack is a program that will allow you to generate texture atlas from input images (BMP, HDR, JPG, PNG, TGA, TIFF, WEBP). The application generates both texture atlas and descriptions file that can be read by a game."
+license = "Zlib"
+repository = "https://luflow.net/git-repos/flow-texpack.git"
+readme = "README.md"
+documentation = "https://luflow.net/git-repos/flow-texpack.git"
+keywords = ["texture atlas", "image atlas", "generator"]
+exclude = [".github", "/ci/*", "*.log"]
+
+[dependencies]
+flow-rbp = { git = "https://luflow.net/git/flow-rbp.git", tag = "v0.1.0" }
+tokio = { version = "1.52.3", features = ["macros", "fs", "io-util", "rt-multi-thread"] }
+async-recursion = "1.1.1"
+rich_rust = "0.2.1"
+image = "0.25.10"
+clap = { version = "4.6.1", features = ["derive"] }
+fern = "0.7.1"
+log = "0.4.32"
+humantime = "2.3.0"
+
+[dev-dependencies]
+assert_cmd = "2.2.2"
--- /dev/null
+flow-texpack: A program that will allow you to generate texture atlas.
+Copyright (C) 2026-2026 Andreas Widen <aw@luflow.net>
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
--- /dev/null
+# flow-texpack
+
+`flow-texpack` is a program that will allow you to generate texture atlas from
+input images (BMP, HDR, JPG, PNG, TGA, TIFF, WEBP). The application generates
+both texture atlas and descriptions file that can be read by a game.
+
+## Installation
+
+Install `Rust` from your package manager or by downloading from here:
+[https://rust-lang.org/](https://rust-lang.org/).
+
+## Build
+
+Build using release mode and install locally (on GNU/Linux this is `~/.cargo/bin`):
+
+```sh
+cargo install --locked --path .
+```
+
+## Example usage
+
+Show available options:
+
+```sh
+flow-texpack -h
+flow-texpack --help
+```
+
+Examples:
+
+```sh
+flow-texpack -i data/characters data/tiles -o out/atlas -m -t -u -r -p 2 -v
+flow-texpack -i data/characters data/tiles -o out/atlas -m -t -u -r -p 2 -v --load-filter png tga
+flow-texpack -i data -e data/tiles -o out/atlas -m -t -u -r --atlas-size pot2048 --rect-heuristic area-fit -v
+flow-texpack --input-file input.txt --exlude-file exclude.txt -o out/atlas -v
+flow-texpack -i data/characters data/tiles -o out/atlas -m -t -u -r --adjust-size -v
+flow-texpack -i data/characters data/tiles -o out/atlas -m -t -u -r --adjust-fit -v
+```
+
+## LICENSE
+
+See the file 'LICENSE' for license information.
--- /dev/null
+# git-cliff ~ configuration file
+# https://git-cliff.org/docs/configuration
+
+[changelog]
+# A Tera template to be rendered as the changelog's header.
+# See https://keats.github.io/tera/docs/#introduction
+header = """
+# Changelog\n
+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).\n
+"""
+# A Tera template to be rendered for each release in the changelog.
+# See https://keats.github.io/tera/docs/#introduction
+body = """
+{% if version -%}
+ ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
+{% else -%}
+ ## [Unreleased]
+{% endif -%}
+{% for group, commits in commits | group_by(attribute="group") %}
+ ### {{ group | upper_first }}
+ {% for commit in commits %}
+ - {{ commit.message | split(pat="\n") | first | upper_first | trim }}\
+ {% endfor %}
+{% endfor %}\n
+"""
+# A Tera template to be rendered as the changelog's footer.
+# See https://keats.github.io/tera/docs/#introduction
+footer = """
+{% for release in releases -%}
+ {% if release.version -%}
+ {% if release.previous.version -%}
+ [{{ release.version | trim_start_matches(pat="v") }}]: \
+ https://luflow.net/git-repos/flow-texpack.git\
+ /compare/{{ release.previous.version }}..{{ release.version }}
+ {% else -%}
+ [{{ release.version | trim_start_matches(pat="v") }}]: \
+ https://luflow.net/git-repos/flow-texpack.git\
+ /tree/{{ release.version }}
+ {% endif -%}
+ {% else -%}
+ [unreleased]: https://luflow.net/git-repos/flow-texpack.git\
+ /compare/{{ release.previous.version }}..HEAD
+ {% endif -%}
+{% endfor %}
+<!-- generated by git-cliff -->
+"""
+# Remove leading and trailing whitespaces from the changelog's body.
+trim = true
+
+[git]
+# Parse commits according to the conventional commits specification.
+# See https://www.conventionalcommits.org
+conventional_commits = true
+# Exclude commits that do not match the conventional commits specification.
+filter_unconventional = false
+# An array of regex based parsers for extracting data from the commit message.
+# Assigns commits to groups.
+# Optionally sets the commit's scope and can decide to exclude commits from further processing.
+commit_parsers = [
+ { message = "^[a|A]dd", group = "Added" },
+ { message = "^[s|S]upport", group = "Added" },
+ { message = "^[r|R]emove", group = "Removed" },
+ { message = "^.*: add", group = "Added" },
+ { message = "^.*: support", group = "Added" },
+ { message = "^.*: remove", group = "Removed" },
+ { message = "^.*: delete", group = "Removed" },
+ { message = "^test", group = "Fixed" },
+ { message = "^fix", group = "Fixed" },
+ { message = "^.*: fix", group = "Fixed" },
+ { message = "^.*", group = "Changed" },
+]
+# Prevent commits that are breaking from being excluded by commit parsers.
+filter_commits = false
+# Order releases topologically instead of chronologically.
+topo_order = false
+# Order of commits in each group/release within the changelog.
+# Allowed values: newest, oldest
+sort_commits = "oldest"
--- /dev/null
+// flow-texpack: A program that will allow you to generate texture atlas.
+// zlib License (see LICENSE)
+
+#[doc(hidden)]
+pub mod texpack;
+
+// re-export types:
+#[doc(hidden)]
+pub use crate::texpack::app::App;
+
+#[doc(hidden)]
+pub use crate::texpack::app::get_atlas_image_extension;
+
+#[doc(hidden)]
+pub use crate::texpack::app::create_dir_all;
+
+#[doc(hidden)]
+pub use crate::texpack::app::remove_dir_all;
+
+#[doc(hidden)]
+pub use crate::texpack::app::remove_file;
+
+#[doc(hidden)]
+pub use crate::texpack::app::exists_dir;
+
+#[doc(hidden)]
+pub use crate::texpack::app::exists_file;
+
+#[doc(hidden)]
+pub use crate::texpack::app::write_file_sync;
+
+#[doc(hidden)]
+pub use crate::texpack::packer::Packer;
+
+#[doc(hidden)]
+pub use crate::texpack::packer::PackerError;
+
+#[doc(hidden)]
+pub use crate::texpack::texture::Texture;
+
+#[doc(hidden)]
+pub use crate::texpack::texture::TextureError;
--- /dev/null
+// flow-texpack: A program that will allow you to generate texture atlas.
+// zlib License (see LICENSE)
+
+use flow_texpack::App;
+
+#[tokio::main]
+async fn main() {
+ let mut app = App::new();
+ app.run().await;
+}
--- /dev/null
+// flow-texpack: A program that will allow you to generate texture atlas.
+// zlib License (see LICENSE)
+
+pub mod app;
+pub mod packer;
+pub mod texture;
--- /dev/null
+// flow-texpack: A program that will allow you to generate texture atlas.
+// zlib License (see LICENSE)
+
+use crate::Packer;
+use crate::Texture;
+
+use flow_rbp::FreeRectHeuristic;
+use rich_rust::console::Console;
+use rich_rust::interactive::Status;
+
+use clap::{Parser, ValueEnum};
+
+use tokio::io;
+use tokio::task;
+
+use std::time::Instant;
+
+use log::info;
+
+use std::collections::HashSet;
+use std::ffi::OsString;
+use std::fs;
+use std::hash::{DefaultHasher, Hash, Hasher};
+use std::io::Write;
+use std::path::PathBuf;
+use std::sync::Arc;
+
+/// name of the application:
+const NAME: &'static str = "https://luflow.net/git-repos/flow-texpack.git";
+
+/// short about description shown for option '-h':
+const ABOUT: &'static str = "
+flow-texpack is a program that will allow you to generate texture atlas from input images (BMP, HDR,
+JPG, PNG, TGA, TIFF, WEBP). The application generates both texture atlas and descriptions file that
+can be read by a game.";
+
+/// long about description shown for option '--help':
+const LONG_ABOUT: &'static str = "
+flow-texpack is a program that will allow you to generate texture atlas from input images (BMP, HDR,
+JPG, PNG, TGA, TIFF, WEBP). The application generates both texture atlas and descriptions file that
+can be read by a game.
+
+Examples:
+flow-texpack -i data/characters data/tiles -o out/atlas -m -t -u -r -p 2 -v
+flow-texpack -i data/characters data/tiles -o out/atlas -m -t -u -r -p 2 -v --load-filter png tga
+flow-texpack -i data -e data/tiles -o out/atlas -m -t -u -r --atlas-size pot2048 --rect-heuristic area-fit -v
+flow-texpack --input-file input.txt --exlude-file exclude.txt -o out/atlas -v
+flow-texpack -i data/characters data/tiles -o out/atlas -m -t -u -r --adjust-size -v
+flow-texpack -i data/characters data/tiles -o out/atlas -m -t -u -r --adjust-fit -v";
+
+/// Specifies the different atlas descriptor types.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
+enum AtlasDescriptor {
+ /// atlas descriptor type: JSON
+ Json,
+ /// atlas descriptor type: Txt
+ Txt,
+ /// atlas descriptor type: Txt (with description header)
+ TxtDesc,
+}
+
+/// Specifies the different atlas image types.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
+pub enum AtlasImage {
+ /// atlas image type: PNG
+ Png,
+ /// atlas image type: TGA
+ Tga,
+ /// atlas image type: TIFF
+ Tiff,
+ /// atlas image type: Webp
+ Webp,
+}
+
+/// Specifies the different load filter types.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
+pub enum LoadFilter {
+ /// load filter type: BMP
+ Bmp,
+ /// load filter type: HDR
+ Hdr,
+ /// load filter type: JPG
+ Jpg,
+ /// load filter type: PNG
+ Png,
+ /// load filter type: TGA
+ Tga,
+ /// load filter type: TIFF
+ Tiff,
+ /// load filter type: Webp
+ Webp,
+}
+
+/// Specifies the different atlas output sizes.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
+enum AtlasSize {
+ /// power-of-two 64x64 size:
+ Pot64 = 64,
+ /// power-of-two 128x128 size:
+ Pot128 = 128,
+ /// power-of-two 256x256 size:
+ Pot256 = 256,
+ /// power-of-two 512x512 size:
+ Pot512 = 512,
+ /// power-of-two 1024x1024 size:
+ Pot1024 = 1024,
+ /// power-of-two 2048x2048 size:
+ Pot2048 = 2048,
+ /// power-of-two 4096x4096 size:
+ Pot4096 = 4096,
+ /// power-of-two 8192x8192 size:
+ Pot8192 = 8192,
+}
+
+/// Specifies the different heuristic types.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
+enum RectHeuristic {
+ /// Choose to pack `R` into such `Fi` that `min(wf - w, hf - h)` is the smallest. In other words, we
+ /// minimize the length of the shorter leftover side.
+ ShortSideFit,
+
+ /// Pack `R` into an `Fi` such that `max(wf - w, hf - h)` is the smallest. That is, we minimize
+ /// the length of the longer leftover side.
+ LongSideFit,
+
+ /// Pick the `Fi ∈ F` that is smallest in area to place the next rectangle `R` into. If there is a
+ /// tie, we use the `ShortSideFit` rule to break it.
+ AreaFit,
+
+ /// Orient and place each rectangle to the position where the y-coordinate of the top side of the
+ /// rectangle is the smallest and if there are several such valid positions, pick the one that has
+ /// the smallest x-coordinate value.
+ BottomLeft,
+
+ /// Place `R` into a position where the length of the perimeter of `R` that is touched by the bin
+ /// edge or by a previously packed rectangle is maximized.
+ ContactPoint,
+}
+
+#[derive(Parser, Debug)]
+#[command(version, about = ABOUT, long_about = LONG_ABOUT)]
+struct CliArgs {
+ /// input files/directories separated by space (' ')
+ #[arg(short = 'i', long = "input", value_delimiter = ' ', num_args = 1.., group = "input_group")]
+ input: Option<Vec<PathBuf>>,
+
+ /// input file containing files/directories
+ /// (each entry needs to be on a new line)
+ #[arg(long = "input-file", group = "input_group", verbatim_doc_comment)]
+ input_file: Option<PathBuf>,
+
+ /// exclude files/directories separated by space (' ')
+ #[arg(short = 'e', long = "exclude", value_delimiter = ' ', num_args = 1.., group = "exclude_group")]
+ exclude: Option<Vec<PathBuf>>,
+
+ /// exclude file containing files/directories
+ /// (each entry needs to be on a new line)
+ #[arg(long = "exclude-file", group = "exclude_group", verbatim_doc_comment)]
+ exclude_file: Option<PathBuf>,
+
+ /// output file
+ #[arg(short = 'o', long = "output", requires = "input_group")]
+ output: PathBuf,
+
+ /// atlas descriptor
+ #[arg(long = "atlas-descriptor", value_enum, default_value_t = AtlasDescriptor::Json)]
+ atlas_descriptor: AtlasDescriptor,
+
+ /// atlas image
+ #[arg(long = "atlas-image", value_enum, default_value_t = AtlasImage::Png)]
+ atlas_image: AtlasImage,
+
+ /// max atlas output size POT
+ #[arg(long = "atlas-size", value_enum, default_value_t = AtlasSize::Pot1024)]
+ atlas_size: AtlasSize,
+
+ /// load filter
+ #[arg(long = "load-filter", value_enum, value_delimiter = ' ', num_args = 1..)]
+ load_filter: Option<Vec<LoadFilter>>,
+
+ /// max atlases that can be created (value in the range 1 - 4096)
+ #[arg(long = "max-atlases", default_value_t = 64, value_parser = clap::value_parser!(u16).range(1..=4096))]
+ max_atlases: u16,
+
+ /// enable premultiply the pixels of the textures by their alpha channel
+ #[arg(short = 'm', long = "premultiply")]
+ premultiply: bool,
+
+ /// enable trim excess transparency off the textures
+ #[arg(short = 't', long = "trim")]
+ trim: bool,
+
+ /// enable force packer to re-pack (ignore stored hashes)
+ #[arg(short = 'f', long = "force")]
+ force: bool,
+
+ /// enable remove duplicate textures from the atlas
+ #[arg(short = 'u', long = "unique")]
+ unique: bool,
+
+ /// enable rotation of textures (90 degrees clockwise)
+ #[arg(short = 'r', long = "rotate")]
+ rotate: bool,
+
+ /// enable force atlas POT square size
+ #[arg(long = "force-square")]
+ force_square: bool,
+
+ /// enable adjust atlas size automatically so that texture will fit
+ #[arg(long = "adjust-size")]
+ adjust_size: bool,
+
+ /// enable adjust texture size so that it will fit given atlas size
+ #[arg(long = "adjust-fit")]
+ adjust_fit: bool,
+
+ /// enable generation of mipmaps (rendering hint)
+ #[arg(long = "generate-mipmaps")]
+ generate_mipmaps: bool,
+
+ /// padding between textures (value in the range 0 - 16)
+ #[arg(short = 'p', long = "pad", default_value_t = 1, value_parser = clap::value_parser!(u8).range(0..=16) )]
+ pad: u8,
+
+ /// heuristic rule to use when deciding where to place a new rectangle
+ #[arg(long = "rect-heuristic", value_enum, default_value_t = RectHeuristic::LongSideFit)]
+ rect_heuristic: RectHeuristic,
+
+ /// enable verbose output of progress
+ #[arg(short = 'v', long = "verbose")]
+ verbose: bool,
+
+ /// enable logging to log file 'flow-texpack.log'
+ #[arg(short = 'l', long = "log")]
+ log: bool,
+}
+
+/// Specifies the properties of a App.
+pub struct App {
+ /// is the cli arguments program is called with:
+ cli_args: CliArgs,
+ /// is the console used for colored text output and progress bars (verbose mode enabled):
+ console: Arc<Console>,
+ /// is the set holding all input files (excluding those in exclude_files if any):
+ input_files: HashSet<PathBuf>,
+ /// is the set holding all exclude files:
+ exclude_files: HashSet<PathBuf>,
+ /// is the set holding supported image extensions for load:
+ supported_extensions_load: HashSet<OsString>,
+ /// is the set holding supported image extensions for save:
+ supported_extensions_save: HashSet<OsString>,
+ /// is the vector holding the loaded textures:
+ textures: Vec<Texture>,
+ /// is the vector holding the packers:
+ packers: Vec<Arc<Packer>>,
+ /// is the default hasher:
+ hasher: DefaultHasher,
+ /// is the hash value:
+ hash_value: u64,
+}
+
+impl App {
+ /// Instantiates a new App instance.
+ ///
+ /// # Panics
+ ///
+ /// If initialization failed.
+ pub fn new() -> Self {
+ Self {
+ cli_args: CliArgs::parse(),
+ console: Console::new().shared(),
+ input_files: HashSet::new(),
+ exclude_files: HashSet::new(),
+ supported_extensions_load: HashSet::from([
+ OsString::from("bmp"),
+ OsString::from("hdr"),
+ OsString::from("jpg"),
+ OsString::from("png"),
+ OsString::from("tga"),
+ OsString::from("tiff"),
+ OsString::from("webp"),
+ ]),
+ supported_extensions_save: HashSet::from([
+ OsString::from("png"),
+ OsString::from("tga"),
+ OsString::from("tiff"),
+ OsString::from("webp"),
+ ]),
+ textures: Vec::new(),
+ packers: Vec::new(),
+ hasher: DefaultHasher::new(),
+ hash_value: 0,
+ }
+ }
+
+ /// Execute the main application loop.
+ ///
+ /// # Panics
+ ///
+ /// If something unexpected happens.
+ pub async fn run(&mut self) {
+ // start timer:
+ let start_time = Instant::now();
+
+ // initialize:
+ self.initialize().await;
+
+ if !self.identical_hash().await {
+ if self.input_files.len() > 0 {
+ // remove old atlas files if any:
+ self.remove_old_files();
+
+ // load all input textures:
+ self.load_textures().await;
+
+ // sort textures by area (largest area first):
+ self.sort_textures();
+
+ // make sure out directory exists:
+ if !exists_dir(&self.cli_args.output) {
+ if let Some(parent_dir) = self.cli_args.output.parent() {
+ create_dir_all(&parent_dir.to_path_buf());
+ }
+ }
+
+ // pack the textures:
+ self.pack_textures();
+
+ // save atlas image/s:
+ self.save_atlas_images().await;
+
+ // save atlas descriptor:
+ self.save_atlas_descriptor();
+
+ // save new hash value:
+ self.save_input_hash();
+ } else {
+ if self.cli_args.verbose {
+ self.console.print("[dim]No input files...[/]");
+ }
+ }
+ } else {
+ if self.cli_args.verbose {
+ self.console
+ .print("[dim]Identical hash value. No need to continue...[/]");
+ }
+ }
+
+ // we're done:
+ if self.cli_args.verbose {
+ self.console.print("");
+ self.console.print(&format!(
+ "[dim]Completed in {:.1}s[/]",
+ start_time.elapsed().as_secs_f64()
+ ));
+ self.console.print("[green]Done![/]");
+ }
+ }
+
+ /// initialize logger and prepare input files vector:
+ async fn initialize(&mut self) {
+ // initialize logger:
+ self.init_logger();
+
+ // prepare input files:
+ self.prepare().await;
+
+ // log cli_args options:
+ self.log_options();
+ }
+
+ /// determine whether current hash is the same as previous:
+ async fn identical_hash(&mut self) -> bool {
+ if !self.cli_args.force {
+ // identical hash from prev run = no need to continue:
+ if self.cli_args.verbose {
+ if let Ok(_status) = Status::new(&self.console, "Checking input hash...") {
+ return self.check_input_hash().await;
+ }
+ } else {
+ return self.check_input_hash().await;
+ }
+ }
+
+ return false;
+ }
+
+ /// initialize logger:
+ fn init_logger(&self) {
+ if self.cli_args.log {
+ fern::Dispatch::new()
+ .format(|out, message, record| {
+ out.finish(format_args!("[{}] {}", record.level(), message))
+ })
+ .level(log::LevelFilter::Debug)
+ .chain(
+ std::fs::OpenOptions::new()
+ .create(true)
+ .write(true)
+ .truncate(true)
+ .append(false)
+ .open("flow-texpack.log")
+ .unwrap(),
+ )
+ .apply()
+ .unwrap();
+ }
+ }
+
+ /// log command line arguments program was called with:
+ fn log_options(&self) {
+ info!("Options that will be used:");
+
+ info!("Exclude files:");
+ for path in &self.exclude_files {
+ info!("\t {}", path.display());
+ }
+
+ info!("Input files (does not include excludes above):");
+ for path in &self.input_files {
+ info!("\t {}", path.display());
+ }
+
+ info!("Load filters:");
+ for ext in &self.supported_extensions_load {
+ info!("\t {}", ext.display());
+ }
+
+ info!("Output dir: {}", self.cli_args.output.display());
+ info!("AtlasDescriptor: {:?}", self.cli_args.atlas_descriptor);
+ info!("AtlasImage: {:?}", self.cli_args.atlas_image);
+ info!("AtlasSize: {:?}", self.cli_args.atlas_size);
+ info!("LoadFilter: {:?}", self.cli_args.load_filter);
+ info!("Max atlases: {}", self.cli_args.max_atlases);
+ info!("Premultiply: {}", self.cli_args.premultiply);
+ info!("Trim: {}", self.cli_args.trim);
+ info!("Force: {}", self.cli_args.force);
+ info!("Unique: {}", self.cli_args.unique);
+ info!("Rotate: {}", self.cli_args.rotate);
+ info!("Force square: {}", self.cli_args.force_square);
+ info!("Adjust size: {}", self.cli_args.adjust_size);
+ info!("Adjust fit: {}", self.cli_args.adjust_fit);
+ info!("Generate mipmaps: {}", self.cli_args.generate_mipmaps);
+ info!("Pad: {}", self.cli_args.pad);
+ info!("Rect heuristic: {:?}", self.cli_args.rect_heuristic);
+ info!("Verbose: {:?}", self.cli_args.verbose);
+ }
+
+ /// prepare input files vector (exclude those in exclude/exclude_file if any):
+ async fn prepare(&mut self) {
+ // prepare load filters (default includes all if no args set):
+ self.prepare_load_filter();
+
+ // prepare files to exlude:
+ if let Some(exclude_vec) = self.cli_args.exclude.clone() {
+ for path in &exclude_vec {
+ self.prepare_exclude_files(path).await;
+ }
+ } else if let Some(exclude_file) = self.cli_args.exclude_file.clone() {
+ if exclude_file.is_file() {
+ self.read_exclude_file(&exclude_file).await;
+ }
+ }
+
+ // prepare input files and exclude those above if any:
+ if let Some(input_vec) = self.cli_args.input.clone() {
+ for path in &input_vec {
+ self.prepare_input_files(path).await;
+ }
+ } else if let Some(input_file) = self.cli_args.input_file.clone() {
+ if input_file.is_file() {
+ self.read_input_file(&input_file).await;
+ }
+ }
+ }
+
+ fn prepare_load_filter(&mut self) {
+ if let Some(load_filters) = self.cli_args.load_filter.clone() {
+ self.supported_extensions_load.clear();
+ for load_filter in load_filters {
+ let ext = get_load_filter_extension(load_filter);
+ self.supported_extensions_load.insert(OsString::from(ext));
+ }
+ }
+ }
+
+ /// prepare exclude files vector:
+ #[async_recursion::async_recursion]
+ async fn prepare_exclude_files(&mut self, path: &PathBuf) {
+ if path.is_file() {
+ self.add_exclude_file(path);
+ } else {
+ let mut reader = tokio::fs::read_dir(path).await.unwrap();
+ loop {
+ if let Some(f) = reader.next_entry().await.unwrap() {
+ if f.path().is_dir() {
+ self.prepare_exclude_files(&f.path()).await;
+ } else if f.path().is_file() {
+ self.add_exclude_file(&f.path());
+ }
+ } else {
+ break;
+ }
+ }
+ }
+ }
+
+ fn add_exclude_file(&mut self, path: &PathBuf) {
+ if let Some(extension) = path.extension() {
+ let ext = OsString::from(extension);
+ if self.supported_extensions_load.contains(&ext) {
+ self.exclude_files.insert(path.clone());
+ }
+ }
+ }
+
+ /// read exclude file and parse each line and put dir/file into exclude vector:
+ async fn read_exclude_file(&mut self, exclude_file: &PathBuf) {
+ let contents = tokio::fs::read_to_string(exclude_file).await.unwrap();
+
+ for line in contents.lines() {
+ let path = PathBuf::from(line);
+
+ if path.is_file() {
+ self.exclude_files.insert(path.clone());
+ } else {
+ self.prepare_exclude_files(&path).await;
+ }
+ }
+ }
+
+ /// prepare input files vector (exclude those in exclude_files if any and also filter out so
+ /// that only the supported extensions is included):
+ #[async_recursion::async_recursion]
+ async fn prepare_input_files(&mut self, path: &PathBuf) {
+ if path.is_file() {
+ self.add_input_file(path);
+ } else {
+ let mut reader = tokio::fs::read_dir(path).await.unwrap();
+ loop {
+ if let Some(f) = reader.next_entry().await.unwrap() {
+ if f.path().is_dir() {
+ self.prepare_input_files(&f.path()).await;
+ } else if f.path().is_file() {
+ self.add_input_file(&f.path());
+ }
+ } else {
+ break;
+ }
+ }
+ }
+ }
+
+ /// add input file if it's extension is valid and not to be excluded:
+ fn add_input_file(&mut self, path: &PathBuf) {
+ if let Some(extension) = path.extension() {
+ let ext = OsString::from(extension);
+ if self.supported_extensions_load.contains(&ext) {
+ if !self.exclude_files.contains(path) {
+ self.input_files.insert(path.clone());
+
+ // hash each value if hashing mode:
+ if !self.cli_args.force {
+ let data = std::fs::read(path).unwrap();
+ data.hash(&mut self.hasher);
+ }
+ }
+ }
+ }
+ }
+
+ /// read include file and parse each line and put dir/file into include vector:
+ async fn read_input_file(&mut self, input_file: &PathBuf) {
+ let contents = tokio::fs::read_to_string(input_file).await.unwrap();
+
+ for line in contents.lines() {
+ let path = PathBuf::from(line);
+
+ if path.is_file() {
+ self.input_files.insert(path.clone());
+ } else {
+ self.prepare_input_files(&path).await;
+ }
+ }
+ }
+
+ /// compares current hash value with previous value if any:
+ async fn check_input_hash(&mut self) -> bool {
+ self.hash_value = self.hasher.finish();
+ let old_hash_value = self.get_old_input_hash().await;
+
+ info!(
+ "Hash value new: {} old: {}",
+ self.hash_value, old_hash_value
+ );
+
+ if !self.cli_args.force && self.cli_args.verbose {
+ self.console.print("[dim]Checked input hash[/]");
+ }
+
+ if self.hash_value == old_hash_value {
+ info!("Identical hash value. No need to continue...");
+ return true;
+ }
+
+ return false;
+ }
+
+ /// returns the hash value stored on disk if any exists:
+ async fn get_old_input_hash(&self) -> u64 {
+ let file_path = format!("{}.hash", self.cli_args.output.display());
+ let result = tokio::fs::read_to_string(file_path).await;
+
+ let old_hash_value = match result {
+ Ok(old_hash_value) => old_hash_value.parse().unwrap(),
+ Err(_) => 0,
+ };
+
+ return old_hash_value;
+ }
+
+ /// removes all atlas related files from previous run with same name if any exists:
+ fn remove_old_files(&self) {
+ let hash_file_path = PathBuf::from(format!("{}.hash", self.cli_args.output.display()));
+ let json_file_path = PathBuf::from(format!("{}.json", self.cli_args.output.display()));
+ let txt_file_path = PathBuf::from(format!("{}.txt", self.cli_args.output.display()));
+
+ if exists_file(&hash_file_path) {
+ remove_file(&hash_file_path);
+ }
+
+ if exists_file(&json_file_path) {
+ remove_file(&json_file_path);
+ }
+
+ if exists_file(&txt_file_path) {
+ remove_file(&txt_file_path);
+ }
+
+ let mut removed = false;
+ for i in 0..4096 {
+ for img_ext in &self.supported_extensions_save {
+ let atlas_file_path = PathBuf::from(format!(
+ "{}{}.{}",
+ self.cli_args.output.display(),
+ i,
+ img_ext.display()
+ ));
+ if exists_file(&atlas_file_path) {
+ remove_file(&atlas_file_path);
+ removed = true;
+ }
+ }
+ if !removed {
+ break;
+ }
+ }
+ }
+
+ /// loads all textures in input_files vector as separate async tasks and then store each in
+ /// textures vector:
+ async fn load_textures(&mut self) {
+ let mut join_handles: Vec<task::JoinHandle<Texture>> = Vec::new();
+ for path in &self.input_files {
+ join_handles.push(tokio::spawn(load_texture(
+ path.clone(),
+ self.cli_args.premultiply,
+ self.cli_args.trim,
+ self.cli_args.adjust_fit,
+ self.cli_args.pad.try_into().unwrap(),
+ self.cli_args.atlas_size as u32,
+ )));
+ }
+
+ if self.cli_args.verbose {
+ if let Ok(_status) = Status::new(&self.console, "Loading textures...") {
+ for join_handle in join_handles {
+ self.textures.push(join_handle.await.unwrap());
+ }
+ }
+ } else {
+ for join_handle in join_handles {
+ self.textures.push(join_handle.await.unwrap());
+ }
+ }
+
+ if self.cli_args.verbose {
+ self.console.print("[dim]Loaded textures[/]");
+ }
+ }
+
+ /// sort all textures by area (from largest to smallest):
+ fn sort_textures(&mut self) {
+ if self.cli_args.verbose {
+ if let Ok(_status) = Status::new(&self.console, "Sorting textures by area...") {
+ self.textures
+ .sort_by(|a, b| a.get_area().cmp(&b.get_area()));
+ }
+ } else {
+ self.textures
+ .sort_by(|a, b| a.get_area().cmp(&b.get_area()));
+ }
+
+ if self.cli_args.verbose {
+ self.console.print("[dim]Sorted textures[/]");
+ }
+ }
+
+ /// pack all loaded textures into 1-n bins of atlas size and store each packer in packers
+ /// vector as those will be used to save atlas images/descriptor later as separate async tasks:
+ fn pack_textures(&mut self) {
+ if self.cli_args.verbose {
+ if let Ok(_status) = Status::new(&self.console, "Packing textures...") {
+ self.pack();
+ }
+ } else {
+ self.pack();
+ }
+
+ if self.cli_args.verbose {
+ self.console.print("[dim]Packed textures[/]");
+ }
+ }
+
+ /// pack textures into each packer of atlas size and remove each packed texture from `textures`
+ /// vector and create as many packers as needed up until `max_atlases` or if texture doesn't fit
+ /// in given `atlas size`:
+ fn pack(&mut self) {
+ while !self.textures.is_empty() {
+ let width: u32 = self.cli_args.atlas_size as u32;
+ let height = width;
+
+ let mut packer = Packer::new(
+ width,
+ height,
+ self.cli_args.pad as i32,
+ self.cli_args.generate_mipmaps,
+ )
+ .unwrap();
+
+ let heuristic = self.convert_rect_heuristic(&self.cli_args.rect_heuristic);
+ packer.pack(
+ &mut self.textures,
+ self.cli_args.unique,
+ self.cli_args.rotate,
+ self.cli_args.force_square,
+ self.cli_args.adjust_size,
+ heuristic,
+ );
+
+ self.packers.push(packer.shared());
+
+ if self.packers.len() > self.cli_args.max_atlases as usize {
+ panic!(
+ "Packing failed. There is a limit of {} atlases being created. Use a larger atlas output size (--atlas-size SIZE)",
+ self.cli_args.max_atlases
+ );
+ }
+
+ let lock = self.packers.last().unwrap().state.lock().unwrap();
+ if lock.textures.is_empty() {
+ panic!(
+ "Packing failed: Could not fit texture {}",
+ self.textures.last().unwrap().file_name
+ );
+ }
+ }
+ }
+
+ /// saves all atlas images to disk:
+ async fn save_atlas_images(&self) {
+ if self.cli_args.verbose {
+ if let Ok(_status) = Status::new(&self.console, "Writing atlas images...") {
+ self.save_images().await;
+ }
+ } else {
+ self.save_images().await;
+ }
+ }
+
+ /// saves all atlas images (an async task for each save operation to speed things up):
+ async fn save_images(&self) {
+ let image_extension = get_atlas_image_extension(self.cli_args.atlas_image);
+ let mut join_handles: Vec<task::JoinHandle<PathBuf>> = Vec::new();
+
+ for i in 0..self.packers.len() {
+ let file_path = PathBuf::from(format!(
+ "{}{}.{}",
+ self.cli_args.output.display(),
+ i,
+ image_extension
+ ));
+
+ if let Some(packer) = self.packers.get(i) {
+ join_handles.push(tokio::spawn(save_image(
+ file_path.clone(),
+ packer.clone(),
+ self.cli_args.atlas_image,
+ )));
+ }
+ }
+
+ for join_handle in join_handles {
+ let file_path = join_handle.await.unwrap();
+ if self.cli_args.verbose {
+ let msg = format!("Wrote '{}'", file_path.display());
+ info!("{}", msg);
+ self.console.print(&format!("[dim]{}[/]", msg));
+ }
+ }
+ }
+
+ /// saves atlas descriptor to disk:
+ fn save_atlas_descriptor(&self) {
+ match self.cli_args.atlas_descriptor {
+ AtlasDescriptor::Json => self.save_atlas_json(),
+ AtlasDescriptor::Txt => self.save_atlas_txt(),
+ AtlasDescriptor::TxtDesc => self.save_atlas_txt(),
+ }
+ }
+
+ /// saves atlas descriptor in JSON format:
+ fn save_atlas_json(&self) {
+ let file_path = PathBuf::from(format!("{}.json", self.cli_args.output.display()));
+ let mut file = std::fs::File::create(file_path.clone()).unwrap();
+
+ file.write(String::from("{\n").as_bytes()).unwrap();
+ file.write(String::from("\t\"ImageAtlas\":\n").as_bytes())
+ .unwrap();
+ file.write(String::from("\t{\n").as_bytes()).unwrap();
+
+ // info part:
+ file.write(String::from("\t\t\"info\":\n").as_bytes())
+ .unwrap();
+ file.write(String::from("\t\t{\n").as_bytes()).unwrap();
+ file.write(format!("\t\t\t\"numberOfAtlasImages\": {},\n", self.packers.len()).as_bytes())
+ .unwrap();
+ file.write(format!("\t\t\t\"generatedWith\": \"{}\"\n", NAME).as_bytes())
+ .unwrap();
+ file.write(String::from("\t\t},\n").as_bytes()).unwrap();
+ file.write(String::from("\t\t\"AtlasImage\":\n").as_bytes())
+ .unwrap();
+ file.write(String::from("\t\t[\n").as_bytes()).unwrap();
+
+ for i in 0..self.packers.len() {
+ let img_ext = get_atlas_image_extension(self.cli_args.atlas_image);
+ let file_path_stripped =
+ PathBuf::from(format!("{}{}", self.cli_args.output.display(), i));
+
+ if let Some(packer) = self.packers.get(i) {
+ if i > 0 {
+ file.write(String::from(",\n").as_bytes()).unwrap();
+ }
+
+ if let Some(file_name) = file_path_stripped.file_name() {
+ if let Some(file_name_str) = file_name.to_str() {
+ packer.save_json(&mut file, file_name_str, &img_ext);
+ }
+ }
+ }
+ }
+ file.write(String::from("\n\t\t]\n").as_bytes()).unwrap();
+ file.write(String::from("\t}\n").as_bytes()).unwrap();
+ file.write(String::from("}\n").as_bytes()).unwrap();
+
+ if self.cli_args.verbose {
+ let msg = format!("Wrote '{}'", file_path.display());
+ info!("{}", msg);
+ self.console.print(&format!("[dim]{}[/]", msg));
+ }
+ }
+
+ /// saves atlas descriptor in TXT format:
+ fn save_atlas_txt(&self) {
+ let file_path = PathBuf::from(format!("{}.txt", self.cli_args.output.display()));
+ let mut file = std::fs::File::create(file_path.clone()).unwrap();
+
+ if self.cli_args.atlas_descriptor == AtlasDescriptor::TxtDesc {
+ self.write_txt_header(&mut file);
+ }
+
+ // info part:
+ file.write(format!("{},{}\n", self.packers.len(), NAME).as_bytes())
+ .unwrap();
+
+ for i in 0..self.packers.len() {
+ let img_ext = get_atlas_image_extension(self.cli_args.atlas_image);
+ let file_path_stripped =
+ PathBuf::from(format!("{}{}", self.cli_args.output.display(), i));
+
+ if let Some(packer) = self.packers.get(i) {
+ if let Some(file_name) = file_path_stripped.file_name() {
+ if let Some(file_name_str) = file_name.to_str() {
+ packer.save_txt(&mut file, file_name_str, &img_ext);
+ }
+ }
+ }
+ }
+
+ if self.cli_args.verbose {
+ let msg = format!("Wrote '{}'", file_path.display());
+ info!("{}", msg);
+ self.console.print(&format!("[dim]{}[/]", msg));
+ }
+ }
+
+ /// writes the description header for TXT atlas descriptor:
+ fn write_txt_header(&self, file: &mut fs::File) {
+ file.write(String::from("/*\n").as_bytes()).unwrap();
+ file.write(
+ String::from("\t ************************************************\n").as_bytes(),
+ )
+ .unwrap();
+ file.write(format!("\t * Generated with: {}\n", NAME).as_bytes())
+ .unwrap();
+ file.write(
+ String::from("\t ************************************************\n").as_bytes(),
+ )
+ .unwrap();
+ file.write(String::from("\n").as_bytes()).unwrap();
+ file.write(
+ String::from("\t ************************************************\n").as_bytes(),
+ )
+ .unwrap();
+ file.write(String::from("\t * Format description:\n").as_bytes())
+ .unwrap();
+ file.write(
+ String::from("\t ************************************************\n").as_bytes(),
+ )
+ .unwrap();
+ file.write(String::from("\t [info]\n").as_bytes()).unwrap();
+ file.write(String::from("\t numberOfAtlasImages,generatedWith\n").as_bytes())
+ .unwrap();
+ file.write(String::from("\n").as_bytes()).unwrap();
+ file.write(String::from("\t [AtlasImage (repeated numberOfAtlasImages)]\n").as_bytes())
+ .unwrap();
+ file.write(String::from("\t atlasImageName,numberOfImages,atlasImageWidth,atlasImageHeight,generateMipMaps\n").as_bytes())
+ .unwrap();
+ file.write(String::from("\n").as_bytes()).unwrap();
+ file.write(String::from("\t [Image (repeated numberOfImages)]\n").as_bytes())
+ .unwrap();
+ file.write(String::from("\t name,x,y,w,h,trimmed,rotated,fx,fy,fw,fh (NOTE: fx,fy,fw,fh valid if trimmed==1)\n").as_bytes())
+ .unwrap();
+ file.write(String::from("\n").as_bytes()).unwrap();
+ file.write(String::from("\t Text format example:\n").as_bytes())
+ .unwrap();
+ file.write(String::from("\t [info]\n").as_bytes()).unwrap();
+ file.write(String::from("\t [AtlasImage]\n").as_bytes())
+ .unwrap();
+ file.write(String::from("\t [Image]\n").as_bytes()).unwrap();
+ file.write(String::from("\t [Image]\n").as_bytes()).unwrap();
+ file.write(String::from("\t ...\n").as_bytes()).unwrap();
+ file.write(String::from("\t [AtlasImage]\n").as_bytes())
+ .unwrap();
+ file.write(String::from("\t [Image]\n").as_bytes()).unwrap();
+ file.write(String::from("\t [Image]\n").as_bytes()).unwrap();
+ file.write(String::from("\t ...\n").as_bytes()).unwrap();
+ file.write(String::from("*/@\n").as_bytes()).unwrap();
+ }
+
+ /// converts rect heuristic to the enum used in flow_rbp:
+ fn convert_rect_heuristic(&self, heuristic: &RectHeuristic) -> FreeRectHeuristic {
+ let output_heuristic = match heuristic {
+ RectHeuristic::ShortSideFit => FreeRectHeuristic::ShortSideFit,
+ RectHeuristic::LongSideFit => FreeRectHeuristic::LongSideFit,
+ RectHeuristic::AreaFit => FreeRectHeuristic::AreaFit,
+ RectHeuristic::BottomLeft => FreeRectHeuristic::BottomLeft,
+ RectHeuristic::ContactPoint => FreeRectHeuristic::ContactPoint,
+ };
+
+ return output_heuristic;
+ }
+
+ /// saves input hash value to disk:
+ fn save_input_hash(&self) {
+ let file_path = PathBuf::from(format!("{}.hash", self.cli_args.output.display()));
+ let data = format!("{}", self.hash_value);
+
+ write_file_sync(&file_path, &data.as_bytes()).unwrap();
+
+ if self.cli_args.verbose {
+ self.console
+ .print(&format!("[dim]Wrote '{}'[/]", file_path.display()));
+ }
+ }
+}
+
+/// Get the atlas image extension for given `atlas_image`.
+///
+/// # Arguments
+///
+/// * `atlas_image` - is the atlas image type.
+pub fn get_atlas_image_extension(atlas_image: AtlasImage) -> String {
+ let image_extension = match atlas_image {
+ AtlasImage::Png => String::from("png"),
+ AtlasImage::Tga => String::from("tga"),
+ AtlasImage::Tiff => String::from("tiff"),
+ AtlasImage::Webp => String::from("webp"),
+ };
+
+ return image_extension;
+}
+
+/// Get the load filter extension for given `load_filter`.
+///
+/// # Arguments
+///
+/// * `load_filter` - is the load filter.
+pub fn get_load_filter_extension(load_filter: LoadFilter) -> String {
+ let image_extension = match load_filter {
+ LoadFilter::Bmp => String::from("bmp"),
+ LoadFilter::Hdr => String::from("hdr"),
+ LoadFilter::Jpg => String::from("jpg"),
+ LoadFilter::Png => String::from("png"),
+ LoadFilter::Tga => String::from("tga"),
+ LoadFilter::Tiff => String::from("tiff"),
+ LoadFilter::Webp => String::from("webp"),
+ };
+
+ return image_extension;
+}
+
+/// Create given `dir` recursively.
+///
+/// # Arguments
+///
+/// * `dir` - is the directory to create recursively.
+///
+/// # Panics
+///
+/// If failed to create given directory.
+pub fn create_dir_all<'a>(dir: &'a PathBuf) {
+ match fs::create_dir_all(dir) {
+ Ok(()) => info!("Created dir: '{}'", dir.display()),
+ Err(err) => panic!(
+ "Failed to create dir: '{}'. Error msg: '{}'",
+ dir.display(),
+ err
+ ),
+ };
+}
+
+/// Remove given `dir` recursively.
+///
+/// # Arguments
+///
+/// * `dir` - is the directory to remove recursively.
+///
+/// # Panics
+///
+/// If failed to remove given directory.
+pub fn remove_dir_all<'a>(dir: &'a PathBuf) {
+ match fs::remove_dir_all(dir) {
+ Ok(()) => info!("Removed dir: '{}'", dir.display()),
+ Err(err) => panic!(
+ "Failed to remove dir: '{}'. Error msg: '{}'",
+ dir.display(),
+ err
+ ),
+ };
+}
+
+/// Remove given `file_path`.
+///
+/// # Arguments
+///
+/// * `file_path` - is the path to file to remove.
+///
+/// # Panics
+///
+/// If failed to remove given `file_path`.
+pub fn remove_file<'a>(file_path: &'a PathBuf) {
+ match fs::remove_file(file_path) {
+ Ok(()) => info!("Removed file: '{}'", file_path.display()),
+ Err(err) => panic!(
+ "Failed to remove file: '{}'. Error msg: '{}'",
+ file_path.display(),
+ err
+ ),
+ };
+}
+
+/// Determine whether given `dir` exists.
+///
+/// # Arguments
+///
+/// * `dir` - is the directory.
+pub fn exists_dir<'a>(dir: &'a PathBuf) -> bool {
+ return dir.as_path().exists();
+}
+
+/// Determine whether given `file_path` exists.
+///
+/// # Arguments
+///
+/// * `file_path` - is the file path.
+pub fn exists_file<'a>(file_path: &'a PathBuf) -> bool {
+ return file_path.is_file();
+}
+
+/// Write `data` to given `file_path`.
+///
+/// # Arguments
+///
+/// * `file_path` - is the file path.
+/// * `data` - is the data to write.
+pub fn write_file_sync<'a>(file_path: &'a PathBuf, data: &'a [u8]) -> io::Result<()> {
+ // create output file:
+ let mut file = std::fs::File::create(file_path)?;
+
+ // write data to file:
+ file.write_all(data)?;
+
+ info!("Wrote '{}' successfully", file_path.display());
+ Ok(())
+}
+
+/// load an individual texture from disk and return it (used for async tasks):
+async fn load_texture(
+ file_path: PathBuf,
+ premultiply: bool,
+ trim: bool,
+ adjust_fit: bool,
+ pad: u32,
+ atlas_size: u32,
+) -> Texture {
+ let mut texture = Texture::new();
+
+ texture.load(&file_path, premultiply, trim, adjust_fit, pad, atlas_size);
+
+ return texture;
+}
+
+/// saves an individual atlas image to disk (used for async tasks):
+async fn save_image(file_path: PathBuf, packer: Arc<Packer>, image_type: AtlasImage) -> PathBuf {
+ packer.save_image(&file_path, image_type);
+ return file_path;
+}
--- /dev/null
+// flow-texpack: A program that will allow you to generate texture atlas.
+// zlib License (see LICENSE)
+
+use log::info;
+use std::collections::HashMap;
+use std::fs::File;
+use std::io::Write;
+use std::path::PathBuf;
+use std::sync::{Arc, Mutex};
+
+use flow_rbp::FreeRectHeuristic;
+use flow_rbp::RectsBinPack;
+
+use crate::Texture;
+use crate::texpack::app::AtlasImage;
+
+/// is the minimum allowed size.
+pub const MIN_SIZE: u32 = 64;
+/// is the maximum allowed size.
+pub const MAX_SIZE: u32 = 8192;
+
+/// Specifies the different error types that can occur.
+#[derive(PartialEq, Clone, Debug)]
+pub enum PackerError {
+ /// Invalid argument
+ InvalidArg,
+}
+
+/// Specifies the properties of a `Point`.
+#[derive(Clone, Debug)]
+pub struct Point {
+ /// is the x offset.
+ pub x: i32,
+ /// is the y offset.
+ pub y: i32,
+ /// is the duplicate ID.
+ pub duplicate_id: usize,
+ /// is the flag determining whether rotated or not.
+ pub rotate: bool,
+}
+
+impl Point {
+ /// Instantiates a new `Point` instance.
+ pub fn new() -> Self {
+ Self {
+ x: 0,
+ y: 0,
+ duplicate_id: 0,
+ rotate: false,
+ }
+ }
+}
+
+/// Specifies the properties of a `PackerState`.
+#[derive(Clone, Debug)]
+pub struct PackerState {
+ /// is the width.
+ pub width: u32,
+ /// is the height.
+ pub height: u32,
+ /// is the padding.
+ pub padding: i32,
+ /// is the flag determining whether mip maps should be generated (rendering hint).
+ pub generate_mipmaps: bool,
+ /// is the vector holding the textures to pack.
+ pub textures: Vec<Texture>,
+ /// is the vector holding the points used for `unique` lookups.
+ pub points: Vec<Point>,
+ /// is the hash map holding texture hash and duplicate_id.
+ pub duplicates: HashMap<u64, usize>,
+}
+
+impl PackerState {
+ /// Instantiates a new `PackerState` instance.
+ ///
+ /// # Arguments
+ ///
+ /// * `width` - is the width.
+ /// * `height` - is the height.
+ /// * `padding` - is the padding.
+ /// * `generate_mipmaps` - is the flag determining whether to generate mip maps (rendering hint)
+ /// or not.
+ pub fn new(width: u32, height: u32, padding: i32, generate_mipmaps: bool) -> Self {
+ Self {
+ width,
+ height,
+ padding,
+ generate_mipmaps,
+ textures: Vec::new(),
+ points: Vec::new(),
+ duplicates: HashMap::new(),
+ }
+ }
+}
+
+/// Specifies the properties of a `Packer`.
+#[derive(Debug)]
+pub struct Packer {
+ /// is the `PackerState` protected by a `Mutex`.
+ pub state: Mutex<PackerState>,
+}
+
+impl Packer {
+ /// Instantiates a new `Packer` instance.
+ ///
+ /// # Arguments
+ ///
+ /// * `width` - is the width.
+ /// * `height` - is the height.
+ /// * `padding` - is the padding.
+ /// * `generate_mipmaps` - is the flag determining whether to generate mip maps (rendering hint)
+ /// or not.
+ ///
+ /// # Errors
+ ///
+ /// [`InvalidArg`](crate::texpack::packer::PackerError) error is returned if:
+ /// `width != SIZE_IN_POWER_OF_TWO || height != SIZE_IN_POWER_OF_TWO ||
+ /// width < 64 || width > 8192 ||
+ /// height < 64 || height > 8192`.
+ pub fn new(
+ width: u32,
+ height: u32,
+ padding: i32,
+ generate_mipmaps: bool,
+ ) -> Result<Self, PackerError> {
+ // make sure width/height is power-of-two and 64 - 8192 in size:
+ if width >= MIN_SIZE
+ && width <= MAX_SIZE
+ && height >= MIN_SIZE
+ && height <= MAX_SIZE
+ && (width & (width - 1)) == 0
+ && (height & (height - 1)) == 0
+ {
+ Ok(Self {
+ state: Mutex::new(PackerState::new(width, height, padding, generate_mipmaps)),
+ })
+ } else {
+ Err(PackerError::InvalidArg)
+ }
+ }
+
+ /// Returns a shared `Arc` `Packer` instance.
+ pub fn shared(self) -> Arc<Self> {
+ Arc::new(self)
+ }
+
+ /// Packs all textures that will fit from given `textures` and pops each packed from vector.
+ ///
+ /// # Arguments
+ ///
+ /// * `textures` - is the vector holding the textures to pack.
+ /// * `unique` - is a flag determining whether to include only unique textures or not (a
+ /// unique texture is determined by its combined hash value of `width`, `height` and `buffer`).
+ /// * `rotate` - is a flag determining whether rotation of textures is allowed or not.
+ /// * `square` - is a flag determining whether packers size must be POWER OF TWO in size for both width
+ /// and height.
+ /// * `adjust_fit` - is a flag determining whether to adjust fit automatically or not.
+ /// * `heuristic` - is the heuristic method to use for determining where to place the texture.
+ ///
+ /// # Panics
+ ///
+ /// If packing fails.
+ pub fn pack(
+ &mut self,
+ textures: &mut Vec<Texture>,
+ unique: bool,
+ rotate: bool,
+ square: bool,
+ adjust_size: bool,
+ heuristic: FreeRectHeuristic,
+ ) {
+ assert!(textures.len() > 0);
+
+ let mut exists_larger = false;
+ if !square {
+ exists_larger = self.exists_larger_texture(textures);
+ }
+
+ // make sure each texture fit within size:
+ if adjust_size {
+ self.adjust_size_to_fit(textures);
+ }
+
+ let mut lock = self.state.lock().unwrap();
+ let mut rbp = RectsBinPack::new(
+ lock.width.try_into().unwrap(),
+ lock.height.try_into().unwrap(),
+ rotate,
+ )
+ .unwrap();
+
+ let mut ww: u32 = 0;
+ let mut hh: u32 = 0;
+ while !textures.is_empty() {
+ if let Some(texture) = textures.last() {
+ if unique {
+ if let Some(value) = lock.duplicates.get(&texture.hash_value) {
+ if let Some(point) = lock.points.get(*value) {
+ info!(
+ "Texture '{}' with hash: {} is not unique (not packed but will be added in descriptor)",
+ texture.file_name, texture.hash_value
+ );
+ let mut p = point.clone();
+ p.duplicate_id = *value;
+ lock.points.push(p);
+ lock.textures.push(texture.clone());
+ textures.pop();
+ continue;
+ }
+ }
+ }
+
+ {
+ let tw: i32 = texture.width.try_into().unwrap();
+ let th: i32 = texture.height.try_into().unwrap();
+ let width: i32 = tw + lock.padding;
+ let height: i32 = th + lock.padding;
+ if let Some(rect) = rbp.insert(width, height, heuristic.clone()) {
+ if unique {
+ let num_points = lock.points.len();
+ lock.duplicates.insert(texture.hash_value, num_points);
+ }
+
+ // check if we rotated:
+ let mut p = Point::new();
+ p.x = rect.x;
+ p.y = rect.y;
+ p.duplicate_id = std::usize::MAX;
+ p.rotate = rotate && tw != rect.width - lock.padding;
+
+ info!(
+ "Packed '{}' w: {} h: {} rotated: {} hash: {}",
+ texture.file_name,
+ texture.width,
+ texture.height,
+ p.rotate,
+ texture.hash_value
+ );
+ lock.points.push(p);
+ lock.textures.push(texture.clone());
+ textures.pop();
+
+ ww = std::cmp::max((rect.x + rect.width).try_into().unwrap(), ww);
+ hh = std::cmp::max((rect.y + rect.height).try_into().unwrap(), hh);
+ } else {
+ break;
+ }
+ }
+ } else {
+ panic!("texture.last() failed!");
+ }
+ }
+
+ // tweak power-of-two size so that it's optimized for largest found width/height:
+ if !square && !exists_larger {
+ while lock.width / 2 >= ww {
+ lock.width /= 2;
+ }
+
+ while lock.height / 2 >= hh {
+ lock.height /= 2;
+ }
+ }
+ }
+
+ /// Saves the packed textures to disk.
+ ///
+ /// # Arguments
+ ///
+ /// * `file_path` - is the output file path.
+ /// * `image_type` - is the output image type.
+ ///
+ /// # Panics
+ ///
+ /// If save fails.
+ pub fn save_image(&self, file_path: &PathBuf, image_type: AtlasImage) {
+ let lock = self.state.lock().unwrap();
+ let mut texture = Texture::with_details(lock.width, lock.height).unwrap();
+
+ for i in 0..lock.textures.len() {
+ if let Some(src) = lock.textures.get(i)
+ && let Some(point) = lock.points.get(i)
+ {
+ if point.duplicate_id == std::usize::MAX {
+ if point.rotate {
+ texture.copy_pixels_rot_90cw(
+ src,
+ point.x.try_into().unwrap(),
+ point.y.try_into().unwrap(),
+ );
+ } else {
+ texture.copy_pixels(
+ src,
+ point.x.try_into().unwrap(),
+ point.y.try_into().unwrap(),
+ );
+ }
+ }
+ }
+ }
+
+ texture.save(file_path, image_type);
+ }
+
+ /// Saves the atlas descriptor to disk in JSON format.
+ ///
+ /// # Arguments
+ ///
+ /// * `file` - is a reference to already opened for write `File` to use when writing.
+ /// * `file_name` - is the name of the atlas image.
+ /// * `image_ext` - is the extension of the atlas image ("png", "tga" etc).
+ pub fn save_json(&self, file: &mut File, file_name: &str, image_ext: &str) {
+ let lock = self.state.lock().unwrap();
+ file.write(String::from("\t\t\t{\n").as_bytes()).unwrap();
+ file.write(format!("\t\t\t\t\"n\": \"{}.{}\",\n", file_name, image_ext).as_bytes())
+ .unwrap();
+ file.write(format!("\t\t\t\t\"numImages\": {},\n", lock.textures.len()).as_bytes())
+ .unwrap();
+ file.write(format!("\t\t\t\t\"width\": {},\n", lock.width).as_bytes())
+ .unwrap();
+ file.write(format!("\t\t\t\t\"height\": {},\n", lock.height).as_bytes())
+ .unwrap();
+ file.write(
+ format!(
+ "\t\t\t\t\"generateMipMaps\": {},\n",
+ lock.generate_mipmaps as u8
+ )
+ .as_bytes(),
+ )
+ .unwrap();
+ file.write(String::from("\t\t\t\t\"img\":\n").as_bytes())
+ .unwrap();
+ file.write(String::from("\t\t\t\t[\n").as_bytes()).unwrap();
+
+ for i in 0..lock.textures.len() {
+ if let Some(texture) = lock.textures.get(i)
+ && let Some(point) = lock.points.get(i)
+ {
+ let mut trimmed = false;
+ if texture.frame_w != texture.width || texture.frame_h != texture.height {
+ trimmed = true;
+ }
+
+ if i > 0 {
+ file.write(String::from(",\n").as_bytes()).unwrap();
+ }
+
+ file.write(String::from("\t\t\t\t\t{\n").as_bytes())
+ .unwrap();
+ file.write(format!("\t\t\t\t\t\t\"n\": \"{}\", ", texture.file_name).as_bytes())
+ .unwrap();
+ file.write(format!("\"x\": {}, ", point.x).as_bytes())
+ .unwrap();
+ file.write(format!("\"y\": {}, ", point.y).as_bytes())
+ .unwrap();
+ file.write(format!("\"w\": {}, ", texture.width).as_bytes())
+ .unwrap();
+ file.write(format!("\"h\": {}, ", texture.height).as_bytes())
+ .unwrap();
+ file.write(format!("\"trimmed\": {}, ", trimmed as u8).as_bytes())
+ .unwrap();
+ file.write(format!("\"rotated\": {}, ", point.rotate as u8).as_bytes())
+ .unwrap();
+ file.write(format!("\"fx\": {}, ", texture.frame_x).as_bytes())
+ .unwrap();
+ file.write(format!("\"fy\": {}, ", texture.frame_y).as_bytes())
+ .unwrap();
+ file.write(format!("\"fw\": {}, ", texture.frame_w).as_bytes())
+ .unwrap();
+ file.write(format!("\"fh\": {}\n", texture.frame_h).as_bytes())
+ .unwrap();
+ file.write(String::from("\t\t\t\t\t}").as_bytes()).unwrap();
+ }
+ }
+ file.write(String::from("\n\t\t\t\t]\n").as_bytes())
+ .unwrap();
+ file.write(String::from("\t\t\t}").as_bytes()).unwrap();
+ }
+
+ /// Saves the atlas descriptor to disk in plain TXT format.
+ ///
+ /// # Arguments
+ ///
+ /// * `file` - is a reference to already opened for write `File` to use when writing.
+ /// * `file_name` - is the name of the atlas image.
+ /// * `image_ext` - is the extension of the atlas image ("png", "tga" etc).
+ pub fn save_txt(&self, file: &mut File, file_name: &str, image_ext: &str) {
+ let lock = self.state.lock().unwrap();
+ file.write(format!("{}.{}", file_name, image_ext).as_bytes())
+ .unwrap();
+ file.write(format!(",{}", lock.textures.len()).as_bytes())
+ .unwrap();
+ file.write(format!(",{}", lock.width).as_bytes()).unwrap();
+ file.write(format!(",{}", lock.height).as_bytes()).unwrap();
+ file.write(format!(",{}\n", lock.generate_mipmaps as u8).as_bytes())
+ .unwrap();
+
+ for i in 0..lock.textures.len() {
+ if let Some(texture) = lock.textures.get(i)
+ && let Some(point) = lock.points.get(i)
+ {
+ let mut trimmed = false;
+ if texture.frame_w != texture.width || texture.frame_h != texture.height {
+ trimmed = true;
+ }
+
+ file.write(format!("{}", texture.file_name).as_bytes())
+ .unwrap();
+ file.write(format!(",{}", point.x).as_bytes()).unwrap();
+ file.write(format!(",{}", point.y).as_bytes()).unwrap();
+ file.write(format!(",{}", texture.width).as_bytes())
+ .unwrap();
+ file.write(format!(",{}", texture.height).as_bytes())
+ .unwrap();
+ file.write(format!(",{}", trimmed as u8).as_bytes())
+ .unwrap();
+ file.write(format!(",{}", point.rotate as u8).as_bytes())
+ .unwrap();
+ file.write(format!(",{}", texture.frame_x).as_bytes())
+ .unwrap();
+ file.write(format!(",{}", texture.frame_y).as_bytes())
+ .unwrap();
+ file.write(format!(",{}", texture.frame_w).as_bytes())
+ .unwrap();
+ file.write(format!(",{}\n", texture.frame_h).as_bytes())
+ .unwrap();
+ }
+ }
+ }
+
+ /// Adjusts the packer width and height so that given `textures` will fit.
+ ///
+ /// # Arguments
+ ///
+ /// * `textures` - is the vector holding the textures.
+ ///
+ /// # Panics
+ ///
+ /// If new adjusted width / height is > 8192.
+ fn adjust_size_to_fit(&mut self, textures: &Vec<Texture>) -> bool {
+ let mut lock = self.state.lock().unwrap();
+ let mut adjusted_size = false;
+ let padding: u32 = lock.padding.try_into().unwrap();
+
+ for i in 0..textures.len() {
+ if let Some(texture) = textures.get(i) {
+ if texture.width + padding > lock.width {
+ lock.width *= 2;
+ lock.height = lock.width;
+ adjusted_size = true;
+ }
+
+ if texture.height + padding > lock.height {
+ lock.height *= 2;
+ lock.width = lock.height;
+ adjusted_size = true;
+ }
+
+ if lock.width > MAX_SIZE || lock.height > MAX_SIZE {
+ panic!(
+ "adjust_size_to_fit failed. Maximum allowed width / height is {}",
+ MAX_SIZE
+ );
+ }
+
+ // make sure width is at least minimum size:
+ if lock.width < MIN_SIZE {
+ lock.width = MIN_SIZE;
+ }
+
+ // make sure height is at least minimum size:
+ if lock.height < MIN_SIZE {
+ lock.height = MIN_SIZE;
+ }
+ }
+ }
+
+ if adjusted_size {
+ info!(
+ "Packer: Adjusted size to {}x{} to fit textures.",
+ lock.width, lock.height
+ );
+ }
+
+ return adjusted_size;
+ }
+
+ /// Determines whether there exists a texture in given `textures` which size is larger then
+ /// that of packer width and height.
+ ///
+ /// # Arguments
+ ///
+ /// * `textures` - is the vector holding the textures.
+ fn exists_larger_texture(&self, textures: &Vec<Texture>) -> bool {
+ let lock = self.state.lock().unwrap();
+ let padding: u32 = lock.padding.try_into().unwrap();
+
+ for i in 0..textures.len() {
+ if let Some(texture) = textures.get(i) {
+ if texture.width + padding > lock.width || texture.height + padding > lock.height {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+}
+
+// unit tests:
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::texpack::app::{exists_file, get_atlas_image_extension, remove_file};
+
+ #[test]
+ fn point_basics() {
+ let p = Point::new();
+
+ assert_eq!(p.x, 0);
+ assert_eq!(p.y, 0);
+ assert_eq!(p.duplicate_id, 0);
+ assert_eq!(p.rotate, false);
+ }
+
+ #[test]
+ fn packer_error() {
+ assert_eq!(
+ Packer::new(0, 0, 1, false).unwrap_err(),
+ PackerError::InvalidArg
+ );
+
+ assert_eq!(
+ Packer::new(32, 32, 1, false).unwrap_err(),
+ PackerError::InvalidArg
+ );
+
+ assert_eq!(
+ Packer::new(8192, 8193, 1, false).unwrap_err(),
+ PackerError::InvalidArg
+ );
+
+ assert_eq!(
+ Packer::new(32, 64, 1, false).unwrap_err(),
+ PackerError::InvalidArg
+ );
+
+ assert_eq!(
+ Packer::new(64, 32, 1, false).unwrap_err(),
+ PackerError::InvalidArg
+ );
+ }
+
+ fn load_textures() -> Vec<Texture> {
+ let mut textures: Vec<Texture> = Vec::new();
+ let mut t1 = Texture::new();
+ t1.load(
+ &PathBuf::from("test_data/white_32x32.png"),
+ false,
+ false,
+ false,
+ 0,
+ 64,
+ );
+ textures.push(t1);
+
+ let mut t2 = Texture::new();
+ t2.load(
+ &PathBuf::from("test_data/red_32x32.png"),
+ false,
+ false,
+ false,
+ 0,
+ 64,
+ );
+ textures.push(t2);
+
+ let mut t3 = Texture::new();
+ t3.load(
+ &PathBuf::from("test_data/green_32x32.png"),
+ false,
+ false,
+ false,
+ 0,
+ 64,
+ );
+ textures.push(t3);
+
+ let mut t4 = Texture::new();
+ t4.load(
+ &PathBuf::from("test_data/blue_32x32.png"),
+ false,
+ false,
+ false,
+ 0,
+ 64,
+ );
+ textures.push(t4);
+
+ return textures;
+ }
+
+ #[test]
+ fn packer_basics_short_side_fit() {
+ let mut textures = load_textures();
+ assert_eq!(textures.len(), 4);
+
+ let mut packer = Packer::new(64, 64, 0, true).unwrap();
+ packer.pack(
+ &mut textures,
+ true,
+ false,
+ false,
+ false,
+ FreeRectHeuristic::ShortSideFit,
+ );
+ assert_eq!(textures.len() == 0, true);
+
+ let file_path = PathBuf::from("test_data/atlas_short_side_fit.png");
+
+ packer.save_image(&file_path, AtlasImage::Png);
+ assert_eq!(exists_file(&file_path), true);
+ remove_file(&file_path);
+ assert_eq!(exists_file(&file_path), false);
+ }
+
+ #[test]
+ fn packer_basics_long_side_fit() {
+ let mut textures = load_textures();
+ assert_eq!(textures.len(), 4);
+
+ let mut packer = Packer::new(64, 64, 0, true).unwrap();
+ packer.pack(
+ &mut textures,
+ true,
+ false,
+ false,
+ false,
+ FreeRectHeuristic::LongSideFit,
+ );
+ assert_eq!(textures.len() == 0, true);
+
+ let file_path = PathBuf::from("test_data/atlas_long_side_fit.png");
+
+ packer.save_image(&file_path, AtlasImage::Png);
+ assert_eq!(exists_file(&file_path), true);
+ remove_file(&file_path);
+ assert_eq!(exists_file(&file_path), false);
+ }
+
+ #[test]
+ fn packer_basics_area_fit() {
+ let mut textures = load_textures();
+ assert_eq!(textures.len(), 4);
+
+ let mut packer = Packer::new(64, 64, 0, true).unwrap();
+ packer.pack(
+ &mut textures,
+ true,
+ false,
+ false,
+ false,
+ FreeRectHeuristic::AreaFit,
+ );
+ assert_eq!(textures.len() == 0, true);
+
+ let file_path = PathBuf::from("test_data/atlas_area_fit.png");
+
+ packer.save_image(&file_path, AtlasImage::Png);
+ assert_eq!(exists_file(&file_path), true);
+ remove_file(&file_path);
+ assert_eq!(exists_file(&file_path), false);
+ }
+
+ #[test]
+ fn packer_basics_bottom_left() {
+ let mut textures = load_textures();
+ assert_eq!(textures.len(), 4);
+
+ let mut packer = Packer::new(64, 64, 0, true).unwrap();
+ packer.pack(
+ &mut textures,
+ true,
+ false,
+ false,
+ false,
+ FreeRectHeuristic::BottomLeft,
+ );
+ assert_eq!(textures.len() == 0, true);
+
+ let file_path = PathBuf::from("test_data/atlas_bottom_left.png");
+
+ packer.save_image(&file_path, AtlasImage::Png);
+ assert_eq!(exists_file(&file_path), true);
+ remove_file(&file_path);
+ assert_eq!(exists_file(&file_path), false);
+ }
+
+ #[test]
+ fn packer_basics_contact_point() {
+ let mut textures = load_textures();
+ assert_eq!(textures.len(), 4);
+
+ let mut packer = Packer::new(64, 64, 0, true).unwrap();
+ packer.pack(
+ &mut textures,
+ true,
+ false,
+ false,
+ false,
+ FreeRectHeuristic::ContactPoint,
+ );
+ assert_eq!(textures.len() == 0, true);
+
+ let file_path = PathBuf::from("test_data/atlas_contact_point.png");
+
+ packer.save_image(&file_path, AtlasImage::Png);
+ assert_eq!(exists_file(&file_path), true);
+ remove_file(&file_path);
+ assert_eq!(exists_file(&file_path), false);
+ }
+
+ #[test]
+ fn packer_adjust_size_to_fit() {
+ let mut textures: Vec<Texture> = Vec::new();
+ let mut t1 = Texture::new();
+ t1.load(
+ &PathBuf::from("test_data/white_128x128.png"),
+ false,
+ false,
+ true,
+ 0,
+ 64,
+ );
+ textures.push(t1);
+
+ let mut packer = Packer::new(64, 64, 0, true).unwrap();
+ packer.pack(
+ &mut textures,
+ true,
+ false,
+ false,
+ false,
+ FreeRectHeuristic::ContactPoint,
+ );
+ assert_eq!(textures.len() == 0, true);
+
+ let file_path = PathBuf::from("test_data/atlas_adjust_size_to_fit.png");
+
+ packer.save_image(&file_path, AtlasImage::Png);
+ assert_eq!(exists_file(&file_path), true);
+
+ let mut output = Texture::new();
+ output.load(
+ &PathBuf::from("test_data/atlas_adjust_size_to_fit.png"),
+ false,
+ false,
+ false,
+ 0,
+ 64,
+ );
+ assert_eq!(output.width, 64);
+ assert_eq!(output.height, 64);
+
+ remove_file(&file_path);
+ assert_eq!(exists_file(&file_path), false);
+ }
+
+ #[test]
+ fn packer_trim() {
+ let mut textures: Vec<Texture> = Vec::new();
+ let mut t1 = Texture::new();
+ t1.load(
+ &PathBuf::from("test_data/blue_trimmable_128x128.png"),
+ false,
+ true,
+ false,
+ 0,
+ 128,
+ );
+ textures.push(t1);
+
+ let mut packer = Packer::new(128, 128, 0, true).unwrap();
+ packer.pack(
+ &mut textures,
+ true,
+ false,
+ false,
+ false,
+ FreeRectHeuristic::BottomLeft,
+ );
+ assert_eq!(textures.len() == 0, true);
+
+ let file_path = PathBuf::from("test_data/atlas_trimmed.png");
+
+ packer.save_image(&file_path, AtlasImage::Png);
+ assert_eq!(exists_file(&file_path), true);
+
+ let mut output = Texture::new();
+ output.load(
+ &PathBuf::from("test_data/atlas_trimmed.png"),
+ false,
+ false,
+ false,
+ 0,
+ 64,
+ );
+ assert_eq!(output.width, 32);
+ assert_eq!(output.height, 32);
+
+ remove_file(&file_path);
+ assert_eq!(exists_file(&file_path), false);
+ }
+
+ #[test]
+ fn packer_rotated() {
+ let mut textures: Vec<Texture> = Vec::new();
+ let mut t1 = Texture::new();
+ t1.load(
+ &PathBuf::from("test_data/white_128x64.png"),
+ false,
+ false,
+ false,
+ 0,
+ 64,
+ );
+ textures.push(t1);
+ assert_eq!(textures.len(), 1);
+
+ let mut packer = Packer::new(64, 128, 0, true).unwrap();
+ packer.pack(
+ &mut textures,
+ true,
+ true,
+ false,
+ false,
+ FreeRectHeuristic::LongSideFit,
+ );
+ assert_eq!(textures.len() == 0, true);
+
+ let file_path = PathBuf::from("test_data/atlas_rotated.png");
+
+ packer.save_image(&file_path, AtlasImage::Png);
+ assert_eq!(exists_file(&file_path), true);
+
+ let mut output = Texture::new();
+ output.load(
+ &PathBuf::from("test_data/atlas_rotated.png"),
+ false,
+ false,
+ false,
+ 0,
+ 64,
+ );
+ assert_eq!(output.width, 64);
+ assert_eq!(output.height, 128);
+
+ remove_file(&file_path);
+ assert_eq!(exists_file(&file_path), false);
+ }
+
+ #[test]
+ fn packer_save_all_supported_types() {
+ let mut textures = load_textures();
+ assert_eq!(textures.len(), 4);
+
+ let mut packer = Packer::new(64, 64, 0, true).unwrap();
+ packer.pack(
+ &mut textures,
+ true,
+ false,
+ false,
+ false,
+ FreeRectHeuristic::ShortSideFit,
+ );
+ assert_eq!(textures.len() == 0, true);
+
+ let base_file_path = PathBuf::from("test_data/atlas_save");
+ let image_types = vec![
+ AtlasImage::Png,
+ AtlasImage::Tga,
+ AtlasImage::Tiff,
+ AtlasImage::Webp,
+ ];
+
+ for image_type in image_types {
+ let file_path = PathBuf::from(format!(
+ "{}.{}",
+ base_file_path.display(),
+ get_atlas_image_extension(image_type.clone())
+ ));
+
+ println!("{}", file_path.display());
+
+ packer.save_image(&file_path, image_type.clone());
+ assert_eq!(exists_file(&file_path), true);
+ remove_file(&file_path);
+ assert_eq!(exists_file(&file_path), false);
+ }
+ }
+}
--- /dev/null
+// flow-texpack: A program that will allow you to generate texture atlas.
+// zlib License (see LICENSE)
+
+use crate::texpack::app::AtlasImage;
+use crate::texpack::packer::MAX_SIZE;
+
+use image::GenericImageView;
+use image::{DynamicImage, ImageBuffer, ImageReader, Rgba, RgbaImage, imageops::FilterType};
+use log::info;
+use std::hash::{DefaultHasher, Hash, Hasher};
+use std::path::PathBuf;
+
+/// Specifies the different error types that can occur.
+#[derive(PartialEq, Clone, Debug)]
+pub enum TextureError {
+ /// Invalid argument
+ InvalidArg,
+}
+
+/// Specifies the properties of a `Texture`.
+#[derive(Clone, Debug)]
+pub struct Texture {
+ /// is the file path.
+ pub file_path: PathBuf,
+ /// is the file name.
+ pub file_name: String,
+ /// is the width.
+ pub width: u32,
+ /// is the height.
+ pub height: u32,
+ /// is the orignal x position (valid if trimmed).
+ pub frame_x: i32,
+ /// is the orignal y position (valid if trimmed).
+ pub frame_y: i32,
+ /// is the orignal width (valid if trimmed).
+ pub frame_w: u32,
+ /// is the orignal height (valid if trimmed).
+ pub frame_h: u32,
+ /// is the hash value (width, height and buffer combined).
+ pub hash_value: u64,
+ /// is the raw buffer in RGBA format.
+ pub buffer: RgbaImage,
+}
+
+impl Texture {
+ /// Instantiates a new `Texture` instance.
+ pub fn new() -> Self {
+ Self {
+ file_path: PathBuf::new(),
+ file_name: String::new(),
+ width: 0,
+ height: 0,
+ frame_x: 0,
+ frame_y: 0,
+ frame_w: 0,
+ frame_h: 0,
+ hash_value: 0,
+ buffer: RgbaImage::new(1, 1),
+ }
+ }
+
+ /// Instantiates a new `Texture` instance based on given input params.
+ ///
+ /// # Arguments
+ ///
+ /// * `width` - is the `Texture` width.
+ /// * `height` - is the `Texture` height.
+ ///
+ /// # Errors
+ ///
+ /// [`InvalidArg`](crate::texpack::texture::TextureError) error is returned if:
+ /// `width == 0 || width > 8192 ||
+ /// height == 0 || height > 8192`.
+ pub fn with_details(width: u32, height: u32) -> Result<Self, TextureError> {
+ if width > 0 && width <= MAX_SIZE && height > 0 && height <= MAX_SIZE {
+ Ok(Self {
+ file_path: PathBuf::new(),
+ file_name: String::new(),
+ width,
+ height,
+ frame_x: 0,
+ frame_y: 0,
+ frame_w: 0,
+ frame_h: 0,
+ hash_value: 0,
+ buffer: RgbaImage::new(width, height),
+ })
+ } else {
+ Err(TextureError::InvalidArg)
+ }
+ }
+
+ /// Load texture.
+ ///
+ /// # Arguments
+ ///
+ /// * `file_path` - is the texture file path.
+ /// * `premultiply` - is a flag determining whether to premultiply RBG by alpha channel or not.
+ /// * `trim` - is a flag determining whether to trim excess transparent pixels or not.
+ /// * `adjust_fit` - is a flag determining whether to adjust fit automatically or not.
+ /// * `padding` - is the padding to use between textures.
+ /// * `atlas_size` - is the atlas size.
+ ///
+ /// # Panics
+ ///
+ /// If loading fails.
+ pub fn load(
+ &mut self,
+ file_path: &PathBuf,
+ premultiply: bool,
+ trim: bool,
+ adjust_fit: bool,
+ padding: u32,
+ atlas_size: u32,
+ ) {
+ // remember file path and file name:
+ self.file_path = file_path.clone();
+ if let Some(file_name) = file_path.file_name() {
+ if let Some(file_name_str) = file_name.to_str() {
+ self.file_name = String::from(file_name_str);
+ }
+ }
+
+ // load the image:
+ let image = ImageReader::open(file_path).unwrap().decode().unwrap();
+ let (width, height) = image.dimensions();
+ self.update_initial_size(width, height);
+
+ // trim excess transparent pixels off the texture:
+ if trim {
+ self.buffer = self.trim(&image.to_rgba8());
+ } else {
+ self.buffer = image.to_rgba8();
+ }
+
+ // premultiply all the pixels by their alpha value:
+ if premultiply {
+ self.premultiply();
+ }
+
+ // check if needing to adjust / scale texture size to fit atlas size:
+ if adjust_fit
+ && (((self.width + padding) > atlas_size) || ((self.height + padding) > atlas_size))
+ {
+ self.buffer = self.resize_to_fit(padding, atlas_size);
+ }
+
+ let mut hasher = DefaultHasher::new();
+ self.width.hash(&mut hasher);
+ self.height.hash(&mut hasher);
+ self.buffer.hash(&mut hasher);
+ self.hash_value = hasher.finish();
+
+ info!(
+ "Loaded texture: '{}' w: {} h: {}, hash_value: {}",
+ self.file_name, self.width, self.height, self.hash_value
+ );
+ }
+
+ /// Saves texture to disk.
+ ///
+ /// # Arguments
+ ///
+ /// * `file_path` - is the output file path.
+ /// * `image_type` - is the output image type.
+ ///
+ /// # Panics
+ ///
+ /// If save fails.
+ pub fn save(&self, file_path: &PathBuf, image_type: AtlasImage) {
+ let dst_image = DynamicImage::ImageRgba8(self.buffer.clone());
+
+ // make sure extension supplied is valid to help out with 'guessing' of type:
+ if let Some(extension) = file_path.extension() {
+ let ext_lc = extension.to_ascii_lowercase();
+ if image_type == AtlasImage::Png && ext_lc == "png"
+ || image_type == AtlasImage::Tga && ext_lc == "tga"
+ || image_type == AtlasImage::Tiff && ext_lc == "tiff"
+ || image_type == AtlasImage::Webp && ext_lc == "webp"
+ {
+ dst_image.save(file_path).unwrap();
+ } else {
+ panic!(
+ "Supplied file_path: {} does not have a valid extension that matches image type: {:?}!",
+ file_path.display(),
+ image_type
+ );
+ }
+ }
+ }
+
+ /// Copy pixels from given `src` texture into this texture.
+ ///
+ /// # Arguments
+ ///
+ /// * `src` - is the source texture to copy from.
+ /// * `tx` - is the x offset to use when copying.
+ /// * `ty` - is the y offset to use when copying.
+ ///
+ /// # Panics
+ ///
+ /// If pixel is out of bounds.
+ pub fn copy_pixels(&mut self, src: &Texture, tx: u32, ty: u32) {
+ let (src_width, src_height) = src.buffer.dimensions();
+
+ for y in 0..src_height {
+ for x in 0..src_width {
+ let pixel = src.buffer.get_pixel(x, y);
+ self.buffer.put_pixel(x + tx, y + ty, *pixel);
+ }
+ }
+ }
+
+ /// Copy pixels from given `src` texture into this texture rotated 90 degrees clockwise.
+ ///
+ /// # Arguments
+ ///
+ /// * `src` - is the source texture to copy from.
+ /// * `tx` - is the x offset to use when copying.
+ /// * `ty` - is the y offset to use when copying.
+ pub fn copy_pixels_rot_90cw(&mut self, src: &Texture, tx: u32, ty: u32) {
+ let (src_width, src_height) = src.buffer.dimensions();
+ let r = src_height - 1;
+
+ for y in 0..src_height {
+ for x in 0..src_width {
+ let pixel = src.buffer.get_pixel(x, y);
+ self.buffer.put_pixel(r - y + tx, x + ty, *pixel);
+ }
+ }
+ }
+
+ /// Get the texture area (width * height).
+ pub fn get_area(&self) -> u32 {
+ return self.width * self.height;
+ }
+
+ /// Updates initial size properties.
+ fn update_initial_size(&mut self, width: u32, height: u32) {
+ self.frame_x = 0;
+ self.frame_y = 0;
+ self.frame_w = width;
+ self.frame_h = height;
+ self.width = width;
+ self.height = height;
+ }
+
+ /// Trims out excess pixels and returns new RBGA buffer.
+ fn trim(&mut self, img: &RgbaImage) -> RgbaImage {
+ let (width, height) = img.dimensions();
+
+ if width == 0 || height == 0 {
+ return ImageBuffer::new(1, 1);
+ }
+
+ // bounding box of non-transparent pixels:
+ let mut min_x = width;
+ let mut min_y = height;
+ let mut max_x = 0u32;
+ let mut max_y = 0u32;
+
+ for y in 0..height {
+ for x in 0..width {
+ let pixel = img.get_pixel(x, y);
+ if pixel[3] > 0 {
+ // non-transparent pixel:
+ min_x = min_x.min(x);
+ min_y = min_y.min(y);
+ max_x = max_x.max(x);
+ max_y = max_y.max(y);
+ }
+ }
+ }
+
+ // no non-transparent pixels found, return 1x1 transparent buffer:
+ if max_x < min_x || max_y < min_y {
+ return ImageBuffer::from_pixel(1, 1, Rgba([0, 0, 0, 0]));
+ }
+
+ // calc new dimensions (add 1 for bounds):
+ let new_width = (max_x - min_x) + 1;
+ let new_height = (max_y - min_y) + 1;
+
+ // no trimming needed -> just clone it:
+ if new_width == width && new_height == height {
+ return img.clone();
+ }
+
+ // crop the image:
+ let mut trimmed = ImageBuffer::new(new_width, new_height);
+ for y in 0..new_height {
+ for x in 0..new_width {
+ let src_pixel = img.get_pixel(min_x + x, min_y + y);
+ trimmed.put_pixel(x, y, *src_pixel);
+ }
+ }
+
+ let x: i32 = min_x.try_into().unwrap();
+ let y: i32 = min_y.try_into().unwrap();
+ self.frame_x = -x;
+ self.frame_y = -y;
+ self.frame_w = width;
+ self.frame_h = height;
+ self.width = new_width;
+ self.height = new_height;
+
+ return trimmed;
+ }
+
+ /// Premultiply destination pixel by alpha.
+ fn premultiply(&mut self) {
+ let (width, height) = self.buffer.dimensions();
+
+ for y in 0..height {
+ for x in 0..width {
+ // get source pixel:
+ let src_pixel = self.buffer.get_pixel(x, y);
+
+ // premultiply destination pixel by alpha:
+ let alpha = src_pixel[3] as f32 / 255.0;
+ let dst_pixel = Rgba([
+ (src_pixel[0] as f32 * alpha) as u8,
+ (src_pixel[1] as f32 * alpha) as u8,
+ (src_pixel[2] as f32 * alpha) as u8,
+ src_pixel[3],
+ ]);
+
+ // set new pixel value:
+ self.buffer.put_pixel(x, y, dst_pixel);
+ }
+ }
+ }
+
+ /// Resize buffer so that it fits given `atlas_size + padding`.
+ fn resize_to_fit(&mut self, padding: u32, atlas_size: u32) -> RgbaImage {
+ let (src_width, src_height) = self.buffer.dimensions();
+
+ if src_width == 0 || src_height == 0 || atlas_size == 0 {
+ panic!("Invalid internal buffer state or atlas_size is 0");
+ }
+
+ // calculate scale factor:
+ let mut _scale_factor = 0.0;
+ if src_width > src_height {
+ _scale_factor = src_width as f32 / atlas_size as f32;
+ } else if src_height > src_width {
+ _scale_factor = src_height as f32 / atlas_size as f32;
+ } else {
+ _scale_factor = src_width as f32 / atlas_size as f32;
+ }
+ info!("scale_factor is: {}", _scale_factor);
+
+ // calculate new size of texture:
+ let mut new_width = (src_width as f32 / _scale_factor).floor() as u32;
+ let mut new_height = (src_height as f32 / _scale_factor).floor() as u32;
+
+ // adjust for padding too:
+ new_width -= padding;
+ new_height -= padding;
+
+ // make sure width and height is at least 1 pixel after scaling and padding:
+ if new_width <= 0 {
+ new_width = 1;
+ }
+ if new_height <= 0 {
+ new_height = 1;
+ }
+
+ let src_image = DynamicImage::ImageRgba8(self.buffer.clone());
+ let dst_image = src_image.resize(new_width, new_height, FilterType::Lanczos3);
+ info!(
+ "Resized image from {}x{} to {}x{}",
+ src_width, src_height, new_width, new_height
+ );
+
+ // reset:
+ self.frame_x = 0;
+ self.frame_y = 0;
+ self.frame_w = new_width;
+ self.frame_h = new_height;
+ self.width = new_width;
+ self.height = new_height;
+
+ return dst_image.to_rgba8();
+ }
+}
+
+// unit tests:
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::texpack::app::{exists_file, get_atlas_image_extension, remove_file};
+ use std::ffi::OsString;
+
+ #[test]
+ fn texture_error() {
+ assert_eq!(
+ Texture::with_details(0, 0).unwrap_err(),
+ TextureError::InvalidArg
+ );
+
+ assert_eq!(
+ Texture::with_details(32, 0).unwrap_err(),
+ TextureError::InvalidArg
+ );
+
+ assert_eq!(
+ Texture::with_details(0, 32).unwrap_err(),
+ TextureError::InvalidArg
+ );
+
+ assert_eq!(
+ Texture::with_details(8192, 8193).unwrap_err(),
+ TextureError::InvalidArg
+ );
+
+ assert_eq!(
+ Texture::with_details(8193, 8192).unwrap_err(),
+ TextureError::InvalidArg
+ );
+ }
+
+ #[test]
+ fn texture_basics() {
+ let t1 = Texture::new();
+ assert_eq!(t1.width, 0);
+ assert_eq!(t1.height, 0);
+ assert_eq!(t1.frame_x, 0);
+ assert_eq!(t1.frame_y, 0);
+ assert_eq!(t1.frame_w, 0);
+ assert_eq!(t1.frame_h, 0);
+ assert_eq!(t1.hash_value, 0);
+
+ let t2 = Texture::with_details(32, 32).unwrap();
+ assert_eq!(t2.width, 32);
+ assert_eq!(t2.height, 32);
+ }
+
+ #[test]
+ fn texture_load_all_supported_formats() {
+ let supported_extensions = vec![
+ OsString::from("bmp"),
+ OsString::from("hdr"),
+ OsString::from("jpg"),
+ OsString::from("jpeg"),
+ OsString::from("png"),
+ OsString::from("tga"),
+ OsString::from("tiff"),
+ OsString::from("webp"),
+ ];
+
+ for ext in &supported_extensions {
+ let base_file_path = "test_data/white_32x32";
+ let file_path = PathBuf::from(format!("{}.{}", base_file_path, ext.display()));
+
+ let mut t = Texture::new();
+ t.load(&file_path, false, false, false, 0, 64);
+ assert_eq!(t.width, 32);
+ assert_eq!(t.height, 32);
+ assert_eq!(t.file_path, file_path);
+ if let Some(file_name) = file_path.file_name() {
+ if let Some(file_name_str) = file_name.to_str() {
+ assert_eq!(t.file_name, file_name_str);
+ }
+ }
+ assert_eq!(t.frame_x, 0);
+ assert_eq!(t.frame_y, 0);
+ assert_eq!(t.frame_w, 32);
+ assert_eq!(t.frame_h, 32);
+ assert_eq!(t.hash_value > 0, true);
+ }
+ }
+
+ #[test]
+ fn texture_save_all_supported_formats() {
+ let atlas_image_types = vec![
+ AtlasImage::Png,
+ AtlasImage::Tga,
+ AtlasImage::Tiff,
+ AtlasImage::Webp,
+ ];
+
+ let mut t1 = Texture::new();
+ t1.load(
+ &PathBuf::from("test_data/white_32x32.png"),
+ false,
+ false,
+ false,
+ 0,
+ 64,
+ );
+
+ let mut output = Texture::with_details(64, 64).unwrap();
+ output.copy_pixels(&t1, 16, 16);
+
+ let base_file_path = "test_data/save_64x64";
+ for atlas_image_type in &atlas_image_types {
+ let file_path = PathBuf::from(format!(
+ "{}.{}",
+ base_file_path,
+ get_atlas_image_extension(atlas_image_type.clone())
+ ));
+ output.save(&file_path, atlas_image_type.clone());
+
+ assert_eq!(exists_file(&file_path), true);
+ remove_file(&file_path);
+ assert_eq!(exists_file(&file_path), false);
+ }
+ }
+
+ #[test]
+ fn texture_copy_pixels() {
+ let mut t1 = Texture::new();
+ t1.load(
+ &PathBuf::from("test_data/white_32x32.png"),
+ false,
+ false,
+ false,
+ 0,
+ 64,
+ );
+
+ let mut t2 = Texture::new();
+ t2.load(
+ &PathBuf::from("test_data/red_32x32.png"),
+ false,
+ false,
+ false,
+ 0,
+ 64,
+ );
+
+ let mut t3 = Texture::new();
+ t3.load(
+ &PathBuf::from("test_data/green_32x32.png"),
+ false,
+ false,
+ false,
+ 0,
+ 64,
+ );
+
+ let mut t4 = Texture::new();
+ t4.load(
+ &PathBuf::from("test_data/blue_32x32.png"),
+ false,
+ false,
+ false,
+ 0,
+ 64,
+ );
+
+ let mut output = Texture::with_details(64, 64).unwrap();
+ let file_path = PathBuf::from("test_data/copy_pixels_64x64.png");
+
+ output.copy_pixels(&t1, 0, 0);
+ output.copy_pixels(&t2, 32, 0);
+ output.copy_pixels(&t3, 32, 32);
+ output.copy_pixels(&t4, 0, 32);
+
+ output.save(&file_path, AtlasImage::Png);
+ assert_eq!(exists_file(&file_path), true);
+ remove_file(&file_path);
+ assert_eq!(exists_file(&file_path), false);
+ }
+
+ #[test]
+ fn texture_copy_pixels_rot_90cw() {
+ let mut t1 = Texture::new();
+ t1.load(
+ &PathBuf::from("test_data/white_32x16.png"),
+ false,
+ false,
+ false,
+ 0,
+ 64,
+ );
+
+ let mut output = Texture::with_details(64, 64).unwrap();
+ let file_path = PathBuf::from("test_data/copy_pixels_rot_90cw_64x64.png");
+
+ output.copy_pixels_rot_90cw(&t1, 0, 0);
+ output.copy_pixels_rot_90cw(&t1, 32, 0);
+
+ output.save(&file_path, AtlasImage::Png);
+ assert_eq!(exists_file(&file_path), true);
+ remove_file(&file_path);
+ assert_eq!(exists_file(&file_path), false);
+ }
+
+ #[test]
+ fn texture_get_area() {
+ let t1 = Texture::with_details(32, 32).unwrap();
+ assert_eq!(t1.get_area(), 1024);
+ }
+}
--- /dev/null
+#?RADIANCE
+SOFTWARE=GEGL
+FORMAT=32-bit_rle_rgbe
+
+-Y 32 +X 32
+\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81\80\80\80\81
\ No newline at end of file
--- /dev/null
+// flow-texpack: A program that will allow you to generate texture atlas.
+// public domain License
+
+use assert_cmd::cargo::*;
+use std::path::PathBuf;
+
+use flow_texpack::{Texture, exists_file, remove_dir_all, remove_file};
+
+fn verify_texture_size(image_path: &PathBuf, width: u32, height: u32) {
+ let mut t1 = Texture::new();
+ t1.load(image_path, false, false, false, 0, 1024);
+ assert_eq!(t1.width, width);
+ assert_eq!(t1.height, height);
+}
+
+fn validate_descriptor_json(json_path: &PathBuf) {
+ let contents = std::fs::read_to_string(json_path).unwrap();
+ let mut lines = contents.lines();
+
+ assert_eq!(Some("{"), lines.next());
+ assert_eq!(Some("\t\"ImageAtlas\":"), lines.next());
+ assert_eq!(Some("\t{"), lines.next());
+ assert_eq!(Some("\t\t\"info\":"), lines.next());
+ assert_eq!(Some("\t\t{"), lines.next());
+ assert_eq!(Some("\t\t\t\"numberOfAtlasImages\": 1,"), lines.next());
+ assert_eq!(
+ Some("\t\t\t\"generatedWith\": \"https://luflow.net/git-repos/flow-texpack.git\""),
+ lines.next()
+ );
+ assert_eq!(Some("\t\t},"), lines.next());
+ assert_eq!(Some("\t\t\"AtlasImage\":"), lines.next());
+ assert_eq!(Some("\t\t["), lines.next());
+
+ // files part:
+ assert_eq!(Some("\t\t\t{"), lines.next());
+ assert_eq!(Some("\t\t\t\t\"n\": \"atlas0.png\","), lines.next());
+ assert_eq!(Some("\t\t\t\t\"numImages\": 2,"), lines.next());
+ assert_eq!(Some("\t\t\t\t\"width\": 64,"), lines.next());
+ assert_eq!(Some("\t\t\t\t\"height\": 64,"), lines.next());
+ assert_eq!(Some("\t\t\t\t\"generateMipMaps\": 1,"), lines.next());
+ assert_eq!(Some("\t\t\t\t\"img\":"), lines.next());
+ assert_eq!(Some("\t\t\t\t["), lines.next());
+ assert_eq!(Some("\t\t\t\t\t{"), lines.next());
+ assert_eq!(
+ Some(
+ "\t\t\t\t\t\t\"n\": \"white_32x32.png\", \"x\": 0, \"y\": 0, \"w\": 32, \"h\": 32, \"trimmed\": 0, \"rotated\": 0, \"fx\": 0, \"fy\": 0, \"fw\": 32, \"fh\": 32"
+ ),
+ lines.next()
+ );
+ assert_eq!(Some("\t\t\t\t\t},"), lines.next());
+ assert_eq!(Some("\t\t\t\t\t{"), lines.next());
+ assert_eq!(
+ Some(
+ "\t\t\t\t\t\t\"n\": \"white_32x16.png\", \"x\": 0, \"y\": 36, \"w\": 32, \"h\": 16, \"trimmed\": 0, \"rotated\": 0, \"fx\": 0, \"fy\": 0, \"fw\": 32, \"fh\": 16"
+ ),
+ lines.next()
+ );
+ assert_eq!(Some("\t\t\t\t\t}"), lines.next());
+ assert_eq!(Some("\t\t\t\t]"), lines.next());
+ assert_eq!(Some("\t\t\t}"), lines.next());
+
+ assert_eq!(Some("\t\t]"), lines.next());
+ assert_eq!(Some("\t}"), lines.next());
+ assert_eq!(Some("}"), lines.next());
+}
+
+fn validate_descriptor_txt(txt_path: &PathBuf, with_header: bool) {
+ let contents = std::fs::read_to_string(txt_path).unwrap();
+ let mut lines = contents.lines();
+
+ if with_header {
+ assert_eq!(Some("/*"), lines.next());
+ assert_eq!(
+ Some("\t ************************************************"),
+ lines.next()
+ );
+ assert_eq!(
+ Some("\t * Generated with: https://luflow.net/git-repos/flow-texpack.git"),
+ lines.next()
+ );
+ assert_eq!(
+ Some("\t ************************************************"),
+ lines.next()
+ );
+ assert_eq!(Some(""), lines.next());
+ assert_eq!(
+ Some("\t ************************************************"),
+ lines.next()
+ );
+ assert_eq!(Some("\t * Format description:"), lines.next());
+ assert_eq!(
+ Some("\t ************************************************"),
+ lines.next()
+ );
+ assert_eq!(Some("\t [info]"), lines.next());
+ assert_eq!(Some("\t numberOfAtlasImages,generatedWith"), lines.next());
+ assert_eq!(Some(""), lines.next());
+ assert_eq!(
+ Some("\t [AtlasImage (repeated numberOfAtlasImages)]"),
+ lines.next()
+ );
+ assert_eq!(
+ Some(
+ "\t atlasImageName,numberOfImages,atlasImageWidth,atlasImageHeight,generateMipMaps"
+ ),
+ lines.next()
+ );
+ assert_eq!(Some(""), lines.next());
+ assert_eq!(Some("\t [Image (repeated numberOfImages)]"), lines.next());
+ assert_eq!(
+ Some(
+ "\t name,x,y,w,h,trimmed,rotated,fx,fy,fw,fh (NOTE: fx,fy,fw,fh valid if trimmed==1)"
+ ),
+ lines.next()
+ );
+ assert_eq!(Some(""), lines.next());
+ assert_eq!(Some("\t Text format example:"), lines.next());
+ assert_eq!(Some("\t [info]"), lines.next());
+ assert_eq!(Some("\t [AtlasImage]"), lines.next());
+ assert_eq!(Some("\t [Image]"), lines.next());
+ assert_eq!(Some("\t [Image]"), lines.next());
+ assert_eq!(Some("\t ..."), lines.next());
+ assert_eq!(Some("\t [AtlasImage]"), lines.next());
+ assert_eq!(Some("\t [Image]"), lines.next());
+ assert_eq!(Some("\t [Image]"), lines.next());
+ assert_eq!(Some("\t ..."), lines.next());
+ assert_eq!(Some("*/@"), lines.next());
+ }
+
+ assert_eq!(
+ Some("1,https://luflow.net/git-repos/flow-texpack.git"),
+ lines.next()
+ );
+ assert_eq!(Some("atlas0.png,2,64,64,1"), lines.next());
+ assert_eq!(
+ Some("white_32x32.png,0,0,32,32,0,0,0,0,32,32"),
+ lines.next()
+ );
+ assert_eq!(
+ Some("white_32x16.png,0,36,32,16,0,0,0,0,32,16"),
+ lines.next()
+ );
+}
+
+fn get_hash_value(hash_path: &PathBuf) -> u64 {
+ let result = std::fs::read_to_string(hash_path);
+
+ let old_hash_value = match result {
+ Ok(old_hash_value) => old_hash_value.parse().unwrap(),
+ Err(_) => 0,
+ };
+
+ return old_hash_value;
+}
+
+// *************************************************
+// Integration tests that should fail:
+// *************************************************
+#[test]
+fn cli_error_no_arguments() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+#[test]
+fn cli_error_no_input() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("-i");
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+#[test]
+fn cli_error_no_input_v2() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("--input");
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+#[test]
+fn cli_error_no_input_v3() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("-i").arg("test_data2");
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+#[test]
+fn cli_error_no_input_file() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("--input-file");
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+#[test]
+fn cli_error_no_input_file_v2() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("--input-file").arg("test_data/input_not_found.txt");
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+#[test]
+fn cli_error_no_exclude_file() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("-i").arg("test_data").arg("--exclude-file");
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+#[test]
+fn cli_error_no_exclude_file_v2() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("-i")
+ .arg("test_data")
+ .arg("--exclude-file")
+ .arg("test_data/exclude_not_found.txt");
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+#[test]
+fn cli_error_no_output() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("-i").arg("test_data").arg("-o");
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+#[test]
+fn cli_error_no_output_v2() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("-i").arg("test_data").arg("--output");
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+#[test]
+fn cli_error_invalid_atlas_descriptor() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("-i")
+ .arg("test_data")
+ .arg("-o")
+ .arg("test_data/out/atlas")
+ .arg("--atlas-descriptor")
+ .arg("invalid");
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+#[test]
+fn cli_error_invalid_atlas_image() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("-i")
+ .arg("test_data")
+ .arg("-o")
+ .arg("test_data/out/atlas")
+ .arg("--atlas-image")
+ .arg("invalid");
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+#[test]
+fn cli_error_invalid_atlas_size() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("-i")
+ .arg("test_data")
+ .arg("-o")
+ .arg("test_data/out/atlas")
+ .arg("--atlas-size")
+ .arg("invalid");
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+#[test]
+fn cli_error_invalid_load_filter() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("-i")
+ .arg("test_data")
+ .arg("-o")
+ .arg("test_data/out/atlas")
+ .arg("--load-filter")
+ .arg("invalid");
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+#[test]
+fn cli_error_invalid_max_atlases() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("-i")
+ .arg("test_data")
+ .arg("-o")
+ .arg("test_data/out/atlas")
+ .arg("--max-atlases")
+ .arg("4097");
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+#[test]
+fn cli_error_invalid_padding() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("-i")
+ .arg("test_data")
+ .arg("-o")
+ .arg("test_data/out/atlas")
+ .arg("-p")
+ .arg("17");
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+#[test]
+fn cli_error_invalid_padding_v2() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("-i")
+ .arg("test_data")
+ .arg("-o")
+ .arg("test_data/out/atlas")
+ .arg("--pad")
+ .arg("17");
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+#[test]
+fn cli_error_invalid_rect_heuristic() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("-i")
+ .arg("test_data")
+ .arg("-o")
+ .arg("test_data/out/atlas")
+ .arg("--rect-heuristic")
+ .arg("invalid");
+ cmd.assert().failure();
+
+ Ok(())
+}
+
+// *************************************************
+// Integration tests that should succeed:
+// *************************************************
+#[test]
+fn cli_success_input_defaults() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path);
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_v2() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_v2/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("--input")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path);
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_with_exclude_defaults() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_with_exclude_defaults/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("test_data/blue_32x32.png")
+ .arg("-e")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path);
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_with_exclude_defaults_v2() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_with_exclude_defaults_v2/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("test_data/blue_32x32.png")
+ .arg("--exclude")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path);
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_verbose() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_verbose/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("-v");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_verbose_v2() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_verbose_v2/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--verbose");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_log() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_log/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("-l");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+ let log_path = PathBuf::from("flow-texpack.log");
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+ assert_eq!(exists_file(&log_path), true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+ remove_file(&log_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+ assert_eq!(exists_file(&log_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_version() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("-V");
+ cmd.assert().success();
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_version_v2() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("--version");
+ cmd.assert().success();
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_help() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("-h");
+ cmd.assert().success();
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_help_v2() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+
+ cmd.arg("--help");
+ cmd.assert().success();
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_force() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_force/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("-f");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) == 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_force_v2() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_force_v2/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--force");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) == 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_force_square() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_force_square/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x16.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--force-square");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 1024, 1024);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_atlas_image_png() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_atlas_image_png/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/green_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-image")
+ .arg("png");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_atlas_image_tga() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_atlas_image_tga/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/green_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-image")
+ .arg("tga");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.tga", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_atlas_image_tiff() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_atlas_image_tiff/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/green_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-image")
+ .arg("tiff");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.tiff", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_atlas_image_webp() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_atlas_image_webp/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/green_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-image")
+ .arg("webp");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.webp", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_atlas_descriptor_json() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_atlas_descriptor_json/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/green_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-descriptor")
+ .arg("json");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_atlas_descriptor_txt() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_atlas_descriptor_txt/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/green_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-descriptor")
+ .arg("txt");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.txt", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_atlas_descriptor_txt_desc() -> Result<(), Box<dyn std::error::Error>>
+{
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_atlas_descriptor_txt_desc/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/green_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-descriptor")
+ .arg("txt");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.txt", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_atlas_size_64() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_atlas_size_64/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-size")
+ .arg("pot64")
+ .arg("--force-square");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_atlas_size_128() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_atlas_size_128/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-size")
+ .arg("pot128")
+ .arg("--force-square");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 128, 128);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_atlas_size_256() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_atlas_size_256/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-size")
+ .arg("pot256")
+ .arg("--force-square");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 256, 256);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_atlas_size_512() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_atlas_size_512/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-size")
+ .arg("pot512")
+ .arg("--force-square");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 512, 512);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_atlas_size_1024() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_atlas_size_1024/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-size")
+ .arg("pot1024")
+ .arg("--force-square");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 1024, 1024);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_atlas_size_2048() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_atlas_size_2048/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-size")
+ .arg("pot2048")
+ .arg("--force-square");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 2048, 2048);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_atlas_size_4096() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_atlas_size_4096/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-size")
+ .arg("pot4096")
+ .arg("--force-square");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 4096, 4096);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_atlas_size_8192() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_atlas_size_8192/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-size")
+ .arg("pot8192")
+ .arg("--force-square");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 8192, 8192);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_rect_heuristic_short_side_fit()
+-> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_rect_heuristic_short_side_fit/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("test_data/red_32x32.png")
+ .arg("test_data/green_32x32.png")
+ .arg("test_data/blue_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("-p")
+ .arg("0")
+ .arg("--atlas-size")
+ .arg("pot64")
+ .arg("--rect-heuristic")
+ .arg("short-side-fit");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_rect_heuristic_long_side_fit()
+-> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_rect_heuristic_long_side_fit/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("test_data/red_32x32.png")
+ .arg("test_data/green_32x32.png")
+ .arg("test_data/blue_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("-p")
+ .arg("0")
+ .arg("--atlas-size")
+ .arg("pot64")
+ .arg("--rect-heuristic")
+ .arg("long-side-fit");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_rect_heuristic_area_fit() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_rect_heuristic_area_fit/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("test_data/red_32x32.png")
+ .arg("test_data/green_32x32.png")
+ .arg("test_data/blue_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("-p")
+ .arg("0")
+ .arg("--atlas-size")
+ .arg("pot64")
+ .arg("--rect-heuristic")
+ .arg("area-fit");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_rect_heuristic_bottom_left() -> Result<(), Box<dyn std::error::Error>>
+{
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_rect_heuristic_bottom_left/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("test_data/red_32x32.png")
+ .arg("test_data/green_32x32.png")
+ .arg("test_data/blue_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("-p")
+ .arg("0")
+ .arg("--atlas-size")
+ .arg("pot64")
+ .arg("--rect-heuristic")
+ .arg("bottom-left");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_rect_heuristic_contact_point()
+-> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_rect_heuristic_contact_point/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("test_data/red_32x32.png")
+ .arg("test_data/green_32x32.png")
+ .arg("test_data/blue_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("-p")
+ .arg("0")
+ .arg("--atlas-size")
+ .arg("pot64")
+ .arg("--rect-heuristic")
+ .arg("contact-point");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_premultiply() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_premultiply/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("-p")
+ .arg("0");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 32, 32);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_trim() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_trim/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/blue_trimmable_128x128.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("-p")
+ .arg("0")
+ .arg("-t");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 32, 32);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_unique() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_unique/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_128x128.png")
+ .arg("test_data/white_128x128_v2.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("-p")
+ .arg("0")
+ .arg("-u");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 128, 128);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_rotate() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_rotate/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_128x64.png")
+ .arg("test_data/white_32x16.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--rect-heuristic")
+ .arg("long-side-fit")
+ .arg("-r");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 256, 128);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_adjust_size() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_adjust_size/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_128x128.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-size")
+ .arg("pot64")
+ .arg("-p")
+ .arg("0")
+ .arg("--adjust-size");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 128, 128);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_adjust_fit() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_adjust_fit/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_128x128.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-size")
+ .arg("pot64")
+ .arg("-p")
+ .arg("0")
+ .arg("--adjust-fit");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_validate_descriptor_json() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_validate_descriptor_json/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("test_data/white_32x16.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-size")
+ .arg("pot64")
+ .arg("--generate-mipmaps")
+ .arg("-p")
+ .arg("4");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+ validate_descriptor_json(&json_path);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_validate_descriptor_txt() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_validate_descriptor_txt/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("test_data/white_32x16.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-size")
+ .arg("pot64")
+ .arg("--atlas-descriptor")
+ .arg("txt")
+ .arg("--generate-mipmaps")
+ .arg("-p")
+ .arg("4");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let txt_path = PathBuf::from(format!("{}.txt", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&txt_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+ validate_descriptor_txt(&txt_path, false);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&txt_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_validate_descriptor_txt_header()
+-> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_validate_descriptor_txt_header/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("test_data/white_32x16.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--atlas-size")
+ .arg("pot64")
+ .arg("--atlas-descriptor")
+ .arg("txt-desc")
+ .arg("--generate-mipmaps")
+ .arg("--pad")
+ .arg("4");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let txt_path = PathBuf::from(format!("{}.txt", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&txt_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+ validate_descriptor_txt(&txt_path, true);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&txt_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_load_filter_bmp() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_load_filter_bmp/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.bmp")
+ .arg("test_data/white_128x128.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--load-filter")
+ .arg("bmp");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_load_filter_hdr() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_load_filter_hdr/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.hdr")
+ .arg("test_data/white_128x128.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--load-filter")
+ .arg("hdr");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_load_filter_jpg() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_load_filter_jpg/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.jpg")
+ .arg("test_data/white_128x128.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--load-filter")
+ .arg("jpg");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_load_filter_png() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_load_filter_png/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.png")
+ .arg("test_data/white_128x128.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--load-filter")
+ .arg("png");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 256, 256);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_load_filter_tga() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_load_filter_tga/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.tga")
+ .arg("test_data/white_128x128.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--load-filter")
+ .arg("tga");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_load_filter_tiff() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_load_filter_tiff/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.tiff")
+ .arg("test_data/white_128x128.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--load-filter")
+ .arg("tiff");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}
+
+#[test]
+fn cli_success_input_defaults_load_filter_webp() -> Result<(), Box<dyn std::error::Error>> {
+ let mut cmd = cargo_bin_cmd!("flow-texpack");
+ let out_file_path = "test_data/cli_success_input_defaults_load_filter_webp/atlas";
+ let parent_path = PathBuf::from(PathBuf::from(out_file_path).parent().unwrap());
+
+ cmd.arg("-i")
+ .arg("test_data/white_32x32.webp")
+ .arg("test_data/white_128x128.png")
+ .arg("-o")
+ .arg(out_file_path)
+ .arg("--load-filter")
+ .arg("webp");
+ cmd.assert().success();
+
+ let hash_path = PathBuf::from(format!("{}.hash", out_file_path));
+ let json_path = PathBuf::from(format!("{}.json", out_file_path));
+ let image_path = PathBuf::from(format!("{}0.png", out_file_path));
+
+ assert_eq!(exists_file(&hash_path), true);
+ assert_eq!(exists_file(&json_path), true);
+ assert_eq!(exists_file(&image_path), true);
+ assert_eq!(get_hash_value(&hash_path) > 0, true);
+
+ verify_texture_size(&image_path, 64, 64);
+
+ remove_dir_all(&parent_path);
+
+ assert_eq!(exists_file(&hash_path), false);
+ assert_eq!(exists_file(&json_path), false);
+ assert_eq!(exists_file(&image_path), false);
+
+ Ok(())
+}