diff --git a/gifski-api/Cargo.lock b/gifski-api/Cargo.lock index 2504518..c3e2cbd 100644 --- a/gifski-api/Cargo.lock +++ b/gifski-api/Cargo.lock @@ -69,15 +69,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bytemuck" -version = "1.7.3" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f" +checksum = "44f8cb64b4147a528e1e9e77583739e683541973295b35f3bd7e78d42c5971fd" [[package]] name = "cc" -version = "1.0.72" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" [[package]] name = "cexpr" @@ -96,9 +96,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clang-sys" -version = "1.3.1" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cc00842eed744b858222c4c9faf7243aafc6d33f92f96935263ef4d8a41ce21" +checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b" dependencies = [ "glob", "libc", @@ -107,20 +107,29 @@ dependencies = [ [[package]] name = "clap" -version = "3.0.14" +version = "3.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63edc3f163b3c71ec8aa23f9bd6070f77edbf3d1d198b164afa90ff00e4ec62" +checksum = "29e724a68d9319343bb3328c9cc2dfde263f4b3142ee1059a9980580171c954b" dependencies = [ "atty", "bitflags", + "clap_lex", "indexmap", - "lazy_static", - "os_str_bytes", + "once_cell", "strsim", "termcolor", "textwrap", ] +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -138,9 +147,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.2" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" dependencies = [ "cfg-if", "crossbeam-utils", @@ -148,9 +157,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -159,25 +168,26 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.7" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00d6d2ea26e8b151d99093005cb442fb9a37aeaca582a03ec70946f49ab5ed9" +checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" dependencies = [ + "autocfg", "cfg-if", "crossbeam-utils", - "lazy_static", "memoffset", + "once_cell", "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.7" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" dependencies = [ "cfg-if", - "lazy_static", + "once_cell", ] [[package]] @@ -188,24 +198,24 @@ checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" [[package]] name = "either" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" [[package]] name = "fallible_collections" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52db5973b6a19247baf19b30f41c23a1bfffc2e9ce0a5db2f60e3cd5dc8895f7" +checksum = "c195cf4b2285d3c993eb887b4dc56b0d5728bbe1d0f9a99c0ac6bec2da3e4d85" dependencies = [ "hashbrown", ] [[package]] name = "ffmpeg-next" -version = "5.0.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "154799d4348e5105d775d72fe53eb32ebb31e7019d963914c687633e894ded92" +checksum = "4676cda947a87a1e8a42e154059c567e75de64860252cce52c684acd8c074fa0" dependencies = [ "bitflags", "ffmpeg-sys-next", @@ -214,9 +224,8 @@ dependencies = [ [[package]] name = "ffmpeg-sys-next" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81bd9adbcd2903847d680e0d2b0df95f0d514ad5f037a5504e1be1706bd98e0" +version = "4.4.0" +source = "git+https://github.com/kornelski/rust-ffmpeg-sys-1?rev=78458039d5fac99354f9cb078869f3be3f3af5fb#78458039d5fac99354f9cb078869f3be3f3af5fb" dependencies = [ "bindgen", "cc", @@ -228,32 +237,30 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ - "cfg-if", "crc32fast", - "libc", "miniz_oxide", ] [[package]] name = "getrandom" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] name = "gif" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a7187e78088aead22ceedeee99779455b23fc231fe13ec443f99bb71694e5b" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" dependencies = [ "color_quant", "weezl", @@ -282,7 +289,7 @@ dependencies = [ [[package]] name = "gifski" -version = "1.7.0" +version = "1.7.1" dependencies = [ "clap", "crossbeam-channel", @@ -296,6 +303,7 @@ dependencies = [ "lodepng", "loop9", "natord", + "num-traits", "pbr", "quick-error", "resize", @@ -311,9 +319,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ "ahash", ] @@ -329,12 +337,11 @@ dependencies = [ [[package]] name = "imagequant" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f533cecb7eb061d19dee3c938d0e302c02193270497483e1b662a0a1a5e343" +checksum = "fd9c246b5e902559d1f619b08a08f872cb88b03afe21ca9ec0df146f2fb1d34d" dependencies = [ "arrayvec", - "fallible_collections", "noisy_float", "once_cell", "rayon", @@ -344,15 +351,15 @@ dependencies = [ [[package]] name = "imgref" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d0c0db6c932f8262e0ed8909f2e7f8c0e9b1cfb4da884267ce09a10be54365" +checksum = "7d6a8cd48d0dc604999b8beacfbfc2f4eb289a52af175029d4f79fb57b80c7d5" [[package]] name = "indexmap" -version = "1.8.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown", @@ -372,9 +379,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.118" +version = "0.2.131" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06e509672465a0504304aa87f9f176f2b2b716ed8fb105ebe5c02dc6dce96a94" +checksum = "04c3b4822ccebfa39c02fc03d1534441b22ead323fa0f48bb7ddd8e6ba076a40" [[package]] name = "libloading" @@ -388,9 +395,9 @@ dependencies = [ [[package]] name = "lodepng" -version = "3.6.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f84e1fdcdbe8b3f0f9caaadb6b86d0e0647786e993f6ea70686f6837b989ec7" +checksum = "ff45534ec797452c044fcd47861059eddb501e30a8fd9fdadea7957cdff3ebc7" dependencies = [ "crc32fast", "fallible_collections", @@ -410,9 +417,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" @@ -431,12 +438,11 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.4.4" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" dependencies = [ "adler", - "autocfg", ] [[package]] @@ -456,20 +462,19 @@ dependencies = [ [[package]] name = "nom" -version = "7.1.0" +version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" dependencies = [ "memchr", "minimal-lexical", - "version_check", ] [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] @@ -486,18 +491,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" [[package]] name = "os_str_bytes" -version = "6.0.0" +version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" -dependencies = [ - "memchr", -] +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" [[package]] name = "pbr" @@ -519,17 +521,17 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "pkg-config" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] name = "proc-macro2" -version = "1.0.36" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] @@ -540,18 +542,18 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.15" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] [[package]] name = "rayon" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" dependencies = [ "autocfg", "crossbeam-deque", @@ -561,37 +563,36 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "lazy_static", "num_cpus", ] [[package]] name = "regex" -version = "1.5.4" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "resize" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a32135a3cd4df690ee74dc0e1a77ffe2cdd4611b4e58b84df26a856d92b4ef" +checksum = "d05ed0e778666d123be79444a5ddb81506fda18ea6d7c05a8b6a701a6215d310" dependencies = [ "fallible_collections", "rgb", @@ -599,9 +600,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a374af9a0e5fdcdd98c1c7b64f05004f9ea2555b6c75f211daa81268a3c50f1" +checksum = "c3b221de559e4a29df3b957eec92bc0de6bc8eaf6ca9cfed43e5e1d67ff65a34" dependencies = [ "bytemuck", ] @@ -632,18 +633,18 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "termcolor" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] [[package]] name = "textwrap" -version = "0.14.2" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "thread_local" @@ -656,19 +657,20 @@ dependencies = [ [[package]] name = "time" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] [[package]] -name = "unicode-xid" -version = "0.2.2" +name = "unicode-ident" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" [[package]] name = "vcpkg" @@ -684,21 +686,27 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "weezl" -version = "0.1.5" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" [[package]] name = "wild" -version = "2.0.4" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "035793abb854745033f01a07647a79831eba29ec0be377205f2a25b0aa830020" +checksum = "05b116685a6be0c52f5a103334cbff26db643826c7b3735fc0a3ba9871310a74" dependencies = [ "glob", ] diff --git a/gifski-api/Cargo.toml b/gifski-api/Cargo.toml index 7744e78..6351b02 100644 --- a/gifski-api/Cargo.toml +++ b/gifski-api/Cargo.toml @@ -6,13 +6,14 @@ documentation = "https://docs.rs/gifski" homepage = "https://gif.ski" include = ["/README.md", "/Cargo.toml", "/src/**/*.rs", "/src/bin/*.rs"] keywords = ["gif", "encoder", "converter", "maker", "gifquant"] -license = "AGPL-3.0+" +license = "AGPL-3.0-or-later" name = "gifski" readme = "README.md" repository = "https://github.com/ImageOptim/gifski" -version = "1.7.0" +version = "1.7.1" autobins = false edition = "2021" +rust-version = "1.57" [[bin]] doctest = false @@ -25,12 +26,12 @@ required-features = ["png"] [dependencies] gifsicle = { version = "1.92.5", optional = true } -clap = { version = "3.0.14", features = ["cargo"], optional = true } +clap = { version = "3.1.18", features = ["cargo"], optional = true } gif = "0.11.3" gif-dispose = "3.1.1" -imagequant = "4.0.0" -imgref = "1.9.1" -lodepng = { version = "3.6.1", optional = true } +imagequant = "4.0.1" +imgref = "1.9.2" +lodepng = { version = "3.7.0", optional = true } pbr = { version = "1.0.4", optional = true } resize = "0.7.2" rgb = "0.8.31" @@ -40,10 +41,12 @@ quick-error = "2.0.1" dunce = { version = "1.0.2", optional = true } crossbeam-channel = "0.5.2" loop9 = "0.1.3" +# noisy-float 0.2 bug +num-traits = { version = "0.2.14", features = ["i128", "std"] } [dependencies.ffmpeg] package = "ffmpeg-next" -version = ">= 4.4.0, <6" +version = "4.4.0" optional = true default-features = false features = ["codec", "format", "filter", "software-resampling", "software-scaling"] @@ -57,6 +60,7 @@ openmp = [] # deprecated, obsolete openmp-static = [] # deprecated, obsolete video = ["ffmpeg"] video-static = ["video", "ffmpeg/build"] +video-prebuilt-static = ["video", "ffmpeg/static"] [lib] path = "src/lib.rs" diff --git a/gifski-api/README.md b/gifski-api/README.md index 9ea6634..715dcea 100644 --- a/gifski-api/README.md +++ b/gifski-api/README.md @@ -14,7 +14,7 @@ See [releases](https://github.com/ImageOptim/gifski/releases) page for executabl If you have [Homebrew](https://brew.sh/), you can also get it with `brew install gifski`. -If you have [Rust](https://www.rust-lang.org/install.html) 1.49+, you can also build it from source with [`cargo install gifski`](https://lib.rs/crates/gifski). +If you have [Rust](https://www.rust-lang.org/install.html) 1.57+, you can also build it from source with [`cargo install gifski`](https://lib.rs/crates/gifski). ## Usage diff --git a/gifski-api/src/bin/gifski.rs b/gifski-api/src/bin/gifski.rs index 76ae595..8b45b61 100644 --- a/gifski-api/src/bin/gifski.rs +++ b/gifski-api/src/bin/gifski.rs @@ -1,5 +1,3 @@ -#[macro_use] extern crate clap; - use std::ffi::OsStr; use std::io::Read; use gifski::{Settings, Repeat}; @@ -14,7 +12,7 @@ use gifski::progress::{NoProgress, ProgressBar, ProgressReporter}; pub type BinResult> = Result; -use clap::{App, AppSettings, Arg}; +use clap::{Command, AppSettings, Arg}; use std::env; use std::fmt; @@ -41,12 +39,12 @@ fn main() { #[allow(clippy::float_cmp)] fn bin_main() -> BinResult<()> { - let matches = App::new(crate_name!()) - .version(crate_version!()) + let matches = Command::new(clap::crate_name!()) + .version(clap::crate_version!()) .about("https://gif.ski by Kornel Lesiński") .setting(AppSettings::DeriveDisplayOrder) - .setting(AppSettings::ArgRequiredElseHelp) - .setting(AppSettings::AllowNegativeNumbers) + .arg_required_else_help(true) + .allow_negative_numbers(true) .arg(Arg::new("output") .long("output") .short('o') @@ -54,6 +52,7 @@ fn bin_main() -> BinResult<()> { .forbid_empty_values(true) .takes_value(true) .value_name("a.gif") + .allow_invalid_utf8(true) .required(true)) .arg(Arg::new("fps") .long("fps") @@ -110,7 +109,7 @@ fn bin_main() -> BinResult<()> { .help(VIDEO_FRAMES_ARG_HELP) .min_values(1) .forbid_empty_values(true) - .use_delimiter(false) + .use_value_delimiter(false) .required(true)) .arg(Arg::new("repeat") .long("repeat") diff --git a/gifski-api/src/c_api.rs b/gifski-api/src/c_api.rs index b9348cf..7305411 100644 --- a/gifski-api/src/c_api.rs +++ b/gifski-api/src/c_api.rs @@ -515,7 +515,7 @@ fn progress_abort() { assert_eq!(GifskiError::OK, gifski_set_write_callback(g, Some(cb), ptr::null_mut())); assert_eq!(GifskiError::OK, gifski_add_frame_rgb(g, 0, 1, 3, 1, &RGB::new(0,0,0), 3.)); assert_eq!(GifskiError::OK, gifski_add_frame_rgb(g, 0, 1, 3, 1, &RGB::new(0,0,0), 10.)); - assert_eq!(GifskiError::OK, gifski_finish(g)); + assert_eq!(GifskiError::ABORTED, gifski_finish(g)); } } @@ -533,7 +533,7 @@ fn cant_write_after_finish() { } unsafe { assert_eq!(GifskiError::OK, gifski_set_write_callback(g, Some(cb), 0 as _)); - assert_eq!(GifskiError::OTHER, gifski_finish(g)); + assert_eq!(GifskiError::INVALID_STATE, gifski_finish(g)); } } diff --git a/gifski-api/src/denoise.rs b/gifski-api/src/denoise.rs index 737b04b..1680f46 100644 --- a/gifski-api/src/denoise.rs +++ b/gifski-api/src/denoise.rs @@ -67,6 +67,9 @@ pub struct Denoiser { metadatas: Vec, } +#[derive(Debug)] +pub struct WrongSizeError; + impl Denoiser { #[inline] pub fn new(width: usize, height: usize, quality: u8) -> Self { @@ -118,9 +121,10 @@ impl Denoiser { } } - pub fn push_frame(&mut self, frame: ImgRef, frame_metadata: T) { - assert_eq!(frame.width(), self.splat.width()); - assert_eq!(frame.height(), self.splat.height()); + pub fn push_frame(&mut self, frame: ImgRef, frame_metadata: T) -> Result<(), WrongSizeError> { + if frame.width() != self.splat.width() || frame.height() != self.splat.height() { + return Err(WrongSizeError); + } self.metadatas.insert(0, frame_metadata); @@ -128,7 +132,7 @@ impl Denoiser { // Can't output anything yet if self.frames < LOOKAHEAD { self.quick_append(frame); - return; + return Ok(()); } let mut median = Vec::with_capacity(frame.width() * frame.height()); @@ -144,6 +148,7 @@ impl Denoiser { let median = ImgVec::new(median, frame.width(), frame.height()); let imp_map = ImgVec::new(imp_map, frame.width(), frame.height()); self.processed.insert(0, (median, imp_map)); + Ok(()) } pub fn pop(&mut self) -> Denoised { @@ -303,7 +308,7 @@ fn px(f: Denoised) -> (RGBA8, T) { fn one() { let mut d = Denoiser::new(1,1, 100); let w = RGBA8::new(255,255,255,255); - d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 0); + d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 0).unwrap(); assert!(matches!(d.pop(), Denoised::NotYet)); d.flush(); assert_eq!(px(d.pop()), (w, 0)); @@ -315,8 +320,8 @@ fn two() { let mut d = Denoiser::new(1,1, 100); let w = RGBA8::new(254,253,252,255); let b = RGBA8::new(8,7,0,255); - d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 0); - d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), 1); + d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 0).unwrap(); + d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), 1).unwrap(); assert!(matches!(d.pop(), Denoised::NotYet)); d.flush(); assert_eq!(px(d.pop()), (w, 0)); @@ -329,9 +334,9 @@ fn three() { let mut d = Denoiser::new(1,1, 100); let w = RGBA8::new(254,253,252,255); let b = RGBA8::new(8,7,0,255); - d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 0); - d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), 1); - d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), 2); + d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 0).unwrap(); + d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), 1).unwrap(); + d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), 2).unwrap(); assert!(matches!(d.pop(), Denoised::NotYet)); d.flush(); assert_eq!(px(d.pop()), (w, 0)); @@ -347,10 +352,10 @@ fn four() { let w = RGBA8::new(254,253,252,255); let b = RGBA8::new(8,7,0,255); let t = RGBA8::new(0,0,0,0); - d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 0); - d.push_frame(ImgVec::new(vec![t], 1, 1).as_ref(), 1); - d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), 2); - d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 3); + d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 0).unwrap(); + d.push_frame(ImgVec::new(vec![t], 1, 1).as_ref(), 1).unwrap(); + d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), 2).unwrap(); + d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 3).unwrap(); assert!(matches!(d.pop(), Denoised::NotYet)); d.flush(); assert_eq!(px(d.pop()), (w, 0)); @@ -366,12 +371,12 @@ fn five() { let w = RGBA8::new(254,253,252,255); let b = RGBA8::new(8,7,0,255); let t = RGBA8::new(0,0,0,0); - d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 0); - d.push_frame(ImgVec::new(vec![t], 1, 1).as_ref(), 1); - d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), 2); - d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), 3); + d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 0).unwrap(); + d.push_frame(ImgVec::new(vec![t], 1, 1).as_ref(), 1).unwrap(); + d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), 2).unwrap(); + d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), 3).unwrap(); assert!(matches!(d.pop(), Denoised::NotYet)); - d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 4); + d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 4).unwrap(); assert_eq!(px(d.pop()), (w, 0)); d.flush(); assert_eq!(px(d.pop()), (t, 1)); @@ -388,17 +393,17 @@ fn six() { let b = RGBA8::new(8,7,0,255); let t = RGBA8::new(0,0,0,0); let x = RGBA8::new(4,5,6,255); - d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 0); + d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 0).unwrap(); assert!(matches!(d.pop(), Denoised::NotYet)); - d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), 1); + d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), 1).unwrap(); assert!(matches!(d.pop(), Denoised::NotYet)); - d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), 2); + d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), 2).unwrap(); assert!(matches!(d.pop(), Denoised::NotYet)); - d.push_frame(ImgVec::new(vec![t], 1, 1).as_ref(), 3); + d.push_frame(ImgVec::new(vec![t], 1, 1).as_ref(), 3).unwrap(); assert!(matches!(d.pop(), Denoised::NotYet)); - d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 4); + d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), 4).unwrap(); assert_eq!(px(d.pop()), (w, 0)); - d.push_frame(ImgVec::new(vec![x], 1, 1).as_ref(), 5); + d.push_frame(ImgVec::new(vec![x], 1, 1).as_ref(), 5).unwrap(); d.flush(); assert_eq!(px(d.pop()), (b, 1)); assert_eq!(px(d.pop()), (b, 2)); @@ -415,19 +420,19 @@ fn many() { let w = RGBA8::new(255,254,253,255); let b = RGBA8::new(1,2,3,255); let t = RGBA8::new(0,0,0,0); - d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), "w0"); + d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), "w0").unwrap(); assert!(matches!(d.pop(), Denoised::NotYet)); - d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), "w1"); + d.push_frame(ImgVec::new(vec![w], 1, 1).as_ref(), "w1").unwrap(); assert!(matches!(d.pop(), Denoised::NotYet)); - d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), "b2"); + d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), "b2").unwrap(); assert!(matches!(d.pop(), Denoised::NotYet)); - d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), "b3"); + d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), "b3").unwrap(); assert!(matches!(d.pop(), Denoised::NotYet)); - d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), "b4"); + d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), "b4").unwrap(); assert_eq!(px(d.pop()), (w, "w0")); - d.push_frame(ImgVec::new(vec![t], 1, 1).as_ref(), "t5"); + d.push_frame(ImgVec::new(vec![t], 1, 1).as_ref(), "t5").unwrap(); assert_eq!(px(d.pop()), (w, "w1")); - d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), "b6"); + d.push_frame(ImgVec::new(vec![b], 1, 1).as_ref(), "b6").unwrap(); assert_eq!(px(d.pop()), (b, "b2")); d.flush(); assert_eq!(px(d.pop()), (b, "b3")); diff --git a/gifski-api/src/lib.rs b/gifski-api/src/lib.rs index 2c71d5f..e5a36d3 100644 --- a/gifski-api/src/lib.rs +++ b/gifski-api/src/lib.rs @@ -141,12 +141,9 @@ trait Encoder { struct DiffMessage { /// 1.. ordinal_frame_number: usize, - /// presentation timestamp of the next frame (i.e. when this frame finishes being displayed) - end_pts: f64, - dispose: gif::DisposalMethod, + pts: f64, frame_duration: f64, image: ImgVec, importance_map: Vec, - needs_transparency: bool, } /// Frame post quantization, before remap @@ -325,9 +322,30 @@ fn dimensions_for_image((img_w, img_h): (usize, usize), resize_to: (Option, } } +#[derive(Copy, Clone)] +enum LastFrameDuration { + FixedOffset(f64), + FrameRate(f64), +} + +impl LastFrameDuration { + pub fn value(&self) -> f64 { + match self { + Self::FixedOffset(val) | Self::FrameRate(val) => *val, + } + } + + pub fn shift_every_pts_by(&self) -> f64 { + match self { + Self::FixedOffset(offset) => *offset, + _ => 0., + } + } +} + /// Encode collected frames impl Writer { - #[deprecated(note="please don't use, it will be in Settings eventually")] + #[deprecated(note = "please don't use, it will be in Settings eventually")] #[doc(hidden)] pub fn set_extra_effort(&mut self) { self.settings.extra_effort = true; @@ -408,10 +426,10 @@ impl Writer { } } if n_done == 0 { - return Err(Error::NoFrames); + Err(Error::NoFrames) + } else { + enc.finish() } - enc.finish()?; - Ok(()) } /// Start writing frames. This function will not return until `Collector` is dropped. @@ -457,15 +475,22 @@ impl Writer { } fn make_diffs(mut inputs: OrdQueueIter, quant_queue: Sender, settings: &Settings) -> CatResult<()> { - let (first_frame, first_frame_pts) = inputs.next().transpose()?.ok_or(Error::NoFrames)?; - let mut prev_frame_pts = 0.; + let (first_frame, first_frame_raw_pts) = inputs.next().transpose()?.ok_or(Error::NoFrames)?; + + let mut last_frame_duration = if first_frame_raw_pts > 1. / 100. { + // this is gifski's weird rule that a non-zero first-frame pts + // shifts the whole anim and is the delay of the last frame + LastFrameDuration::FixedOffset(first_frame_raw_pts) + } else { + LastFrameDuration::FrameRate(0.) + }; let mut denoiser = Denoiser::new(first_frame.width(), first_frame.height(), settings.quality); - let first_frame_has_transparency = first_frame.pixels().any(|px| px.a < 128); - let mut next_frame = Some((first_frame, first_frame_pts)); + let mut next_frame = Some((first_frame, first_frame_raw_pts)); let mut ordinal_frame_number = 0; + let mut last_frame_pts = 0.; loop { // NB! There are two interleaved loops here: // - one to feed the denoiser @@ -479,52 +504,18 @@ impl Writer { let curr_frame = next_frame.take(); next_frame = inputs.next().transpose()?; - if let Some((image, mut pts)) = curr_frame { - pts -= first_frame_pts; + if let Some((image, raw_pts)) = curr_frame { ordinal_frame_number += 1; - let dispose = if let Some((next, _)) = &next_frame { - if next.width() != image.width() || next.height() != image.height() { - return Err(Error::WrongSize(format!("Frame {} has wrong size ({}×{}, expected {}×{})", ordinal_frame_number, - next.width(), next.height(), image.width(), image.height()))); - } - - // Skip identical frames - if next.as_ref() == image.as_ref() { - prev_frame_pts = pts; - continue; - } - - // If the next frame becomes transparent, this frame has to clear to bg for it - if next.pixels().zip(image.pixels()).any(|(next, curr)| next.a < curr.a) { - gif::DisposalMethod::Background - } else { - gif::DisposalMethod::Keep - } - } else if first_frame_has_transparency { - // Last frame should reset to background to avoid breaking transparent looped anims - gif::DisposalMethod::Background - } else { - // macOS preview gets Background wrong - gif::DisposalMethod::Keep - }; - - // conversion from pts to delay - let end_pts = if let Some((_, next_pts)) = next_frame { - debug_assert!(next_pts > 0.); - next_pts - first_frame_pts - } else if first_frame_pts > 1. / 100. { - // this is gifski's weird rule that non-zero first-frame pts - // shifts the whole anim and is the delay of the last frame - pts + first_frame_pts - } else { - // otherwise assume steady framerate - (pts + (pts - prev_frame_pts)).max(1./100.) - }; - debug_assert!(end_pts > 0.); - prev_frame_pts = pts; + let pts = raw_pts - last_frame_duration.shift_every_pts_by(); + if let LastFrameDuration::FrameRate(duration) = &mut last_frame_duration { + *duration = pts - last_frame_pts; + } + last_frame_pts = pts; - denoiser.push_frame(image.as_ref(), (ordinal_frame_number, end_pts, dispose)); + denoiser.push_frame(image.as_ref(), (ordinal_frame_number, pts, last_frame_duration)).map_err(|_| { + Error::WrongSize(format!("Frame {} has wrong size ({}×{})", ordinal_frame_number, image.width(), image.height())) + })?; if next_frame.is_none() { denoiser.flush(); } @@ -532,7 +523,7 @@ impl Writer { ////////////////////// Consume denoised frames ///////////////////// - let (importance_map, image, (ordinal_frame_number, end_pts, dispose)) = match denoiser.pop() { + let (importance_map, image, (ordinal_frame_number, pts, last_frame_duration)) = match denoiser.pop() { Denoised::Done => { debug_assert!(next_frame.is_none()); break @@ -544,12 +535,10 @@ impl Writer { let (importance_map, ..) = importance_map.into_contiguous_buf(); quant_queue.send(DiffMessage { - dispose, importance_map, ordinal_frame_number, - needs_transparency: ordinal_frame_number > 0 || (ordinal_frame_number == 0 && first_frame_has_transparency), image, - end_pts, + pts, frame_duration: last_frame_duration.value().max(1. / 100.), })?; } @@ -557,11 +546,45 @@ impl Writer { } fn quantize_frames(inputs: Receiver, remap_queue: Sender, settings: &SettingsExt) -> CatResult<()> { + let mut inputs = inputs.into_iter().peekable(); + + let DiffMessage {image: first_frame, ..} = inputs.peek().ok_or(Error::NoFrames)?; + let first_frame_has_transparency = first_frame.pixels().any(|px| px.a < 128); + let mut prev_frame_keeps = false; let mut consecutive_frame_num = 0; - while let Ok(DiffMessage {mut image, end_pts, dispose, ordinal_frame_number, needs_transparency, mut importance_map}) = inputs.recv() { - if !prev_frame_keeps || importance_map.iter().any(|&px| px > 0) { + let mut importance_map = None; + while let Some(DiffMessage { mut image, pts, frame_duration, ordinal_frame_number, importance_map: new_importance_map }) = inputs.next() { + + if importance_map.is_none() { + importance_map = Some(new_importance_map); + } + + let dispose = if let Some(DiffMessage { image: next_image, .. }) = inputs.peek() { + // Skip identical frames + if next_image.as_ref() == image.as_ref() { + // this keeps importance_map of the previous frame in the identical-frame series + // (important, because subsequent identical frames have all-zero importance_map and would be dropped too) + continue; + } + // If the next frame becomes transparent, this frame has to clear to bg for it + if next_image.pixels().zip(image.pixels()).any(|(next, curr)| next.a < curr.a) { + gif::DisposalMethod::Background + } else { + gif::DisposalMethod::Keep + } + } else if first_frame_has_transparency { + // Last frame should reset to background to avoid breaking transparent looped anims + gif::DisposalMethod::Background + } else { + // macOS preview gets Background wrong + gif::DisposalMethod::Keep + }; + + let mut importance_map = importance_map.take().unwrap(); // always set at the beginning + + if !prev_frame_keeps || importance_map.iter().any(|&px| px > 0) { if prev_frame_keeps { // if denoiser says the background didn't change, then believe it // (except higher quality settings, which try to improve it every time) @@ -571,12 +594,21 @@ impl Writer { } } + let needs_transparency = consecutive_frame_num > 0 || (consecutive_frame_num == 0 && first_frame_has_transparency); let (liq, remap, liq_image) = Self::quantize(image, &importance_map, consecutive_frame_num == 0, needs_transparency, prev_frame_keeps, settings)?; let max_loss = settings.s.gifsicle_loss(); for imp in &mut importance_map { // encoding assumes rgba background looks like encoded background, which is not true for lossy *imp = ((256 - (*imp) as u32) * max_loss / 256).min(255) as u8; } + + let end_pts = if let Some(&DiffMessage { pts: next_pts, .. }) = inputs.peek() { + next_pts + } else { + pts + frame_duration + }; + debug_assert!(end_pts > 0.); + remap_queue.send(RemapMessage { ordinal_frame_number, end_pts, @@ -585,57 +617,35 @@ impl Writer { liq_image, })?; consecutive_frame_num += 1; - } prev_frame_keeps = dispose == gif::DisposalMethod::Keep; } + } Ok(()) } fn remap_frames(inputs: Receiver, write_queue: Sender, settings: &Settings) -> CatResult<()> { - let next_frame = inputs.recv().map_err(|_| Error::NoFrames)?; - let mut screen = gif_dispose::Screen::new(next_frame.liq_image.width(), next_frame.liq_image.height(), RGBA8::new(0, 0, 0, 0), None); + let mut inputs = inputs.into_iter().peekable(); + let first_frame = inputs.peek().ok_or(Error::NoFrames)?; + let mut screen = gif_dispose::Screen::new(first_frame.liq_image.width(), first_frame.liq_image.height(), RGBA8::new(0, 0, 0, 0), None); - let mut next_frame = Some(next_frame); - - let mut first_frame = true; - while let Some(RemapMessage {ordinal_frame_number, end_pts, dispose, liq, remap, liq_image}) = { - // that's not the while loop, that block gets the next element - let curr_frame = next_frame.take(); - next_frame = inputs.recv().ok(); - curr_frame - } { + let mut is_first_frame = true; + while let Some(RemapMessage {ordinal_frame_number, end_pts, dispose, liq, remap, liq_image}) = inputs.next() { let screen_width = screen.pixels.width() as u16; let screen_height = screen.pixels.height() as u16; let mut screen_after_dispose = screen.dispose(); let (mut image8, mut image8_pal) = { - let bg = if !first_frame { Some(screen_after_dispose.pixels()) } else { None }; + let bg = if !is_first_frame { Some(screen_after_dispose.pixels()) } else { None }; Self::remap(liq, remap, liq_image, bg, settings)? }; - // Palette may have multiple transparent indices :( - let mut transparent_index = None; - for (i, p) in image8_pal.iter_mut().enumerate() { - if p.a <= 128 { - p.a = 0; - let new_index = i as u8; - if let Some(old_index) = transparent_index { - image8.pixels_mut().filter(|px| **px == new_index).for_each(|px| *px = old_index); - } else { - transparent_index = Some(new_index); - } - } - } - - // Check that palette is fine and has no duplicate transparent indices - debug_assert!(image8_pal.iter().enumerate().all(|(idx, color)| { - Some(idx as u8) == transparent_index || color.a > 128 || !image8.pixels().any(|px| px == idx as u8) - })); + let transparent_index = transparent_index_from_palette(&mut image8_pal, image8.as_mut()); - let (left, top, image8) = if !first_frame && next_frame.is_some() { + let next_frame = inputs.peek(); + let (left, top, image8) = if !is_first_frame && next_frame.is_some() { match trim_image(image8, &image8_pal, transparent_index, screen_after_dispose.pixels()) { Some(trimmed) => trimmed, - None => continue, // no pixels left + None => continue, // no pixels need to be changed after dispose } } else { // must keep first and last frame @@ -661,12 +671,35 @@ impl Writer { frame, })?; - first_frame = false; + is_first_frame = false; } Ok(()) } } +fn transparent_index_from_palette(image8_pal: &mut [RGBA], mut image8: ImgRefMut) -> Option { + // Palette may have multiple transparent indices :( + let mut transparent_index = None; + for (i, p) in image8_pal.iter_mut().enumerate() { + if p.a <= 128 { + p.a = 0; + let new_index = i as u8; + if let Some(old_index) = transparent_index { + image8.pixels_mut().filter(|px| **px == new_index).for_each(|px| *px = old_index); + } else { + transparent_index = Some(new_index); + } + } + } + + // Check that palette is fine and has no duplicate transparent indices + debug_assert!(image8_pal.iter().enumerate().all(|(idx, color)| { + Some(idx as u8) == transparent_index || color.a > 128 || !image8.pixels().any(|px| px == idx as u8) + })); + + transparent_index +} + fn trim_image(mut image8: ImgVec, image8_pal: &[RGBA8], transparent_index: Option, screen: ImgRef) -> Option<(u16, u16, ImgVec)> { let mut image_trimmed = image8.as_ref(); diff --git a/gifski-api/tests/tests.rs b/gifski-api/tests/tests.rs index 2fd0289..b725195 100644 --- a/gifski-api/tests/tests.rs +++ b/gifski-api/tests/tests.rs @@ -46,14 +46,15 @@ fn all_dupe_frames() { t.join().unwrap(); let mut n = 0; + let mut delays = vec![]; for_each_frame(&out, |frame, actual| { let expected = lodepng::decode32_file(frame_filename(1)).unwrap(); let expected = ImgVec::new(expected.buffer, expected.width, expected.height); assert_images_eq(expected.as_ref(), actual, 0.); - assert_eq!(frame.delay, 130); + delays.push(frame.delay); n += 1; }); - assert_eq!(n, 1); + assert_eq!(delays, [130]); } #[test] @@ -70,15 +71,16 @@ fn all_but_one_dupe_frames() { w.write(&mut out, &mut progress::NoProgress {}).unwrap(); t.join().unwrap(); + let mut delays = vec![]; let mut n = 0; for_each_frame(&out, |frame, actual| { let expected = lodepng::decode32_file(frame_filename(if n == 0 {0} else {1})).unwrap(); let expected = ImgVec::new(expected.buffer, expected.width, expected.height); assert_images_eq(expected.as_ref(), actual, 0.25); - assert_eq!(frame.delay, if n == 0 {120} else {2*10}); // 2*, because 1.2…1.3 + 1.3…1.4 (assumed fps) + delays.push(frame.delay); n += 1; }); - assert_eq!(n, 2); + assert_eq!(delays, [120, 20]); } fn frame_filename(n: usize) -> PathBuf {