diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 346dd00c0..32943c471 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: continue-on-error: true strategy: matrix: - build: [ubuntu-stable, macos-stable, win-gnu-stable, win-msvc-stable] + build: [ ubuntu-stable, macos-stable, win-gnu-stable, win-msvc-stable ] include: # latest rust stable :: ubuntu - build: ubuntu-stable @@ -82,12 +82,12 @@ jobs: uses: actions/checkout@v2 - name: install_nasm - if: matrix.build != 'win-msvc' + if: matrix.build != 'win-msvc' uses: ilammy/setup-nasm@v1 # from rav1e - name: install_nasm_msvc - if: matrix.build == 'win-msvc' + if: matrix.build == 'win-msvc-stable' run: | $NASM_VERSION="2.15.05" $LINK = "https://www.nasm.us/pub/nasm/releasebuilds/$NASM_VERSION/win64" @@ -95,7 +95,7 @@ jobs: curl --ssl-no-revoke -LO "$LINK/$NASM_FILE.zip" 7z e -y "$NASM_FILE.zip" -o"C:\nasm" echo "C:\nasm" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - + # from rav1e - name: set_path_msvc_linker if: matrix.build == 'win-msvc-stable' diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ecf0ad25..3adeef139 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,17 @@ It can be used to convert between image formats and manipulate images using imag It aims to include all primary [image](https://github.com/image-rs/image) functionality, and eventually also support the most prominent [imageproc](https://github.com/image-rs/imageproc) manipulation routines. -The changelog below lists notable changes for [sic](https://github.com/foresterre/sic). It doesn't list most internal changes. +The changelog below lists notable changes for [sic](https://github.com/foresterre/sic). It doesn't list most internal +changes. ## [Unreleased] [unreleased]: https://github.com/foresterre/sic/compare/v0.22.4...HEAD +### Removed + +- Removed experimental flag `--enable-output-format-decider-fallback` + ## [0.22.4] - 2023-09-17 ### Notable dependency updates @@ -125,7 +130,8 @@ This is intended to be the last update before we port sic to `image 0.24`. ### Added -- Added animated images support: it is now possible to load and save all frames, and apply operations on all frames, of animated images +- Added animated images support: it is now possible to load and save all frames, and apply operations on all frames, of + animated images ### Changed @@ -165,12 +171,13 @@ This is intended to be the last update before we port sic to `image 0.24`. - Image script: add `overlay` operation which can be used to draw one image over another - `--select-frame` now supports images encoded as APNG -- `--no-skip-unsupported-extensions` CLI flag to enumerate all files when using glob based input; not just files with supported extensions - +- `--no-skip-unsupported-extensions` CLI flag to enumerate all files when using glob based input; not just files with + supported extensions ### Changed -- When using glob paths, `--glob-input` and `--glob-output` should now be used instead of `--input` and `--output` combined with `--mode glob` +- When using glob paths, `--glob-input` and `--glob-output` should now be used instead of `--input` and `--output` + combined with `--mode glob` - Glob based input now skips unsupported files by default (disable with `--no-skip-unsupported-extensions`) ### Removed @@ -183,7 +190,6 @@ This is intended to be the last update before we port sic to `image 0.24`. [0.14.0]: https://github.com/foresterre/sic/compare/v0.12.0...v0.14.0 - ## [0.12.0] - 2020-06-01 ### Added @@ -201,7 +207,6 @@ This is intended to be the last update before we port sic to `image 0.24`. - Changed CLI flag `--set-preserve-aspect-ratio` to `--preserve-aspect-ratio` - Changed CLI flag ` --set-resize-sampling-filter` to `--sampling-filter` - ### Fixed - Folders are now skipped in `glob` mode diff --git a/Cargo.lock b/Cargo.lock index 2e74d15d0..7f859ef15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ab_glyph" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80179d7dd5d7e8c285d67c4a1e652972a92de7475beddfb92028c76463b13225" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + [[package]] name = "ab_glyph_rasterizer" version = "0.1.8" @@ -14,6 +24,18 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.2" @@ -40,9 +62,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "approx" @@ -73,7 +95,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -138,6 +160,21 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "bitreader" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd859c9d97f7c468252795b35aeccc412bdbb1e90ee6969c4fa6328272eaeff" +dependencies = [ + "cfg-if", +] + [[package]] name = "bitstream-io" version = "1.10.0" @@ -248,7 +285,7 @@ checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "ansi_term", "atty", - "bitflags", + "bitflags 1.3.2", "strsim", "textwrap", "unicode-width", @@ -335,6 +372,36 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" +[[package]] +name = "dav1d" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7eb1fa9954b7ae85ff0d43ef6d186d1b3469d4aa1574845d1938bef05377030" +dependencies = [ + "bitflags 2.4.2", + "dav1d-sys", +] + +[[package]] +name = "dav1d-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ecb1c5e8f4dc438eedc1b534a54672fb0e0a56035dae6b50162787bd2c50e95" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "dcv-color-primitives" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ad62edfed069700a5b33af6babd29c498d7e33eb01d96ffa8841ee1841634c" +dependencies = [ + "paste", + "wasm-bindgen", +] + [[package]] name = "digest" version = "0.10.7" @@ -373,6 +440,15 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fallible_collections" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88c69768c0a15262df21899142bc6df9b9b823546d4b4b9a7bc2d6c448ec6fd" +dependencies = [ + "hashbrown 0.13.2", +] + [[package]] name = "fdeflate" version = "0.3.4" @@ -422,24 +498,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", + "wasm-bindgen", ] [[package]] @@ -452,12 +519,6 @@ dependencies = [ "weezl", ] -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - [[package]] name = "globset" version = "0.4.14" @@ -477,7 +538,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" dependencies = [ - "bitflags", + "bitflags 1.3.2", "ignore", "walkdir", ] @@ -500,9 +561,18 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heck" @@ -547,41 +617,57 @@ dependencies = [ [[package]] name = "image" -version = "0.24.9" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +checksum = "a9b4f005360d32e9325029b38ba47ebd7a56f3316df09249368939562d518645" dependencies = [ "bytemuck", "byteorder", "color_quant", + "dav1d", + "dcv-color-primitives", "exr", "gif", - "jpeg-decoder", + "image-webp", + "mp4parse", "num-traits", "png", "qoi", "ravif", + "rayon", "rgb", "tiff", - "webp", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6107a25f04af48ceeb4093eebc9b405ee5a1813a0bab5ecf1805d3eabb3337" +dependencies = [ + "byteorder", + "thiserror", ] [[package]] name = "imageproc" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aee993351d466301a29655d628bfc6f5a35a0d062b6160ca0808f425805fd7" +checksum = "a2a0d7770f428b4615960cc8602775d1f04c75d41b0ccdef862e889ebaae9bbf" dependencies = [ + "ab_glyph", "approx", "conv", + "getrandom", "image", - "itertools 0.10.5", + "itertools 0.12.1", "nalgebra", "num", - "rand 0.7.3", + "rand", "rand_distr", "rayon", - "rusttype", ] [[package]] @@ -602,12 +688,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.0" +version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.3", ] [[package]] @@ -618,7 +704,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -651,9 +737,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] @@ -672,8 +758,14 @@ name = "jpeg-decoder" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ - "rayon", + "wasm-bindgen", ] [[package]] @@ -710,14 +802,10 @@ dependencies = [ ] [[package]] -name = "libwebp-sys" -version = "0.9.5" +name = "libm" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829b6b604f31ed6d2bccbac841fe0788de93dbd87e4eb1ba2c4adfe8c012a838" -dependencies = [ - "cc", - "glob", -] +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "lock_api" @@ -786,11 +874,25 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mp4parse" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63a35203d3c6ce92d5251c77520acb2e57108c88728695aa883f70023624c570" +dependencies = [ + "bitreader", + "byteorder", + "fallible_collections", + "log", + "num-traits", + "static_assertions", +] + [[package]] name = "nalgebra" -version = "0.30.1" +version = "0.32.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb2d0de08694bed883320212c18ee3008576bfe8c306f4c3c4a58b4876998be" +checksum = "4541eb06dce09c0241ebbaab7102f0a01a0c8994afed2e5d0d66775016e25ac2" dependencies = [ "approx", "matrixmultiply", @@ -801,15 +903,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "nasm-rs" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4d98d0065f4b1daf164b3eafb11974c94662e5e2396cf03f32d0bb5c17da51" -dependencies = [ - "rayon", -] - [[package]] name = "new_debug_unreachable" version = "1.0.4" @@ -879,13 +972,13 @@ dependencies = [ [[package]] name = "num-derive" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -927,6 +1020,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -948,9 +1042,9 @@ dependencies = [ [[package]] name = "owned_ttf_parser" -version = "0.15.2" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e6affeb1632d6ff6a23d2cd40ffed138e82f1532571a26f527c8a284bb2fbb" +checksum = "d4586edfe4c648c71797a74c84bacb32b52b212eff5dfe2bb9f2c599844023e7" dependencies = [ "ttf-parser", ] @@ -961,7 +1055,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3033e5499e963aa291be03b370b1157e1236b9ae1418b48b5ce02a53e5343d20" dependencies = [ - "parameterized-macro", + "parameterized-macro 1.1.0", +] + +[[package]] +name = "parameterized" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194bf497674dda552d4f1bd24d325f828f425876c9d522fcb1810cd527e0bd4e" +dependencies = [ + "parameterized-macro 2.0.0", ] [[package]] @@ -976,6 +1079,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "parameterized-macro" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1374bca5edab7a317c4ffbc9df1e239ceb7dcf5426b6b403474408442a9777ac" +dependencies = [ + "indexmap 1.9.3", + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "paste" version = "1.0.14" @@ -1025,7 +1140,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -1051,7 +1166,7 @@ version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", @@ -1066,9 +1181,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -1089,7 +1204,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -1116,19 +1231,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", -] - [[package]] name = "rand" version = "0.8.5" @@ -1136,18 +1238,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", + "rand_chacha", + "rand_core", ] [[package]] @@ -1157,16 +1249,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", + "rand_core", ] [[package]] @@ -1175,25 +1258,17 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.10", + "getrandom", ] [[package]] name = "rand_distr" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96977acbdd3a6576fb1d27391900035bf3863d4a16422973a409b488cf29ffb2" -dependencies = [ - "rand 0.7.3", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ - "rand_core 0.5.1", + "num-traits", + "rand", ] [[package]] @@ -1208,7 +1283,6 @@ dependencies = [ "av1-grain", "bitstream-io 1.10.0", "built 0.5.2", - "cc", "cfg-if", "interpolate_name", "itertools 0.10.5", @@ -1216,15 +1290,14 @@ dependencies = [ "libfuzzer-sys 0.3.5", "log", "maybe-rayon", - "nasm-rs", "new_debug_unreachable", "noop_proc_macro", "num-derive 0.3.3", "num-traits", "once_cell", "paste", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand", + "rand_chacha", "rust_hawktracer", "rustc_version", "simd_helpers", @@ -1246,24 +1319,22 @@ dependencies = [ "av1-grain", "bitstream-io 2.2.0", "built 0.7.1", - "cc", "cfg-if", "interpolate_name", - "itertools 0.12.0", + "itertools 0.12.1", "libc", "libfuzzer-sys 0.4.7", "log", "maybe-rayon", - "nasm-rs", "new_debug_unreachable", "noop_proc_macro", - "num-derive 0.4.0", + "num-derive 0.4.2", "num-traits", "once_cell", "paste", "profiling", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand", + "rand_chacha", "simd_helpers", "system-deps", "thiserror", @@ -1369,16 +1440,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rusttype" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff8374aa04134254b7995b63ad3dc41c7f7236f69528b28553da7d72efaa967" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - [[package]] name = "rustversion" version = "1.0.14" @@ -1435,7 +1496,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -1467,7 +1528,7 @@ dependencies = [ "clap", "globwalk", "open", - "parameterized", + "parameterized 2.0.0", "sic_cli_ops", "sic_core", "sic_image_engine", @@ -1480,7 +1541,7 @@ name = "sic_cli_ops" version = "0.22.1" dependencies = [ "clap", - "parameterized", + "parameterized 1.1.0", "sic_core", "sic_image_engine", "sic_parser", @@ -1494,9 +1555,9 @@ dependencies = [ name = "sic_core" version = "0.22.1" dependencies = [ + "ab_glyph", "image", "imageproc", - "rusttype", "thiserror", ] @@ -1517,7 +1578,7 @@ dependencies = [ name = "sic_io" version = "0.22.1" dependencies = [ - "parameterized", + "parameterized 2.0.0", "sic_core", "sic_testing", "thiserror", @@ -1527,7 +1588,7 @@ dependencies = [ name = "sic_parser" version = "0.22.1" dependencies = [ - "parameterized", + "parameterized 1.1.0", "pest", "pest_derive", "sic_core", @@ -1540,15 +1601,15 @@ dependencies = [ name = "sic_testing" version = "0.22.0" dependencies = [ - "parameterized", + "parameterized 1.1.0", "sic_core", ] [[package]] name = "simba" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3fd720c48c53cace224ae62bef1bbff363a70c68c4802a78b5cc6159618176" +checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae" dependencies = [ "approx", "num-complex", @@ -1587,6 +1648,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.8.0" @@ -1609,7 +1676,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -1625,9 +1692,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", @@ -1643,7 +1710,7 @@ dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml 0.8.2", + "toml 0.8.11", "version-compare", ] @@ -1679,7 +1746,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -1719,9 +1786,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.2" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +checksum = "af06656561d28735e9c1cd63dfd57132c8155426aa6af24f36a00a351f88c48e" dependencies = [ "serde", "serde_spanned", @@ -1740,11 +1807,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.20.7" +version = "0.22.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +checksum = "18769cd1cec395d70860ceb4d932812a0b4d06b1a4bb336745a4d21b9496e992" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.2.5", "serde", "serde_spanned", "toml_datetime", @@ -1753,9 +1820,9 @@ dependencies = [ [[package]] name = "ttf-parser" -version = "0.15.2" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" [[package]] name = "typenum" @@ -1846,12 +1913,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1879,7 +1940,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", "wasm-bindgen-shared", ] @@ -1901,7 +1962,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1912,15 +1973,6 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" -[[package]] -name = "webp" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb5d8e7814e92297b0e1c773ce43d290bef6c17452dafd9fc49e5edb5beba71" -dependencies = [ - "libwebp-sys", -] - [[package]] name = "weezl" version = "0.1.8" @@ -1970,13 +2022,39 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winnow" -version = "0.5.40" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" dependencies = [ "memchr", ] +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + [[package]] name = "zune-inflate" version = "0.2.54" @@ -1985,3 +2063,12 @@ checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" dependencies = [ "simd-adler32", ] + +[[package]] +name = "zune-jpeg" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 4115df86a..7686dddb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ members = [ [dependencies] sic_cli_ops = { version = "0.22.0", path = "crates/sic_cli_ops" } sic_core = { version = "0.22.0", path = "crates/sic_core" } -sic_io = { version = "0.22.0", path = "crates/sic_io" } +sic_io = { version = "0.22.0", path = "crates/sic_io" } sic_image_engine = { version = "0.22.0", path = "crates/sic_image_engine" } sic_parser = { version = "0.22.0", path = "crates/sic_parser" } @@ -47,7 +47,7 @@ open = "5.1.2" [dev-dependencies] clap = "2.34.0" # for examples/gen_completions.rs -parameterized = "1.1.0" +parameterized = "2.0.0" [build-dependencies] diff --git a/crates/sic_cli_ops/src/operations.rs b/crates/sic_cli_ops/src/operations.rs index 328a64d3f..98dafb337 100644 --- a/crates/sic_cli_ops/src/operations.rs +++ b/crates/sic_cli_ops/src/operations.rs @@ -12,7 +12,7 @@ use std::str::FromStr; /// The enumeration of all supported operations. #[derive( - Debug, Copy, Clone, Hash, Eq, PartialEq, IntoStaticStr, EnumIter, EnumString, EnumVariantNames, + Debug, Copy, Clone, Hash, Eq, PartialEq, IntoStaticStr, EnumIter, EnumString, VariantNames, )] #[strum(serialize_all = "kebab_case")] pub enum OperationId { diff --git a/crates/sic_core/Cargo.toml b/crates/sic_core/Cargo.toml index 6ecc54187..2511e21c1 100644 --- a/crates/sic_core/Cargo.toml +++ b/crates/sic_core/Cargo.toml @@ -11,16 +11,16 @@ edition = "2021" rust-version = "1.61" [dependencies] -imageproc = { version = "0.23.0", optional = true } -rusttype = { version = "0.9.3", optional = true } +imageproc = { version = "0.24.0", optional = true } +ab_glyph = { version = "0.2.23", optional = true } thiserror = "1" [dependencies.image] -version = "0.24.9" +version = "0.25.0" features = [ - "avif", # requires (exe: nasm >= 2.14) - "webp-encoder", # requires (native_dependency: libwebp) + "avif-native", # requires (exe: nasm >= 2.14) + "rayon", ] [features] -imageproc-ops = ["imageproc", "rusttype"] +imageproc-ops = ["imageproc", "ab_glyph"] diff --git a/crates/sic_core/src/animated.rs b/crates/sic_core/src/animated.rs index 9dc83a912..9129e3f6f 100644 --- a/crates/sic_core/src/animated.rs +++ b/crates/sic_core/src/animated.rs @@ -11,8 +11,10 @@ pub struct AnimatedImage { impl AnimatedImage { /// Consume a collection of frames to produce an `AnimatedImage` - pub fn from_frames(frames: Vec) -> Self { - Self { frames } + pub fn from_frames(frames: impl IntoIterator) -> Self { + Self { + frames: frames.into_iter().collect(), + } } /// Returns the selected frame from the animated image as static image diff --git a/crates/sic_core/src/errors.rs b/crates/sic_core/src/errors.rs index ddc519443..2fae9eb65 100644 --- a/crates/sic_core/src/errors.rs +++ b/crates/sic_core/src/errors.rs @@ -2,7 +2,7 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum SicCoreError { - #[error("{0}")] + #[error(transparent)] ImageError(image::error::ImageError), #[error("Invalid frame index: index (is {index}) should be < len (is {len}) ")] diff --git a/crates/sic_core/src/lib.rs b/crates/sic_core/src/lib.rs index 1fbc0ce5d..30ac77fd1 100644 --- a/crates/sic_core/src/lib.rs +++ b/crates/sic_core/src/lib.rs @@ -9,7 +9,7 @@ pub use image; #[cfg(feature = "imageproc-ops")] -pub use {imageproc, rusttype}; +pub use {ab_glyph, imageproc}; use image::DynamicImage; use std::convert::TryFrom; diff --git a/crates/sic_image_engine/src/engine.rs b/crates/sic_image_engine/src/engine.rs index d5c14b960..5a8587a7a 100644 --- a/crates/sic_image_engine/src/engine.rs +++ b/crates/sic_image_engine/src/engine.rs @@ -87,7 +87,7 @@ impl ImageEngine { } } - pub fn ignite(&mut self, instructions: &[Instr]) -> Result<&SicImage, SicImageEngineError> { + pub fn ignite(mut self, instructions: &[Instr]) -> Result { for instruction in instructions { match self.process_instruction(instruction) { Ok(_) => continue, @@ -95,7 +95,7 @@ impl ImageEngine { } } - Ok(&self.image) + Ok(*self.image) } fn process_instruction(&mut self, instruction: &Instr) -> Result<(), SicImageEngineError> { @@ -339,7 +339,7 @@ mod tests { let left = sic_testing::open_test_image(sic_testing::in_!(LEFT)); const RIGHT: &str = "3x2_wbaaba.png"; - let mut engine = ImageEngine::new(left); + let engine = ImageEngine::new(left); let out = engine.ignite(&[Instr::Operation(ImgOp::Diff(ImageFromPath::new( PathBuf::from(in_!(RIGHT)), )))]); @@ -359,7 +359,7 @@ mod tests { assert_eq!(out.get_pixel(1, 2), DIFF_PX_DIFF); assert_eq!(out.get_pixel(2, 2), DIFF_PX_NO_OVERLAP); - output_test_image_for_manual_inspection(out, out_!("test_diff_3x3.png")); + output_test_image_for_manual_inspection(&out, out_!("test_diff_3x3.png")); } mod sizes { @@ -401,7 +401,7 @@ mod tests { ) { let left_img = sic_testing::open_test_image(sic_testing::in_!(left)); - let mut engine = ImageEngine::new(left_img); + let engine = ImageEngine::new(left_img); let out = engine.ignite(&[Instr::Operation(ImgOp::Diff(ImageFromPath::new( PathBuf::from(in_!(right)), )))]); @@ -412,7 +412,7 @@ mod tests { assert_eq!(out.height(), expected_height); let name = format!("test_diff_{},{}.png", left, right); - output_test_image_for_manual_inspection(out, out_!(&name)); + output_test_image_for_manual_inspection(&out, out_!(&name)); } } @@ -421,8 +421,8 @@ mod tests { // W 217 H 447 let img = setup_default_test_image(); - let mut engine = ImageEngine::new(img); - let mut engine2 = engine.clone(); + let engine = ImageEngine::new(img); + let engine2 = engine.clone(); let cmp_left = engine.ignite(&[ Instr::EnvAdd(EnvItem::PreserveAspectRatio(true)), Instr::Operation(ImgOp::Resize((100, 100))), @@ -443,12 +443,12 @@ mod tests { assert_eq!((49, 100), left.dimensions()); output_test_image_for_manual_inspection( - left, + &left, out_!("test_resize_preserve_aspect_ratio_left_preserve.png"), ); output_test_image_for_manual_inspection( - right, + &right, out_!("test_resize_preserve_aspect_ratio_right_default.png"), ); } @@ -458,8 +458,8 @@ mod tests { // W 217 H 447 let img = setup_default_test_image(); - let mut engine = ImageEngine::new(img); - let mut engine2 = engine.clone(); + let engine = ImageEngine::new(img); + let engine2 = engine.clone(); let cmp_left = engine.ignite(&[ Instr::EnvAdd(EnvItem::PreserveAspectRatio(false)), Instr::Operation(ImgOp::Resize((100, 100))), @@ -479,12 +479,12 @@ mod tests { assert_eq!((100, 100), left.dimensions()); output_test_image_for_manual_inspection( - left, + &left, out_!("test_resize_preserve_aspect_ratio_left_preserve_f.png"), ); output_test_image_for_manual_inspection( - right, + &right, out_!("test_resize_preserve_aspect_ratio_right_default_f.png"), ); } @@ -493,8 +493,8 @@ mod tests { fn resize_with_sampling_filter_nearest() { let img = setup_default_test_image(); - let mut engine = ImageEngine::new(img); - let mut engine2 = engine.clone(); + let engine = ImageEngine::new(img); + let engine2 = engine.clone(); let cmp_left = engine.ignite(&[ Instr::EnvAdd(EnvItem::CustomSamplingFilter(FilterTypeWrap::new( FilterType::Nearest, @@ -514,12 +514,12 @@ mod tests { assert_ne!(left.raw_pixels(), right.raw_pixels()); output_test_image_for_manual_inspection( - left, + &left, out_!("test_resize_sampling_filter_left_nearest.png"), ); output_test_image_for_manual_inspection( - right, + &right, out_!("test_resize_sampling_filter_right_default_gaussian.png"), ); } @@ -528,8 +528,8 @@ mod tests { fn register_unregister_sampling_filter() { let img = setup_default_test_image(); - let mut engine = ImageEngine::new(img); - let mut engine2 = engine.clone(); + let engine = ImageEngine::new(img); + let engine2 = engine.clone(); let cmp_left = engine.ignite(&[ Instr::EnvAdd(EnvItem::CustomSamplingFilter(FilterTypeWrap::new( @@ -551,12 +551,12 @@ mod tests { assert_eq!(left.raw_pixels(), right.raw_pixels()); output_test_image_for_manual_inspection( - left, + &left, out_!("test_register_unregister_sampling_filter_left.png"), ); output_test_image_for_manual_inspection( - right, + &right, out_!("test_register_unregister_sampling_filter_right.png"), ); } @@ -572,12 +572,12 @@ mod tests { Rgba([255, 50, 50, 50]), ))); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); output_test_image_for_manual_inspection( - done.unwrap(), + &done.unwrap(), out_!("horizontal-gradient-test.png"), ); } @@ -593,11 +593,14 @@ mod tests { Rgba([255, 50, 50, 50]), ))); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); - output_test_image_for_manual_inspection(done.unwrap(), out_!("vertical-gradient-test.png")); + output_test_image_for_manual_inspection( + &done.unwrap(), + out_!("vertical-gradient-test.png"), + ); } #[test] @@ -605,12 +608,12 @@ mod tests { let img = setup_default_test_image(); let operation = ImgOp::Blur(10.0); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); - output_test_image_for_manual_inspection(done.unwrap(), out_!("test_blur.png")); + output_test_image_for_manual_inspection(&done.unwrap(), out_!("test_blur.png")); } #[test] @@ -620,7 +623,7 @@ mod tests { let operation = ImgOp::Brighten(25); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -629,7 +632,7 @@ mod tests { assert_ne!(cmp.raw_pixels(), result_img.raw_pixels()); - output_test_image_for_manual_inspection(result_img, out_!("test_brighten_pos_25.png")); + output_test_image_for_manual_inspection(&result_img, out_!("test_brighten_pos_25.png")); } #[test] @@ -638,7 +641,7 @@ mod tests { let cmp = setup_default_test_image(); let operation = ImgOp::Brighten(0); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -647,7 +650,7 @@ mod tests { assert_eq!(cmp.raw_pixels(), result_img.raw_pixels()); - output_test_image_for_manual_inspection(result_img, out_!("test_brighten_zero.png")); + output_test_image_for_manual_inspection(&result_img, out_!("test_brighten_zero.png")); } #[test] @@ -657,7 +660,7 @@ mod tests { let operation = ImgOp::Brighten(-25); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -666,7 +669,7 @@ mod tests { assert_ne!(cmp.raw_pixels(), result_img.raw_pixels()); - output_test_image_for_manual_inspection(result_img, out_!("test_brighten_neg_25.png")); + output_test_image_for_manual_inspection(&result_img, out_!("test_brighten_neg_25.png")); } #[test] @@ -676,7 +679,7 @@ mod tests { let operation = ImgOp::Contrast(150.9); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -685,7 +688,7 @@ mod tests { assert_ne!(cmp.raw_pixels(), result_img.raw_pixels()); - output_test_image_for_manual_inspection(result_img, out_!("test_contrast_pos_15_9.png")); + output_test_image_for_manual_inspection(&result_img, out_!("test_contrast_pos_15_9.png")); } #[test] @@ -695,7 +698,7 @@ mod tests { let operation = ImgOp::Contrast(-150.9); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -704,7 +707,7 @@ mod tests { assert_ne!(cmp.raw_pixels(), result_img.raw_pixels()); - output_test_image_for_manual_inspection(result_img, out_!("test_contrast_pos_15_9.png")); + output_test_image_for_manual_inspection(&result_img, out_!("test_contrast_pos_15_9.png")); } #[test] @@ -714,7 +717,7 @@ mod tests { let operation = ImgOp::Crop((0, 0, 2, 2)); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -723,7 +726,7 @@ mod tests { assert_eq!(cmp.raw_pixels(), result_img.raw_pixels()); - output_test_image_for_manual_inspection(result_img, out_!("test_crop_no_change.bmp")); + output_test_image_for_manual_inspection(&result_img, out_!("test_crop_no_change.bmp")); } #[test] @@ -733,7 +736,7 @@ mod tests { let operation = ImgOp::Crop((0, 0, 1, 1)); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -748,7 +751,10 @@ mod tests { assert_eq!(Rgba([0, 0, 0, 255]), result_img.get_pixel(0, 0)); - output_test_image_for_manual_inspection(result_img, out_!("test_crop_ok_to_one_pixel.bmp")); + output_test_image_for_manual_inspection( + &result_img, + out_!("test_crop_ok_to_one_pixel.bmp"), + ); } #[test] @@ -758,7 +764,7 @@ mod tests { let operation = ImgOp::Crop((0, 0, 2, 1)); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -775,7 +781,7 @@ mod tests { assert_eq!(Rgba([255, 255, 255, 255]), result_img.get_pixel(1, 0)); output_test_image_for_manual_inspection( - result_img, + &result_img, out_!("test_crop_ok_to_half_horizontal.bmp"), ); } @@ -787,7 +793,7 @@ mod tests { // not rx >= lx let operation = ImgOp::Crop((1, 0, 0, 0)); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_err()); @@ -800,7 +806,7 @@ mod tests { // not rx >= lx let operation = ImgOp::Crop((0, 1, 0, 0)); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_err()); @@ -812,7 +818,7 @@ mod tests { let operation = ImgOp::Crop((3, 0, 1, 1)); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_err()); @@ -824,7 +830,7 @@ mod tests { let operation = ImgOp::Crop((0, 3, 1, 1)); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_err()); @@ -836,7 +842,7 @@ mod tests { let operation = ImgOp::Crop((0, 0, 3, 1)); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_err()); @@ -848,7 +854,7 @@ mod tests { let operation = ImgOp::Crop((0, 0, 1, 3)); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_err()); @@ -861,7 +867,7 @@ mod tests { let operation = ImgOp::Filter3x3([1.0, 0.5, 0.0, 1.0, 0.5, 0.0, 1.0, 0.5, 0.0]); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -870,7 +876,7 @@ mod tests { assert_ne!(cmp.raw_pixels(), result_img.raw_pixels()); - output_test_image_for_manual_inspection(result_img, out_!("test_filter3x3.png")) + output_test_image_for_manual_inspection(&result_img, out_!("test_filter3x3.png")) } #[test] @@ -879,7 +885,7 @@ mod tests { let operation = ImgOp::FlipHorizontal; let (xa, ya) = img.dimensions(); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -890,7 +896,7 @@ mod tests { assert_eq!(xa, xb); assert_eq!(ya, yb); - output_test_image_for_manual_inspection(img_result, out_!("test_fliph.png")); + output_test_image_for_manual_inspection(&img_result, out_!("test_fliph.png")); } #[test] @@ -899,7 +905,7 @@ mod tests { let operation = ImgOp::FlipVertical; let (xa, ya) = img.dimensions(); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -910,7 +916,7 @@ mod tests { assert_eq!(xa, xb); assert_eq!(ya, yb); - output_test_image_for_manual_inspection(img_result, out_!("test_flipv.png")); + output_test_image_for_manual_inspection(&img_result, out_!("test_flipv.png")); } #[test] @@ -920,7 +926,7 @@ mod tests { let img = open_test_image(in_!("rainbow_8x6.bmp")); let operation = ImgOp::Grayscale; - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -942,7 +948,7 @@ mod tests { } } - output_test_image_for_manual_inspection(img_result, out_!("test_gray_scale.png")); + output_test_image_for_manual_inspection(&img_result, out_!("test_gray_scale.png")); } #[test] @@ -952,7 +958,7 @@ mod tests { let operation = ImgOp::HueRotate(-100); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -961,7 +967,7 @@ mod tests { assert_ne!(cmp.raw_pixels(), result_img.raw_pixels()); - output_test_image_for_manual_inspection(result_img, out_!("test_hue_rot_neg_100.png")); + output_test_image_for_manual_inspection(&result_img, out_!("test_hue_rot_neg_100.png")); } #[test] @@ -971,7 +977,7 @@ mod tests { let operation = ImgOp::HueRotate(100); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -980,7 +986,7 @@ mod tests { assert_ne!(cmp.raw_pixels(), result_img.raw_pixels()); - output_test_image_for_manual_inspection(result_img, out_!("test_hue_rot_pos_100.png")); + output_test_image_for_manual_inspection(&result_img, out_!("test_hue_rot_pos_100.png")); } #[test] @@ -990,7 +996,7 @@ mod tests { let operation = ImgOp::HueRotate(0); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -999,7 +1005,7 @@ mod tests { assert_eq!(cmp.raw_pixels(), result_img.raw_pixels()); - output_test_image_for_manual_inspection(result_img, out_!("test_hue_rot_0.png")); + output_test_image_for_manual_inspection(&result_img, out_!("test_hue_rot_0.png")); } #[test] @@ -1009,7 +1015,7 @@ mod tests { let operation = ImgOp::HueRotate(360); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -1021,7 +1027,7 @@ mod tests { let expected = SicImage::from(cmp.as_ref().huerotate(360)); assert_eq!(expected.raw_pixels(), result_img.raw_pixels()); - output_test_image_for_manual_inspection(result_img, out_!("test_hue_rot_pos_360.png")); + output_test_image_for_manual_inspection(&result_img, out_!("test_hue_rot_pos_360.png")); } #[test] @@ -1031,7 +1037,7 @@ mod tests { let operation = ImgOp::HueRotate(460); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -1041,7 +1047,7 @@ mod tests { let expected = SicImage::from(cmp.as_ref().huerotate(100)); assert_ne!(expected.raw_pixels(), result_img.raw_pixels()); - output_test_image_for_manual_inspection(result_img, out_!("test_hue_rot_pos_460.png")); + output_test_image_for_manual_inspection(&result_img, out_!("test_hue_rot_pos_460.png")); } #[test] @@ -1051,7 +1057,7 @@ mod tests { let operation = ImgOp::Invert; - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -1060,7 +1066,7 @@ mod tests { assert_ne!(cmp.raw_pixels(), result_img.raw_pixels()); - output_test_image_for_manual_inspection(result_img, out_!("test_invert.png")); + output_test_image_for_manual_inspection(&result_img, out_!("test_invert.png")); } mod overlay { @@ -1072,7 +1078,7 @@ mod tests { let img = setup_default_test_image(); let overlay = sic_testing::in_!("unsplash_763569_cropped.jpg"); - let mut engine = ImageEngine::new(img.clone()); + let engine = ImageEngine::new(img.clone()); let res = engine.ignite(&[Instr::Operation(ImgOp::Overlay(OverlayInputs::new( ImageFromPath::new(overlay.into()), (0, 0), @@ -1082,7 +1088,7 @@ mod tests { assert_eq!(img.raw_pixels(), res_image.raw_pixels()); output_test_image_for_manual_inspection( - res_image, + &res_image, out_!("test_overlay_self_origin.png"), ); } @@ -1094,7 +1100,7 @@ mod tests { let overlay = sic_testing::in_!("unsplash_763569_cropped.jpg"); - let mut engine = ImageEngine::new(img.clone()); + let engine = ImageEngine::new(img.clone()); let res = engine.ignite(&[Instr::Operation(ImgOp::Overlay(OverlayInputs::new( ImageFromPath::new(overlay.into()), (bounds.0 as i64, bounds.1 as i64), @@ -1104,7 +1110,7 @@ mod tests { assert_eq!(img.raw_pixels(), res_image.raw_pixels()); output_test_image_for_manual_inspection( - res_image, + &res_image, out_!("test_overlay_self_bounds.png"), ); } @@ -1116,7 +1122,7 @@ mod tests { let overlay = sic_testing::in_!("unsplash_763569_cropped.jpg"); - let mut engine = ImageEngine::new(img.clone()); + let engine = ImageEngine::new(img.clone()); let res = engine.ignite(&[ Instr::Operation(ImgOp::Invert), Instr::Operation(ImgOp::Overlay(OverlayInputs::new( @@ -1129,7 +1135,7 @@ mod tests { assert_ne!(img.raw_pixels(), res_image.raw_pixels()); output_test_image_for_manual_inspection( - res_image, + &res_image, out_!("test_overlay_self_se_quarter.png"), ); } @@ -1146,7 +1152,7 @@ mod tests { assert_eq!(xa, 217); assert_eq!(ya, 447); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -1157,7 +1163,7 @@ mod tests { assert_eq!(xb, 100); assert_eq!(yb, 200); - output_test_image_for_manual_inspection(img_result, out_!("test_scale_100x200.png")); + output_test_image_for_manual_inspection(&img_result, out_!("test_scale_100x200.png")); } #[test] @@ -1171,7 +1177,7 @@ mod tests { assert_eq!(xa, 217); assert_eq!(ya, 447); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -1182,7 +1188,7 @@ mod tests { assert_eq!(xb, 250); assert_eq!(yb, 500); - output_test_image_for_manual_inspection(img_result, out_!("test_scale_250x500.png")); + output_test_image_for_manual_inspection(&img_result, out_!("test_scale_250x500.png")); } #[test] @@ -1191,7 +1197,7 @@ mod tests { let operation = ImgOp::Rotate90; let (xa, ya) = img.dimensions(); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -1202,7 +1208,7 @@ mod tests { assert_eq!(xa, yb); assert_eq!(xb, ya); - output_test_image_for_manual_inspection(img_result, out_!("test_rotate90.png")); + output_test_image_for_manual_inspection(&img_result, out_!("test_rotate90.png")); } #[test] @@ -1211,7 +1217,7 @@ mod tests { let operation = ImgOp::Rotate180; let (xa, ya) = img.dimensions(); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -1222,7 +1228,7 @@ mod tests { assert_eq!(xa, xb); assert_eq!(ya, yb); - output_test_image_for_manual_inspection(img_result, out_!("test_rotate180.png")); + output_test_image_for_manual_inspection(&img_result, out_!("test_rotate180.png")); } #[test] @@ -1231,7 +1237,7 @@ mod tests { let operation = ImgOp::Rotate270; let (xa, ya) = img.dimensions(); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -1242,7 +1248,7 @@ mod tests { assert_eq!(xa, yb); assert_eq!(xb, ya); - output_test_image_for_manual_inspection(img_result, out_!("test_rotate270.png")); + output_test_image_for_manual_inspection(&img_result, out_!("test_rotate270.png")); } #[test] @@ -1252,7 +1258,7 @@ mod tests { let operation = ImgOp::Unsharpen((20.1, 20)); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -1260,7 +1266,7 @@ mod tests { assert_ne!(cmp.raw_pixels(), result_img.raw_pixels()); - output_test_image_for_manual_inspection(result_img, out_!("test_unsharpen_20_1_20.png")); + output_test_image_for_manual_inspection(&result_img, out_!("test_unsharpen_20_1_20.png")); } #[test] @@ -1270,7 +1276,7 @@ mod tests { let operation = ImgOp::Unsharpen((-20.1, -20)); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -1279,7 +1285,7 @@ mod tests { assert_ne!(cmp.raw_pixels(), result_img.raw_pixels()); output_test_image_for_manual_inspection( - result_img, + &result_img, out_!("test_unsharpen_neg20_1_neg20.png"), ); } @@ -1292,7 +1298,7 @@ mod tests { let operation = ImgOp::Threshold; - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); @@ -1300,7 +1306,7 @@ mod tests { assert_ne!(cmp.raw_pixels(), result_img.raw_pixels()); - output_test_image_for_manual_inspection(result_img, out_!("test_threshold.png")); + output_test_image_for_manual_inspection(&result_img, out_!("test_threshold.png")); } #[test] @@ -1319,7 +1325,7 @@ mod tests { assert_eq!(ya, 447); assert_eq!(xa, 217); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&operations); assert!(done.is_ok()); @@ -1331,7 +1337,7 @@ mod tests { assert_eq!(xb, 100); assert_eq!(yb, 80); - output_test_image_for_manual_inspection(done_image, out_!("test_multi.png")); + output_test_image_for_manual_inspection(&done_image, out_!("test_multi.png")); } #[cfg(feature = "imageproc-ops")] @@ -1359,14 +1365,14 @@ mod tests { ), )); - let mut operator = ImageEngine::new(img); + let operator = ImageEngine::new(img); let done = operator.ignite(&[Instr::Operation(operation)]); assert!(done.is_ok()); let result_img = done.unwrap(); output_test_image_for_manual_inspection( - result_img, + &result_img, out_!("test_imageproc_ops_draw_text.png"), ); } diff --git a/crates/sic_image_engine/src/errors.rs b/crates/sic_image_engine/src/errors.rs index 96ede5075..929b1aa45 100644 --- a/crates/sic_image_engine/src/errors.rs +++ b/crates/sic_image_engine/src/errors.rs @@ -1,4 +1,4 @@ -use sic_core::SicCoreError; +use sic_core::{ab_glyph, SicCoreError}; use thiserror::Error; #[derive(Debug, Error)] @@ -22,8 +22,8 @@ pub enum SicImageEngineError { UnknownFilterType(String), #[cfg(feature = "imageproc-ops")] - #[error("Unable to load font: invalid format")] - FontError, + #[error("Unable to load font: '{0}'")] + FontError(ab_glyph::InvalidFont), #[cfg(feature = "imageproc-ops")] #[error("Unable to open font file from path: '{0}'")] diff --git a/crates/sic_image_engine/src/operations/draw_text.rs b/crates/sic_image_engine/src/operations/draw_text.rs index eb7dddd5a..a823295c8 100644 --- a/crates/sic_image_engine/src/operations/draw_text.rs +++ b/crates/sic_image_engine/src/operations/draw_text.rs @@ -3,7 +3,7 @@ use crate::operations::ImageOperation; use crate::wrapper::draw_text_inner::DrawTextInner; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; use sic_core::image::DynamicImage; -use sic_core::{image, imageproc, rusttype, SicImage}; +use sic_core::{ab_glyph, image, imageproc, SicImage}; pub struct DrawText<'dt> { text: &'dt DrawTextInner, @@ -33,10 +33,11 @@ fn draw_text_animated_image( let font_options = inner.font_options(); let font_file = std::fs::read(&font_options.font_path).map_err(SicImageEngineError::FontFileLoadError)?; - let font = rusttype::Font::try_from_bytes(&font_file).ok_or(SicImageEngineError::FontError)?; + let font = + ab_glyph::FontVec::try_from_vec(font_file).map_err(SicImageEngineError::FontError)?; frames.par_iter_mut().for_each(|frame| { - *frame.buffer_mut() = imageproc::drawing::draw_text( + imageproc::drawing::draw_text_mut( frame.buffer_mut(), font_options.color, coords.0, @@ -59,7 +60,8 @@ fn draw_text_static_image( let font_options = inner.font_options(); let font_file = std::fs::read(&font_options.font_path).map_err(SicImageEngineError::FontFileLoadError)?; - let font = rusttype::Font::try_from_bytes(&font_file).ok_or(SicImageEngineError::FontError)?; + let font = + ab_glyph::FontVec::try_from_vec(font_file).map_err(SicImageEngineError::FontError)?; *image = DynamicImage::ImageRgba8(imageproc::drawing::draw_text( image, diff --git a/crates/sic_image_engine/src/wrapper/font_options.rs b/crates/sic_image_engine/src/wrapper/font_options.rs index 363472f28..22559ead4 100644 --- a/crates/sic_image_engine/src/wrapper/font_options.rs +++ b/crates/sic_image_engine/src/wrapper/font_options.rs @@ -1,5 +1,5 @@ +use sic_core::ab_glyph; use sic_core::image::Rgba; -use sic_core::rusttype::Scale; use std::path::PathBuf; type FontColor = Rgba; @@ -13,7 +13,7 @@ pub enum FontScale { pub struct FontOptions { pub font_path: PathBuf, pub color: FontColor, - pub scale: Scale, + pub scale: ab_glyph::PxScale, } impl FontOptions { @@ -22,8 +22,8 @@ impl FontOptions { font_path, color, scale: match scale { - FontScale::Uniform(value) => Scale::uniform(value), - FontScale::Scaling(horizontal, vertical) => Scale { + FontScale::Uniform(value) => ab_glyph::PxScale::from(value), + FontScale::Scaling(horizontal, vertical) => ab_glyph::PxScale { x: horizontal, y: vertical, }, diff --git a/crates/sic_image_engine/src/wrapper/image_path.rs b/crates/sic_image_engine/src/wrapper/image_path.rs index 7d09b35f1..5d512a4b3 100644 --- a/crates/sic_image_engine/src/wrapper/image_path.rs +++ b/crates/sic_image_engine/src/wrapper/image_path.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use sic_io::import::{file_reader, load_image, ImportConfig}; +use sic_io::decode::{file_reader, SicImageDecoder}; use crate::errors::SicImageEngineError; use sic_core::SicImage; @@ -17,7 +17,7 @@ impl ImageFromPath { pub fn open_image(&self) -> Result { file_reader(self.path.as_path()) - .and_then(|mut file| load_image(&mut file, &ImportConfig::default())) + .and_then(|mut file| SicImageDecoder::default().decode(&mut file)) .map_err(|_err| SicImageEngineError::LoadImageFromPath) } } diff --git a/crates/sic_io/Cargo.toml b/crates/sic_io/Cargo.toml index 2575f9bc4..8a7cc8c59 100644 --- a/crates/sic_io/Cargo.toml +++ b/crates/sic_io/Cargo.toml @@ -11,10 +11,10 @@ edition = "2021" rust-version = "1.61" [dependencies] -sic_core = { version = "0.22.0", path = "../sic_core"} +sic_core = { version = "0.22.0", path = "../sic_core" } thiserror = "1" [dev-dependencies] -parameterized = "1.1.0" +parameterized = "2.0.0" sic_testing = { version = "0.22.0", path = "../sic_testing" } diff --git a/crates/sic_io/src/conversion.rs b/crates/sic_io/src/conversion.rs index 658eb0379..17bd70eca 100644 --- a/crates/sic_io/src/conversion.rs +++ b/crates/sic_io/src/conversion.rs @@ -1,208 +1,20 @@ use crate::errors::{FormatError, SicIoError}; -use crate::export::ExportSettings; -use crate::WriteSeek; +use crate::preprocessor::Preprocess; use image::buffer::ConvertBuffer; use sic_core::image::codecs::gif::Repeat; -use sic_core::image::codecs::pnm; use sic_core::{image, AnimatedImage, SicImage}; -use std::io::Write; - -#[derive(Clone, Copy, Debug)] -pub enum AutomaticColorTypeAdjustment { - // Usually the default - Enabled, - Disabled, -} - -impl Default for AutomaticColorTypeAdjustment { - fn default() -> Self { - AutomaticColorTypeAdjustment::Enabled - } -} - -#[derive(Clone, Copy, Debug)] -pub enum RepeatAnimation { - Finite(u16), - Infinite, - Never, -} - -impl RepeatAnimation { - pub fn try_from_str(input: &str) -> Result { - match input { - "infinite" => Ok(Self::Infinite), - "never" => Ok(Self::Never), - elsy => elsy - .parse::() - .map(Self::Finite) - .map_err(|_| SicIoError::FormatError(FormatError::GIFRepeatInvalidValue)), - } - } -} - -impl Default for RepeatAnimation { - fn default() -> Self { - Self::Infinite - } -} - -/// Use the ConversionWriter to convert and write image buffers to an output. -pub struct ConversionWriter<'a> { - image: &'a SicImage, -} - -impl<'a> ConversionWriter<'a> { - pub fn new(image: &SicImage) -> ConversionWriter { - ConversionWriter { image } - } - - pub fn write_all( - &self, - writer: &mut W, - output_format: image::ImageOutputFormat, - export_settings: &ExportSettings, - ) -> Result<(), SicIoError> { - let color_processing = &ConversionWriter::pre_process_color_type( - self.image, - &output_format, - export_settings.adjust_color_type, - ); - - let export_buffer = match color_processing { - Some(replacement) => replacement, - None => self.image, - }; - - ConversionWriter::export(writer, export_buffer, output_format, export_settings) - } - - /// Some image output format types require color type pre-processing. - /// This is the case if the output image format does not support the color type held by the image buffer prior to the final conversion. - /// - /// If pre-processing of the color type took place, Some() will be returned. - /// If no pre-processing of the color type is required will return None. - /// Frames of animated images are not adjusted. - fn pre_process_color_type( - image: &SicImage, - output_format: &image::ImageOutputFormat, - color_type_adjustment: AutomaticColorTypeAdjustment, - ) -> Option { - if let AutomaticColorTypeAdjustment::Disabled = color_type_adjustment { - return None; - } - - match image { - SicImage::Animated(_) => None, - SicImage::Static(image) => { - adjust_dynamic_image(image, output_format).map(SicImage::from) - } - } - } - - fn export( - writer: &mut W, - image: &SicImage, - format: image::ImageOutputFormat, - export_settings: &ExportSettings, - ) -> Result<(), SicIoError> { - match image { - SicImage::Animated(image) => { - encode_animated_image(writer, image.collect_frames(), format, export_settings) - } - SicImage::Static(image) => encode_static_image(writer, image, format), - } - } -} - -/// Adjusts the type of image buffer, unless it's determined to be unnecessary -fn adjust_dynamic_image( - image: &image::DynamicImage, - output_format: &image::ImageOutputFormat, -) -> Option { - use image::DynamicImage; - - // A remaining open question: does a user expect for an image to be able to convert to a format even if the color type is not supported? - // And even if the user does, should we? - // I suspect that users expect that color type conversions should happen automatically. - // - // Testing also showed that even bmp with full black full white pixels do not convert correctly as of now. Why exactly is unclear; - // Perhaps the color type of the bmp formatted test image? - - match output_format { - image::ImageOutputFormat::Farbfeld => { - Some(DynamicImage::ImageRgba16(image.to_rgba8().convert())) - } - image::ImageOutputFormat::Pnm(pnm::PnmSubtype::Bitmap(_)) => { - Some(DynamicImage::ImageLuma8(image.to_luma8())) - } - image::ImageOutputFormat::Pnm(pnm::PnmSubtype::Graymap(_)) => { - Some(DynamicImage::ImageLuma8(image.to_luma8())) - } - image::ImageOutputFormat::Pnm(pnm::PnmSubtype::Pixmap(_)) => { - Some(DynamicImage::ImageRgb8(image.to_rgb8())) - } - _ => None, - } -} - -fn encode_static_image( - writer: &mut W, - image: &image::DynamicImage, - format: image::ImageOutputFormat, -) -> Result<(), SicIoError> { - image - .write_to(writer, format) - .map_err(SicIoError::ImageError) -} - -fn encode_animated_image( - writer: &mut W, - frames: Vec, // note: should be owned for the encoder, so can't be a slice - format: image::ImageOutputFormat, - export_settings: &ExportSettings, -) -> Result<(), SicIoError> { - match format { - image::ImageOutputFormat::Gif => { - encode_animated_gif(writer, frames, export_settings.gif_repeat) - } - _ => { - eprintln!("WARN: The animated image buffer could not be encoded to the {:?} format; encoding only the first frame", format); - let image = AnimatedImage::from_frames(frames).try_into_static_image(0)?; - encode_static_image(writer, &image, format) - } - } -} - -fn encode_animated_gif( - writer: &mut W, - frames: Vec, - repeat: RepeatAnimation, -) -> Result<(), SicIoError> { - let mut encoder = image::codecs::gif::GifEncoder::new(writer); - encoder.encode_frames(frames)?; - - match repeat { - RepeatAnimation::Finite(amount) => encoder.set_repeat(Repeat::Finite(amount))?, - RepeatAnimation::Infinite => encoder.set_repeat(Repeat::Infinite)?, - _ => {} - } - - Ok(()) -} - +use std::io::{Seek, Write}; #[cfg(test)] mod tests { use std::fs::File; use std::io::{self, Read}; use parameterized::parameterized; - use sic_core::image::{ImageFormat, ImageOutputFormat}; + use sic_core::image::ImageFormat; use sic_testing::{clean_up_output_path, setup_output_path, setup_test_image}; use super::*; - impl WriteSeek for File {} - // Individual tests: const INPUT: &str = "rainbow_8x6.bmp"; @@ -216,7 +28,7 @@ mod tests { let buffer = image::open(setup_test_image(INPUT)) .expect("Can't open test file.") .into(); - let example_output_format = image::ImageOutputFormat::Png; + let example_output_format = image::ImageFormat::Png; let conversion_processor = ConversionWriter::new(&buffer); conversion_processor .write_all( @@ -243,7 +55,7 @@ mod tests { let buffer = image::open(setup_test_image(INPUT)) .expect("Can't open test file.") .into(); - let example_output_format = image::ImageOutputFormat::Png; + let example_output_format = image::ImageFormat::Png; let conversion_processor = ConversionWriter::new(&buffer); conversion_processor .write_all( @@ -273,7 +85,7 @@ mod tests { let buffer = image::open(setup_test_image(INPUT)) .expect("Can't open test file.") .into(); - let example_output_format = image::ImageOutputFormat::Png; + let example_output_format = image::ImageFormat::Png; let conversion_processor = ConversionWriter::new(&buffer); conversion_processor .write_all( @@ -309,7 +121,7 @@ mod tests { fn test_conversion_with_header_match( input: &str, enc_format: &str, - format: image::ImageOutputFormat, + format: image::ImageFormat, expected_format: image::ImageFormat, ) -> io::Result<()> { let our_output = &format!("header_match_conversion.{}", enc_format); // this is required because tests are run in parallel, and the creation, or deletion can collide. @@ -349,36 +161,36 @@ mod tests { #[parameterized( ext = { - "bmp", - "farbfeld", - "gif", - "ico", - "jpg", - "jpeg", - "png", - "pbm", - "pgm", - "ppm", + "bmp", + "farbfeld", + "gif", + "ico", + "jpg", + "jpeg", + "png", + "pbm", + "pgm", + "ppm", "pam", }, to_format = { - image::ImageOutputFormat::Bmp, - image::ImageOutputFormat::Farbfeld, - image::ImageOutputFormat::Gif, - image::ImageOutputFormat::Ico, - image::ImageOutputFormat::Jpeg(80), - image::ImageOutputFormat::Jpeg(80), - image::ImageOutputFormat::Png, - image::ImageOutputFormat::Pnm(image::codecs::pnm::PnmSubtype::Bitmap( + image::ImageFormat::Bmp, + image::ImageFormat::Farbfeld, + image::ImageFormat::Gif, + image::ImageFormat::Ico, + image::ImageFormat::Jpeg(80), + image::ImageFormat::Jpeg(80), + image::ImageFormat::Png, + image::ImageFormat::Pnm(image::codecs::pnm::PnmSubtype::Bitmap( image::codecs::pnm::SampleEncoding::Binary, )), - image::ImageOutputFormat::Pnm(image::codecs::pnm::PnmSubtype::Graymap( + image::ImageFormat::Pnm(image::codecs::pnm::PnmSubtype::Graymap( image::codecs::pnm::SampleEncoding::Binary, )), - image::ImageOutputFormat::Pnm(image::codecs::pnm::PnmSubtype::Pixmap( + image::ImageFormat::Pnm(image::codecs::pnm::PnmSubtype::Pixmap( image::codecs::pnm::SampleEncoding::Binary, )), - image::ImageOutputFormat::Pnm( + image::ImageFormat::Pnm( image::codecs::pnm::PnmSubtype::ArbitraryMap ), }, @@ -398,7 +210,7 @@ mod tests { )] fn test_conversions_with_header_match( ext: &str, - to_format: ImageOutputFormat, + to_format: ImageFormat, expected_format: ImageFormat, ) { for test_image in INPUT_MULTI.iter() { diff --git a/crates/sic_io/src/import.rs b/crates/sic_io/src/decode.rs similarity index 55% rename from crates/sic_io/src/import.rs rename to crates/sic_io/src/decode.rs index 37423426f..f57b1dd4f 100644 --- a/crates/sic_io/src/import.rs +++ b/crates/sic_io/src/decode.rs @@ -1,76 +1,85 @@ use std::fs::File; -use std::io::{BufReader, Cursor, Read}; +use std::io::{BufRead, BufReader, Cursor, Read, Seek}; use std::path::Path; use crate::errors::SicIoError; -use sic_core::image::{AnimationDecoder, DynamicImage, ImageFormat}; use sic_core::{image, AnimatedImage, SicImage}; -/// Load an image using a reader. -/// All images are currently loaded from memory. -pub fn load_image(reader: &mut R, config: &ImportConfig) -> ImportResult { - let reader = image::io::Reader::new(Cursor::new(load(reader)?)) - .with_guessed_format() - .map_err(SicIoError::Io)?; - - match reader.format() { - Some(ImageFormat::Png) => decode_png(reader, config.selected_frame), - Some(ImageFormat::Gif) => decode_gif(reader, config.selected_frame), - Some(_) => reader - .decode() - .map_err(SicIoError::ImageError) - .map(SicImage::from), - None => Err(SicIoError::ImageError(image::error::ImageError::Decoding( - image::error::DecodingError::from_format_hint(image::error::ImageFormatHint::Unknown), - ))), +#[derive(Default)] +pub struct SicImageDecoder { + /// For animated images, this frame will be used if we can only decode into a static image. + selected_frame: Option, +} + +impl SicImageDecoder { + pub fn new(selected_frame: Option) -> Self { + Self { selected_frame } } } -/// Result which is returned for operations within this module. -type ImportResult = Result; +impl SicImageDecoder { + /// Load an image using a reader. + /// All images are currently loaded from memory. + pub fn decode(&self, reader: &mut R) -> Result { + let reader = image::io::Reader::new(Cursor::new(read_image_to_buffer(reader)?)) + .with_guessed_format() + .map_err(SicIoError::Io)?; + + match reader.format() { + Some(image::ImageFormat::Png) => decode_png(reader, self.selected_frame), + Some(image::ImageFormat::Gif) => decode_gif(reader, self.selected_frame), + Some(_) => reader + .decode() + .map_err(SicIoError::ImageError) + .map(SicImage::from), + None => Err(SicIoError::ImageError(image::error::ImageError::Decoding( + image::error::DecodingError::from_format_hint( + image::error::ImageFormatHint::Unknown, + ), + ))), + } + } +} /// Constructs a reader which reads from the stdin. -pub fn stdin_reader() -> ImportResult> { +pub fn stdin_reader() -> Result, SicIoError> { Ok(Box::new(BufReader::new(std::io::stdin()))) } /// Constructs a reader which reads from a file path. -pub fn file_reader>(path: P) -> ImportResult> { +pub fn file_reader>(path: P) -> Result, SicIoError> { Ok(Box::new(BufReader::new( File::open(path).map_err(SicIoError::Io)?, ))) } // Let the reader store the raw bytes into a buffer. -fn load(reader: &mut R) -> ImportResult> { +fn read_image_to_buffer(reader: &mut R) -> Result, SicIoError> { let mut buffer = Vec::new(); let _size = reader.read_to_end(&mut buffer).map_err(SicIoError::Io)?; Ok(buffer) } -#[derive(Debug, Default)] -pub struct ImportConfig { - /// For animated images; decides which frame will be used as static image. - pub selected_frame: Option, -} - /// Decode an image into frames -fn frames<'decoder, D: AnimationDecoder<'decoder>>(decoder: D) -> ImportResult { +fn frames<'decoder, D: image::AnimationDecoder<'decoder>>( + decoder: D, +) -> Result { let mut frames = decoder .into_frames() .collect_frames() .map_err(SicIoError::ImageError)?; if frames.len() == 1 { + // SAFETY(unwrap): We just checked .len() to be 1 let buffer = frames.pop().unwrap(); let buffer = buffer.into_buffer(); - Ok(SicImage::Static(DynamicImage::ImageRgba8(buffer))) + Ok(SicImage::Static(image::DynamicImage::ImageRgba8(buffer))) } else { Ok(SicImage::Animated(AnimatedImage::from_frames(frames))) } } -fn select_frame(image: SicImage, frame_index: Option) -> ImportResult { +fn select_frame(image: SicImage, frame_index: Option) -> Result { match (image, frame_index) { (SicImage::Animated(animated), Some(index)) => { let max_frames = animated.frames().len(); @@ -100,25 +109,25 @@ impl FrameIndex { } } -fn decode_gif( +fn decode_gif( reader: image::io::Reader, frame_index: Option, -) -> ImportResult { +) -> Result { let decoder = image::codecs::gif::GifDecoder::new(reader.into_inner()).map_err(SicIoError::ImageError)?; frames(decoder).and_then(|image| select_frame(image, frame_index)) } -fn decode_png( +fn decode_png( reader: image::io::Reader, frame: Option, -) -> ImportResult { +) -> Result { let decoder = image::codecs::png::PngDecoder::new(reader.into_inner()).map_err(SicIoError::ImageError)?; - if decoder.is_apng() { - frames(decoder.apng()).and_then(|f| select_frame(f, frame)) + if decoder.is_apng().map_err(SicIoError::ImageError)? { + frames(decoder.apng().map_err(SicIoError::ImageError)?).and_then(|f| select_frame(f, frame)) } else { image::DynamicImage::from_decoder(decoder) .map_err(SicIoError::ImageError) @@ -140,11 +149,10 @@ mod tests { fn load_gif_non_looping_frame_first() { let load_path = setup_test_image(GIF_NO_LOOP); - let config = ImportConfig { - selected_frame: Some(FrameIndex::First), - }; - - let image = load_image(&mut file_reader(load_path).unwrap(), &config).unwrap(); + let decoder = SicImageDecoder::new(Some(FrameIndex::First)); + let image = decoder + .decode(&mut file_reader(load_path).unwrap()) + .unwrap(); // color = red let expected: [u8; 4] = [254, 0, 0, 255]; @@ -155,16 +163,15 @@ mod tests { fn load_gif_non_looping_frame_first_is_zero() { let load_path = setup_test_image(GIF_NO_LOOP); - let first = ImportConfig { - selected_frame: Some(FrameIndex::First), - }; + let decoder_first = SicImageDecoder::new(Some(FrameIndex::First)); + let decoder_zero = SicImageDecoder::new(Some(FrameIndex::Nth(0))); - let zero = ImportConfig { - selected_frame: Some(FrameIndex::Nth(0)), - }; - - let first = load_image(&mut file_reader(&load_path).unwrap(), &first).unwrap(); - let zero = load_image(&mut file_reader(&load_path).unwrap(), &zero).unwrap(); + let first = decoder_first + .decode(&mut file_reader(&load_path).unwrap()) + .unwrap(); + let zero = decoder_zero + .decode(&mut file_reader(&load_path).unwrap()) + .unwrap(); assert_eq!(first.get_pixel(XY, XY).0, zero.get_pixel(XY, XY).0); } @@ -173,16 +180,15 @@ mod tests { fn load_gif_looping_frame_first_is_zero() { let load_path = setup_test_image(GIF_LOOP); - let first = ImportConfig { - selected_frame: Some(FrameIndex::First), - }; - - let zero = ImportConfig { - selected_frame: Some(FrameIndex::Nth(0)), - }; + let decoder_first = SicImageDecoder::new(Some(FrameIndex::First)); + let decoder_zero = SicImageDecoder::new(Some(FrameIndex::Nth(0))); - let first = load_image(&mut file_reader(&load_path).unwrap(), &first).unwrap(); - let zero = load_image(&mut file_reader(&load_path).unwrap(), &zero).unwrap(); + let first = decoder_first + .decode(&mut file_reader(&load_path).unwrap()) + .unwrap(); + let zero = decoder_zero + .decode(&mut file_reader(&load_path).unwrap()) + .unwrap(); assert_eq!(first.get_pixel(XY, XY).0, zero.get_pixel(XY, XY).0); } @@ -205,11 +211,10 @@ mod tests { for (i, expected) in FRAME_COLORS.iter().enumerate() { let load_path = setup_test_image(GIF_NO_LOOP); - let config = ImportConfig { - selected_frame: Some(FrameIndex::Nth(i)), - }; - - let image = load_image(&mut file_reader(load_path).unwrap(), &config).unwrap(); + let decoder = SicImageDecoder::new(Some(FrameIndex::Nth(i))); + let image = decoder + .decode(&mut file_reader(load_path).unwrap()) + .unwrap(); assert_eq!(&image.get_pixel(XY, XY).0, expected); } @@ -220,11 +225,10 @@ mod tests { for (i, expected) in FRAME_COLORS.iter().enumerate() { let load_path = setup_test_image(GIF_LOOP); - let config = ImportConfig { - selected_frame: Some(FrameIndex::Nth(i)), - }; - - let image = load_image(&mut file_reader(load_path).unwrap(), &config).unwrap(); + let decoder = SicImageDecoder::new(Some(FrameIndex::Nth(i))); + let image = decoder + .decode(&mut file_reader(load_path).unwrap()) + .unwrap(); assert_eq!(&image.get_pixel(XY, XY).0, expected); } @@ -234,11 +238,8 @@ mod tests { fn load_gif_non_looping_frame_nth_beyond_length() { let load_path = setup_test_image(GIF_NO_LOOP); - let config = ImportConfig { - selected_frame: Some(FrameIndex::Nth(8)), - }; - - let result = load_image(&mut file_reader(load_path).unwrap(), &config); + let decoder = SicImageDecoder::new(Some(FrameIndex::Nth(8))); + let result = decoder.decode(&mut file_reader(load_path).unwrap()); assert!(result.is_err()); } @@ -247,11 +248,8 @@ mod tests { fn load_gif_looping_frame_nth_beyond_length() { let load_path = setup_test_image(GIF_LOOP); - let config = ImportConfig { - selected_frame: Some(FrameIndex::Nth(8)), - }; - - let result = load_image(&mut file_reader(load_path).unwrap(), &config); + let decoder = SicImageDecoder::new(Some(FrameIndex::Nth(8))); + let result = decoder.decode(&mut file_reader(load_path).unwrap()); assert!(result.is_err()); } @@ -259,16 +257,15 @@ mod tests { fn load_gif_non_looping_frame_last_is_seven_index() { let load_path = setup_test_image(GIF_NO_LOOP); - let last = ImportConfig { - selected_frame: Some(FrameIndex::Last), - }; - - let seven = ImportConfig { - selected_frame: Some(FrameIndex::Nth(7)), - }; + let decoder_last = SicImageDecoder::new(Some(FrameIndex::Last)); + let decoder_seventh = SicImageDecoder::new(Some(FrameIndex::Nth(7))); - let last = load_image(&mut file_reader(&load_path).unwrap(), &last).unwrap(); - let seven = load_image(&mut file_reader(&load_path).unwrap(), &seven).unwrap(); + let last = decoder_last + .decode(&mut file_reader(&load_path).unwrap()) + .unwrap(); + let seven = decoder_seventh + .decode(&mut file_reader(&load_path).unwrap()) + .unwrap(); assert_eq!(last.get_pixel(XY, XY).0, seven.get_pixel(XY, XY).0); } @@ -277,16 +274,15 @@ mod tests { fn load_gif_looping_frame_last_is_seven_index() { let load_path = setup_test_image(GIF_LOOP); - let last = ImportConfig { - selected_frame: Some(FrameIndex::Last), - }; + let decoder_last = SicImageDecoder::new(Some(FrameIndex::Last)); + let decoder_seventh = SicImageDecoder::new(Some(FrameIndex::Nth(7))); - let seven = ImportConfig { - selected_frame: Some(FrameIndex::Nth(7)), - }; - - let last = load_image(&mut file_reader(&load_path).unwrap(), &last).unwrap(); - let seven = load_image(&mut file_reader(&load_path).unwrap(), &seven).unwrap(); + let last = decoder_last + .decode(&mut file_reader(&load_path).unwrap()) + .unwrap(); + let seven = decoder_seventh + .decode(&mut file_reader(&load_path).unwrap()) + .unwrap(); assert_eq!(last.get_pixel(XY, XY).0, seven.get_pixel(XY, XY).0); } @@ -301,8 +297,8 @@ mod tests { fn load_not_gif_formatted() { for path in NOT_GIFS.iter() { let load_path = setup_test_image(path); - let config = ImportConfig::default(); - let result = load_image(&mut file_reader(load_path).unwrap(), &config); + let decoder = SicImageDecoder::default(); + let result = decoder.decode(&mut file_reader(load_path).unwrap()); assert!(result.is_ok()); } } @@ -333,15 +329,12 @@ mod tests { fn apng(frame: Option, expected_color: Option<[u8; 4]>) { let load_path = setup_test_image(APNG_SAMPLE); - let config = ImportConfig { - selected_frame: frame, - }; - - let image = load_image(&mut file_reader(load_path).unwrap(), &config); + let decoder = SicImageDecoder::new(frame); + let result = decoder.decode(&mut file_reader(load_path).unwrap()); match expected_color { - Some(expected) => assert_eq!(image.unwrap().get_pixel(0, 0).0, expected), - None => assert!(image.is_err()), + Some(expected) => assert_eq!(result.unwrap().get_pixel(0, 0).0, expected), + None => assert!(result.is_err()), } } } diff --git a/crates/sic_io/src/encode.rs b/crates/sic_io/src/encode.rs new file mode 100644 index 000000000..6b973facf --- /dev/null +++ b/crates/sic_io/src/encode.rs @@ -0,0 +1,84 @@ +use std::io::{Seek, Write}; +use std::path::Path; + +use sic_core::image::ImageEncoder; +use sic_core::{image, AnimatedImage, SicImage}; + +use crate::errors::SicIoError; +use crate::format::gif::RepeatAnimation; +use crate::format::DynamicEncoder; +use crate::preprocessor::color_type::{ColorTypeAdjustment, ColorTypePreprocessor}; +use crate::preprocessor::Preprocess; + +pub struct SicImageEncoder { + pub adjust_color_type: ColorTypeAdjustment, + pub gif_repeat: RepeatAnimation, +} + +impl SicImageEncoder { + pub fn new(adjust_color_type: ColorTypeAdjustment, gif_repeat: RepeatAnimation) -> Self { + Self { + adjust_color_type, + gif_repeat, + } + } + + pub fn encode( + &self, + image: SicImage, + dynamic_encoder: DynamicEncoder, + ) -> Result<(), SicIoError> { + if self.adjust_color_type.is_enabled() { + let image = ColorTypePreprocessor::new(dynamic_encoder.format()).preprocess(image)?; + encode(dynamic_encoder, &image) + } else { + encode(dynamic_encoder, &image) + } + } +} + +fn encode(encoder: DynamicEncoder, image: &SicImage) -> Result<(), SicIoError> { + match image { + SicImage::Static(img) => encode_static_image(encoder, img), + SicImage::Animated(img) => encode_animated_image(encoder, img), + } +} + +fn encode_static_image( + encoder: DynamicEncoder, + image: &image::DynamicImage, +) -> Result<(), SicIoError> { + encoder + .write_image( + image.as_bytes(), + image.width(), + image.height(), + image.color().into(), + ) + .map_err(SicIoError::ImageError) +} + +fn encode_animated_image( + encoder: DynamicEncoder, + image: &AnimatedImage, +) -> Result<(), SicIoError> { + let frames = image.collect_frames(); + let fmt = encoder.format(); + + match encoder { + DynamicEncoder::Gif(mut enc) => enc.encode_frames(frames).map_err(SicIoError::ImageError), + enc => { + eprintln!("WARN: Unable to encode animated image buffer with format '{:?}': encoding first frame only", fmt); + let image = AnimatedImage::from_frames(frames).try_into_static_image(0)?; + encode_static_image(enc, &image) + } + } +} + +pub struct EmptyPath; + +impl AsRef for EmptyPath { + fn as_ref(&self) -> &Path { + Path::new("") + } +} diff --git a/crates/sic_io/src/errors.rs b/crates/sic_io/src/errors.rs index 4b3586b45..a3925b2d7 100644 --- a/crates/sic_io/src/errors.rs +++ b/crates/sic_io/src/errors.rs @@ -1,3 +1,4 @@ +use crate::format; use sic_core::image::ImageError; use sic_core::SicCoreError; use std::path::PathBuf; @@ -5,16 +6,16 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum SicIoError { - #[error("{0}")] + #[error(transparent)] SicCoreError(#[from] SicCoreError), - #[error("{0}")] + #[error(transparent)] ImageError(#[from] ImageError), - #[error("{0}")] + #[error(transparent)] Io(std::io::Error), - #[error("{0}")] + #[error(transparent)] FormatError(FormatError), #[error( @@ -32,27 +33,28 @@ pub enum SicIoError { #[error( "No supported image output format was found. The following identifier was provided: {0}." )] - UnknownImageIdentifier(String), + UnknownImageFormat(String), #[error( "Unable to determine the image format from the file extension. The following path was given: {0}." )] - UnableToDetermineImageFormatFromFileExtension(PathBuf), + UnknownImageFormatFromFileExtension(PathBuf), +} + +#[cfg(test)] +impl PartialEq for SicIoError { + fn eq(&self, other: &Self) -> bool { + format!("{:?}", self) == format!("{:?}", other) + } } #[derive(Debug, Error)] pub enum FormatError { - #[error("Unable to determine JPEG quality.")] - JPEGQualityLevelNotSet, - - #[error("JPEG Quality should range between 1 and 100 (inclusive).")] - JPEGQualityLevelNotInRange, + #[error(transparent)] + JPEGQuality(format::jpeg::JpegQualityError), #[error( "The GIF repeat value has to be either a positive integer < 65536, 'infinite' or 'never'" )] GIFRepeatInvalidValue, - - #[error("Using PNM requires the sample encoding to be set.")] - PNMSamplingEncodingNotSet, } diff --git a/crates/sic_io/src/export.rs b/crates/sic_io/src/export.rs deleted file mode 100644 index 5f7a3186e..000000000 --- a/crates/sic_io/src/export.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::path::Path; - -use sic_core::{image, SicImage}; - -use crate::conversion::{AutomaticColorTypeAdjustment, ConversionWriter, RepeatAnimation}; -use crate::errors::SicIoError; -use crate::WriteSeek; - -pub fn export( - image: &SicImage, - writer: &mut W, - format: image::ImageOutputFormat, - export_settings: ExportSettings, -) -> Result<(), SicIoError> { - let conv = ConversionWriter::new(image); - conv.write_all(writer, format, &export_settings) -} - -#[derive(Debug, Default)] -pub struct ExportSettings { - pub adjust_color_type: AutomaticColorTypeAdjustment, - pub gif_repeat: RepeatAnimation, -} - -pub struct EmptyPath; - -impl AsRef for EmptyPath { - fn as_ref(&self) -> &Path { - Path::new("") - } -} diff --git a/crates/sic_io/src/format.rs b/crates/sic_io/src/format.rs index 9da5b55d8..c80205976 100644 --- a/crates/sic_io/src/format.rs +++ b/crates/sic_io/src/format.rs @@ -1,435 +1,535 @@ +use std::fmt; +use std::fmt::Formatter; +use std::io::{Seek, Write}; use std::path::Path; use sic_core::image; -use sic_core::image::codecs::pnm; -use crate::errors::{FormatError, SicIoError}; +use crate::errors::SicIoError; +use crate::format::gif::RepeatAnimation; +use crate::format::jpeg::JpegQuality; -pub trait EncodingFormatByExtension { +pub mod gif; +pub mod jpeg; + +pub trait IntoImageEncoder { /// Determine the encoding format based on the extension of a file path. - fn by_extension>(&self, path: P) - -> Result; -} + fn from_extension( + writer: W, + path: &Path, + settings: &EncoderSettings, + ) -> Result, SicIoError>; -pub trait EncodingFormatByIdentifier { - /// Determine the encoding format based on the method of exporting. /// Determine the encoding format based on a recognized given identifier. - fn by_identifier(&self, identifier: &str) -> Result; -} - -pub trait EncodingFormatJPEGQuality { - /// Returns a validated jpeg quality value. - /// If no such value exists, it will return an error instead. - fn jpeg_quality(&self) -> Result; + fn from_identifier( + writer: W, + identifier: impl AsRef, + settings: &EncoderSettings, + ) -> Result, SicIoError>; } -pub trait EncodingFormatPNMSampleEncoding { - /// Returns a pnm sample encoding type. - /// If no such value exists, it will return an error instead. - fn pnm_encoding_type(&self) -> Result; +pub struct EncoderSettings { + pub pnm_sample_encoding: image::codecs::pnm::SampleEncoding, + pub jpeg_quality: JpegQuality, + pub repeat_animation: RepeatAnimation, } -/// This struct ensures no invalid JPEG qualities can be stored. -/// Using this struct instead of `u8` directly should ensure no panics occur because of invalid -/// quality values. -#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)] -pub struct JPEGQuality { - quality: u8, -} - -impl Default for JPEGQuality { - /// The default JPEG quality is `80`. +impl Default for EncoderSettings { fn default() -> Self { - Self { quality: 80 } + Self { + pnm_sample_encoding: image::codecs::pnm::SampleEncoding::Binary, + jpeg_quality: JpegQuality::default(), + repeat_animation: RepeatAnimation::default(), + } } } -impl JPEGQuality { - /// Returns an Ok result if the quality requested is between 1 and 100 (inclusive). - pub fn try_from(quality: u8) -> Result { - if (1u8..=100u8).contains(&quality) { - Ok(JPEGQuality { quality }) - } else { - Err(SicIoError::FormatError( - FormatError::JPEGQualityLevelNotInRange, - )) +#[allow(private_interfaces)] +pub enum DynamicEncoder { + Avif(image::codecs::avif::AvifEncoder), + Bmp(BmpEncoder), + Exr(image::codecs::openexr::OpenExrEncoder), + Farbfeld(image::codecs::farbfeld::FarbfeldEncoder), + Gif(image::codecs::gif::GifEncoder), + Ico(image::codecs::ico::IcoEncoder), + Jpeg(JpegEncoder), + Pnm(image::codecs::pnm::PnmEncoder), + Png(image::codecs::png::PngEncoder), + Qoi(image::codecs::qoi::QoiEncoder), + Tga(image::codecs::tga::TgaEncoder), + Tiff(image::codecs::tiff::TiffEncoder), + Webp(image::codecs::webp::WebPEncoder), +} + +impl fmt::Debug for DynamicEncoder { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + use DynamicEncoder::*; + + match self { + Avif(_) => f.write_str("DynamicEncoder(Avif)"), + Bmp(_) => f.write_str("DynamicEncoder(Bmp)"), + Exr(_) => f.write_str("DynamicEncoder(Exr)"), + Farbfeld(_) => f.write_str("DynamicEncoder(Farbfeld)"), + Gif(_) => f.write_str("DynamicEncoder(Gif)"), + Ico(_) => f.write_str("DynamicEncoder(Ico)"), + Jpeg(_) => f.write_str("DynamicEncoder(Jpeg)"), + Pnm(_) => f.write_str("DynamicEncoder(Pnm)"), + Png(_) => f.write_str("DynamicEncoder(Png)"), + Qoi(_) => f.write_str("DynamicEncoder(Qoi)"), + Tga(_) => f.write_str("DynamicEncoder(Tga)"), + Tiff(_) => f.write_str("DynamicEncoder(Tiff)"), + Webp(_) => f.write_str("DynamicEncoder(Webp)"), } } +} - /// Return the valid quality value. - pub fn as_u8(self) -> u8 { - self.quality +impl DynamicEncoder { + /// Create a BMP encoder. + pub fn bmp(writer: W) -> Result, SicIoError> { + Ok(Self::Bmp(BmpEncoder::new(writer))) } } -impl EncodingFormatByExtension for DetermineEncodingFormat { +impl IntoImageEncoder for DynamicEncoder { /// Determines the encoding format based on the extension of the given path. /// If the path has no extension, it will return an error. /// The extension if existing is matched against the identifiers, which currently /// are the extensions used. - fn by_extension>( - &self, - path: P, - ) -> Result { - let extension = path.as_ref().extension().and_then(|v| v.to_str()); + fn from_extension( + writer: W, + path: &Path, + settings: &EncoderSettings, + ) -> Result, SicIoError> { + let extension = path.extension().and_then(|v| v.to_str()); match extension { - Some(some) => self.by_identifier(some), - None => Err(SicIoError::UnableToDetermineImageFormatFromFileExtension( - path.as_ref().to_path_buf(), + Some(ext) => { + as IntoImageEncoder>::from_identifier(writer, ext, settings) + } + None => Err(SicIoError::UnknownImageFormatFromFileExtension( + path.to_path_buf(), )), } } -} -impl EncodingFormatByIdentifier for DetermineEncodingFormat { /// Determines an image output format based on a given `&str` identifier. /// Identifiers are based on common output file extensions. - fn by_identifier(&self, identifier: &str) -> Result { - match identifier.to_ascii_lowercase().as_str() { - "avif" => Ok(image::ImageOutputFormat::Avif), - "bmp" => Ok(image::ImageOutputFormat::Bmp), - "exr" => Ok(image::ImageOutputFormat::OpenExr), - "farbfeld" => Ok(image::ImageOutputFormat::Farbfeld), - "gif" => Ok(image::ImageOutputFormat::Gif), - "ico" => Ok(image::ImageOutputFormat::Ico), - "jpeg" | "jpg" => Ok(image::ImageOutputFormat::Jpeg(self.jpeg_quality()?.as_u8())), - "pam" => Ok(image::ImageOutputFormat::Pnm(pnm::PnmSubtype::ArbitraryMap)), - "pbm" => Ok(image::ImageOutputFormat::Pnm(pnm::PnmSubtype::Bitmap( - self.pnm_encoding_type()?, - ))), - "pgm" => Ok(image::ImageOutputFormat::Pnm(pnm::PnmSubtype::Graymap( - self.pnm_encoding_type()?, - ))), - "png" => Ok(image::ImageOutputFormat::Png), - "ppm" => Ok(image::ImageOutputFormat::Pnm(pnm::PnmSubtype::Pixmap( - self.pnm_encoding_type()?, - ))), - "qoi" => Ok(image::ImageOutputFormat::Qoi), - "tga" => Ok(image::ImageOutputFormat::Tga), - "tiff" | "tif" => Ok(image::ImageOutputFormat::Tiff), - "webp" => Ok(image::ImageOutputFormat::WebP), - _ => Err(SicIoError::UnknownImageIdentifier(identifier.to_string())), - } + fn from_identifier( + writer: W, + identifier: impl AsRef, + settings: &EncoderSettings, + ) -> Result, SicIoError> { + use DynamicEncoder::*; + + let id = identifier.as_ref(); + + Ok(match id.to_ascii_lowercase().as_str() { + "avif" => Avif(image::codecs::avif::AvifEncoder::new(writer)), + "bmp" => Bmp(BmpEncoder::new(writer)), + "exr" => Exr(image::codecs::openexr::OpenExrEncoder::new(writer)), + "ff" | "farbfeld" => Farbfeld(image::codecs::farbfeld::FarbfeldEncoder::new(writer)), + "gif" => { + let mut encoder = image::codecs::gif::GifEncoder::new(writer); + encoder + .set_repeat(settings.repeat_animation.into()) + .map_err(SicIoError::ImageError)?; + Gif(encoder) + } + "ico" => Ico(image::codecs::ico::IcoEncoder::new(writer)), + "jpeg" | "jpg" => Jpeg(JpegEncoder::new(writer, settings.jpeg_quality)), + "pam" => Pnm(image::codecs::pnm::PnmEncoder::new(writer) + .with_subtype(image::codecs::pnm::PnmSubtype::ArbitraryMap)), + "pbm" => Pnm(image::codecs::pnm::PnmEncoder::new(writer).with_subtype( + image::codecs::pnm::PnmSubtype::Bitmap(settings.pnm_sample_encoding), + )), + "pgm" => Pnm(image::codecs::pnm::PnmEncoder::new(writer).with_subtype( + image::codecs::pnm::PnmSubtype::Graymap(settings.pnm_sample_encoding), + )), + "png" => Png(image::codecs::png::PngEncoder::new(writer)), + "ppm" => Pnm(image::codecs::pnm::PnmEncoder::new(writer).with_subtype( + image::codecs::pnm::PnmSubtype::Pixmap(settings.pnm_sample_encoding), + )), + "qoi" => Qoi(image::codecs::qoi::QoiEncoder::new(writer)), + "tga" => Tga(image::codecs::tga::TgaEncoder::new(writer)), + "tiff" | "tif" => Tiff(image::codecs::tiff::TiffEncoder::new(writer)), + "webp" => Webp(image::codecs::webp::WebPEncoder::new_lossless(writer)), + _ => return Err(SicIoError::UnknownImageFormat(id.to_string())), + }) } } -pub struct DetermineEncodingFormat { - pub pnm_sample_encoding: Option, - pub jpeg_quality: Option, -} - -impl Default for DetermineEncodingFormat { - fn default() -> Self { - Self { - pnm_sample_encoding: Some(pnm::SampleEncoding::Binary), - jpeg_quality: Some(Default::default()), +impl image::ImageEncoder for DynamicEncoder { + fn write_image( + self, + buf: &[u8], + width: u32, + height: u32, + color_type: image::ExtendedColorType, + ) -> image::ImageResult<()> { + match self { + Self::Avif(enc) => enc.write_image(buf, width, height, color_type), + Self::Bmp(enc) => enc.write_image(buf, width, height, color_type), + Self::Exr(enc) => enc.write_image(buf, width, height, color_type), + Self::Farbfeld(enc) => enc.write_image(buf, width, height, color_type), + Self::Gif(mut enc) => { + // The `ColorTypePreprocessor` will, if enabled, convert the image to `RgbaImage` + // if necessary. + // This is unfortunate though, we're making a copy for sauce. + let image_buffer = image::RgbaImage::from_raw(width, height, buf.to_vec()) + .ok_or_else(|| { + image::ImageError::Encoding(image::error::EncodingError::new( + image::error::ImageFormatHint::Exact(image::ImageFormat::Gif), + "sic: Unable to construct frame from raw buffer".to_string(), + )) + })?; + + enc.encode_frame(image::Frame::new(image_buffer)) + } + Self::Ico(enc) => enc.write_image(buf, width, height, color_type), + Self::Jpeg(enc) => enc.write_image(buf, width, height, color_type), + Self::Pnm(enc) => enc.write_image(buf, width, height, color_type), + Self::Png(enc) => enc.write_image(buf, width, height, color_type), + Self::Qoi(enc) => enc.write_image(buf, width, height, color_type), + Self::Tga(enc) => enc.write_image(buf, width, height, color_type), + Self::Tiff(enc) => enc.write_image(buf, width, height, color_type), + Self::Webp(enc) => enc.write_image(buf, width, height, color_type), } } } -impl EncodingFormatPNMSampleEncoding for DetermineEncodingFormat { - fn pnm_encoding_type(&self) -> Result { - self.pnm_sample_encoding.ok_or(SicIoError::FormatError( - FormatError::PNMSamplingEncodingNotSet, - )) +impl DynamicEncoder { + pub fn format(&self) -> image::ImageFormat { + match self { + Self::Avif(_) => image::ImageFormat::Avif, + Self::Bmp(_) => image::ImageFormat::Bmp, + Self::Exr(_) => image::ImageFormat::Jpeg, + Self::Farbfeld(_) => image::ImageFormat::Farbfeld, + Self::Gif(_) => image::ImageFormat::Gif, + Self::Ico(_) => image::ImageFormat::Ico, + Self::Jpeg(_) => image::ImageFormat::Jpeg, + Self::Pnm(_) => image::ImageFormat::Pnm, + Self::Png(_) => image::ImageFormat::Pnm, + Self::Qoi(_) => image::ImageFormat::Qoi, + Self::Tga(_) => image::ImageFormat::Tga, + Self::Tiff(_) => image::ImageFormat::Tiff, + Self::Webp(_) => image::ImageFormat::WebP, + } } } -impl EncodingFormatJPEGQuality for DetermineEncodingFormat { - fn jpeg_quality(&self) -> Result { - self.jpeg_quality - .ok_or(SicIoError::FormatError(FormatError::JPEGQualityLevelNotSet)) - } +/// Wrapper for [`BmpEncoder`], which takes the writer by value, instead of by mutable reference. +/// All other encoders in `image` take the writer by value. Our [`DynamicEncoder`] wraps all formats +/// and also requires the writer to be given by value. This wrapper creates a new [`BmpEncoder`] +/// when writing the image, so it doesn't have to hold on to a mutable reference to its internal +/// writer. +/// +/// [`BmpEncoder`]: image::codecs::bmp::BmpEncoder +struct BmpEncoder { + writer: W, } -#[cfg(test)] -mod tests { - use super::*; - - const INPUT_FORMATS: &[&str] = &[ - "avif", "bmp", "exr", "farbfeld", "gif", "ico", "jpg", "jpeg", "png", "pbm", "pgm", "ppm", - "pam", "qoi", "tga", "tiff", "tif", - ]; - - const EXPECTED_VALUES: &[image::ImageOutputFormat] = &[ - image::ImageOutputFormat::Avif, - image::ImageOutputFormat::Bmp, - image::ImageOutputFormat::OpenExr, - image::ImageOutputFormat::Farbfeld, - image::ImageOutputFormat::Gif, - image::ImageOutputFormat::Ico, - image::ImageOutputFormat::Jpeg(80), - image::ImageOutputFormat::Jpeg(80), - image::ImageOutputFormat::Png, - image::ImageOutputFormat::Pnm(pnm::PnmSubtype::Bitmap(pnm::SampleEncoding::Binary)), - image::ImageOutputFormat::Pnm(pnm::PnmSubtype::Graymap(pnm::SampleEncoding::Binary)), - image::ImageOutputFormat::Pnm(pnm::PnmSubtype::Pixmap(pnm::SampleEncoding::Binary)), - image::ImageOutputFormat::Pnm(pnm::PnmSubtype::ArbitraryMap), - image::ImageOutputFormat::Qoi, - image::ImageOutputFormat::Tga, - image::ImageOutputFormat::Tiff, - image::ImageOutputFormat::Tiff, - ]; - - fn setup_default_format_determiner() -> DetermineEncodingFormat { - DetermineEncodingFormat { - pnm_sample_encoding: Some(pnm::SampleEncoding::Binary), - jpeg_quality: Some(JPEGQuality::try_from(80).unwrap()), - } +impl BmpEncoder { + pub fn new(writer: W) -> Self { + Self { writer } } +} - // - fn test_with_extensions(ext: &str, expected: &image::ImageOutputFormat) { - let path = format!("w_ext.{}", ext); - - let format_determiner = setup_default_format_determiner(); - let result = format_determiner.by_extension(path.as_str()); - - assert_eq!(result.unwrap(), *expected); +impl image::ImageEncoder for BmpEncoder { + fn write_image( + mut self, + buf: &[u8], + width: u32, + height: u32, + color_type: image::ExtendedColorType, + ) -> image::ImageResult<()> { + image::codecs::bmp::BmpEncoder::new(&mut self.writer) + .write_image(buf, width, height, color_type) } +} - #[test] - fn extension_with_defaults() { - let zipped = INPUT_FORMATS.iter().zip(EXPECTED_VALUES.iter()); +/// Box wrapper for [`JpegEncoder`], which is at least 4187 bytes large, exploding the size of the +/// [`DynamicEncoder`]. +struct JpegEncoder { + writer: Box>, +} - for (ext, exp) in zipped { - test_with_extensions(ext, exp); +impl JpegEncoder { + pub fn new(writer: W, quality: JpegQuality) -> Self { + Self { + writer: Box::new(image::codecs::jpeg::JpegEncoder::new_with_quality( + writer, + quality.as_u8(), + )), } } +} - // - #[test] - #[should_panic] - fn extension_unknown_extension() { - let path = "w_ext.h"; - let format_determiner = setup_default_format_determiner(); - let result = format_determiner.by_extension(path); - - result.unwrap(); - } - - // - #[test] - #[should_panic] - fn extension_no_extension() { - let path = "png"; - let format_determiner = setup_default_format_determiner(); - let result = format_determiner.by_extension(path); - - result.unwrap(); - } - - // - #[test] - #[should_panic] - fn extension_invalid_extension() { - let path = ".png"; - let format_determiner = setup_default_format_determiner(); - let result = format_determiner.by_extension(path); - - result.unwrap(); +impl image::ImageEncoder for JpegEncoder { + fn write_image( + self, + buf: &[u8], + width: u32, + height: u32, + color_type: image::ExtendedColorType, + ) -> image::ImageResult<()> { + self.writer.write_image(buf, width, height, color_type) } +} - // - fn test_with_identifier(identifier: &str, expected: &image::ImageOutputFormat) { - let format_determiner = setup_default_format_determiner(); - let result = format_determiner.by_identifier(identifier); - - assert_eq!(result.unwrap(), *expected); - } +#[cfg(test)] +mod tests { + use super::*; + use parameterized::parameterized; + use std::io::SeekFrom; - #[test] - fn identifier_with_defaults() { - let zipped = INPUT_FORMATS.iter().zip(EXPECTED_VALUES.iter()); + #[derive(Debug)] + struct DummyMem; - for (id, exp) in zipped { - test_with_identifier(id, exp); + impl Seek for DummyMem { + fn seek(&mut self, _pos: SeekFrom) -> std::io::Result { + Ok(0) } } - #[test] - fn uppercase_formats() { - let uppercase_formats = INPUT_FORMATS - .iter() - .map(|v| v.to_ascii_uppercase()) - .zip(EXPECTED_VALUES.iter()); - - for (id, exp) in uppercase_formats { - test_with_identifier(id.as_str(), exp); - } - } - - // - #[test] - #[should_panic] - fn identifier_unknown_identifier() { - let format_determiner = setup_default_format_determiner(); - let result = format_determiner.by_identifier(""); - result.unwrap(); - } - - // non default: pnm ascii + "pbm" - #[test] - fn identifier_custom_pnm_sample_encoding_ascii_pbm() { - let format_determiner = DetermineEncodingFormat { - pnm_sample_encoding: Some(pnm::SampleEncoding::Ascii), - jpeg_quality: None, - }; - - let result = format_determiner.by_identifier("pbm").unwrap(); - let expected = - image::ImageOutputFormat::Pnm(pnm::PnmSubtype::Bitmap(pnm::SampleEncoding::Ascii)); + impl Write for DummyMem { + fn write(&mut self, _buf: &[u8]) -> std::io::Result { + Ok(0) + } - assert_eq!(result, expected); + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } } - // non default: pnm ascii + "pgm" - #[test] - fn identifier_custom_pnm_sample_encoding_ascii_pgm() { - let format_determiner = DetermineEncodingFormat { - pnm_sample_encoding: Some(pnm::SampleEncoding::Ascii), - jpeg_quality: None, - }; + #[parameterized( + ext = { + "avif", + "bmp", + "exr", + "farbfeld", + "gif", + "ico", + "jpg", + "jpeg", + "png", + "pbm", + "pgm", + "ppm", + "pam", + "qoi", + "tga", + "tiff", + "tif" + }, + expected = { + image::ImageFormat::Avif, + image::ImageFormat::Bmp, + image::ImageFormat::OpenExr, + image::ImageFormat::Farbfeld, + image::ImageFormat::Gif, + image::ImageFormat::Ico, + image::ImageFormat::Jpeg, + image::ImageFormat::Jpeg, + image::ImageFormat::Png, + image::ImageFormat::Pnm, + image::ImageFormat::Pnm, + image::ImageFormat::Pnm, + image::ImageFormat::Pnm, + image::ImageFormat::Qoi, + image::ImageFormat::Tga, + image::ImageFormat::Tiff, + image::ImageFormat::Tiff, + } + )] + fn test_with_extensions(ext: &str, expected: image::ImageFormat) { + let path = format!("image.{}", ext); + let path = Path::new(&path); - let result = format_determiner.by_identifier("pgm").unwrap(); - let expected = - image::ImageOutputFormat::Pnm(pnm::PnmSubtype::Graymap(pnm::SampleEncoding::Ascii)); + let settings = EncoderSettings::default(); + let mut mem = DummyMem; + let dynamic_encoder = DynamicEncoder::from_extension(&mut mem, path, &settings); - assert_eq!(result, expected); + assert_eq!(dynamic_encoder.unwrap().format(), expected); } - // non default: pnm ascii + "ppm" - #[test] - fn identifier_custom_pnm_sample_encoding_ascii_ppm() { - let format_determiner = DetermineEncodingFormat { - pnm_sample_encoding: Some(pnm::SampleEncoding::Ascii), - jpeg_quality: None, - }; - - let result = format_determiner.by_identifier("ppm").unwrap(); - let expected = - image::ImageOutputFormat::Pnm(pnm::PnmSubtype::Pixmap(pnm::SampleEncoding::Ascii)); + #[parameterized( + identifier = { + "avif", + "bmp", + "exr", + "farbfeld", + "gif", + "ico", + "jpg", + "jpeg", + "png", + "pbm", + "pgm", + "ppm", + "pam", + "qoi", + "tga", + "tiff", + "tif" + }, + expected = { + image::ImageFormat::Avif, + image::ImageFormat::Bmp, + image::ImageFormat::OpenExr, + image::ImageFormat::Farbfeld, + image::ImageFormat::Gif, + image::ImageFormat::Ico, + image::ImageFormat::Jpeg, + image::ImageFormat::Jpeg, + image::ImageFormat::Png, + image::ImageFormat::Pnm, + image::ImageFormat::Pnm, + image::ImageFormat::Pnm, + image::ImageFormat::Pnm, + image::ImageFormat::Qoi, + image::ImageFormat::Tga, + image::ImageFormat::Tiff, + image::ImageFormat::Tiff, + } + )] + fn test_with_identifier(identifier: &str, expected: image::ImageFormat) { + let settings = EncoderSettings::default(); + let mut mem = DummyMem; + let dynamic_encoder = DynamicEncoder::from_identifier(&mut mem, identifier, &settings); - assert_eq!(result, expected); + assert_eq!(dynamic_encoder.unwrap().format(), expected); } - // non default: jpeg custom, quality lower bound - #[test] - fn identifier_custom_jpeg_quality_in_range_lower() { - let format_determiner = DetermineEncodingFormat { - pnm_sample_encoding: None, - jpeg_quality: Some(JPEGQuality::try_from(1).unwrap()), - }; - - let result = format_determiner.by_identifier("jpg").unwrap(); - let expected = image::ImageOutputFormat::Jpeg(1); + #[parameterized( + identifier = { + "AVIF", + "BMP", + "EXR", + "FARBFELD", + "GIF", + "ICO", + "JPG", + "JPEG", + "PNG", + "PBM", + "PGM", + "PPM", + "PAM", + "QOI", + "TGA", + "TIFF", + "TIF" + }, + expected = { + image::ImageFormat::Avif, + image::ImageFormat::Bmp, + image::ImageFormat::OpenExr, + image::ImageFormat::Farbfeld, + image::ImageFormat::Gif, + image::ImageFormat::Ico, + image::ImageFormat::Jpeg, + image::ImageFormat::Jpeg, + image::ImageFormat::Png, + image::ImageFormat::Pnm, + image::ImageFormat::Pnm, + image::ImageFormat::Pnm, + image::ImageFormat::Pnm, + image::ImageFormat::Qoi, + image::ImageFormat::Tga, + image::ImageFormat::Tiff, + image::ImageFormat::Tiff, + } + )] + fn test_with_identifier_uppercase(identifier: &str, expected: image::ImageFormat) { + let settings = EncoderSettings::default(); + let mut mem = DummyMem; + let dynamic_encoder = DynamicEncoder::from_identifier(&mut mem, identifier, &settings); - assert_eq!(result, expected); + assert_eq!(dynamic_encoder.unwrap().format(), expected); } - // non default: jpeg custom, quality upper bound #[test] - fn identifier_custom_jpeg_quality_in_range_upper() { - let format_determiner = DetermineEncodingFormat { - pnm_sample_encoding: None, - jpeg_quality: Some(JPEGQuality::try_from(100).unwrap()), - }; - - let result = format_determiner.by_identifier("jpg").unwrap(); - let expected = image::ImageOutputFormat::Jpeg(100); - - assert_eq!(result, expected); + fn extension_unknown_extension() { + let path = Path::new("w_ext.h"); + let settings = EncoderSettings::default(); + let mut mem = DummyMem; + let dynamic_encoder = DynamicEncoder::from_extension(&mut mem, path, &settings); + + assert_eq!( + dynamic_encoder.unwrap_err(), + SicIoError::UnknownImageFormatFromFileExtension(Path::new("w_ext.h").to_path_buf()) + ); } - // if we were to test 'identifier_custom_jpeg_quality_OUT_range_[lower/upper]' - // ^^^ - // our DetermineEncodingFormat would fail on creation by JPEGQuality::try_from which fails - // on outbound ranges - - // #[test] - fn jpeg_quality_in_range_lower() { - let result = JPEGQuality::try_from(1).unwrap(); - let expected = JPEGQuality { quality: 1 }; - - assert_eq!(result, expected); + fn extension_no_extension() { + let path = Path::new("png"); + let settings = EncoderSettings::default(); + let mut mem = DummyMem; + let dynamic_encoder = DynamicEncoder::from_extension(&mut mem, path, &settings); + + assert_eq!( + dynamic_encoder.unwrap_err(), + SicIoError::UnknownImageFormatFromFileExtension(Path::new("png").to_path_buf()) + ); } - // #[test] - fn jpeg_quality_in_range_upper() { - let result = JPEGQuality::try_from(100).unwrap(); - let expected = JPEGQuality { quality: 100 }; - - assert_eq!(result, expected); + fn extension_invalid_extension() { + let path = Path::new(".png"); + let settings = EncoderSettings::default(); + let mut mem = DummyMem; + let dynamic_encoder = DynamicEncoder::from_extension(&mut mem, path, &settings); + + assert_eq!( + dynamic_encoder.unwrap_err(), + SicIoError::UnknownImageFormatFromFileExtension(Path::new(".png").to_path_buf()) + ); } - // #[test] - #[should_panic] - fn jpeg_quality_out_range_lower() { - let result = JPEGQuality::try_from(0).unwrap(); - let expected = JPEGQuality { quality: 0 }; - - assert_eq!(result, expected); + fn identifier_unknown_identifier() { + let path = Path::new(""); + let settings = EncoderSettings::default(); + let mut mem = DummyMem; + let dynamic_encoder = DynamicEncoder::from_extension(&mut mem, path, &settings); + + assert_eq!( + dynamic_encoder.unwrap_err(), + SicIoError::UnknownImageFormatFromFileExtension(Path::new("").to_path_buf()) + ); } - // + // non default: pnm ascii + "pbm" #[test] - #[should_panic] - fn jpeg_quality_out_range_upper() { - let result = JPEGQuality::try_from(101).unwrap(); - let expected = JPEGQuality { quality: 101 }; + #[allow(clippy::field_reassign_with_default)] + fn identifier_custom_pnm_sample_encoding_ascii_pbm() { + let mut settings = EncoderSettings::default(); + settings.pnm_sample_encoding = image::codecs::pnm::SampleEncoding::Ascii; - assert_eq!(result, expected); - } + let mut mem = DummyMem; + let dynamic_encoder = DynamicEncoder::from_identifier(&mut mem, "pbm", &settings).unwrap(); - // DetermineEncodingFormat has None, while Some required: pbm - #[test] - #[should_panic] - fn identifier_requires_pnm_sample_encoding_to_be_set_pbm() { - let format_determiner = DetermineEncodingFormat { - pnm_sample_encoding: None, - jpeg_quality: None, - }; - - format_determiner.by_identifier("pbm").unwrap(); + assert_eq!(dynamic_encoder.format(), image::ImageFormat::Pnm); } - // DetermineEncodingFormat has None, while Some required: pbm - #[test] - #[should_panic] - fn identifier_requires_pnm_sample_encoding_to_be_set_pgm() { - let format_determiner = DetermineEncodingFormat { - pnm_sample_encoding: None, - jpeg_quality: None, - }; - - format_determiner.by_identifier("pgm").unwrap(); - } + // non default: pnm ascii + "pgm" + #[parameterized( + identifier = { + "pbm", + "pgm", + "ppm", + } + )] + #[allow(clippy::field_reassign_with_default)] + fn identifier_custom_pnm_sample_encoding_ascii_pgm(identifier: &str) { + let mut settings = EncoderSettings::default(); + settings.pnm_sample_encoding = image::codecs::pnm::SampleEncoding::Ascii; - // DetermineEncodingFormat has None, while Some required: ppm - #[test] - #[should_panic] - fn identifier_requires_pnm_sample_encoding_to_be_set_ppm() { - let format_determiner = DetermineEncodingFormat { - pnm_sample_encoding: None, - jpeg_quality: None, - }; - - format_determiner.by_identifier("ppm").unwrap(); - } + let mut mem = DummyMem; + let dynamic_encoder = + DynamicEncoder::from_identifier(&mut mem, identifier, &settings).unwrap(); - // DetermineEncodingFormat has None, while Some required: jpg - #[test] - #[should_panic] - fn identifier_requires_pnm_sample_encoding_to_be_set_jpg() { - let format_determiner = DetermineEncodingFormat { - pnm_sample_encoding: None, - jpeg_quality: None, - }; - - format_determiner.by_identifier("jpg").unwrap(); + assert_eq!(dynamic_encoder.format(), image::ImageFormat::Pnm); } } diff --git a/crates/sic_io/src/format/gif.rs b/crates/sic_io/src/format/gif.rs new file mode 100644 index 000000000..7c93cedae --- /dev/null +++ b/crates/sic_io/src/format/gif.rs @@ -0,0 +1,38 @@ +use crate::errors::{FormatError, SicIoError}; +use sic_core::image; + +#[derive(Clone, Copy, Debug)] +pub enum RepeatAnimation { + Finite(u16), + Infinite, + Never, +} + +impl RepeatAnimation { + pub fn try_from_str(input: &str) -> Result { + match input { + "infinite" => Ok(Self::Infinite), + "never" => Ok(Self::Never), + elsy => elsy + .parse::() + .map(Self::Finite) + .map_err(|_| SicIoError::FormatError(FormatError::GIFRepeatInvalidValue)), + } + } +} + +impl Default for RepeatAnimation { + fn default() -> Self { + Self::Infinite + } +} + +impl From for image::codecs::gif::Repeat { + fn from(value: RepeatAnimation) -> Self { + match value { + RepeatAnimation::Finite(v) => image::codecs::gif::Repeat::Finite(v), + RepeatAnimation::Infinite => image::codecs::gif::Repeat::Infinite, + RepeatAnimation::Never => image::codecs::gif::Repeat::Finite(0), + } + } +} diff --git a/crates/sic_io/src/format/jpeg.rs b/crates/sic_io/src/format/jpeg.rs new file mode 100644 index 000000000..6ce5d3c77 --- /dev/null +++ b/crates/sic_io/src/format/jpeg.rs @@ -0,0 +1,67 @@ +/// This struct ensures no invalid JPEG qualities can be stored. +/// Using this struct instead of `u8` directly should ensure no panics occur because of invalid +/// quality values. +#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)] +pub struct JpegQuality(pub u8); + +impl Default for JpegQuality { + /// The default JPEG quality is `80`. + fn default() -> Self { + Self(80) + } +} + +impl JpegQuality { + /// Returns an Ok result if the quality requested is between 1 and 100 (inclusive). + pub fn try_from(quality: u8) -> Result { + if (1u8..=100u8).contains(&quality) { + Ok(JpegQuality(quality)) + } else { + Err(JpegQualityError { value: quality }) + } + } + + /// Return the valid quality value. + pub fn as_u8(self) -> u8 { + self.0 + } +} + +#[derive(Debug, thiserror::Error)] +#[error("JPEG quality should range between 1 and 100 (inclusive), but was {}", .value)] +pub struct JpegQualityError { + pub value: u8, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn jpeg_quality_in_range_lower() { + let result = JpegQuality::try_from(1).unwrap(); + let expected = JpegQuality(1); + + assert_eq!(result, expected); + } + + #[test] + fn jpeg_quality_in_range_upper() { + let result = JpegQuality::try_from(100).unwrap(); + let expected = JpegQuality(100); + + assert_eq!(result, expected); + } + + #[test] + #[should_panic] + fn jpeg_quality_out_range_lower() { + JpegQuality::try_from(0).unwrap(); + } + + #[test] + #[should_panic] + fn jpeg_quality_out_range_upper() { + JpegQuality::try_from(101).unwrap(); + } +} diff --git a/crates/sic_io/src/lib.rs b/crates/sic_io/src/lib.rs index 2589e265c..acb575654 100644 --- a/crates/sic_io/src/lib.rs +++ b/crates/sic_io/src/lib.rs @@ -1,11 +1,7 @@ #![deny(clippy::all)] -#![allow(clippy::upper_case_acronyms)] -pub mod export; -pub mod import; - -pub mod conversion; +pub mod decode; +pub mod encode; pub mod errors; pub mod format; - -pub trait WriteSeek: std::io::Write + std::io::Seek {} +pub mod preprocessor; diff --git a/crates/sic_io/src/preprocessor.rs b/crates/sic_io/src/preprocessor.rs new file mode 100644 index 000000000..4460d8ebc --- /dev/null +++ b/crates/sic_io/src/preprocessor.rs @@ -0,0 +1,9 @@ +use sic_core::SicImage; + +pub mod color_type; + +pub trait Preprocess { + type Err; + + fn preprocess(self, image: SicImage) -> Result; +} diff --git a/crates/sic_io/src/preprocessor/color_type.rs b/crates/sic_io/src/preprocessor/color_type.rs new file mode 100644 index 000000000..67860c45d --- /dev/null +++ b/crates/sic_io/src/preprocessor/color_type.rs @@ -0,0 +1,73 @@ +use crate::errors::SicIoError; +use crate::preprocessor::Preprocess; +use sic_core::image::buffer::ConvertBuffer; +use sic_core::image::DynamicImage; +use sic_core::{image, SicImage}; + +pub struct ColorTypePreprocessor { + format: image::ImageFormat, +} + +impl ColorTypePreprocessor { + pub fn new(format: image::ImageFormat) -> Self { + Self { format } + } +} + +impl Preprocess for ColorTypePreprocessor { + type Err = SicIoError; + + fn preprocess(self, image: SicImage) -> Result { + match image { + SicImage::Static(image) if self.format == image::ImageFormat::Farbfeld => { + // A remaining open question: does a user expect for an image to be able to convert to a format even if the color type is not supported? + // And even if the user does, should we? + // I suspect that users expect that color type conversions should happen automatically. + // + // Testing also showed that even bmp with full black full white pixels do not convert correctly as of now. Why exactly is unclear; + // Perhaps the color type of the bmp formatted test image? + let out = DynamicImage::ImageRgba16(image.to_rgba8().convert()); + Ok(SicImage::Static(out)) + } + // We must pre-process when the image format is Gif, since image::Frame only supports + // RgbaImage = ImageBuffer, Vec>, and our `DynamicEncoder` is unaware of the + // underlying format + SicImage::Static(image) + if self.format == image::ImageFormat::Gif + && image.color() != image::ColorType::Rgba8 => + { + Ok(SicImage::Static(DynamicImage::ImageRgba8(image.to_rgba8()))) + } + elsy => Ok(elsy), + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum ColorTypeAdjustment { + // Usually the default + Enabled, + Disabled, +} + +impl From for ColorTypeAdjustment { + fn from(value: bool) -> Self { + if value { + ColorTypeAdjustment::Enabled + } else { + ColorTypeAdjustment::Disabled + } + } +} + +impl Default for ColorTypeAdjustment { + fn default() -> Self { + ColorTypeAdjustment::Enabled + } +} + +impl ColorTypeAdjustment { + pub fn is_enabled(&self) -> bool { + matches!(self, Self::Enabled) + } +} diff --git a/src/cli/app.rs b/src/cli/app.rs index cadc9b46d..6c3ff0355 100644 --- a/src/cli/app.rs +++ b/src/cli/app.rs @@ -6,8 +6,8 @@ use arg_names::*; use clap::{App, AppSettings, Arg, ArgGroup, ArgMatches}; use sic_cli_ops::create_image_ops; use sic_cli_ops::operations::OperationId; -use sic_io::conversion::RepeatAnimation; -use sic_io::import::FrameIndex; +use sic_io::decode::FrameIndex; +use sic_io::format::gif::RepeatAnimation; use std::path::Path; use std::str::FromStr; @@ -43,7 +43,6 @@ define_arg_consts!(arg_names, { ARG_FORCED_OUTPUT_FORMAT, ARG_JPEG_ENCODING_QUALITY, ARG_PNM_ENCODING_ASCII, - ARG_IMAGE_CRATE_FALLBACK, ARG_GIF_REPEAT, // provide image operations using image script @@ -200,11 +199,6 @@ pub fn create_app( .takes_value(true) ) - .arg(Arg::with_name(ARG_IMAGE_CRATE_FALLBACK) - .long("enable-output-format-decider-fallback") - .help("[experimental] When this flag is set, sic will attempt to fallback to an alternative output format decider (image crate version), \ - *if* sic's own decider can't find a suitable format. Setting this flag may introduce unwanted behaviour; use with caution.")) - // image-operations(script): .arg(Arg::with_name(ARG_APPLY_OPERATIONS) .long("apply-operations") @@ -437,10 +431,6 @@ pub fn build_app_config<'a>(matches: &'a ArgMatches) -> anyhow::Result>, filter_unsupported: bool, - image_crate_fallback_enabled: bool, ) -> anyhow::Result> { let paths: Vec = inputs .map(|entry| { @@ -115,7 +113,7 @@ impl InputOutputMode { .collect::>>()?; Ok(if filter_unsupported { - filter_unsupported_paths(paths, image_crate_fallback_enabled) + filter_unsupported_paths(paths) } else { paths }) @@ -123,21 +121,18 @@ impl InputOutputMode { } // remove paths with extensions we don't recognise -fn filter_unsupported_paths(paths: Vec, fallback_enabled: bool) -> Vec { - use crate::cli::pipeline::fallback::guess_output_by_path; - use crate::combinators::FallbackIf; - use sic_io::format::DetermineEncodingFormat; - use sic_io::format::EncodingFormatByExtension; - - let checker = DetermineEncodingFormat::default(); +fn filter_unsupported_paths(paths: Vec) -> Vec { + // NB: We currently support all image formats which are also suported by `image`. + // If that changes, we should also update this. paths .into_iter() .filter(|path| { - checker - .by_extension(path) - .fallback_if(fallback_enabled, guess_output_by_path, path) - .is_ok() + if let Ok(p) = image::ImageFormat::from_path(path) { + p.writing_enabled() + } else { + false + } }) .collect() } @@ -216,9 +211,6 @@ impl Default for Config<'_> { // Defaults to infinite repeat gif_repeat: RepeatAnimation::default(), - - // Do not fallback to image crate output recognition by default - image_output_format_fallback: false, }, // Defaults to no provided image operations script. @@ -290,14 +282,6 @@ impl<'a> ConfigBuilder<'a> { self } - pub fn image_output_format_decider_fallback( - mut self, - enable_fallback: bool, - ) -> ConfigBuilder<'a> { - self.settings.encoding_settings.image_output_format_fallback = enable_fallback; - self - } - // image-operations pub fn image_operations_program(mut self, program: Vec) -> ConfigBuilder<'a> { self.settings.image_operations_program = program; @@ -320,9 +304,6 @@ pub struct FormatEncodingSettings { pub jpeg_quality: u8, pub pnm_use_ascii_format: bool, pub gif_repeat: RepeatAnimation, - - // Whether to fallback on the image crate to determine the output format if sic doesn't support it yet - pub image_output_format_fallback: bool, } /// Strictly speaking not necessary here since the responsible owners will validate the quality as well. @@ -404,7 +385,7 @@ mod tests { let path_bufs = to_path_bufs(paths); let expected_path_bufs = to_path_bufs(&[paths[0], paths[1], paths[2]]); - let filtered = filter_unsupported_paths(path_bufs, false); + let filtered = filter_unsupported_paths(path_bufs); assert_eq!(filtered, expected_path_bufs); } @@ -425,17 +406,8 @@ mod tests { &[], &["a.farbfeld", "a.ff"], &["a.farbfeld"], - }, fallback_on_imagecrate = { - false, - false, - true, - false, })] - fn are_unsupported_paths_getting_filtered( - paths_in: &[&str], - paths_expected: &[&str], - fallback_on_imagecrate: bool, - ) { + fn are_unsupported_paths_getting_filtered(paths_in: &[&str], paths_expected: &[&str]) { fn to_path_bufs<'s>(paths: impl IntoIterator) -> Vec { paths .into_iter() @@ -445,7 +417,7 @@ mod tests { let path_bufs = to_path_bufs(paths_in); let expected_path_bufs = to_path_bufs(paths_expected); - let filtered = filter_unsupported_paths(path_bufs, fallback_on_imagecrate); + let filtered = filter_unsupported_paths(path_bufs); assert_eq!(filtered, expected_path_bufs); } diff --git a/src/cli/pipeline/mod.rs b/src/cli/pipeline.rs similarity index 65% rename from src/cli/pipeline/mod.rs rename to src/cli/pipeline.rs index bba6b4be9..4dbc8348f 100644 --- a/src/cli/pipeline/mod.rs +++ b/src/cli/pipeline.rs @@ -5,18 +5,14 @@ use std::io::{self, Read, Seek, SeekFrom, Stdout, Write}; use crate::cli::config::{Config, InputOutputMode, InputOutputModeType, PathVariant}; use crate::cli::license::LicenseTexts; use crate::cli::license::PrintTextFor; -use crate::cli::pipeline::fallback::{guess_output_by_identifier, guess_output_by_path}; -use crate::combinators::FallbackIf; use anyhow::{anyhow, bail, Context}; use sic_core::image; use sic_image_engine::engine::ImageEngine; -use sic_io::conversion::AutomaticColorTypeAdjustment; -use sic_io::format::{ - DetermineEncodingFormat, EncodingFormatByExtension, EncodingFormatByIdentifier, JPEGQuality, -}; -use sic_io::{export, import, WriteSeek}; - -pub mod fallback; +use sic_io::decode; +use sic_io::decode::SicImageDecoder; +use sic_io::encode::SicImageEncoder; +use sic_io::format::jpeg::JpegQuality; +use sic_io::format::{DynamicEncoder, EncoderSettings, IntoImageEncoder}; pub fn run_with_devices<'c>( in_and_output: InputOutputMode, @@ -31,8 +27,8 @@ pub fn run_with_devices<'c>( run( || create_reader(&input), |ext: Option<&str>| create_writer(&output, ext), - || create_format_decider(&output, config), config, + &output, ) .with_context(|| format!("With: {}", input.describe_input())) } @@ -49,8 +45,8 @@ pub fn run_with_devices<'c>( run( || create_reader(input), |ext: Option<&str>| create_writer(output, ext), - || create_format_decider(output, config), config, + output, ) .with_context(|| format!("With input: {}", input.describe_input()))? } @@ -67,27 +63,24 @@ fn warn_default_std_output_format() { ); } -fn run( +// TODO: simplify inputs of this function +fn run( supply_reader: R, supply_writer: W, - format_decider: F, config: &Config, + output_path_variant: &PathVariant, ) -> anyhow::Result<()> where R: Fn() -> anyhow::Result>, W: Fn(Option<&str>) -> anyhow::Result, - WS: WriteSeek, - F: Fn() -> anyhow::Result, + WS: Write + Seek, { let mut reader = supply_reader()?; - let img = import::load_image( - &mut reader, - &import::ImportConfig { - selected_frame: config.selected_frame, - }, - )?; - let mut image_engine = ImageEngine::new(img); + let decoder = SicImageDecoder::new(config.selected_frame); + let img = decoder.decode(&mut reader)?; + + let image_engine = ImageEngine::new(img); let buffer = image_engine .ignite(&config.image_operations_program) .with_context(|| "Unable to apply image operations.")?; @@ -99,39 +92,65 @@ where } else { None }; - let mut export_writer = supply_writer(format)?; - let encoding_format = format_decider()?; - export::export( - buffer, - &mut export_writer, - encoding_format, - export::ExportSettings { - adjust_color_type: if config.disable_automatic_color_type_adjustment { - AutomaticColorTypeAdjustment::Disabled - } else { - AutomaticColorTypeAdjustment::Enabled - }, - gif_repeat: config.encoding_settings.gif_repeat, - }, - ) - .with_context(|| "Unable to save image.") + let adjust_color_type = !config.disable_automatic_color_type_adjustment; + let encoder = SicImageEncoder::new( + adjust_color_type.into(), + config.encoding_settings.gif_repeat, + ); + + let writer = supply_writer(format)?; + let dynamic_encoder = create_dynamic_encoder(writer, config, output_path_variant)?; + + encoder + .encode(buffer, dynamic_encoder) + .with_context(|| "Unable to write image") } /// Create a reader which will be used to load the image. /// The reader can be a file or the stdin. /// If no file path is provided, the stdin will be assumed. -fn create_reader(io_device: &PathVariant) -> anyhow::Result> { - match io_device { +fn create_reader(path_variant: &PathVariant) -> anyhow::Result> { + match path_variant { PathVariant::StdStream if atty::is(atty::Stream::Stdin) => bail!( "An input image should be given by providing a path using the input argument or \ by piping an image to the stdin." ), - PathVariant::StdStream => Ok(import::stdin_reader()?), - PathVariant::Path(path) => Ok(import::file_reader(path)?), + PathVariant::StdStream => Ok(decode::stdin_reader()?), + PathVariant::Path(path) => Ok(decode::file_reader(path)?), } } +fn create_dynamic_encoder( + writer: W, + config: &Config, + path_variant: &PathVariant, +) -> anyhow::Result> { + let settings = EncoderSettings { + pnm_sample_encoding: if config.encoding_settings.pnm_use_ascii_format { + image::codecs::pnm::SampleEncoding::Ascii + } else { + image::codecs::pnm::SampleEncoding::Binary + }, + jpeg_quality: { JpegQuality::try_from(config.encoding_settings.jpeg_quality)? }, + repeat_animation: config.encoding_settings.gif_repeat, + }; + + Ok(match &config.forced_output_format { + Some(format) => DynamicEncoder::from_identifier(writer, format, &settings)?, + None => match path_variant { + PathVariant::Path(out) => DynamicEncoder::from_extension(writer, out, &settings)?, + PathVariant::StdStream => DynamicEncoder::bmp(writer)?, + }, + }) +} + +#[derive(Debug)] +enum OutputType { + File(File), + Stdout(Stdout), +} + #[derive(Debug)] struct Output { output_type: OutputType, @@ -154,14 +173,6 @@ impl Output { } } -#[derive(Debug)] -enum OutputType { - File(File), - Stdout(Stdout), -} - -impl WriteSeek for Output {} - impl Write for Output { fn write(&mut self, buf: &[u8]) -> io::Result { match self.output_type.borrow_mut() { @@ -192,8 +203,8 @@ impl Seek for Output { } } -fn create_writer(io_device: &PathVariant, adjust_ext: Option<&str>) -> anyhow::Result { - match io_device { +fn create_writer(path_variant: &PathVariant, adjust_ext: Option<&str>) -> anyhow::Result { + match path_variant { PathVariant::Path(out) => { let base = out.as_path().parent().ok_or_else(|| { anyhow::anyhow!("Unable to create output directory for output path") @@ -213,42 +224,6 @@ fn create_writer(io_device: &PathVariant, adjust_ext: Option<&str>) -> anyhow::R } } -fn create_format_decider( - io_device: &PathVariant, - config: &Config, -) -> anyhow::Result { - let format_resolver = DetermineEncodingFormat { - pnm_sample_encoding: if config.encoding_settings.pnm_use_ascii_format { - Some(image::codecs::pnm::SampleEncoding::Ascii) - } else { - Some(image::codecs::pnm::SampleEncoding::Binary) - }, - jpeg_quality: { - Some(JPEGQuality::try_from( - config.encoding_settings.jpeg_quality, - )?) - }, - }; - - let format = match &config.forced_output_format { - Some(format) => format_resolver.by_identifier(format).fallback_if( - config.encoding_settings.image_output_format_fallback, - guess_output_by_identifier, - format, - )?, - None => match io_device { - PathVariant::Path(out) => format_resolver.by_extension(out).fallback_if( - config.encoding_settings.image_output_format_fallback, - guess_output_by_path, - out, - )?, - PathVariant::StdStream => image::ImageOutputFormat::Bmp, - }, - }; - - Ok(format) -} - pub fn run_display_licenses(config: &Config, texts: &LicenseTexts) -> anyhow::Result<()> { config .show_license_text_of diff --git a/src/cli/pipeline/fallback.rs b/src/cli/pipeline/fallback.rs deleted file mode 100644 index 536d95184..000000000 --- a/src/cli/pipeline/fallback.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::path::Path; - -use sic_core::image::error::{ImageFormatHint, UnsupportedError}; -use sic_core::image::{ImageError, ImageFormat, ImageOutputFormat}; -use sic_io::errors::SicIoError; - -pub(crate) fn guess_output_by_identifier(id: &str) -> Result { - // HACK: image crate doesn't use identifiers, so we'll use an extension as identifier - guess_output_by_path(Path::new(&format!("0.{}", id))) -} - -pub(crate) fn guess_output_by_path>( - path: P, -) -> Result { - ImageFormat::from_path(path) - .and_then(into_image_output_format) - .map_err(SicIoError::ImageError) -} - -fn into_image_output_format(format: ImageFormat) -> Result { - let out = Into::::into(format); - - // Assuming we'll never hit the __NonExhaustive marker workaround by the image crate - match out { - ImageOutputFormat::Unsupported(name) => Err(ImageError::Unsupported( - UnsupportedError::from(ImageFormatHint::Name(name)), - )), - f => Ok(f), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - ide!(); - - #[parameterized(input = { - "jpg", - "jpeg", - "png", - "gif", - "bmp", - "ico", - }, expected = { - ImageOutputFormat::Jpeg(75), - ImageOutputFormat::Jpeg(75), - ImageOutputFormat::Png, - ImageOutputFormat::Gif, - ImageOutputFormat::Bmp, - ImageOutputFormat::Ico, - })] - fn formats_by_image_crate_ok(input: &str, expected: ImageOutputFormat) { - let by_id = guess_output_by_identifier(input).unwrap(); - let by_path = guess_output_by_path(format!("my_file_name.{}", input)).unwrap(); - - assert_eq!(by_id, by_path); - assert_eq!(by_id, expected); - } - #[parameterized(input = { - "dds", - "hdr", - "docx", - "", - "😀", - })] - fn formats_by_image_crate_err(input: &str) { - let by_id = guess_output_by_identifier(input); - let by_path = guess_output_by_path(format!("my_file_name.{}", input)); - - assert!(by_id.is_err()); - assert!(by_path.is_err()); - } - - #[test] - fn format_by_image_crate_err_no_ext() { - let by_path = guess_output_by_path("my_file_name"); - - assert!(by_path.is_err()); - } -} diff --git a/src/lib.rs b/src/lib.rs index d56f825c0..90fbba1f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,6 @@ #![deny(clippy::all)] #[cfg(test)] -#[macro_use] extern crate parameterized; pub mod cli;