diff --git a/.editorconfig b/.editorconfig index a50e07fb..c866f704 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,10 +1,11 @@ root = true [*] +charset = utf-8 end_of_line = lf +indent_style = space insert_final_newline = true +trim_trailing_whitespace = true [*.rs] -charset = utf-8 -indent_style = space indent_size = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..205021e4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Enforce Unix newlines +* text=auto eol=lf diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..ce9df8b6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: monthly diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 01ae14f8..ce1a9032 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,8 +6,9 @@ on: - 'v*.*.*' jobs: - create-windows-binaries: + runs-on: windows-latest + if: github.repository == 'shssoichiro/oxipng' strategy: matrix: @@ -16,51 +17,53 @@ jobs: # and I don't have a Windows machine set up to experiment with fixing it. # conf: [x86_64, i686] - runs-on: windows-latest - steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + with: + persist-credentials: false - - name: Install stable - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - target: ${{ matrix.conf }}-pc-windows-msvc - override: true + - name: Install stable + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + target: ${{ matrix.conf }}-pc-windows-msvc + override: true - - name: Build oxipng - run: | - cargo build --release --target ${{ matrix.conf }}-pc-windows-msvc + - name: Build oxipng + run: | + cargo build --release --target ${{ matrix.conf }}-pc-windows-msvc - - name: Get the version - shell: bash - id: tagName - run: | - VERSION=$(cargo pkgid | cut -d# -f2 | cut -d: -f2) - echo "::set-output name=tag::$VERSION" + - name: Get the version + shell: bash + id: tagName + run: | + VERSION=$(cargo pkgid | cut -d# -f2 | cut -d: -f2) + echo "::set-output name=tag::$VERSION" - - name: Build package - id: package - shell: bash - run: | - ARCHIVE_TARGET="${{ matrix.conf }}-pc-windows-msvc" - ARCHIVE_NAME="oxipng-${{ steps.tagName.outputs.tag }}-$ARCHIVE_TARGET" - ARCHIVE_FILE="${ARCHIVE_NAME}.zip" - mv LICENSE LICENSE.txt - 7z a ${ARCHIVE_FILE} \ - ./target/${{ matrix.conf }}-pc-windows-msvc/release/oxipng.exe \ - ./CHANGELOG.md ./LICENSE.txt ./README.md - echo "::set-output name=file::${ARCHIVE_FILE}" - echo "::set-output name=name::${ARCHIVE_NAME}.zip" + - name: Build package + id: package + shell: bash + run: | + ARCHIVE_TARGET="${{ matrix.conf }}-pc-windows-msvc" + ARCHIVE_NAME="oxipng-${{ steps.tagName.outputs.tag }}-$ARCHIVE_TARGET" + ARCHIVE_FILE="${ARCHIVE_NAME}.zip" + mv LICENSE LICENSE.txt + 7z a ${ARCHIVE_FILE} \ + ./target/${{ matrix.conf }}-pc-windows-msvc/release/oxipng.exe \ + ./CHANGELOG.md ./LICENSE.txt ./README.md + echo "::set-output name=file::${ARCHIVE_FILE}" + echo "::set-output name=name::${ARCHIVE_NAME}.zip" - - name: Upload artifacts - uses: actions/upload-artifact@v2 - with: - name: ${{ steps.package.outputs.name }} - path: ${{ steps.package.outputs.file }} + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: ${{ steps.package.outputs.name }} + path: ${{ steps.package.outputs.file }} create-unix-binaries: + runs-on: ${{ matrix.os }} + if: github.repository == 'shssoichiro/oxipng' strategy: matrix: @@ -71,67 +74,68 @@ jobs: - os: macos-latest target: x86_64-apple-darwin - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + with: + persist-credentials: false - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - target: ${{ matrix.target }} - override: true + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + target: ${{ matrix.target }} + override: true - - name: Install musl - if: contains(matrix.target, 'linux-musl') - run: | - sudo apt-get install musl-tools + - name: Install musl + if: contains(matrix.target, 'linux-musl') + run: | + sudo apt-get install musl-tools - - name: Build oxipng - run: | - cargo build --release --target ${{ matrix.target }} + - name: Build oxipng + run: | + cargo build --release --target ${{ matrix.target }} - - name: Strip binary - run: | - strip target/${{ matrix.target }}/release/oxipng + - name: Strip binary + run: | + strip target/${{ matrix.target }}/release/oxipng - - name: Get the version - id: tagName - run: | - VERSION=$(cargo pkgid | cut -d# -f2 | cut -d: -f2) - echo "::set-output name=tag::$VERSION" + - name: Get the version + id: tagName + run: | + VERSION=$(cargo pkgid | cut -d# -f2 | cut -d: -f2) + echo "::set-output name=tag::$VERSION" - - name: Build package - id: package - run: | - ARCHIVE_TARGET=${{ matrix.target }} - ARCHIVE_NAME="oxipng-${{ steps.tagName.outputs.tag }}-$ARCHIVE_TARGET" - ARCHIVE_FILE="${ARCHIVE_NAME}.tar.gz" - mkdir "/tmp/${ARCHIVE_NAME}" - cp README.md CHANGELOG.md LICENSE \ - target/${{ matrix.target }}/release/oxipng \ - /tmp/${ARCHIVE_NAME} - tar -czf ${PWD}/${ARCHIVE_FILE} -C /tmp/ ${ARCHIVE_NAME} - echo ::set-output "name=file::${ARCHIVE_FILE}" - echo ::set-output "name=name::${ARCHIVE_NAME}.tar.gz" + - name: Build package + id: package + run: | + ARCHIVE_TARGET=${{ matrix.target }} + ARCHIVE_NAME="oxipng-${{ steps.tagName.outputs.tag }}-$ARCHIVE_TARGET" + ARCHIVE_FILE="${ARCHIVE_NAME}.tar.gz" + mkdir "/tmp/${ARCHIVE_NAME}" + cp README.md CHANGELOG.md LICENSE \ + target/${{ matrix.target }}/release/oxipng \ + /tmp/${ARCHIVE_NAME} + tar -czf ${PWD}/${ARCHIVE_FILE} -C /tmp/ ${ARCHIVE_NAME} + echo ::set-output "name=file::${ARCHIVE_FILE}" + echo ::set-output "name=name::${ARCHIVE_NAME}.tar.gz" - - name: Upload artifacts - uses: actions/upload-artifact@v2 - with: - name: ${{ steps.package.outputs.name }} - path: ${{ steps.package.outputs.file }} + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: ${{ steps.package.outputs.name }} + path: ${{ steps.package.outputs.file }} deploy: - + runs-on: ubuntu-latest + if: github.repository == 'shssoichiro/oxipng' needs: [create-windows-binaries, create-unix-binaries] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + with: + persist-credentials: false - name: Get version and release description id: tagName @@ -141,7 +145,7 @@ jobs: echo "::set-output name=tag::$VERSION" - name: Download artifacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: path: ./binaries diff --git a/.github/workflows/oxipng.yml b/.github/workflows/oxipng.yml index dfdb48bf..eb0edabf 100644 --- a/.github/workflows/oxipng.yml +++ b/.github/workflows/oxipng.yml @@ -7,10 +7,13 @@ on: pull_request: branches: - master + workflow_dispatch: jobs: test: + runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: target: - x86_64-unknown-linux-gnu @@ -45,12 +48,25 @@ jobs: toolchain: beta - target: x86_64-unknown-linux-musl toolchain: nightly - runs-on: ${{ matrix.os }} + steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + with: + persist-credentials: false - name: Install musl tools run: sudo apt-get install musl-tools if: "contains(matrix.target, 'musl')" + - name: Cache cargo registry + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-${{ matrix.toolchain }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.toolchain }}-cargo-registry- - name: Install ${{ matrix.toolchain }} uses: actions-rs/toolchain@v1 with: @@ -58,13 +74,6 @@ jobs: toolchain: ${{ matrix.toolchain }} override: true components: clippy, rustfmt - - name: Cache cargo registry - uses: actions/cache@v1 - with: - path: ~/.cargo/registry/cache - key: ${{ matrix.target }}-${{ matrix.toolchain }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ matrix.target }}-${{ matrix.toolchain }}-cargo-registry- - name: Run rustfmt if: matrix.toolchain == 'stable' uses: actions-rs/cargo@v1 @@ -78,7 +87,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} args: -- -D warnings - name: Run tests - run: cargo test + run: cargo test --features sanity-checks - name: Build benchmarks if: matrix.toolchain == 'nightly' run: cargo bench --no-run diff --git a/Cargo.lock b/Cargo.lock index b3008eeb..d18c9f11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,9 +45,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" [[package]] name = "byteorder" @@ -57,9 +57,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cc" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" [[package]] name = "cfg-if" @@ -69,9 +69,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.2.23" +version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "atty", "bitflags", @@ -108,9 +108,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ "cfg-if", "crossbeam-utils", @@ -118,9 +118,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -129,9 +129,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.13" +version = "0.9.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" dependencies = [ "autocfg", "cfg-if", @@ -142,24 +142,33 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" dependencies = [ "cfg-if", ] [[package]] name = "either" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] [[package]] name = "filetime" -version = "0.2.19" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" +checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" dependencies = [ "cfg-if", "libc", @@ -169,9 +178,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", "miniz_oxide", @@ -185,9 +194,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "glob" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "hashbrown" @@ -215,9 +224,9 @@ dependencies = [ [[package]] name = "image" -version = "0.24.5" +version = "0.24.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" +checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" dependencies = [ "bytemuck", "byteorder", @@ -229,9 +238,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown", @@ -240,9 +249,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.139" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "libdeflate-sys" @@ -273,20 +282,21 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" dependencies = [ "autocfg", ] [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", + "simd-adler32", ] [[package]] @@ -331,15 +341,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.16.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "os_str_bytes" -version = "6.4.1" +version = "6.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" [[package]] name = "oxipng" @@ -364,12 +374,13 @@ dependencies = [ [[package]] name = "png" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" +checksum = "aaeebc51f9e7d2c150d3f3bfeb667f2aa985db5ef1e3d212847bdedb488beeaa" dependencies = [ "bitflags", "crc32fast", + "fdeflate", "flate2", "miniz_oxide", ] @@ -382,9 +393,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rayon" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" dependencies = [ "either", "rayon-core", @@ -392,9 +403,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -413,9 +424,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.34" +version = "0.8.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3603b7d71ca82644f79b5a06d1220e9a58ede60bd32255f698cb1af8838b8db3" +checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" dependencies = [ "bytemuck", ] @@ -443,9 +454,15 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "semver" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" + +[[package]] +name = "simd-adler32" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" [[package]] name = "simd-adler32" @@ -494,10 +511,11 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thread_local" -version = "1.1.4" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ + "cfg-if", "once_cell", ] @@ -549,9 +567,18 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -564,45 +591,45 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "wyz" @@ -615,11 +642,10 @@ dependencies = [ [[package]] name = "zopfli" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5b2bed49d3f0af28729a2338ee8c3a48eba2133a78ebc560779be161ebaaad8" +checksum = "4e0650ae6a051326d798eb099b632f1afb0d323d25ee4ec82ffb0779512084d5" dependencies = [ - "byteorder", "crc32fast", "log", "simd-adler32", diff --git a/Cargo.toml b/Cargo.toml index db2107b2..355d7b34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,40 +22,44 @@ name = "oxipng" path = "src/main.rs" required-features = ["binary"] +[[bench]] +name = "zopfli" +required-features = ["zopfli"] + [dependencies] -zopfli = { version = "0.7.2", optional = true } -rgb = "0.8.33" -indexmap = "1.9.1" -libdeflater = "0.13.0" +zopfli = { version = "0.7.4", optional = true, default-features = false, features = ["std", "zlib"] } +rgb = "0.8.36" +indexmap = "1.9.3" +libdeflater = "0.11.0" log = "0.4.17" -stderrlog = { version = "0.5.3", optional = true, default-features = false } +stderrlog = { version = "0.5.4", optional = true, default-features = false } bitvec = "1.0.1" rustc-hash = "1.1.0" [dependencies.crossbeam-channel] optional = true -version = "0.5.6" +version = "0.5.8" [dependencies.filetime] optional = true -version = "0.2.17" +version = "0.2.21" [dependencies.rayon] optional = true -version = "1.5.3" +version = "1.7.0" [dependencies.clap] -optional = true -version = "3.2.23" +version = "3.2.25" [dependencies.wild] optional = true version = "2.1.0" [dependencies.image] +optional = true default-features = false features = ["png"] -version = "0.24.3" +version = "0.24.6" [build-dependencies] rustc_version = "0.4.0" @@ -65,6 +69,7 @@ binary = ["clap", "wild", "stderrlog"] default = ["binary", "filetime", "parallel", "zopfli"] parallel = ["rayon", "indexmap/rayon", "crossbeam-channel"] freestanding = ["libdeflater/freestanding"] +sanity-checks = ["image"] [lib] name = "oxipng" diff --git a/benches/deflate.rs b/benches/deflate.rs index de9e0318..d8cc984b 100644 --- a/benches/deflate.rs +++ b/benches/deflate.rs @@ -3,15 +3,15 @@ extern crate oxipng; extern crate test; +use oxipng::internal_tests::*; +use oxipng::*; use std::path::PathBuf; use test::Bencher; -use oxipng::internal_tests::*; - #[bench] fn deflate_16_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { let min = AtomicMin::new(None); @@ -22,7 +22,7 @@ fn deflate_16_bits(b: &mut Bencher) { #[bench] fn deflate_8_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { let min = AtomicMin::new(None); @@ -35,7 +35,7 @@ fn deflate_4_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { let min = AtomicMin::new(None); @@ -48,7 +48,7 @@ fn deflate_2_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { let min = AtomicMin::new(None); @@ -61,7 +61,7 @@ fn deflate_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { let min = AtomicMin::new(None); @@ -72,7 +72,7 @@ fn deflate_1_bits(b: &mut Bencher) { #[bench] fn inflate_generic(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| inflate(png.idat_data.as_ref(), png.raw.ihdr.raw_data_size())); } diff --git a/benches/filters.rs b/benches/filters.rs index b1ce453a..67c701a6 100644 --- a/benches/filters.rs +++ b/benches/filters.rs @@ -3,14 +3,15 @@ extern crate oxipng; extern crate test; -use oxipng::{internal_tests::*, RowFilter}; +use oxipng::internal_tests::*; +use oxipng::*; use std::path::PathBuf; use test::Bencher; #[bench] fn filters_16_bits_filter_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::None, false); @@ -20,7 +21,7 @@ fn filters_16_bits_filter_0(b: &mut Bencher) { #[bench] fn filters_8_bits_filter_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::None, false); @@ -32,7 +33,7 @@ fn filters_4_bits_filter_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::None, false); @@ -44,7 +45,7 @@ fn filters_2_bits_filter_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::None, false); @@ -56,7 +57,7 @@ fn filters_1_bits_filter_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::None, false); @@ -66,7 +67,7 @@ fn filters_1_bits_filter_0(b: &mut Bencher) { #[bench] fn filters_16_bits_filter_1(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Sub, false); @@ -76,7 +77,7 @@ fn filters_16_bits_filter_1(b: &mut Bencher) { #[bench] fn filters_8_bits_filter_1(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Sub, false); @@ -88,7 +89,7 @@ fn filters_4_bits_filter_1(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Sub, false); @@ -100,7 +101,7 @@ fn filters_2_bits_filter_1(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Sub, false); @@ -112,7 +113,7 @@ fn filters_1_bits_filter_1(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Sub, false); @@ -122,7 +123,7 @@ fn filters_1_bits_filter_1(b: &mut Bencher) { #[bench] fn filters_16_bits_filter_2(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Up, false); @@ -132,7 +133,7 @@ fn filters_16_bits_filter_2(b: &mut Bencher) { #[bench] fn filters_8_bits_filter_2(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Up, false); @@ -144,7 +145,7 @@ fn filters_4_bits_filter_2(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Up, false); @@ -156,7 +157,7 @@ fn filters_2_bits_filter_2(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Up, false); @@ -168,7 +169,7 @@ fn filters_1_bits_filter_2(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Up, false); @@ -178,7 +179,7 @@ fn filters_1_bits_filter_2(b: &mut Bencher) { #[bench] fn filters_16_bits_filter_3(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Average, false); @@ -188,7 +189,7 @@ fn filters_16_bits_filter_3(b: &mut Bencher) { #[bench] fn filters_8_bits_filter_3(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Average, false); @@ -200,7 +201,7 @@ fn filters_4_bits_filter_3(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Average, false); @@ -212,7 +213,7 @@ fn filters_2_bits_filter_3(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Average, false); @@ -224,7 +225,7 @@ fn filters_1_bits_filter_3(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Average, false); @@ -234,7 +235,7 @@ fn filters_1_bits_filter_3(b: &mut Bencher) { #[bench] fn filters_16_bits_filter_4(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Paeth, false); @@ -244,7 +245,7 @@ fn filters_16_bits_filter_4(b: &mut Bencher) { #[bench] fn filters_8_bits_filter_4(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Paeth, false); @@ -256,7 +257,7 @@ fn filters_4_bits_filter_4(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Paeth, false); @@ -268,7 +269,7 @@ fn filters_2_bits_filter_4(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Paeth, false); @@ -280,7 +281,7 @@ fn filters_1_bits_filter_4(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Paeth, false); @@ -290,7 +291,7 @@ fn filters_1_bits_filter_4(b: &mut Bencher) { #[bench] fn filters_16_bits_filter_5(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::MinSum, false); @@ -300,7 +301,7 @@ fn filters_16_bits_filter_5(b: &mut Bencher) { #[bench] fn filters_8_bits_filter_5(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::MinSum, false); @@ -312,7 +313,7 @@ fn filters_4_bits_filter_5(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::MinSum, false); @@ -324,7 +325,7 @@ fn filters_2_bits_filter_5(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::MinSum, false); @@ -336,7 +337,7 @@ fn filters_1_bits_filter_5(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::MinSum, false); diff --git a/benches/interlacing.rs b/benches/interlacing.rs index def3d5a4..3c17eadc 100644 --- a/benches/interlacing.rs +++ b/benches/interlacing.rs @@ -3,14 +3,15 @@ extern crate oxipng; extern crate test; -use oxipng::{internal_tests::*, Interlacing}; +use oxipng::internal_tests::*; +use oxipng::*; use std::path::PathBuf; use test::Bencher; #[bench] fn interlacing_16_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::Adam7)); } @@ -18,7 +19,7 @@ fn interlacing_16_bits(b: &mut Bencher) { #[bench] fn interlacing_8_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::Adam7)); } @@ -28,7 +29,7 @@ fn interlacing_4_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::Adam7)); } @@ -38,7 +39,7 @@ fn interlacing_2_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::Adam7)); } @@ -48,7 +49,7 @@ fn interlacing_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::Adam7)); } @@ -58,7 +59,7 @@ fn deinterlacing_16_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/interlaced_rgb_16_should_be_rgb_16.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::None)); } @@ -68,7 +69,7 @@ fn deinterlacing_8_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/interlaced_rgb_8_should_be_rgb_8.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::None)); } @@ -78,7 +79,7 @@ fn deinterlacing_4_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/interlaced_palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::None)); } @@ -88,7 +89,7 @@ fn deinterlacing_2_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/interlaced_palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::None)); } @@ -98,7 +99,7 @@ fn deinterlacing_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/interlaced_palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| png.raw.change_interlacing(Interlacing::None)); } diff --git a/benches/reductions.rs b/benches/reductions.rs index be9b6e06..46fb8f49 100644 --- a/benches/reductions.rs +++ b/benches/reductions.rs @@ -4,15 +4,24 @@ extern crate oxipng; extern crate test; use oxipng::internal_tests::*; +use oxipng::*; use std::path::PathBuf; use test::Bencher; #[bench] fn reductions_16_to_8_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_16_to_8(&png.raw, false)); +} + +#[bench] +fn reductions_16_to_8_bits_scaled(b: &mut Bencher) { + let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); + let png = PngData::new(&input, &Options::default()).unwrap(); + + b.iter(|| bit_depth::reduced_bit_depth_16_to_8(&png.raw, true)); } #[bench] @@ -20,9 +29,9 @@ fn reductions_8_to_4_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_8_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -30,9 +39,9 @@ fn reductions_8_to_2_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_8_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -40,9 +49,9 @@ fn reductions_8_to_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_8_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -50,9 +59,9 @@ fn reductions_4_to_2_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -60,9 +69,9 @@ fn reductions_4_to_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -70,9 +79,9 @@ fn reductions_2_to_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -80,9 +89,9 @@ fn reductions_grayscale_8_to_4_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/grayscale_8_should_be_grayscale_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -90,9 +99,9 @@ fn reductions_grayscale_8_to_2_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/grayscale_8_should_be_grayscale_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -100,9 +109,9 @@ fn reductions_grayscale_8_to_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/grayscale_8_should_be_grayscale_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -110,9 +119,9 @@ fn reductions_grayscale_4_to_2_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/grayscale_4_should_be_grayscale_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -120,9 +129,9 @@ fn reductions_grayscale_4_to_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/grayscale_4_should_be_grayscale_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -130,25 +139,25 @@ fn reductions_grayscale_2_to_1_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/grayscale_2_should_be_grayscale_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] fn reductions_rgba_to_rgb_16(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgba_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| alpha::reduced_alpha_channel(&png.raw, false)); } #[bench] fn reductions_rgba_to_rgb_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| alpha::reduced_alpha_channel(&png.raw, false)); } #[bench] @@ -156,9 +165,9 @@ fn reductions_rgba_to_grayscale_alpha_16(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/rgba_16_should_be_grayscale_alpha_16.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduced_rgb_to_grayscale(&png.raw)); } #[bench] @@ -166,9 +175,9 @@ fn reductions_rgba_to_grayscale_alpha_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/rgba_8_should_be_grayscale_alpha_8.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduced_rgb_to_grayscale(&png.raw)); } #[bench] @@ -176,9 +185,12 @@ fn reductions_rgba_to_grayscale_16(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/rgba_16_should_be_grayscale_16.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| { + color::reduced_rgb_to_grayscale(&png.raw) + .and_then(|r| alpha::reduced_alpha_channel(&r, false)) + }); } #[bench] @@ -186,9 +198,12 @@ fn reductions_rgba_to_grayscale_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/rgba_8_should_be_grayscale_8.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| { + color::reduced_rgb_to_grayscale(&png.raw) + .and_then(|r| alpha::reduced_alpha_channel(&r, false)) + }); } #[bench] @@ -196,33 +211,53 @@ fn reductions_rgb_to_grayscale_16(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/rgb_16_should_be_grayscale_16.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduced_rgb_to_grayscale(&png.raw)); } #[bench] fn reductions_rgb_to_grayscale_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_grayscale_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduced_rgb_to_grayscale(&png.raw)); } #[bench] fn reductions_rgba_to_palette_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_palette_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduced_to_indexed(&png.raw)); } #[bench] fn reductions_rgb_to_palette_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_palette_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduced_to_indexed(&png.raw)); +} + +#[bench] +fn reductions_grayscale_8_to_palette_8(b: &mut Bencher) { + let input = test::black_box(PathBuf::from( + "tests/files/grayscale_8_should_be_palette_8.png", + )); + let png = PngData::new(&input, &Options::default()).unwrap(); + + b.iter(|| color::reduced_to_indexed(&png.raw)); +} + +#[bench] +fn reductions_palette_8_to_grayscale_8(b: &mut Bencher) { + let input = test::black_box(PathBuf::from( + "tests/files/palette_8_should_be_grayscale_8.png", + )); + let png = PngData::new(&input, &Options::default()).unwrap(); + + b.iter(|| color::indexed_to_channels(&png.raw, true)); } #[bench] @@ -230,9 +265,9 @@ fn reductions_palette_duplicate_reduction(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_should_be_reduced_with_dupes.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| reduced_palette(&png.raw, false)); + b.iter(|| palette::reduced_palette(&png.raw, false)); } #[bench] @@ -240,9 +275,9 @@ fn reductions_palette_unused_reduction(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_should_be_reduced_with_unused.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| reduced_palette(&png.raw, false)); + b.iter(|| palette::reduced_palette(&png.raw, false)); } #[bench] @@ -250,15 +285,25 @@ fn reductions_palette_full_reduction(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_should_be_reduced_with_both.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| reduced_palette(&png.raw, false)); + b.iter(|| palette::reduced_palette(&png.raw, false)); +} + +#[bench] +fn reductions_palette_sort(b: &mut Bencher) { + let input = test::black_box(PathBuf::from( + "tests/files/palette_8_should_be_palette_8.png", + )); + let png = PngData::new(&input, &Options::default()).unwrap(); + + b.iter(|| palette::sorted_palette(&png.raw)); } #[bench] fn reductions_alpha(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| alpha::cleaned_alpha_channel(&png.raw)); } diff --git a/benches/strategies.rs b/benches/strategies.rs index de5a009a..c3aab816 100644 --- a/benches/strategies.rs +++ b/benches/strategies.rs @@ -3,14 +3,15 @@ extern crate oxipng; extern crate test; -use oxipng::{internal_tests::*, RowFilter}; +use oxipng::internal_tests::*; +use oxipng::*; use std::path::PathBuf; use test::Bencher; #[bench] fn filters_minsum(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::MinSum, false); @@ -20,7 +21,7 @@ fn filters_minsum(b: &mut Bencher) { #[bench] fn filters_entropy(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Entropy, false); @@ -30,7 +31,7 @@ fn filters_entropy(b: &mut Bencher) { #[bench] fn filters_bigrams(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Bigrams, false); @@ -40,7 +41,7 @@ fn filters_bigrams(b: &mut Bencher) { #[bench] fn filters_bigent(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::BigEnt, false); @@ -50,7 +51,7 @@ fn filters_bigent(b: &mut Bencher) { #[bench] fn filters_brute(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { png.raw.filter_image(RowFilter::Brute, false); diff --git a/benches/zopfli.rs b/benches/zopfli.rs index a9b91e08..03f83f65 100644 --- a/benches/zopfli.rs +++ b/benches/zopfli.rs @@ -4,6 +4,7 @@ extern crate oxipng; extern crate test; use oxipng::internal_tests::*; +use oxipng::*; use std::num::NonZeroU8; use std::path::PathBuf; use test::Bencher; @@ -14,7 +15,7 @@ const DEFAULT_ZOPFLI_ITERATIONS: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(1 #[bench] fn zopfli_16_bits_strategy_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); @@ -24,7 +25,7 @@ fn zopfli_16_bits_strategy_0(b: &mut Bencher) { #[bench] fn zopfli_8_bits_strategy_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); @@ -36,7 +37,7 @@ fn zopfli_4_bits_strategy_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_4_should_be_palette_4.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); @@ -48,7 +49,7 @@ fn zopfli_2_bits_strategy_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_2_should_be_palette_2.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); @@ -60,7 +61,7 @@ fn zopfli_1_bits_strategy_0(b: &mut Bencher) { let input = test::black_box(PathBuf::from( "tests/files/palette_1_should_be_palette_1.png", )); - let png = PngData::new(&input, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| { zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); diff --git a/src/colors.rs b/src/colors.rs index e339092b..744ec720 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -1,119 +1,124 @@ -use std::fmt; +use rgb::{RGB16, RGBA8}; +use std::{fmt, fmt::Display}; -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +use crate::PngError; + +#[derive(Debug, PartialEq, Eq, Clone)] /// The color type used to represent this image pub enum ColorType { /// Grayscale, with one color channel - Grayscale, + Grayscale { + /// Optional shade of gray that should be rendered as transparent + transparent_shade: Option, + }, /// RGB, with three color channels - RGB, - /// Indexed, with one byte per pixel representing one of up to 256 colors in the image - Indexed, + RGB { + /// Optional color value that should be rendered as transparent + transparent_color: Option, + }, + /// Indexed, with one byte per pixel representing a color from the palette + Indexed { + /// The palette containing the colors used, up to 256 entries + palette: Vec, + }, /// Grayscale + Alpha, with two color channels GrayscaleAlpha, /// RGBA, with four color channels RGBA, } -impl fmt::Display for ColorType { +impl Display for ColorType { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match *self { - ColorType::Grayscale => "Grayscale", - ColorType::RGB => "RGB", - ColorType::Indexed => "Indexed", - ColorType::GrayscaleAlpha => "Grayscale + Alpha", - ColorType::RGBA => "RGB + Alpha", + match self { + ColorType::Grayscale { .. } => Display::fmt("Grayscale", f), + ColorType::RGB { .. } => Display::fmt("RGB", f), + ColorType::Indexed { palette } => { + Display::fmt(&format!("Indexed ({} colors)", palette.len()), f) } - ) + ColorType::GrayscaleAlpha => Display::fmt("Grayscale + Alpha", f), + ColorType::RGBA => Display::fmt("RGB + Alpha", f), + } } } impl ColorType { /// Get the code used by the PNG specification to denote this color type #[inline] - pub fn png_header_code(self) -> u8 { + pub fn png_header_code(&self) -> u8 { match self { - ColorType::Grayscale => 0, - ColorType::RGB => 2, - ColorType::Indexed => 3, + ColorType::Grayscale { .. } => 0, + ColorType::RGB { .. } => 2, + ColorType::Indexed { .. } => 3, ColorType::GrayscaleAlpha => 4, ColorType::RGBA => 6, } } #[inline] - pub fn channels_per_pixel(self) -> u8 { + pub(crate) fn channels_per_pixel(&self) -> u8 { match self { - ColorType::Grayscale | ColorType::Indexed => 1, + ColorType::Grayscale { .. } | ColorType::Indexed { .. } => 1, ColorType::GrayscaleAlpha => 2, - ColorType::RGB => 3, + ColorType::RGB { .. } => 3, ColorType::RGBA => 4, } } + + #[inline] + pub(crate) fn is_rgb(&self) -> bool { + matches!(self, ColorType::RGB { .. } | ColorType::RGBA) + } + + #[inline] + pub(crate) fn has_alpha(&self) -> bool { + matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA) + } + + #[inline] + pub(crate) fn has_trns(&self) -> bool { + match self { + ColorType::Grayscale { transparent_shade } => transparent_shade.is_some(), + ColorType::RGB { transparent_color } => transparent_color.is_some(), + _ => false, + } + } } +#[repr(u8)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] /// The number of bits to be used per channel per pixel pub enum BitDepth { /// One bit per channel per pixel - One, + One = 1, /// Two bits per channel per pixel - Two, + Two = 2, /// Four bits per channel per pixel - Four, + Four = 4, /// Eight bits per channel per pixel - Eight, + Eight = 8, /// Sixteen bits per channel per pixel - Sixteen, + Sixteen = 16, } -impl fmt::Display for BitDepth { +impl TryFrom for BitDepth { + type Error = PngError; + + fn try_from(value: u8) -> Result { + match value { + 1 => Ok(Self::One), + 2 => Ok(Self::Two), + 4 => Ok(Self::Four), + 8 => Ok(Self::Eight), + 16 => Ok(Self::Sixteen), + _ => Err(PngError::new("Unexpected bit depth")), + } + } +} + +impl Display for BitDepth { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match *self { - BitDepth::One => "1", - BitDepth::Two => "2", - BitDepth::Four => "4", - BitDepth::Eight => "8", - BitDepth::Sixteen => "16", - } - ) - } -} - -impl BitDepth { - /// Retrieve the number of bits per channel per pixel as a `u8` - #[inline] - pub fn as_u8(self) -> u8 { - match self { - BitDepth::One => 1, - BitDepth::Two => 2, - BitDepth::Four => 4, - BitDepth::Eight => 8, - BitDepth::Sixteen => 16, - } - } - /// Parse a number of bits per channel per pixel into a `BitDepth` - /// - /// # Panics - /// - /// If depth is unsupported - #[inline] - pub fn from_u8(depth: u8) -> BitDepth { - match depth { - 1 => BitDepth::One, - 2 => BitDepth::Two, - 4 => BitDepth::Four, - 8 => BitDepth::Eight, - 16 => BitDepth::Sixteen, - _ => panic!("Unsupported bit depth"), - } + Display::fmt(&(*self as u8).to_string(), f) } } diff --git a/src/deflate/deflater.rs b/src/deflate/deflater.rs index a2f9dacb..efffe123 100644 --- a/src/deflate/deflater.rs +++ b/src/deflate/deflater.rs @@ -13,13 +13,8 @@ pub fn deflate(data: &[u8], level: u8, max_size: &AtomicMin) -> PngResult PngError::DeflatedDataTooLong(capacity), + CompressionError::InsufficientSpace => PngError::DeflatedDataTooLong(capacity - 9), })?; - if let Some(max) = max_size.get() { - if len > max { - return Err(PngError::DeflatedDataTooLong(max)); - } - } dest.truncate(len); Ok(dest) } diff --git a/src/deflate/mod.rs b/src/deflate/mod.rs index 9606a852..0e8a65f3 100644 --- a/src/deflate/mod.rs +++ b/src/deflate/mod.rs @@ -1,7 +1,10 @@ mod deflater; +use crate::AtomicMin; +use crate::{PngError, PngResult}; pub use deflater::crc32; pub use deflater::deflate; pub use deflater::inflate; +use std::{fmt, fmt::Display}; #[cfg(feature = "zopfli")] use std::num::NonZeroU8; @@ -27,3 +30,30 @@ pub enum Deflaters { iterations: NonZeroU8, }, } + +impl Deflaters { + pub(crate) fn deflate(self, data: &[u8], max_size: &AtomicMin) -> PngResult> { + let compressed = match self { + Self::Libdeflater { compression } => deflate(data, compression, max_size)?, + #[cfg(feature = "zopfli")] + Self::Zopfli { iterations } => zopfli_deflate(data, iterations)?, + }; + if let Some(max) = max_size.get() { + if compressed.len() > max { + return Err(PngError::DeflatedDataTooLong(max)); + } + } + Ok(compressed) + } +} + +impl Display for Deflaters { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Libdeflater { compression } => Display::fmt(compression, f), + #[cfg(feature = "zopfli")] + Self::Zopfli { .. } => Display::fmt("zopfli", f), + } + } +} diff --git a/src/error.rs b/src/error.rs index 0f099d5d..550f0316 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,4 @@ +use crate::colors::{BitDepth, ColorType}; use std::error::Error; use std::fmt; @@ -11,6 +12,8 @@ pub enum PngError { InvalidData, TruncatedData, ChunkMissing(&'static str), + InvalidDepthForType(BitDepth, ColorType), + IncorrectDataLength(usize, usize), Other(Box), } @@ -30,6 +33,14 @@ impl fmt::Display for PngError { } PngError::APNGNotSupported => f.write_str("APNG files are not (yet) supported"), PngError::ChunkMissing(s) => write!(f, "Chunk {} missing or empty", s), + PngError::InvalidDepthForType(d, ref c) => { + write!(f, "Invalid bit depth {} for color type {}", d, c) + } + PngError::IncorrectDataLength(l1, l2) => write!( + f, + "Data length {} does not match the expected length {}", + l1, l2 + ), PngError::Other(ref s) => f.write_str(s), } } diff --git a/src/evaluate.rs b/src/evaluate.rs index 23cc9832..8c05fde7 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -4,14 +4,15 @@ use crate::atomicmin::AtomicMin; use crate::deflate; use crate::filters::RowFilter; -use crate::png::PngData; use crate::png::PngImage; #[cfg(not(feature = "parallel"))] use crate::rayon; use crate::Deadline; +use crate::PngError; #[cfg(feature = "parallel")] use crossbeam_channel::{unbounded, Receiver, Sender}; use indexmap::IndexSet; +use log::trace; use rayon::prelude::*; #[cfg(not(feature = "parallel"))] use std::cell::RefCell; @@ -20,7 +21,9 @@ use std::sync::atomic::Ordering::SeqCst; use std::sync::Arc; pub struct Candidate { - pub image: PngData, + pub image: Arc, + pub idat_data: Vec, + pub filtered: Vec, pub filter: RowFilter, pub is_reduction: bool, // first wins tie-breaker @@ -30,9 +33,9 @@ pub struct Candidate { impl Candidate { fn cmp_key(&self) -> impl Ord { ( - self.image.idat_data.len(), - self.image.raw.data.len(), - self.image.raw.ihdr.bit_depth, + self.idat_data.len() + self.image.key_chunks_size(), + self.image.data.len(), + self.image.ihdr.bit_depth, self.filter, self.nth, ) @@ -79,7 +82,7 @@ impl Evaluator { } /// Wait for all evaluations to finish and return smallest reduction - /// Or `None` if all reductions were worse than baseline. + /// Or `None` if the queue is empty. #[cfg(feature = "parallel")] pub fn get_best_candidate(self) -> Option { let (eval_send, eval_recv) = self.eval_channel; @@ -131,16 +134,21 @@ impl Evaluator { return; } let filtered = image.filter_image(filter, optimize_alpha); - if let Ok(idat_data) = - deflate::deflate(&filtered, compression, &best_candidate_size) - { - best_candidate_size.set_min(idat_data.len()); + let idat_data = deflate::deflate(&filtered, compression, &best_candidate_size); + if let Ok(idat_data) = idat_data { + let size = idat_data.len() + image.key_chunks_size(); + best_candidate_size.set_min(size); + trace!( + "Eval: {}-bit {:20} {:8} {} bytes", + image.ihdr.bit_depth, + image.ihdr.color_type, + filter, + size + ); let new = Candidate { - image: PngData { - idat_data, - filtered, - raw: Arc::clone(&image), - }, + image: image.clone(), + idat_data, + filtered, filter, is_reduction, nth, @@ -158,6 +166,14 @@ impl Evaluator { best => *best = Some(new), } } + } else if let Err(PngError::DeflatedDataTooLong(size)) = idat_data { + trace!( + "Eval: {}-bit {:20} {:8} >{} bytes", + image.ihdr.bit_depth, + image.ihdr.color_type, + filter, + size + ); } }); }); diff --git a/src/filters.rs b/src/filters.rs index d43c2833..a2136139 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -1,4 +1,5 @@ -use std::{fmt::Display, mem::transmute}; +use std::mem::transmute; +use std::{fmt, fmt::Display}; use crate::error::PngError; @@ -31,11 +32,9 @@ impl TryFrom for RowFilter { } impl Display for RowFilter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{:8}", - match *self { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt( + match self { Self::None => "None", Self::Sub => "Sub", Self::Up => "Up", @@ -46,17 +45,19 @@ impl Display for RowFilter { Self::Bigrams => "Bigrams", Self::BigEnt => "BigEnt", Self::Brute => "Brute", - } + }, + f, ) } } impl RowFilter { pub const LAST: u8 = Self::Brute as u8; - pub const STANDARD: [Self; 5] = [Self::None, Self::Sub, Self::Up, Self::Average, Self::Paeth]; - pub const SINGLE_LINE: [Self; 2] = [Self::None, Self::Sub]; + pub(crate) const STANDARD: [Self; 5] = + [Self::None, Self::Sub, Self::Up, Self::Average, Self::Paeth]; + pub(crate) const SINGLE_LINE: [Self; 2] = [Self::None, Self::Sub]; - pub fn filter_line( + pub(crate) fn filter_line( self, bpp: usize, data: &mut [u8], @@ -176,7 +177,7 @@ impl RowFilter { } } - pub fn unfilter_line( + pub(crate) fn unfilter_line( self, bpp: usize, data: &[u8], diff --git a/src/headers.rs b/src/headers.rs index 7fdeef1e..274173d7 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -1,13 +1,15 @@ use crate::colors::{BitDepth, ColorType}; -use crate::deflate::crc32; +use crate::deflate::{crc32, inflate}; use crate::error::PngError; use crate::interlace::Interlacing; +use crate::AtomicMin; +use crate::Deflaters; use crate::PngResult; use indexmap::IndexSet; -use std::io; -use std::io::{Cursor, Read}; +use log::warn; +use rgb::{RGB16, RGBA8}; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] /// Headers from the IHDR chunk of the image pub struct IhdrData { /// The width of the image in pixels @@ -18,10 +20,6 @@ pub struct IhdrData { pub color_type: ColorType, /// The bit depth of the image pub bit_depth: BitDepth, - /// The compression method used for this image (0 for DEFLATE) - pub compression: u8, - /// The filter mode used for this image (currently only 0 is valid) - pub filter: u8, /// The interlacing mode of the image pub interlaced: Interlacing, } @@ -30,8 +28,8 @@ impl IhdrData { /// Bits per pixel #[must_use] #[inline] - pub fn bpp(&self) -> u8 { - self.bit_depth.as_u8() * self.color_type.channels_per_pixel() + pub fn bpp(&self) -> usize { + self.bit_depth as usize * self.color_type.channels_per_pixel() as usize } /// Byte length of IDAT that is correct for this IHDR @@ -41,8 +39,8 @@ impl IhdrData { let h = self.height as usize; let bpp = self.bpp(); - fn bitmap_size(bpp: u8, w: usize, h: usize) -> usize { - (((w / 8) * bpp as usize) + ((w & 7) * bpp as usize + 7) / 8) * h + fn bitmap_size(bpp: usize, w: usize, h: usize) -> usize { + ((w * bpp + 7) / 8) * h } if self.interlaced == Interlacing::None { @@ -65,21 +63,42 @@ impl IhdrData { } } +#[derive(Debug, Clone)] +pub struct Chunk { + pub name: [u8; 4], + pub data: Vec, +} + #[derive(Debug, PartialEq, Eq, Clone)] -/// Options to use for performing operations on headers (such as stripping) -pub enum Headers { +/// Options to use when stripping chunks +pub enum StripChunks { /// None None, /// Remove specific chunks - Strip(Vec), - /// Headers that won't affect rendering (all but cICP, iCCP, sBIT, sRGB, pHYs) + Strip(IndexSet<[u8; 4]>), + /// Remove all chunks that won't affect rendering Safe, /// Remove all non-critical chunks except these - Keep(IndexSet), - /// All non-critical headers + Keep(IndexSet<[u8; 4]>), + /// All non-critical chunks All, } +impl StripChunks { + /// List of chunks that will be kept when using the `Safe` option + pub const KEEP_SAFE: [[u8; 4]; 4] = [*b"cICP", *b"iCCP", *b"sRGB", *b"pHYs"]; + + pub(crate) fn keep(&self, name: &[u8; 4]) -> bool { + match &self { + StripChunks::None => true, + StripChunks::Keep(names) => names.contains(name), + StripChunks::Strip(names) => !names.contains(name), + StripChunks::Safe => Self::KEEP_SAFE.contains(name), + StripChunks::All => false, + } + } +} + #[inline] pub fn file_header_is_valid(bytes: &[u8]) -> bool { let expected_header: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; @@ -88,27 +107,26 @@ pub fn file_header_is_valid(bytes: &[u8]) -> bool { } #[derive(Debug, Clone, Copy)] -pub struct RawHeader<'a> { +pub struct RawChunk<'a> { pub name: [u8; 4], pub data: &'a [u8], } -pub fn parse_next_header<'a>( +pub fn parse_next_chunk<'a>( byte_data: &'a [u8], byte_offset: &mut usize, fix_errors: bool, -) -> PngResult>> { - let mut rdr = Cursor::new( +) -> PngResult>> { + let length = read_be_u32( byte_data .get(*byte_offset..*byte_offset + 4) .ok_or(PngError::TruncatedData)?, ); - let length = read_be_u32(&mut rdr).unwrap(); *byte_offset += 4; - let header_start = *byte_offset; + let chunk_start = *byte_offset; let chunk_name = byte_data - .get(header_start..header_start + 4) + .get(chunk_start..chunk_start + 4) .ok_or(PngError::TruncatedData)?; if chunk_name == b"IEND" { // End of data @@ -120,61 +138,150 @@ pub fn parse_next_header<'a>( .get(*byte_offset..*byte_offset + length as usize) .ok_or(PngError::TruncatedData)?; *byte_offset += length as usize; - let mut rdr = Cursor::new( + let crc = read_be_u32( byte_data .get(*byte_offset..*byte_offset + 4) .ok_or(PngError::TruncatedData)?, ); - let crc = read_be_u32(&mut rdr).unwrap(); *byte_offset += 4; - let header_bytes = byte_data - .get(header_start..header_start + 4 + length as usize) + let chunk_bytes = byte_data + .get(chunk_start..chunk_start + 4 + length as usize) .ok_or(PngError::TruncatedData)?; - if !fix_errors && crc32(header_bytes) != crc { + if !fix_errors && crc32(chunk_bytes) != crc { return Err(PngError::new(&format!( - "CRC Mismatch in {} header; May be recoverable by using --fix", + "CRC Mismatch in {} chunk; May be recoverable by using --fix", String::from_utf8_lossy(chunk_name) ))); } - let mut name = [0_u8; 4]; - name.copy_from_slice(chunk_name); - Ok(Some(RawHeader { name, data })) + let name: [u8; 4] = chunk_name.try_into().unwrap(); + Ok(Some(RawChunk { name, data })) } -pub fn parse_ihdr_header(byte_data: &[u8]) -> PngResult { +pub fn parse_ihdr_chunk( + byte_data: &[u8], + palette_data: Option>, + trns_data: Option>, +) -> PngResult { // This eliminates bounds checks for the rest of the function let interlaced = byte_data.get(12).copied().ok_or(PngError::TruncatedData)?; - let mut rdr = Cursor::new(&byte_data[0..8]); Ok(IhdrData { color_type: match byte_data[9] { - 0 => ColorType::Grayscale, - 2 => ColorType::RGB, - 3 => ColorType::Indexed, + 0 => ColorType::Grayscale { + transparent_shade: trns_data + .filter(|t| t.len() >= 2) + .map(|t| u16::from_be_bytes([t[0], t[1]])), + }, + 2 => ColorType::RGB { + transparent_color: trns_data.filter(|t| t.len() >= 6).map(|t| RGB16 { + r: u16::from_be_bytes([t[0], t[1]]), + g: u16::from_be_bytes([t[2], t[3]]), + b: u16::from_be_bytes([t[4], t[5]]), + }), + }, + 3 => ColorType::Indexed { + palette: palette_to_rgba(palette_data, trns_data).unwrap_or_default(), + }, 4 => ColorType::GrayscaleAlpha, 6 => ColorType::RGBA, _ => return Err(PngError::new("Unexpected color type in header")), }, - bit_depth: match byte_data[8] { - 1 => BitDepth::One, - 2 => BitDepth::Two, - 4 => BitDepth::Four, - 8 => BitDepth::Eight, - 16 => BitDepth::Sixteen, - _ => return Err(PngError::new("Unexpected bit depth in header")), - }, - width: read_be_u32(&mut rdr).map_err(|_| PngError::TruncatedData)?, - height: read_be_u32(&mut rdr).map_err(|_| PngError::TruncatedData)?, - compression: byte_data[10], - filter: byte_data[11], + bit_depth: byte_data[8].try_into()?, + width: read_be_u32(&byte_data[0..4]), + height: read_be_u32(&byte_data[4..8]), interlaced: interlaced.try_into()?, }) } -#[inline] -fn read_be_u32>(rdr: &mut Cursor) -> Result { - let mut int_buf = [0; 4]; - rdr.read_exact(&mut int_buf)?; - Ok(u32::from_be_bytes(int_buf)) +/// Construct an RGBA palette from the raw palette and transparency data +fn palette_to_rgba( + palette_data: Option>, + trns_data: Option>, +) -> Result, PngError> { + let palette_data = palette_data.ok_or_else(|| PngError::new("no palette in indexed image"))?; + let mut palette: Vec<_> = palette_data + .chunks(3) + .map(|color| RGBA8::new(color[0], color[1], color[2], 255)) + .collect(); + + if let Some(trns_data) = trns_data { + for (color, trns) in palette.iter_mut().zip(trns_data) { + color.a = trns; + } + } + Ok(palette) +} + +#[inline] +fn read_be_u32(bytes: &[u8]) -> u32 { + u32::from_be_bytes(bytes.try_into().unwrap()) +} + +/// Extract and decompress the ICC profile from an iCCP chunk +pub fn extract_icc(iccp: &Chunk) -> Option> { + // Skip (useless) profile name + let mut data = iccp.data.as_slice(); + loop { + let (&n, rest) = data.split_first()?; + data = rest; + if n == 0 { + break; + } + } + + let (&compression_method, compressed_data) = data.split_first()?; + if compression_method != 0 { + return None; // The profile is supposed to be compressed (method 0) + } + // The decompressed size is unknown so we have to guess the required buffer size + let max_size = compressed_data.len() * 2 + 1000; + match inflate(compressed_data, max_size) { + Ok(icc) => Some(icc), + Err(e) => { + // Log the error so we can know if the buffer size needs to be adjusted + warn!("Failed to decompress icc: {}", e); + None + } + } +} + +/// Construct an iCCP chunk by compressing the ICC profile +pub fn construct_iccp(icc: &[u8], deflater: Deflaters) -> PngResult { + let mut compressed = deflater.deflate(icc, &AtomicMin::new(None))?; + let mut data = Vec::with_capacity(compressed.len() + 5); + data.extend(b"icc"); // Profile name - generally unused, can be anything + data.extend([0, 0]); // Null separator, zlib compression method + data.append(&mut compressed); + Ok(Chunk { + name: *b"iCCP", + data, + }) +} + +/// If the profile is sRGB, extracts the rendering intent value from it +pub fn srgb_rendering_intent(icc_data: &[u8]) -> Option { + let rendering_intent = *icc_data.get(67)?; + + // The known profiles are the same as in libpng's `png_sRGB_checks`. + // The Profile ID header of ICC has a fixed layout, + // and is supposed to contain MD5 of profile data at this offset + match icc_data.get(84..100)? { + b"\x29\xf8\x3d\xde\xaf\xf2\x55\xae\x78\x42\xfa\xe4\xca\x83\x39\x0d" + | b"\xc9\x5b\xd6\x37\xe9\x5d\x8a\x3b\x0d\xf3\x8f\x99\xc1\x32\x03\x89" + | b"\xfc\x66\x33\x78\x37\xe2\x88\x6b\xfd\x72\xe9\x83\x82\x28\xf1\xb8" + | b"\x34\x56\x2a\xbf\x99\x4c\xcd\x06\x6d\x2c\x57\x21\xd0\xd6\x8c\x5d" => { + Some(rendering_intent) + } + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" => { + // Known-bad profiles are identified by their CRC + match (crc32(icc_data), icc_data.len()) { + (0x5d51_29ce, 3024) | (0x182e_a552, 3144) | (0xf29e_526d, 3144) => { + Some(rendering_intent) + } + _ => None, + } + } + _ => None, + } } diff --git a/src/interlace.rs b/src/interlace.rs index 8ac0fdc7..0293dce1 100644 --- a/src/interlace.rs +++ b/src/interlace.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::{fmt, fmt::Display}; use crate::headers::IhdrData; use crate::png::PngImage; @@ -25,14 +25,13 @@ impl TryFrom for Interlacing { } impl Display for Interlacing { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match *self { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt( + match self { Self::None => "non-interlaced", Self::Adam7 => "interlaced", - } + }, + f, ) } } @@ -45,11 +44,11 @@ pub fn interlace_image(png: &PngImage) -> PngImage { let bit_vec = line.data.view_bits::(); for (i, bit) in bit_vec.iter().by_vals().enumerate() { // Avoid moving padded 0's into new image - if i >= (png.ihdr.width * u32::from(bits_per_pixel)) as usize { + if i >= (png.ihdr.width as usize * bits_per_pixel) { break; } // Copy pixels into interlaced passes - let pix_modulo = (i / bits_per_pixel as usize) % 8; + let pix_modulo = (i / bits_per_pixel) % 8; match index % 8 { 0 => match pix_modulo { 0 => passes[0].push(bit), @@ -87,12 +86,10 @@ pub fn interlace_image(png: &PngImage) -> PngImage { PngImage { data: output, ihdr: IhdrData { + color_type: png.ihdr.color_type.clone(), interlaced: Interlacing::Adam7, ..png.ihdr }, - aux_headers: png.aux_headers.clone(), - palette: png.palette.clone(), - transparency_pixel: png.transparency_pixel.clone(), } } @@ -103,19 +100,17 @@ pub fn deinterlace_image(png: &PngImage) -> PngImage { _ => deinterlace_bits(png), }, ihdr: IhdrData { + color_type: png.ihdr.color_type.clone(), interlaced: Interlacing::None, ..png.ihdr }, - aux_headers: png.aux_headers.clone(), - palette: png.palette.clone(), - transparency_pixel: png.transparency_pixel.clone(), } } /// Deinterlace by bits, for images with less than 8bpp fn deinterlace_bits(png: &PngImage) -> Vec { let bits_per_pixel = png.ihdr.bpp(); - let bits_per_line = bits_per_pixel as usize * png.ihdr.width as usize; + let bits_per_line = bits_per_pixel * png.ihdr.width as usize; // Initialize each output line with blank data let mut lines: Vec> = vec![bitvec![u8, Msb0; 0; bits_per_line]; png.ihdr.height as usize]; @@ -128,22 +123,22 @@ fn deinterlace_bits(png: &PngImage) -> Vec { + u32::from(pass_constants.x_step) - 1) / u32::from(pass_constants.x_step)) as usize - * bits_per_pixel as usize; + * bits_per_pixel; for (i, bit) in bit_vec.iter().by_vals().enumerate() { // Avoid moving padded 0's into new image if i >= bits_in_line { break; } let current_x: usize = pass_constants.x_shift as usize - + (i / bits_per_pixel as usize) * pass_constants.x_step as usize; + + (i / bits_per_pixel) * pass_constants.x_step as usize; // Copy this bit into the output line - let index = (i % bits_per_pixel as usize) + current_x * bits_per_pixel as usize; + let index = (i % bits_per_pixel) + current_x * bits_per_pixel; lines[current_y].set(index, bit); } // Calculate the next line and move to next pass if necessary current_y += pass_constants.y_step as usize; if current_y >= png.ihdr.height as usize { - if !increment_pass(&mut current_pass, png.ihdr) { + if !increment_pass(&mut current_pass, &png.ihdr) { break; } pass_constants = interlaced_constants(current_pass); @@ -163,7 +158,7 @@ fn deinterlace_bits(png: &PngImage) -> Vec { /// Deinterlace by bytes, for images with at least 8bpp fn deinterlace_bytes(png: &PngImage) -> Vec { let bytes_per_pixel = png.ihdr.bpp() / 8; - let bytes_per_line = bytes_per_pixel as usize * png.ihdr.width as usize; + let bytes_per_line = bytes_per_pixel * png.ihdr.width as usize; // Initialize each output line with some blank data let mut lines: Vec> = vec![vec![0; bytes_per_line]; png.ihdr.height as usize]; let mut current_pass = 1; @@ -172,15 +167,15 @@ fn deinterlace_bytes(png: &PngImage) -> Vec { for line in png.scan_lines(false) { for (i, byte) in line.data.iter().enumerate() { let current_x: usize = pass_constants.x_shift as usize - + (i / bytes_per_pixel as usize) * pass_constants.x_step as usize; + + (i / bytes_per_pixel) * pass_constants.x_step as usize; // Copy this byte into the output line - let index = (i % bytes_per_pixel as usize) + current_x * bytes_per_pixel as usize; + let index = (i % bytes_per_pixel) + current_x * bytes_per_pixel; lines[current_y][index] = *byte; } // Calculate the next line and move to next pass if necessary current_y += pass_constants.y_step as usize; if current_y >= png.ihdr.height as usize { - if !increment_pass(&mut current_pass, png.ihdr) { + if !increment_pass(&mut current_pass, &png.ihdr) { break; } pass_constants = interlaced_constants(current_pass); @@ -190,7 +185,7 @@ fn deinterlace_bytes(png: &PngImage) -> Vec { lines.concat() } -fn increment_pass(current_pass: &mut u8, ihdr: IhdrData) -> bool { +fn increment_pass(current_pass: &mut u8, ihdr: &IhdrData) -> bool { if *current_pass == 7 { return false; } diff --git a/src/lib.rs b/src/lib.rs index 614246ce..bbb9919e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,29 +25,29 @@ extern crate rayon; mod rayon; use crate::atomicmin::AtomicMin; -use crate::colors::BitDepth; -use crate::deflate::{crc32, inflate}; use crate::evaluate::Evaluator; +use crate::headers::*; use crate::png::PngData; use crate::png::PngImage; use crate::reduction::*; -use image::{DynamicImage, GenericImageView, ImageFormat, Pixel}; -use log::{debug, error, info, warn}; +use log::{debug, info, trace, warn}; use rayon::prelude::*; use std::fmt; use std::fs::{copy, File, Metadata}; -use std::io::{stdin, stdout, BufWriter, Cursor, Read, Write}; +use std::io::{stdin, stdout, BufWriter, Read, Write}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; +pub use crate::colors::{BitDepth, ColorType}; pub use crate::deflate::Deflaters; pub use crate::error::PngError; pub use crate::filters::RowFilter; -pub use crate::headers::Headers; +pub use crate::headers::StripChunks; pub use crate::interlace::Interlacing; -pub use indexmap::{indexset, IndexMap, IndexSet}; +pub use indexmap::{indexset, IndexSet}; +pub use rgb::{RGB16, RGBA8}; mod atomicmin; mod colors; @@ -59,16 +59,18 @@ mod headers; mod interlace; mod png; mod reduction; +#[cfg(feature = "sanity-checks")] +mod sanity_checks; /// Private to oxipng; don't use outside tests and benches #[doc(hidden)] pub mod internal_tests { pub use crate::atomicmin::*; - pub use crate::colors::*; pub use crate::deflate::*; - pub use crate::headers::*; pub use crate::png::*; pub use crate::reduction::*; + #[cfg(feature = "sanity-checks")] + pub use crate::sanity_checks::*; } #[derive(Clone, Debug)] @@ -157,7 +159,7 @@ pub struct Options { /// /// `Some(x)` will change the file to interlacing mode `x`. /// - /// Default: `None` + /// Default: `Some(None)` pub interlace: Option, /// Whether to allow transparent pixels to be altered to improve compression. pub optimize_alpha: bool, @@ -184,10 +186,14 @@ pub struct Options { /// /// Default: `true` pub idat_recoding: bool, - /// Which headers to strip from the PNG file, if any + /// Whether to forcibly reduce 16-bit to 8-bit by scaling + /// + /// Default: `false` + pub scale_16: bool, + /// Which chunks to strip from the PNG file, if any /// /// Default: `None` - pub strip: Headers, + pub strip: StripChunks, /// Which DEFLATE algorithm to use /// /// Default: `Libdeflater` @@ -294,14 +300,15 @@ impl Default for Options { force: false, preserve_attrs: false, filter: indexset! {RowFilter::None, RowFilter::Sub, RowFilter::Entropy, RowFilter::Bigrams}, - interlace: None, + interlace: Some(Interlacing::None), optimize_alpha: false, bit_depth_reduction: true, color_type_reduction: true, palette_reduction: true, grayscale_reduction: true, idat_recoding: true, - strip: Headers::None, + scale_16: false, + strip: StripChunks::None, deflate: Deflaters::Libdeflater { compression: 11 }, fast_evaluation: true, timeout: None, @@ -309,6 +316,94 @@ impl Default for Options { } } +#[derive(Debug)] +/// A raw image definition which can be used to create an optimized png +pub struct RawImage { + png: Arc, + aux_chunks: Vec, +} + +impl RawImage { + /// Construct a new raw image definition + /// + /// * `width` - The width of the image in pixels + /// * `height` - The height of the image in pixels + /// * `color_type` - The color type of the image + /// * `bit_depth` - The bit depth of the image + /// * `data` - The raw pixel data of the image + pub fn new( + width: u32, + height: u32, + color_type: ColorType, + bit_depth: BitDepth, + data: Vec, + ) -> Result { + // Validate bit depth + let valid_depth = match color_type { + ColorType::Grayscale { .. } => true, + ColorType::Indexed { .. } => (bit_depth as u8) <= 8, + _ => (bit_depth as u8) >= 8, + }; + if !valid_depth { + return Err(PngError::InvalidDepthForType(bit_depth, color_type)); + } + + // Validate data length + let bpp = bit_depth as usize * color_type.channels_per_pixel() as usize; + let row_bytes = (bpp * width as usize + 7) / 8; + let expected_len = row_bytes * height as usize; + if data.len() != expected_len { + return Err(PngError::IncorrectDataLength(data.len(), expected_len)); + } + + Ok(Self { + png: Arc::new(PngImage { + ihdr: IhdrData { + width, + height, + color_type, + bit_depth, + interlaced: Interlacing::None, + }, + data, + }), + aux_chunks: Vec::new(), + }) + } + + /// Add a png chunk, such as "iTXt", to be included in the output + pub fn add_png_chunk(&mut self, name: [u8; 4], data: Vec) { + self.aux_chunks.push(Chunk { name, data }); + } + + /// Add an ICC profile for the image + pub fn add_icc_profile(&mut self, data: &[u8]) { + // Compress with fastest compression level - will be recompressed during optimization + let deflater = Deflaters::Libdeflater { compression: 1 }; + if let Ok(iccp) = construct_iccp(data, deflater) { + self.aux_chunks.push(iccp); + } + } + + /// Create an optimized png from the raw image data using the options provided + pub fn create_optimized_png(&self, opts: &Options) -> PngResult> { + let deadline = Arc::new(Deadline::new(opts.timeout)); + let mut png = optimize_raw(self.png.clone(), opts, deadline, None) + .ok_or_else(|| PngError::new("Failed to optimize input data"))?; + + // Process aux chunks + png.aux_chunks = self + .aux_chunks + .iter() + .filter(|c| opts.strip.keep(&c.name)) + .cloned() + .collect(); + postprocess_chunks(&mut png, opts, &self.png.ihdr); + + Ok(png.output()) + } +} + /// Perform optimization on the input file using the options provided pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<()> { // Read in the file and try to decode as PNG. @@ -331,7 +426,7 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<( )) }) .map(Some)?; - debug!("preserving metadata: {:?}", opt_metadata_preserved); + trace!("preserving metadata: {:?}", opt_metadata_preserved); } else { opt_metadata_preserved = None; } @@ -347,7 +442,7 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<( } }; - let mut png = PngData::from_slice(&in_data, opts.fix_errors)?; + let mut png = PngData::from_slice(&in_data, opts)?; if opts.check { info!("Running in check mode, not optimizing"); @@ -435,7 +530,7 @@ pub fn optimize_from_memory(data: &[u8], opts: &Options) -> PngResult> { let deadline = Arc::new(Deadline::new(opts.timeout)); let original_size = data.len(); - let mut png = PngData::from_slice(data, opts.fix_errors)?; + let mut png = PngData::from_slice(data, opts)?; // Run the optimizer on the decoded PNG. let optimized_output = optimize_png(&mut png, data, opts, deadline)?; @@ -448,13 +543,7 @@ pub fn optimize_from_memory(data: &[u8], opts: &Options) -> PngResult> { } } -#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] -/// Defines options to be used for a single compression trial -struct TrialOptions { - pub filter: RowFilter, - pub compression: u8, -} -type TrialWithData = (TrialOptions, Vec); +type TrialResult = (RowFilter, Vec); /// Perform optimization on the input PNG object using the options provided fn optimize_png( @@ -466,167 +555,26 @@ fn optimize_png( // Print png info let file_original_size = original_data.len(); let idat_original_size = png.idat_data.len(); + let raw = png.raw.clone(); debug!( " {}x{} pixels, PNG format", - png.raw.ihdr.width, png.raw.ihdr.height + raw.ihdr.width, raw.ihdr.height ); - report_format(" ", &png.raw); + report_format(" ", &raw); debug!(" IDAT size = {} bytes", idat_original_size); debug!(" File size = {} bytes", file_original_size); - // Do this first so that reductions can ignore certain chunks such as bKGD - perform_strip(png, opts); - let stripped_png = png.clone(); - - // Interlacing is not part of the evaluator trials but must be done first to evaluate the rest correctly - let mut reduction_occurred = false; - if let Some(interlacing) = opts.interlace { - if let Some(reduced) = png.raw.change_interlacing(interlacing) { - png.raw = Arc::new(reduced); - reduction_occurred = true; - } - } - - // If alpha optimization is enabled, perform a black alpha reduction before evaluating reductions - // This can allow reductions from alpha to indexed which may not have been possible otherwise - if opts.optimize_alpha { - if let Some(reduced) = cleaned_alpha_channel(&png.raw) { - png.raw = Arc::new(reduced); - } - } - - // Must use normal (lazy) compression, as faster ones (greedy) are not representative - let eval_compression = 5; - // None and Bigrams work well together, especially for alpha reductions - let eval_filters = indexset! {RowFilter::None, RowFilter::Bigrams}; - // This will collect all versions of images and pick one that compresses best - let eval = Evaluator::new( - deadline.clone(), - eval_filters.clone(), - eval_compression, - false, - ); - perform_reductions(png.raw.clone(), opts, &deadline, &eval); - let mut eval_filter = if let Some(result) = eval.get_best_candidate() { - *png = result.image; - if result.is_reduction { - reduction_occurred = true; - } - Some(result.filter) - } else { + let max_size = if opts.force { None + } else { + Some(png.estimated_output_size()) }; - - if reduction_occurred { - report_format("Reducing image to ", &png.raw); + if let Some(new_png) = optimize_raw(raw.clone(), opts, deadline, max_size) { + png.raw = new_png.raw; + png.idat_data = new_png.idat_data; } - if opts.idat_recoding || reduction_occurred { - let mut filters = opts.filter.clone(); - let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_filter.is_some()); - let best: Option = if fast_eval { - // Perform a fast evaluation of selected filters followed by a single main compression trial - - if eval_filter.is_some() { - // Some filters have already been evaluated, we don't need to try them again - filters = filters.difference(&eval_filters).cloned().collect(); - } - - if !filters.is_empty() { - debug!("Evaluating: {} filters", filters.len()); - let eval = Evaluator::new(deadline, filters, eval_compression, opts.optimize_alpha); - if eval_filter.is_some() { - eval.set_best_size(png.idat_data.len()); - } - eval.try_image(png.raw.clone()); - if let Some(result) = eval.get_best_candidate() { - *png = result.image; - eval_filter = Some(result.filter); - } - } - - let trial = TrialOptions { - filter: eval_filter.unwrap(), - compression: match opts.deflate { - Deflaters::Libdeflater { compression } => compression, - _ => 0, - }, - }; - if trial.compression > 0 && trial.compression <= eval_compression { - // No further compression required - if png.idat_data.len() < idat_original_size || opts.force { - Some((trial, png.idat_data.clone())) - } else { - None - } - } else { - debug!("Trying: {}", trial.filter); - let original_len = idat_original_size; - let best_size = AtomicMin::new(if opts.force { None } else { Some(original_len) }); - perform_trial(&png.filtered, opts, trial, &best_size) - } - } else { - // Perform full compression trials of selected filters and determine the best - - if filters.is_empty() { - // Pick a filter automatically - if png.raw.ihdr.bit_depth.as_u8() >= 8 { - // Bigrams is the best all-rounder when there's at least one byte per pixel - filters.insert(RowFilter::Bigrams); - } else { - // Otherwise delta filters generally don't work well, so just stick with None - filters.insert(RowFilter::None); - } - } - - let mut results: Vec = Vec::with_capacity(filters.len()); - - for f in &filters { - results.push(TrialOptions { - filter: *f, - compression: match opts.deflate { - Deflaters::Libdeflater { compression } => compression, - _ => 0, - }, - }); - } - - debug!("Trying: {} filters", results.len()); - - let original_len = idat_original_size; - let best_size = AtomicMin::new(if opts.force { None } else { Some(original_len) }); - let results_iter = results.into_par_iter().with_max_len(1); - let best = results_iter.filter_map(|trial| { - if deadline.passed() { - return None; - } - let filtered = &png.raw.filter_image(trial.filter, opts.optimize_alpha); - perform_trial(filtered, opts, trial, &best_size) - }); - best.reduce_with(|i, j| { - if i.1.len() < j.1.len() || (i.1.len() == j.1.len() && i.0 < j.0) { - i - } else { - j - } - }) - }; - - if let Some((opts, idat_data)) = best { - png.idat_data = idat_data; - debug!("Found better combination:"); - debug!( - " zc = {} f = {} {} bytes", - opts.compression, - opts.filter, - png.idat_data.len() - ); - } else { - *png = stripped_png; - } - } else if png.idat_data.len() >= idat_original_size { - *png = stripped_png; - } + postprocess_chunks(png, opts, &raw.ihdr); let output = png.output(); @@ -659,103 +607,180 @@ fn optimize_png( ); } - debug_assert!(validate_output(&output, original_data)); + #[cfg(feature = "sanity-checks")] + assert!(sanity_checks::validate_output(&output, original_data)); Ok(output) } -fn perform_reductions( +/// Perform optimization on the input image data using the options provided +fn optimize_raw( mut png: Arc, opts: &Options, - deadline: &Deadline, - eval: &Evaluator, -) { - // The eval baseline will be set from the original png only if we attempt any reductions - let baseline = png.clone(); - let mut reduction_occurred = false; - - if opts.palette_reduction { - if let Some(reduced) = reduced_palette(&png, opts.optimize_alpha) { - png = Arc::new(reduced); - eval.try_image(png.clone()); + deadline: Arc, + max_size: Option, +) -> Option { + // Must use normal (lazy) compression, as faster ones (greedy) are not representative + let eval_compression = 5; + // None and Bigrams work well together, especially for alpha reductions + let eval_filters = indexset! {RowFilter::None, RowFilter::Bigrams}; + // This will collect all versions of images and pick one that compresses best + let eval = Evaluator::new( + deadline.clone(), + eval_filters.clone(), + eval_compression, + false, + ); + let (baseline, mut reduction_occurred) = + perform_reductions(png.clone(), opts, &deadline, &eval); + png = baseline; + let mut eval_result = eval.get_best_candidate(); + if let Some(ref result) = eval_result { + if result.is_reduction { + png = result.image.clone(); reduction_occurred = true; } - if deadline.passed() { - return; - } - } - - if opts.bit_depth_reduction { - if let Some(reduced) = reduce_bit_depth(&png, 1) { - let previous = png.clone(); - let bits = reduced.ihdr.bit_depth; - png = Arc::new(reduced); - eval.try_image(png.clone()); - if (bits == BitDepth::One || bits == BitDepth::Two) - && previous.ihdr.bit_depth != BitDepth::Four - { - // Also try 16-color mode for all lower bits images, since that may compress better - if let Some(reduced) = reduce_bit_depth(&previous, 4) { - eval.try_image(Arc::new(reduced)); - } - } - reduction_occurred = true; - } - if deadline.passed() { - return; - } - } - - if opts.color_type_reduction { - if let Some(reduced) = - reduce_color_type(&png, opts.grayscale_reduction, opts.optimize_alpha) - { - png = Arc::new(reduced); - eval.try_image(png.clone()); - reduction_occurred = true; - } - if deadline.passed() { - return; - } } if reduction_occurred { - eval.set_baseline(baseline); + report_format("Reducing image to ", &png); } + + if opts.idat_recoding || reduction_occurred { + let mut filters = opts.filter.clone(); + let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_result.is_some()); + let best: Option = if fast_eval { + // Perform a fast evaluation of selected filters followed by a single main compression trial + + if eval_result.is_some() { + // Some filters have already been evaluated, we don't need to try them again + filters = filters.difference(&eval_filters).cloned().collect(); + } + + if !filters.is_empty() { + trace!("Evaluating: {} filters", filters.len()); + let eval = Evaluator::new(deadline, filters, eval_compression, opts.optimize_alpha); + if let Some(ref result) = eval_result { + eval.set_best_size(result.idat_data.len()); + } + eval.try_image(png.clone()); + if let Some(result) = eval.get_best_candidate() { + eval_result = Some(result); + } + } + // We should have a result here - fail if not (e.g. deadline passed) + let result = eval_result?; + + match opts.deflate { + Deflaters::Libdeflater { compression } if compression <= eval_compression => { + // No further compression required + Some((result.filter, result.idat_data)) + } + _ => { + debug!("Trying: {}", result.filter); + let best_size = AtomicMin::new(max_size); + perform_trial(&result.filtered, opts, result.filter, &best_size) + } + } + } else { + // Perform full compression trials of selected filters and determine the best + + if filters.is_empty() { + // Pick a filter automatically + if png.ihdr.bit_depth as u8 >= 8 { + // Bigrams is the best all-rounder when there's at least one byte per pixel + filters.insert(RowFilter::Bigrams); + } else { + // Otherwise delta filters generally don't work well, so just stick with None + filters.insert(RowFilter::None); + } + } + + debug!("Trying: {} filters", filters.len()); + + let best_size = AtomicMin::new(max_size); + let results_iter = filters.into_par_iter().with_max_len(1); + let best = results_iter.filter_map(|filter| { + if deadline.passed() { + return None; + } + let filtered = &png.filter_image(filter, opts.optimize_alpha); + perform_trial(filtered, opts, filter, &best_size) + }); + best.reduce_with(|i, j| { + if i.1.len() < j.1.len() || (i.1.len() == j.1.len() && i.0 < j.0) { + i + } else { + j + } + }) + }; + + if let Some((filter, idat_data)) = best { + let image = PngData { + raw: png, + idat_data, + aux_chunks: Vec::new(), + }; + if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) { + debug!("Found better combination:"); + debug!( + " zc = {} f = {:8} {} bytes", + opts.deflate, + filter, + image.idat_data.len() + ); + return Some(image); + } + } + } else if let Some(result) = eval_result { + // If idat_recoding is off and reductions were attempted but ended up choosing the baseline, + // we should still check if the evaluator compressed the baseline smaller than the original. + let image = PngData { + raw: result.image, + idat_data: result.idat_data, + aux_chunks: Vec::new(), + }; + if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) { + debug!("Found better combination:"); + debug!( + " zc = {} f = {:8} {} bytes", + eval_compression, + result.filter, + image.idat_data.len() + ); + return Some(image); + } + } + + None } /// Execute a compression trial fn perform_trial( filtered: &[u8], opts: &Options, - trial: TrialOptions, + filter: RowFilter, best_size: &AtomicMin, -) -> Option { - let new_idat = match opts.deflate { - Deflaters::Libdeflater { .. } => deflate::deflate(filtered, trial.compression, best_size), - #[cfg(feature = "zopfli")] - Deflaters::Zopfli { iterations } => deflate::zopfli_deflate(filtered, iterations), - }; - - // update best size or convert to error if not smaller - let new_idat = match new_idat { - Ok(n) if !best_size.set_min(n.len()) => Err(PngError::DeflatedDataTooLong(n.len())), - _ => new_idat, - }; - - match new_idat { - Ok(n) => { - let bytes = n.len(); - debug!( - " zc = {} f = {} {} bytes", - trial.compression, trial.filter, bytes +) -> Option { + match opts.deflate.deflate(filtered, best_size) { + Ok(new_idat) => { + let bytes = new_idat.len(); + best_size.set_min(bytes); + trace!( + " zc = {} f = {:8} {} bytes", + opts.deflate, + filter, + bytes ); - Some((trial, n)) + Some((filter, new_idat)) } Err(PngError::DeflatedDataTooLong(bytes)) => { - debug!( - " zc = {} f = {} >{} bytes", - trial.compression, trial.filter, bytes, + trace!( + " zc = {} f = {:8} >{} bytes", + opts.deflate, + filter, + bytes, ); None } @@ -814,126 +839,71 @@ impl Deadline { /// Display the format of the image data fn report_format(prefix: &str, png: &PngImage) { - if let Some(ref palette) = png.palette { - debug!( - "{}{} bits/pixel, {} colors in palette ({})", - prefix, - png.ihdr.bit_depth, - palette.len(), - png.ihdr.interlaced - ); - } else { - debug!( - "{}{}x{} bits/pixel, {} ({})", - prefix, - png.channels_per_pixel(), - png.ihdr.bit_depth, - png.ihdr.color_type, - png.ihdr.interlaced - ); - } + debug!( + "{}{}-bit {}, {}", + prefix, png.ihdr.bit_depth, png.ihdr.color_type, png.ihdr.interlaced + ); } -/// Strip headers from the `PngData` object, as requested by the passed `Options` -fn perform_strip(png: &mut PngData, opts: &Options) { - let raw = Arc::make_mut(&mut png.raw); - match opts.strip { - // Strip headers - Headers::None => (), - Headers::Keep(ref hdrs) => raw - .aux_headers - .retain(|hdr, _| std::str::from_utf8(hdr).map_or(false, |name| hdrs.contains(name))), - Headers::Strip(ref hdrs) => { - for hdr in hdrs { - raw.aux_headers.remove(hdr.as_bytes()); - } - } - Headers::Safe => { - const PRESERVED_HEADERS: [[u8; 4]; 5] = - [*b"cICP", *b"iCCP", *b"sBIT", *b"sRGB", *b"pHYs"]; - let keys: Vec<[u8; 4]> = raw.aux_headers.keys().cloned().collect(); - for hdr in &keys { - if !PRESERVED_HEADERS.contains(hdr) { - raw.aux_headers.remove(hdr); - } - } - } - Headers::All => { - raw.aux_headers = IndexMap::new(); - } - } - - let may_replace_iccp = match opts.strip { - Headers::Keep(ref hdrs) => hdrs.contains("sRGB"), - Headers::Strip(ref hdrs) => !hdrs.iter().any(|v| v == "sRGB"), - Headers::Safe => true, - Headers::None | Headers::All => false, - }; - - if may_replace_iccp { - if raw.aux_headers.get(b"sRGB").is_some() { +/// Perform cleanup of certain chunks from the `PngData` object, after optimization has been completed +fn postprocess_chunks(png: &mut PngData, opts: &Options, orig_ihdr: &IhdrData) { + if let Some(iccp_idx) = png.aux_chunks.iter().position(|c| &c.name == b"iCCP") { + // See if we can replace an iCCP chunk with an sRGB chunk + let may_replace_iccp = opts.strip != StripChunks::None && opts.strip.keep(b"sRGB"); + if may_replace_iccp && png.aux_chunks.iter().any(|c| &c.name == b"sRGB") { // Files aren't supposed to have both chunks, so we chose to honor sRGB - raw.aux_headers.remove(b"iCCP"); - } else if let Some(intent) = raw - .aux_headers - .get(b"iCCP") - .and_then(|iccp| srgb_rendering_intent(iccp)) - { - // sRGB-like profile can be safely replaced with - // an sRGB chunk with the same rendering intent - raw.aux_headers.remove(b"iCCP"); - raw.aux_headers.insert(*b"sRGB", vec![intent]); - } - } -} - -/// If the profile is sRGB, extracts the rendering intent value from it -fn srgb_rendering_intent(mut iccp: &[u8]) -> Option { - // Skip (useless) profile name - loop { - let (&n, rest) = iccp.split_first()?; - iccp = rest; - if n == 0 { - break; - } - } - - let (&compression_method, compressed_data) = iccp.split_first()?; - if compression_method != 0 { - return None; // The profile is supposed to be compressed (method 0) - } - // The decompressed size is unknown so we have to guess the required buffer size - let max_size = (compressed_data.len() * 2).max(1000); - let icc_data = inflate(compressed_data, max_size).ok()?; - - let rendering_intent = *icc_data.get(67)?; - - // The known profiles are the same as in libpng's `png_sRGB_checks`. - // The Profile ID header of ICC has a fixed layout, - // and is supposed to contain MD5 of profile data at this offset - match icc_data.get(84..100)? { - b"\x29\xf8\x3d\xde\xaf\xf2\x55\xae\x78\x42\xfa\xe4\xca\x83\x39\x0d" - | b"\xc9\x5b\xd6\x37\xe9\x5d\x8a\x3b\x0d\xf3\x8f\x99\xc1\x32\x03\x89" - | b"\xfc\x66\x33\x78\x37\xe2\x88\x6b\xfd\x72\xe9\x83\x82\x28\xf1\xb8" - | b"\x34\x56\x2a\xbf\x99\x4c\xcd\x06\x6d\x2c\x57\x21\xd0\xd6\x8c\x5d" => { - Some(rendering_intent) - } - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" => { - // Known-bad profiles are identified by their CRC - match (crc32(&icc_data), icc_data.len()) { - (0x5d51_29ce, 3024) | (0x182e_a552, 3144) | (0xf29e_526d, 3144) => { - Some(rendering_intent) + trace!("Removing iCCP chunk due to conflict with sRGB chunk"); + png.aux_chunks.remove(iccp_idx); + } else if let Some(icc) = extract_icc(&png.aux_chunks[iccp_idx]) { + let intent = if may_replace_iccp { + srgb_rendering_intent(&icc) + } else { + None + }; + // sRGB-like profile can be replaced with an sRGB chunk with the same rendering intent + // Otherwise try recompressing the profile + if let Some(intent) = intent { + trace!("Replacing iCCP chunk with equivalent sRGB chunk"); + png.aux_chunks[iccp_idx] = Chunk { + name: *b"sRGB", + data: vec![intent], + }; + } else if let Ok(iccp) = construct_iccp(&icc, opts.deflate) { + let cur_len = png.aux_chunks[iccp_idx].data.len(); + let new_len = iccp.data.len(); + if new_len < cur_len { + debug!( + "Recompressed iCCP chunk: {} ({} bytes decrease)", + new_len, + cur_len - new_len + ); + png.aux_chunks[iccp_idx] = iccp; } - _ => None, } } - _ => None, + } + + // If the depth/color type has changed, some chunks may be invalid and should be dropped + // While these could potentially be converted, they have no known use case today and are + // generally more trouble than they're worth + let ihdr = &png.raw.ihdr; + if orig_ihdr.bit_depth != ihdr.bit_depth || orig_ihdr.color_type != ihdr.color_type { + png.aux_chunks.retain(|c| { + let invalid = &c.name == b"bKGD" || &c.name == b"sBIT" || &c.name == b"hIST"; + if invalid { + warn!( + "Removing {} chunk as it no longer matches the image data", + std::str::from_utf8(&c.name).unwrap() + ); + } + !invalid + }); } } /// Check if an image was already optimized prior to oxipng's operations fn is_fully_optimized(original_size: usize, optimized_size: usize, opts: &Options) -> bool { - original_size <= optimized_size && !opts.force && opts.interlace.is_none() + original_size <= optimized_size && !opts.force } fn perform_backup(input_path: &Path) -> PngResult<()> { @@ -1032,9 +1002,10 @@ fn copy_times(_: &Metadata, _: &Path) -> PngResult<()> { fn copy_times(input_path_meta: &Metadata, out_path: &Path) -> PngResult<()> { let atime = filetime::FileTime::from_last_access_time(input_path_meta); let mtime = filetime::FileTime::from_last_modification_time(input_path_meta); - debug!( + trace!( "attempting to set file times: atime: {:?}, mtime: {:?}", - atime, mtime + atime, + mtime ); filetime::set_file_times(out_path, atime, mtime).map_err(|err_io| { PngError::new(&format!( @@ -1043,47 +1014,3 @@ fn copy_times(input_path_meta: &Metadata, out_path: &Path) -> PngResult<()> { )) }) } - -/// Validate that the output png data still matches the original image -fn validate_output(output: &[u8], original_data: &[u8]) -> bool { - let (old_png, new_png) = rayon::join( - || load_png_image_from_memory(original_data), - || load_png_image_from_memory(output), - ); - - match (new_png, old_png) { - (Err(new_err), _) => { - error!("Failed to read output image for validation: {}", new_err); - false - } - (_, Err(old_err)) => { - // The original image might be invalid if, for example, there is a CRC error, - // and we set fix_errors to true. In that case, all we can do is check that the - // new image is decodable. - warn!("Failed to read input image for validation: {}", old_err); - true - } - (Ok(new_png), Ok(old_png)) => images_equal(&old_png, &new_png), - } -} - -/// Loads a PNG image from memory to a [DynamicImage] -fn load_png_image_from_memory(png_data: &[u8]) -> Result { - let mut reader = image::io::Reader::new(Cursor::new(png_data)); - reader.set_format(ImageFormat::Png); - reader.no_limits(); - reader.decode() -} - -/// Compares images pixel by pixel for equivalent content -fn images_equal(old_png: &DynamicImage, new_png: &DynamicImage) -> bool { - let a = old_png.pixels().filter(|x| { - let p = x.2.channels(); - !(p.len() == 4 && p[3] == 0) - }); - let b = new_png.pixels().filter(|x| { - let p = x.2.channels(); - !(p.len() == 4 && p[3] == 0) - }); - a.eq(b) -} diff --git a/src/main.rs b/src/main.rs index b484fac6..71c66fa3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,13 +13,13 @@ #![warn(clippy::range_plus_one)] #![allow(clippy::cognitive_complexity)] -use clap::{AppSettings, Arg, ArgMatches, Command}; +use clap::{AppSettings, Arg, ArgAction, ArgMatches, Command}; use indexmap::IndexSet; use log::{error, warn}; use oxipng::Deflaters; -use oxipng::Headers; use oxipng::Options; use oxipng::RowFilter; +use oxipng::StripChunks; use oxipng::{InFile, OutFile}; use std::fs::DirBuilder; #[cfg(feature = "zopfli")] @@ -144,19 +144,26 @@ fn main() { ) .arg( Arg::new("interlace") - .help("PNG interlace type") + .help("PNG interlace type - Default: 0") .short('i') .long("interlace") .takes_value(true) - .value_name("0/1") + .value_name("type") .possible_value("0") - .possible_value("1"), + .possible_value("1") + .possible_value("keep"), + ) + .arg( + Arg::new("scale16") + .help("Forcibly reduce 16-bit images to 8-bit") + .long("scale16"), ) .arg( Arg::new("verbose") - .help("Run in verbose mode") + .help("Run in verbose mode (use multiple times to increase verbosity)") .short('v') .long("verbose") + .action(ArgAction::Count) .conflicts_with("quiet"), ) .arg( @@ -381,7 +388,7 @@ fn parse_opts_into_struct( stderrlog::new() .module(module_path!()) .quiet(matches.is_present("quiet")) - .verbosity(if matches.is_present("verbose") { 3 } else { 2 }) + .verbosity(matches.get_count("verbose") as usize + 2) .show_level(false) .init() .unwrap(); @@ -393,7 +400,11 @@ fn parse_opts_into_struct( }; if let Some(x) = matches.value_of("interlace") { - opts.interlace = x.parse::().unwrap().try_into().ok(); + opts.interlace = if x == "keep" { + None + } else { + x.parse::().unwrap().try_into().ok() + }; } if let Some(x) = matches.value_of("filters") { @@ -440,6 +451,10 @@ fn parse_opts_into_struct( opts.optimize_alpha = true; } + if matches.is_present("scale16") { + opts.scale_16 = true; + } + if matches.is_present("fast") { opts.fast_evaluation = true; } @@ -495,40 +510,44 @@ fn parse_opts_into_struct( opts.idat_recoding = false; } - if let Some(hdrs) = matches.value_of("keep") { - opts.strip = Headers::Keep(hdrs.split(',').map(|x| x.trim().to_owned()).collect()) + if let Some(keep) = matches.value_of("keep") { + let names = keep + .split(',') + .map(parse_chunk_name) + .collect::>()?; + opts.strip = StripChunks::Keep(names) } - if let Some(hdrs) = matches.value_of("strip") { - let hdrs = hdrs - .split(',') - .map(|x| x.trim().to_owned()) - .collect::>(); - if hdrs.contains(&"safe".to_owned()) || hdrs.contains(&"all".to_owned()) { - if hdrs.len() > 1 { - return Err( - "'safe' or 'all' presets for --strip should be used by themselves".to_owned(), - ); - } - if hdrs[0] == "safe" { - opts.strip = Headers::Safe; - } else { - opts.strip = Headers::All; - } + if let Some(strip) = matches.value_of("strip") { + if strip == "safe" { + opts.strip = StripChunks::Safe; + } else if strip == "all" { + opts.strip = StripChunks::All; } else { const FORBIDDEN_CHUNKS: [[u8; 4]; 5] = [*b"IHDR", *b"IDAT", *b"tRNS", *b"PLTE", *b"IEND"]; - for i in &hdrs { - if FORBIDDEN_CHUNKS.iter().any(|chunk| chunk == i.as_bytes()) { - return Err(format!("{} chunk is not allowed to be stripped", i)); - } - } - opts.strip = Headers::Strip(hdrs); + let names = strip + .split(',') + .map(|x| { + if x == "safe" || x == "all" { + return Err( + "'safe' or 'all' presets for --strip should be used by themselves" + .to_owned(), + ); + } + let name = parse_chunk_name(x)?; + if FORBIDDEN_CHUNKS.contains(&name) { + return Err(format!("{} chunk is not allowed to be stripped", x)); + } + Ok(name) + }) + .collect::>()?; + opts.strip = StripChunks::Strip(names); } } if matches.is_present("strip-safe") { - opts.strip = Headers::Safe; + opts.strip = StripChunks::Safe; } if matches.is_present("zopfli") { @@ -555,6 +574,13 @@ fn parse_opts_into_struct( Ok((out_file, out_dir, opts)) } +fn parse_chunk_name(name: &str) -> Result<[u8; 4], String> { + name.trim() + .as_bytes() + .try_into() + .map_err(|_| format!("Invalid chunk name {}", name)) +} + fn parse_numeric_range_opts( input: &str, min_value: u8, diff --git a/src/png/mod.rs b/src/png/mod.rs index f3d712d0..49f7a89d 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -1,14 +1,13 @@ -use crate::colors::ColorType; +use crate::colors::{BitDepth, ColorType}; use crate::deflate; use crate::error::PngError; use crate::filters::*; use crate::headers::*; use crate::interlace::{deinterlace_image, interlace_image, Interlacing}; +use crate::Options; use bitvec::bitarr; -use indexmap::IndexMap; use libdeflater::{CompressionLvl, Compressor}; use rgb::ComponentSlice; -use rgb::RGBA8; use rustc_hash::FxHashMap; use std::fs::File; use std::io::{BufReader, Read, Write}; @@ -31,13 +30,6 @@ pub struct PngImage { pub ihdr: IhdrData, /// The uncompressed, unfiltered data from the IDAT chunk pub data: Vec, - /// The palette containing colors used in an Indexed image - /// Contains 3 bytes per color (R+G+B), up to 768 - pub palette: Option>, - /// The pixel value that should be rendered as transparent - pub transparency_pixel: Option>, - /// All non-critical headers from the PNG are stored here - pub aux_headers: IndexMap<[u8; 4], Vec>, } /// Contains all data relevant to a PNG image @@ -47,19 +39,17 @@ pub struct PngData { pub raw: Arc, /// The filtered and compressed data of the IDAT chunk pub idat_data: Vec, - /// The filtered, uncompressed data of the IDAT chunk - pub filtered: Vec, + /// All non-critical chunks from the PNG are stored here + pub aux_chunks: Vec, } -type PaletteWithTrns = (Option>, Option>); - impl PngData { /// Create a new `PngData` struct by opening a file #[inline] - pub fn new(filepath: &Path, fix_errors: bool) -> Result { + pub fn new(filepath: &Path, opts: &Options) -> Result { let byte_data = Self::read_file(filepath)?; - Self::from_slice(&byte_data, fix_errors) + Self::from_slice(&byte_data, opts) } pub fn read_file(filepath: &Path) -> Result, PngError> { @@ -88,7 +78,7 @@ impl PngData { } /// Create a new `PngData` struct by reading a slice - pub fn from_slice(byte_data: &[u8], fix_errors: bool) -> Result { + pub fn from_slice(byte_data: &[u8], opts: &Options) -> Result { let mut byte_offset: usize = 0; // Test that png header is valid let header = byte_data.get(0..8).ok_or(PngError::TruncatedData)?; @@ -96,79 +86,65 @@ impl PngData { return Err(PngError::NotPNG); } byte_offset += 8; - // Read the data headers - let mut aux_headers: IndexMap<[u8; 4], Vec> = IndexMap::new(); - let mut idat_headers: Vec = Vec::new(); - while let Some(header) = parse_next_header(byte_data, &mut byte_offset, fix_errors)? { - match &header.name { - b"IDAT" => idat_headers.extend_from_slice(header.data), + + // Read the data chunks + let mut idat_data: Vec = Vec::new(); + let mut key_chunks: FxHashMap<[u8; 4], Vec> = FxHashMap::default(); + let mut aux_chunks: Vec = Vec::new(); + while let Some(chunk) = parse_next_chunk(byte_data, &mut byte_offset, opts.fix_errors)? { + match &chunk.name { + b"IDAT" => idat_data.extend_from_slice(chunk.data), b"acTL" => return Err(PngError::APNGNotSupported), + b"IHDR" | b"PLTE" | b"tRNS" => { + key_chunks.insert(chunk.name, chunk.data.to_owned()); + } _ => { - aux_headers.insert(header.name, header.data.to_owned()); + if opts.strip.keep(&chunk.name) { + aux_chunks.push(Chunk { + name: chunk.name, + data: chunk.data.to_owned(), + }) + } } } } - // Parse the headers into our PngData - if idat_headers.is_empty() { + + // Parse the chunks into our PngData + if idat_data.is_empty() { return Err(PngError::ChunkMissing("IDAT")); } - let ihdr = match aux_headers.remove(b"IHDR") { + let ihdr_chunk = match key_chunks.remove(b"IHDR") { Some(ihdr) => ihdr, None => return Err(PngError::ChunkMissing("IHDR")), }; - let ihdr_header = parse_ihdr_header(&ihdr)?; - let raw_data = deflate::inflate(idat_headers.as_ref(), ihdr_header.raw_data_size())?; + let ihdr = parse_ihdr_chunk( + &ihdr_chunk, + key_chunks.remove(b"PLTE"), + key_chunks.remove(b"tRNS"), + )?; + let raw_data = deflate::inflate(idat_data.as_ref(), ihdr.raw_data_size())?; // Reject files with incorrect width/height or truncated data - if raw_data.len() != ihdr_header.raw_data_size() { + if raw_data.len() != ihdr.raw_data_size() { return Err(PngError::TruncatedData); } - let (palette, transparency_pixel) = Self::palette_to_rgba( - ihdr_header.color_type, - aux_headers.remove(b"PLTE"), - aux_headers.remove(b"tRNS"), - )?; - let mut raw = PngImage { - ihdr: ihdr_header, + ihdr, data: raw_data, - palette, - transparency_pixel, - aux_headers, }; - let unfiltered = raw.unfilter_image()?; + raw.data = raw.unfilter_image()?; // Return the PngData Ok(Self { - idat_data: idat_headers, - filtered: std::mem::replace(&mut raw.data, unfiltered), + idat_data, raw: Arc::new(raw), + aux_chunks, }) } - /// Handle transparency header - fn palette_to_rgba( - color_type: ColorType, - palette_data: Option>, - trns_data: Option>, - ) -> Result { - if color_type == ColorType::Indexed { - let palette_data = - palette_data.ok_or_else(|| PngError::new("no palette in indexed image"))?; - let mut palette: Vec<_> = palette_data - .chunks(3) - .map(|color| RGBA8::new(color[0], color[1], color[2], 255)) - .collect(); - - if let Some(trns_data) = trns_data { - for (color, trns) in palette.iter_mut().zip(trns_data) { - color.a = trns; - } - } - Ok((Some(palette), None)) - } else { - Ok((None, trns_data)) - } + /// Return an estimate of the output size which can help with evaluation of very small data + pub fn estimated_output_size(&self) -> usize { + self.idat_data.len() + self.raw.key_chunks_size() } /// Format the `PngData` struct into a valid PNG bytestream @@ -181,7 +157,7 @@ impl PngData { ihdr_data .write_all(&self.raw.ihdr.height.to_be_bytes()) .ok(); - ihdr_data.write_all(&[self.raw.ihdr.bit_depth.as_u8()]).ok(); + ihdr_data.write_all(&[self.raw.ihdr.bit_depth as u8]).ok(); ihdr_data .write_all(&[self.raw.ihdr.color_type.png_header_code()]) .ok(); @@ -189,58 +165,49 @@ impl PngData { ihdr_data.write_all(&[0]).ok(); // Filter method -- 5-way adaptive filtering ihdr_data.write_all(&[self.raw.ihdr.interlaced as u8]).ok(); write_png_block(b"IHDR", &ihdr_data, &mut output); - // Ancillary headers - for (key, header) in self - .raw - .aux_headers + // Ancillary chunks + for chunk in self + .aux_chunks .iter() - .filter(|&(key, _)| !(key == b"bKGD" || key == b"hIST" || key == b"tRNS")) + .filter(|c| !(&c.name == b"bKGD" || &c.name == b"hIST" || &c.name == b"tRNS")) { - write_png_block(key, header, &mut output); + write_png_block(&chunk.name, &chunk.data, &mut output); } - // Palette - if let Some(ref palette) = self.raw.palette { - let mut palette_data = Vec::with_capacity(palette.len() * 3); - let mut max_palette_size = 1 << (self.raw.ihdr.bit_depth.as_u8() as usize); - // Ensure bKGD color doesn't get truncated from palette - if let Some(&idx) = self.raw.aux_headers.get(b"bKGD").and_then(|b| b.first()) { - max_palette_size = max_palette_size.max(idx as usize + 1); + // Palette and transparency + match &self.raw.ihdr.color_type { + ColorType::Indexed { palette } => { + let mut palette_data = Vec::with_capacity(palette.len() * 3); + for px in palette { + palette_data.extend_from_slice(px.rgb().as_slice()); + } + write_png_block(b"PLTE", &palette_data, &mut output); + if let Some(last_trns) = palette.iter().rposition(|px| px.a != 255) { + let trns_data: Vec<_> = palette[0..=last_trns].iter().map(|px| px.a).collect(); + write_png_block(b"tRNS", &trns_data, &mut output); + } } - for px in palette.iter().take(max_palette_size) { - palette_data.extend_from_slice(px.rgb().as_slice()); + ColorType::Grayscale { + transparent_shade: Some(trns), + } => { + // Transparency pixel - 2 byte u16 + write_png_block(b"tRNS", &trns.to_be_bytes(), &mut output); } - write_png_block(b"PLTE", &palette_data, &mut output); - let num_transparent = - palette - .iter() - .take(max_palette_size) - .enumerate() - .fold( - 0, - |prev, (index, px)| { - if px.a == 255 { - prev - } else { - index + 1 - } - }, - ); - if num_transparent > 0 { - let trns_data: Vec<_> = palette[0..num_transparent].iter().map(|px| px.a).collect(); + ColorType::RGB { + transparent_color: Some(trns), + } => { + // Transparency pixel - 6 byte RGB16 + let trns_data: Vec<_> = trns.iter().flat_map(|c| c.to_be_bytes()).collect(); write_png_block(b"tRNS", &trns_data, &mut output); } - } else if let Some(ref transparency_pixel) = self.raw.transparency_pixel { - // Transparency pixel - write_png_block(b"tRNS", transparency_pixel, &mut output); + _ => {} } - // Special ancillary headers that need to come after PLTE but before IDAT - for (key, header) in self - .raw - .aux_headers + // Special ancillary chunks that need to come after PLTE but before IDAT + for chunk in self + .aux_chunks .iter() - .filter(|&(key, _)| key == b"bKGD" || key == b"hIST" || key == b"tRNS") + .filter(|c| &c.name == b"bKGD" || &c.name == b"hIST" || &c.name == b"tRNS") { - write_png_block(key, header, &mut output); + write_png_block(&chunk.name, &chunk.data, &mut output); } // IDAT data write_png_block(b"IDAT", &self.idat_data, &mut output); @@ -274,8 +241,36 @@ impl PngImage { /// Return the number of channels in the image, based on color type #[inline] - pub fn channels_per_pixel(&self) -> u8 { - self.ihdr.color_type.channels_per_pixel() + pub fn channels_per_pixel(&self) -> usize { + self.ihdr.color_type.channels_per_pixel() as usize + } + + /// Return the number of bytes per channel in the image + #[inline] + pub fn bytes_per_channel(&self) -> usize { + match self.ihdr.bit_depth { + BitDepth::Sixteen => 2, + // Depths lower than 8 will round up to 1 byte + _ => 1, + } + } + + /// Calculate the size of the PLTE and tRNS chunks + pub fn key_chunks_size(&self) -> usize { + match &self.ihdr.color_type { + ColorType::Indexed { palette } => { + let plte = 12 + palette.len() * 3; + let trns = palette.iter().filter(|p| p.a != 255).count(); + if trns != 0 { + plte + 12 + trns + } else { + plte + } + } + ColorType::Grayscale { transparent_shade } if transparent_shade.is_some() => 12 + 2, + ColorType::RGB { transparent_color } if transparent_color.is_some() => 12 + 6, + _ => 0, + } } /// Return an iterator over the scanlines of the image @@ -287,7 +282,7 @@ impl PngImage { /// Reverse all filters applied on the image, returning an unfiltered IDAT bytestream fn unfilter_image(&self) -> Result, PngError> { let mut unfiltered = Vec::with_capacity(self.data.len()); - let bpp = ((self.ihdr.bit_depth.as_u8() * self.channels_per_pixel() + 7) / 8) as usize; + let bpp = self.bytes_per_channel() * self.channels_per_pixel(); let mut last_line: Vec = Vec::new(); let mut last_pass = None; let mut unfiltered_buf = Vec::new(); @@ -309,13 +304,12 @@ impl PngImage { /// Apply the specified filter type to all rows in the image pub fn filter_image(&self, filter: RowFilter, optimize_alpha: bool) -> Vec { let mut filtered = Vec::with_capacity(self.data.len()); - let bpp = ((self.ihdr.bit_depth.as_u8() * self.channels_per_pixel() + 7) / 8) as usize; + let bpp = self.bytes_per_channel() * self.channels_per_pixel(); // If alpha optimization is enabled, determine how many bytes of alpha there are per pixel - let alpha_bytes = match self.ihdr.color_type { - ColorType::RGBA | ColorType::GrayscaleAlpha if optimize_alpha => { - (self.ihdr.bit_depth.as_u8() / 8) as usize - } - _ => 0, + let alpha_bytes = if optimize_alpha && self.ihdr.color_type.has_alpha() { + self.bytes_per_channel() + } else { + 0 }; let mut prev_line = Vec::new(); @@ -465,14 +459,15 @@ impl PngImage { filtered } } -fn write_png_block(key: &[u8], header: &[u8], output: &mut Vec) { - let mut header_data = Vec::with_capacity(header.len() + 4); - header_data.extend_from_slice(key); - header_data.extend_from_slice(header); - output.reserve(header_data.len() + 8); - output.extend_from_slice(&(header_data.len() as u32 - 4).to_be_bytes()); - let crc = deflate::crc32(&header_data); - output.append(&mut header_data); + +fn write_png_block(key: &[u8], chunk: &[u8], output: &mut Vec) { + let mut chunk_data = Vec::with_capacity(chunk.len() + 4); + chunk_data.extend_from_slice(key); + chunk_data.extend_from_slice(chunk); + output.reserve(chunk_data.len() + 8); + output.extend_from_slice(&(chunk_data.len() as u32 - 4).to_be_bytes()); + let crc = deflate::crc32(&chunk_data); + output.append(&mut chunk_data); output.extend_from_slice(&crc.to_be_bytes()); } diff --git a/src/png/scan_lines.rs b/src/png/scan_lines.rs index 509373b2..1c612f89 100644 --- a/src/png/scan_lines.rs +++ b/src/png/scan_lines.rs @@ -43,7 +43,7 @@ impl<'a> Iterator for ScanLines<'a> { struct ScanLineRanges { /// Current pass number, and 0-indexed row within the pass pass: Option<(u8, u32)>, - bits_per_pixel: u8, + bits_per_pixel: usize, width: u32, height: u32, left: usize, @@ -53,7 +53,7 @@ struct ScanLineRanges { impl ScanLineRanges { pub fn new(png: &PngImage, has_filter: bool) -> Self { Self { - bits_per_pixel: png.ihdr.bit_depth.as_u8() * png.channels_per_pixel(), + bits_per_pixel: png.ihdr.bpp(), width: png.ihdr.width, height: png.ihdr.height, left: png.data.len(), @@ -143,8 +143,8 @@ impl Iterator for ScanLineRanges { // Standard, non-interlaced PNG scanlines (self.width, None) }; - let bits_per_line = pixels_per_line * u32::from(self.bits_per_pixel); - let mut len = ((bits_per_line + 7) / 8) as usize; + let bits_per_line = pixels_per_line as usize * self.bits_per_pixel; + let mut len = (bits_per_line + 7) / 8; if self.has_filter { len += 1; } diff --git a/src/rayon.rs b/src/rayon.rs index 768db04c..ae1914c2 100644 --- a/src/rayon.rs +++ b/src/rayon.rs @@ -52,6 +52,7 @@ where impl ParallelIterator for I {} +#[allow(dead_code)] pub fn join(a: impl FnOnce() -> A, b: impl FnOnce() -> B) -> (A, B) { (a(), b()) } diff --git a/src/reduction/alpha.rs b/src/reduction/alpha.rs index cf8c22ad..3045ddf6 100644 --- a/src/reduction/alpha.rs +++ b/src/reduction/alpha.rs @@ -1,23 +1,21 @@ +use rgb::RGB16; + use crate::colors::{BitDepth, ColorType}; use crate::headers::IhdrData; use crate::png::PngImage; /// Clean the alpha channel by setting the color of all fully transparent pixels to black pub fn cleaned_alpha_channel(png: &PngImage) -> Option { - let (bpc, bpp) = match png.ihdr.color_type { - ColorType::RGBA | ColorType::GrayscaleAlpha => { - let cpp = png.channels_per_pixel(); - let bpc = png.ihdr.bit_depth.as_u8() / 8; - (bpc as usize, (bpc * cpp) as usize) - } - _ => { - return None; - } - }; + if !png.ihdr.color_type.has_alpha() { + return None; + } + let byte_depth = png.bytes_per_channel(); + let bpp = png.channels_per_pixel() * byte_depth; + let colored_bytes = bpp - byte_depth; let mut reduced = Vec::with_capacity(png.data.len()); for pixel in png.data.chunks(bpp) { - if pixel.iter().skip(bpp - bpc).all(|b| *b == 0) { + if pixel.iter().skip(colored_bytes).all(|b| *b == 0) { reduced.resize(reduced.len() + bpp, 0); } else { reduced.extend_from_slice(pixel); @@ -26,23 +24,17 @@ pub fn cleaned_alpha_channel(png: &PngImage) -> Option { Some(PngImage { data: reduced, - ihdr: png.ihdr, - palette: png.palette.clone(), - transparency_pixel: png.transparency_pixel.clone(), - aux_headers: png.aux_headers.clone(), + ihdr: png.ihdr.clone(), }) } #[must_use] pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option { - let target_color_type = match png.ihdr.color_type { - ColorType::GrayscaleAlpha => ColorType::Grayscale, - ColorType::RGBA => ColorType::RGB, - _ => return None, - }; - let byte_depth = (png.ihdr.bit_depth.as_u8() >> 3) as usize; - let channels = png.channels_per_pixel() as usize; - let bpp = channels * byte_depth; + if !png.ihdr.color_type.has_alpha() { + return None; + } + let byte_depth = png.bytes_per_channel(); + let bpp = png.channels_per_pixel() * byte_depth; let colored_bytes = bpp - byte_depth; // If alpha optimisation is enabled, see if the image contains only fully opaque and fully transparent pixels. @@ -66,13 +58,7 @@ pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option vec![unused_color; colored_bytes], - // 8-bit is still stored as 16-bit, with the high byte set to 0 - _ => [0, unused_color].repeat(colored_bytes), - }) + Some(used_colors.iter().position(|b| !*b)? as u8) } else { None }; @@ -80,20 +66,26 @@ pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option { - raw_data.resize(raw_data.len() + colored_bytes, trns[1]); + Some(trns) if pixel.iter().skip(colored_bytes).all(|b| *b == 0) => { + raw_data.resize(raw_data.len() + colored_bytes, trns); } _ => raw_data.extend_from_slice(&pixel[0..colored_bytes]), }; } - let mut aux_headers = png.aux_headers.clone(); - // sBIT contains information about alpha channel's original depth, - // and alpha has just been removed - if let Some(sbit_header) = png.aux_headers.get(b"sBIT") { - // Some programs save the sBIT header as RGB even if the image is RGBA. - aux_headers.insert(*b"sBIT", sbit_header.iter().cloned().take(3).collect()); - } + // Construct the color type with appropriate transparency data + let transparent = transparency_pixel.map(|trns| match png.ihdr.bit_depth { + BitDepth::Sixteen => (trns as u16) << 8 | trns as u16, + _ => trns as u16, + }); + let target_color_type = match png.ihdr.color_type { + ColorType::GrayscaleAlpha => ColorType::Grayscale { + transparent_shade: transparent, + }, + _ => ColorType::RGB { + transparent_color: transparent.map(|t| RGB16::new(t, t, t)), + }, + }; Some(PngImage { data: raw_data, @@ -101,8 +93,5 @@ pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option Option { +pub fn reduced_bit_depth_16_to_8(png: &PngImage, force_scale: bool) -> Option { if png.ihdr.bit_depth != BitDepth::Sixteen { - if png.ihdr.color_type == ColorType::Indexed || png.ihdr.color_type == ColorType::Grayscale - { - return reduce_bit_depth_8_or_less(png, minimum_bits); - } return None; } + if force_scale { + return scaled_bit_depth_16_to_8(png); + } + // Reduce from 16 to 8 bits per channel per pixel if png.data.chunks(2).any(|pair| pair[0] != pair[1]) { // Can't reduce @@ -23,56 +22,72 @@ pub fn reduce_bit_depth(png: &PngImage, minimum_bits: usize) -> Option Some(PngImage { data: png.data.iter().step_by(2).cloned().collect(), ihdr: IhdrData { + color_type: png.ihdr.color_type.clone(), bit_depth: BitDepth::Eight, ..png.ihdr }, - palette: None, - transparency_pixel: png.transparency_pixel.clone(), - aux_headers: png.aux_headers.clone(), }) } +/// Forcibly reduce a 16-bit image to 8-bit by scaling, returning the reduced image if successful #[must_use] -pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Option { +pub fn scaled_bit_depth_16_to_8(png: &PngImage) -> Option { + if png.ihdr.bit_depth != BitDepth::Sixteen { + return None; + } + + // Reduce from 16 to 8 bits per channel per pixel by scaling when necessary + let data = png + .data + .chunks(2) + .map(|pair| { + if pair[0] == pair[1] { + return pair[0]; + } + // See: http://www.libpng.org/pub/png/spec/1.2/PNG-Decoders.html#D.Sample-depth-rescaling + // This allows values such as 0x00FF to be rounded to 0x01 rather than truncated to 0x00 + let val = u16::from_be_bytes([pair[0], pair[1]]) as f64; + (val * 255.0 / 65535.0).round() as u8 + }) + .collect(); + + Some(PngImage { + data, + ihdr: IhdrData { + color_type: png.ihdr.color_type.clone(), + bit_depth: BitDepth::Eight, + ..png.ihdr + }, + }) +} + +/// Attempt to reduce an 8/4/2-bit image to a lower bit depth, returning the reduced image if successful +#[must_use] +pub fn reduced_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Option { assert!((1..8).contains(&minimum_bits)); - let bit_depth: usize = png.ihdr.bit_depth.as_u8() as usize; - if minimum_bits >= bit_depth || bit_depth > 8 { + let bit_depth = png.ihdr.bit_depth as usize; + if minimum_bits >= bit_depth || bit_depth > 8 || png.channels_per_pixel() != 1 { return None; } // Calculate the current number of pixels per byte let ppb = 8 / bit_depth; - if png.ihdr.color_type == ColorType::Indexed { - for line in png.scan_lines(false) { - let line_max = line - .data - .iter() - .map(|&byte| match png.ihdr.bit_depth { - BitDepth::Two => (byte & 0x3) - .max((byte >> 2) & 0x3) - .max((byte >> 4) & 0x3) - .max(byte >> 6), - BitDepth::Four => (byte & 0xF).max(byte >> 4), - _ => byte, - }) - .max() - .unwrap_or(0); - let required_bits = match line_max { - x if x > 0x0F => 8, - x if x > 0x03 => 4, - x if x > 0x01 => 2, - _ => 1, - }; - if required_bits > minimum_bits { - minimum_bits = required_bits; - if minimum_bits >= bit_depth { - // Not reducable - return None; - } - } + if let ColorType::Indexed { palette } = &png.ihdr.color_type { + // We can easily determine minimum depth by the palette size + let required_bits = match palette.len() { + 0..=2 => 1, + 3..=4 => 2, + 5..=16 => 4, + _ => 8, + }; + if required_bits >= bit_depth { + // Not reducable + return None; + } else if required_bits > minimum_bits { + minimum_bits = required_bits; } } else { - // Checking for grayscale depth reduction is quite different than for indexed + // Finding minimum depth for grayscale is much more complicated let mut mask = (1 << minimum_bits) - 1; let mut divisions = 1..(bit_depth / minimum_bits); for &b in &png.data { @@ -129,12 +144,11 @@ pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Op } // If the image is grayscale we also need to reduce the transparency pixel - let mut transparency_pixel = png - .transparency_pixel - .clone() - .filter(|t| png.ihdr.color_type == ColorType::Grayscale && t.len() >= 2); - if let Some(trans) = transparency_pixel { - let reduced_trans = trans[1] >> (bit_depth - minimum_bits); + let color_type = if let ColorType::Grayscale { + transparent_shade: Some(trans), + } = png.ihdr.color_type + { + let reduced_trans = (trans & 0xFF) >> (bit_depth - minimum_bits); // Verify the reduction is valid by restoring back to original bit depth let mut check = reduced_trans; let mut bits = minimum_bits; @@ -142,22 +156,24 @@ pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Op check = check << bits | check; bits <<= 1; } - if trans[0] == 0 && trans[1] == check { - transparency_pixel = Some(vec![0, reduced_trans]); - } else { - // The transparency doesn't fit the new bit depth and is therefore unused - set it to None - transparency_pixel = None; + // If the transparency doesn't fit the new bit depth it is therefore unused - set it to None + ColorType::Grayscale { + transparent_shade: if trans == check { + Some(reduced_trans) + } else { + None + }, } - } + } else { + png.ihdr.color_type.clone() + }; Some(PngImage { data: reduced, ihdr: IhdrData { - bit_depth: BitDepth::from_u8(minimum_bits as u8), + color_type, + bit_depth: (minimum_bits as u8).try_into().unwrap(), ..png.ihdr }, - aux_headers: png.aux_headers.clone(), - palette: png.palette.clone(), - transparency_pixel, }) } diff --git a/src/reduction/color.rs b/src/reduction/color.rs index 5e7135f8..457c4422 100644 --- a/src/reduction/color.rs +++ b/src/reduction/color.rs @@ -1,151 +1,104 @@ use crate::colors::{BitDepth, ColorType}; use crate::headers::IhdrData; use crate::png::PngImage; -use indexmap::IndexMap; -use rgb::{FromSlice, RGB8, RGBA, RGBA8}; +use indexmap::IndexSet; +use rgb::alt::Gray; +use rgb::{ComponentMap, ComponentSlice, FromSlice, RGB, RGBA}; use rustc_hash::FxHasher; use std::hash::{BuildHasherDefault, Hash}; -type FxIndexMap = IndexMap>; +type FxIndexSet = IndexSet>; -fn reduce_scanline_to_palette( +/// Maximum size difference between indexed and channels to consider a candidate for evaluation +pub const INDEXED_MAX_DIFF: usize = 20000; + +fn build_palette( iter: impl IntoIterator, - palette: &mut FxIndexMap, reduced: &mut Vec, -) -> bool +) -> Option> where T: Eq + Hash, { + let mut palette = FxIndexSet::default(); + palette.reserve(257); for pixel in iter { - let idx = if let Some(&idx) = palette.get(&pixel) { - idx - } else { - let len = palette.len(); - if len == 256 { - return false; - } - let idx = len as u8; - palette.insert(pixel, idx); - idx - }; - reduced.push(idx); + let (idx, _) = palette.insert_full(pixel); + if idx == 256 { + return None; + } + reduced.push(idx as u8); } - true + Some(palette) } #[must_use] -pub fn reduce_to_palette(png: &PngImage) -> Option { +pub fn reduced_to_indexed(png: &PngImage) -> Option { if png.ihdr.bit_depth != BitDepth::Eight { return None; } - let mut raw_data = Vec::with_capacity(png.data.len()); - let mut palette = FxIndexMap::default(); - palette.reserve(257); - let transparency_pixel = png - .transparency_pixel - .as_ref() - .filter(|t| png.ihdr.color_type == ColorType::RGB && t.len() >= 6) - .map(|t| RGB8::new(t[1], t[3], t[5])); - let ok = if png.ihdr.color_type == ColorType::RGB { - reduce_scanline_to_palette( - png.data.as_rgb().iter().cloned().map(|px| { - px.alpha(if Some(px) != transparency_pixel { - 255 - } else { - 0 + if matches!(png.ihdr.color_type, ColorType::Indexed { .. }) { + return None; + } + + let mut raw_data = Vec::with_capacity(png.data.len() / png.channels_per_pixel()); + let palette: Vec<_> = match png.ihdr.color_type { + ColorType::Grayscale { transparent_shade } => { + let pmap = build_palette(png.data.as_gray().iter().cloned(), &mut raw_data)?; + // Convert the Gray16 transparency to Gray8 + let transparency_pixel = transparent_shade.map(|t| Gray::from(t as u8)); + pmap.into_iter() + .map(|px| { + RGB::from(px).alpha(if Some(px) != transparency_pixel { + 255 + } else { + 0 + }) }) - }), - &mut palette, - &mut raw_data, - ) - } else if png.ihdr.color_type == ColorType::GrayscaleAlpha { - reduce_scanline_to_palette( - png.data.as_gray_alpha().iter().cloned().map(|px| RGBA { - r: px.0, - g: px.0, - b: px.0, - a: px.1, - }), - &mut palette, - &mut raw_data, - ) - } else { - debug_assert_eq!(png.ihdr.color_type, ColorType::RGBA); - reduce_scanline_to_palette( - png.data.as_rgba().iter().cloned(), - &mut palette, - &mut raw_data, - ) - }; - if !ok { - return None; - } - - let num_transparent = palette - .iter() - .filter_map(|(px, &idx)| { - if px.a != 255 { - Some(idx as usize + 1) - } else { - None - } - }) - .max(); - let trns_size = num_transparent.map_or(0, |n| n + 8); - - let headers_size = palette.len() * 3 + 8 + trns_size; - if raw_data.len() + headers_size > png.data.len() { - // Reduction would result in a larger image - return None; - } - - let mut aux_headers = png.aux_headers.clone(); - if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") { - if bkgd_header.len() != 6 { - // malformed chunk? - return None; + .collect() } - // In bKGD 16-bit values are used even for 8-bit images - let bg = RGBA8::new(bkgd_header[1], bkgd_header[3], bkgd_header[5], 255); - let entry = if let Some(&entry) = palette.get(&bg) { - entry - } else if palette.len() < 256 { - let entry = palette.len() as u8; - palette.insert(bg, entry); - entry - } else { - return None; // No space in palette to store the bg as an index - }; - aux_headers.insert(*b"bKGD", vec![entry]); - } - - if let Some(sbit_header) = png.aux_headers.get(b"sBIT") { - // Some programs save the sBIT header as RGB even if the image is RGBA. - aux_headers.insert(*b"sBIT", sbit_header.iter().cloned().take(3).collect()); - } - - let mut palette_vec = vec![RGBA8::new(0, 0, 0, 0); palette.len()]; - for (color, idx) in palette { - palette_vec[idx as usize] = color; - } + ColorType::RGB { transparent_color } => { + let pmap = build_palette(png.data.as_rgb().iter().cloned(), &mut raw_data)?; + // Convert the RGB16 transparency to RGB8 + let transparency_pixel = transparent_color.map(|t| t.map(|c| c as u8)); + pmap.into_iter() + .map(|px| { + px.alpha(if Some(px) != transparency_pixel { + 255 + } else { + 0 + }) + }) + .collect() + } + ColorType::GrayscaleAlpha => { + let pmap = build_palette(png.data.as_gray_alpha().iter().cloned(), &mut raw_data)?; + pmap.into_iter().map(RGBA::from).collect() + } + ColorType::RGBA => { + let pmap = build_palette(png.data.as_rgba().iter().cloned(), &mut raw_data)?; + pmap.into_iter().collect() + } + _ => return None, + }; Some(PngImage { data: raw_data, ihdr: IhdrData { - color_type: ColorType::Indexed, + color_type: ColorType::Indexed { palette }, ..png.ihdr }, - aux_headers, - transparency_pixel: None, - palette: Some(palette_vec), }) } #[must_use] -pub fn reduce_rgb_to_grayscale(png: &PngImage) -> Option { +pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option { + if !png.ihdr.color_type.is_rgb() { + return None; + } + let mut reduced = Vec::with_capacity(png.data.len()); - let byte_depth = png.ihdr.bit_depth.as_u8() as usize >> 3; - let bpp = png.channels_per_pixel() as usize * byte_depth; + let byte_depth = png.bytes_per_channel(); + let bpp = png.channels_per_pixel() * byte_depth; let last_color = 2 * byte_depth; for pixel in png.data.chunks(bpp) { if byte_depth == 1 { @@ -158,39 +111,75 @@ pub fn reduce_rgb_to_grayscale(png: &PngImage) -> Option { reduced.extend_from_slice(&pixel[last_color..]); } - let transparency_pixel = if let Some(ref trns) = png.transparency_pixel { - if trns.len() != 6 || trns[0..2] != trns[2..4] || trns[2..4] != trns[4..6] { - None - } else { - Some(trns[0..2].to_owned()) - } - } else { - png.transparency_pixel.clone() + let color_type = match png.ihdr.color_type { + ColorType::RGB { transparent_color } => ColorType::Grayscale { + // Copy the transparent component if it is also gray + transparent_shade: transparent_color + .filter(|t| t.r == t.g && t.g == t.b) + .map(|t| t.r), + }, + _ => ColorType::GrayscaleAlpha, }; - let mut aux_headers = png.aux_headers.clone(); - if let Some(sbit_header) = png.aux_headers.get(b"sBIT") { - if let Some(&byte) = sbit_header.first() { - aux_headers.insert(*b"sBIT", vec![byte]); - } - } - if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") { - if let Some(b) = bkgd_header.get(0..2) { - aux_headers.insert(*b"bKGD", b.to_owned()); - } - } - Some(PngImage { data: reduced, ihdr: IhdrData { - color_type: match png.ihdr.color_type { - ColorType::RGBA => ColorType::GrayscaleAlpha, - _ => ColorType::Grayscale, - }, + color_type, ..png.ihdr }, - aux_headers, - palette: None, - transparency_pixel, + }) +} + +/// Attempt to convert indexed to a different color type, returning the resulting image if successful +#[must_use] +pub fn indexed_to_channels(png: &PngImage, allow_grayscale: bool) -> Option { + if png.ihdr.bit_depth != BitDepth::Eight { + return None; + } + let palette = match &png.ihdr.color_type { + ColorType::Indexed { palette } => palette, + _ => return None, + }; + + // Determine which channels are required + let is_gray = if allow_grayscale { + palette.iter().all(|c| c.r == c.g && c.g == c.b) + } else { + false + }; + let has_alpha = palette.iter().any(|c| c.a != 255); + let color_type = match (is_gray, has_alpha) { + (false, true) => ColorType::RGBA, + (false, false) => ColorType::RGB { + transparent_color: None, + }, + (true, true) => ColorType::GrayscaleAlpha, + (true, false) => ColorType::Grayscale { + transparent_shade: None, + }, + }; + + // Don't proceed if output would be too much larger + let out_size = color_type.channels_per_pixel() as usize * png.data.len(); + if out_size - png.data.len() > INDEXED_MAX_DIFF { + return None; + } + + // Construct the new data + let black = RGBA::new(0, 0, 0, 255); + let ch_start = if is_gray { 2 } else { 0 }; + let ch_end = if has_alpha { 3 } else { 2 }; + let mut data = Vec::with_capacity(out_size); + for b in &png.data { + let color = palette.get(*b as usize).unwrap_or(&black); + data.extend_from_slice(&color.as_slice()[ch_start..=ch_end]); + } + + Some(PngImage { + ihdr: IhdrData { + color_type, + ..png.ihdr + }, + data, }) } diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index 9bedcf62..e07f57db 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -1,251 +1,143 @@ -use crate::colors::{BitDepth, ColorType}; -use crate::headers::IhdrData; +use crate::evaluate::Evaluator; use crate::png::PngImage; -use indexmap::map::{Entry::*, IndexMap}; -use rgb::RGBA8; -use std::borrow::Cow; +use crate::Deadline; +use crate::Options; +use std::sync::Arc; pub mod alpha; -use crate::alpha::reduced_alpha_channel; +use crate::alpha::*; pub mod bit_depth; -use crate::bit_depth::reduce_bit_depth_8_or_less; +use crate::bit_depth::*; pub mod color; use crate::color::*; +pub mod palette; +use crate::palette::*; -pub(crate) use crate::alpha::cleaned_alpha_channel; -pub(crate) use crate::bit_depth::reduce_bit_depth; +pub(crate) fn perform_reductions( + mut png: Arc, + opts: &Options, + deadline: &Deadline, + eval: &Evaluator, +) -> (Arc, bool) { + let mut reduction_occurred = false; + let mut evaluation_added = false; -/// Attempt to reduce the number of colors in the palette -/// Returns `None` if palette hasn't changed -pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option { - if png.ihdr.color_type != ColorType::Indexed { - // Can't reduce if there is no palette - return None; - } - if png.ihdr.bit_depth == BitDepth::One { - // Gains from 1-bit images will be at most 1 byte - // Not worth the CPU time - return None; + // Interlacing must be processed first in order to evaluate the rest correctly + if let Some(interlacing) = opts.interlace { + if let Some(reduced) = png.change_interlacing(interlacing) { + png = Arc::new(reduced); + reduction_occurred = true; + } } - let mut palette_map = [None; 256]; - let mut used = [false; 256]; - { - let palette = png.palette.as_ref()?; - - // Find palette entries that are never used - match png.ihdr.bit_depth { - BitDepth::Eight => { - for &byte in &png.data { - used[byte as usize] = true; - } - } - BitDepth::Four => { - for &byte in &png.data { - used[(byte & 0x0F) as usize] = true; - used[(byte >> 4) as usize] = true; - } - } - BitDepth::Two => { - for &byte in &png.data { - used[(byte & 0x03) as usize] = true; - used[((byte >> 2) & 0x03) as usize] = true; - used[((byte >> 4) & 0x03) as usize] = true; - used[(byte >> 6) as usize] = true; - } - } - _ => unreachable!(), + // If alpha optimization is enabled, clean the alpha channel before continuing + // This can allow some color type reductions which may not have been possible otherwise + if opts.optimize_alpha && !deadline.passed() { + if let Some(reduced) = cleaned_alpha_channel(&png) { + png = Arc::new(reduced); + // This does not count as a reduction } + } - let mut used_enumerated: Vec<(usize, &bool)> = used.iter().enumerate().collect(); - used_enumerated.sort_by(|a, b| { - //Sort by ascending alpha and descending luma. - let color_val = |i| { - let color = palette - .get(i) - .copied() - .unwrap_or_else(|| RGBA8::new(0, 0, 0, 255)); - ((color.a as i32) << 18) - // These are coefficients for standard sRGB to luma conversion - - i32::from(color.r) * 299 - - i32::from(color.g) * 587 - - i32::from(color.b) * 114 - }; - color_val(a.0).cmp(&color_val(b.0)) - }); - - // Make sure the background is also included, but only after sorting since it may not be used in idat - if let Some(&idx) = png.aux_headers.get(b"bKGD").and_then(|b| b.first()) { - if !used[idx as usize] { - used_enumerated.push((idx as usize, &true)); - } + // Attempt to reduce 16-bit to 8-bit + // This is just removal of bytes and does not need to be evaluated + if opts.bit_depth_reduction && !deadline.passed() { + if let Some(reduced) = reduced_bit_depth_16_to_8(&png, opts.scale_16) { + png = Arc::new(reduced); + reduction_occurred = true; } + } - let mut next_index = 0_u16; - let mut seen = IndexMap::with_capacity(palette.len()); - for (i, used) in used_enumerated.iter().cloned() { - if !used { - continue; - } - // There are invalid files that use pixel indices beyond palette size - let mut color = palette - .get(i) - .cloned() - .unwrap_or_else(|| RGBA8::new(0, 0, 0, 255)); - // If there are multiple fully transparent entries, reduce them into one - if optimize_alpha && color.a == 0 { - color.r = 0; - color.g = 0; - color.b = 0; - } - match seen.entry(color) { - Vacant(new) => { - palette_map[i] = Some(next_index as u8); - new.insert(next_index as u8); - next_index += 1; - } - Occupied(remap_to) => palette_map[i] = Some(*remap_to.get()), + // Attempt to reduce RGB to grayscale + // This is just removal of bytes and does not need to be evaluated + if opts.color_type_reduction && opts.grayscale_reduction && !deadline.passed() { + if let Some(reduced) = reduced_rgb_to_grayscale(&png) { + png = Arc::new(reduced); + reduction_occurred = true; + } + } + + // Attempt to reduce the palette + // This may change bytes but should always be beneficial + if opts.palette_reduction && !deadline.passed() { + if let Some(reduced) = reduced_palette(&png, opts.optimize_alpha) { + png = Arc::new(reduced); + reduction_occurred = true; + } + } + + // Now retain the current png for the evaluator baseline + // It will only be entered into the evaluator if there are also others to evaluate + let mut baseline = png.clone(); + + // Attempt alpha removal + if opts.color_type_reduction && !deadline.passed() { + if let Some(reduced) = reduced_alpha_channel(&png, opts.optimize_alpha) { + png = Arc::new(reduced); + // For small differences, if a tRNS chunk is required then enter this into the evaluator + // Otherwise it is mostly just removal of bytes and should become the baseline + if png.ihdr.color_type.has_trns() && baseline.data.len() - png.data.len() <= 1000 { + eval.try_image(png.clone()); + evaluation_added = true; + } else { + baseline = png.clone(); + reduction_occurred = true; } } } - do_palette_reduction(png, &palette_map) -} - -#[must_use] -fn do_palette_reduction(png: &PngImage, palette_map: &[Option; 256]) -> Option { - let byte_map = palette_map_to_byte_map(png, palette_map)?; - - // Reassign data bytes to new indices - let raw_data = png.data.iter().map(|b| byte_map[*b as usize]).collect(); - - let mut aux_headers = png.aux_headers.clone(); - if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") { - if let Some(Some(map_to)) = bkgd_header - .first() - .and_then(|&idx| palette_map.get(idx as usize)) - { - aux_headers.insert(*b"bKGD", vec![*map_to]); - } - } - - Some(PngImage { - ihdr: IhdrData { - color_type: ColorType::Indexed, - ..png.ihdr - }, - data: raw_data, - transparency_pixel: None, - palette: Some(reordered_palette(png.palette.as_ref()?, palette_map)), - aux_headers, - }) -} - -fn palette_map_to_byte_map(png: &PngImage, palette_map: &[Option; 256]) -> Option<[u8; 256]> { - if (0..256).all(|i| palette_map[i].map_or(true, |to| to == i as u8)) { - // No reduction necessary - return None; - } - - let mut byte_map = [0_u8; 256]; - - // low bit-depths can be pre-computed for every byte value - match png.ihdr.bit_depth { - BitDepth::Eight => { - for byte in 0..=255usize { - byte_map[byte] = palette_map[byte].unwrap_or(0) - } - } - BitDepth::Four => { - for byte in 0..=255usize { - byte_map[byte] = palette_map[(byte & 0x0F)].unwrap_or(0) - | (palette_map[(byte >> 4)].unwrap_or(0) << 4); - } - } - BitDepth::Two => { - for byte in 0..=255usize { - byte_map[byte] = palette_map[(byte & 0x03)].unwrap_or(0) - | (palette_map[((byte >> 2) & 0x03)].unwrap_or(0) << 2) - | (palette_map[((byte >> 4) & 0x03)].unwrap_or(0) << 4) - | (palette_map[(byte >> 6)].unwrap_or(0) << 6); - } - } - _ => {} - } - - Some(byte_map) -} - -fn reordered_palette(palette: &[RGBA8], palette_map: &[Option; 256]) -> Vec { - let max_index = palette_map.iter().cloned().flatten().max().unwrap_or(0) as usize; - let mut new_palette = vec![RGBA8::new(0, 0, 0, 255); max_index + 1]; - for (&color, &map_to) in palette.iter().zip(palette_map.iter()) { - if let Some(map_to) = map_to { - new_palette[map_to as usize] = color; - } - } - new_palette -} - -/// Attempt to reduce the color type of the image -/// Returns true if the color type was reduced, false otherwise -pub fn reduce_color_type( - png: &PngImage, - grayscale_reduction: bool, - optimize_alpha: bool, -) -> Option { - let mut should_reduce_bit_depth = false; - let mut reduced = Cow::Borrowed(png); - - // Go down one step at a time - // Maybe not the most efficient, but it's safe - if grayscale_reduction && matches!(reduced.ihdr.color_type, ColorType::RGBA | ColorType::RGB) { - if let Some(r) = reduce_rgb_to_grayscale(&reduced) { - reduced = Cow::Owned(r); - should_reduce_bit_depth = reduced.ihdr.color_type == ColorType::Grayscale; - } - } - - // Attempt grayscale alpha reduction before palette, as grayscale will typically be smaller than indexed - if reduced.ihdr.color_type == ColorType::GrayscaleAlpha { - if let Some(r) = reduced_alpha_channel(&reduced, optimize_alpha) { - reduced = Cow::Owned(r); - should_reduce_bit_depth = true; - } - } - - if matches!( - reduced.ihdr.color_type, - ColorType::RGBA | ColorType::RGB | ColorType::GrayscaleAlpha - ) { - if let Some(r) = reduce_to_palette(&reduced) { - reduced = Cow::Owned(r); - should_reduce_bit_depth = true; - - // Make sure that palette gets sorted. Ideally, this should be done within reduce_to_palette. - if let Some(r) = reduced_palette(&reduced, optimize_alpha) { - reduced = Cow::Owned(r); - } - } - } - - // Attempt RGBA alpha reduction after palette, so it can be skipped if palette was successful - if reduced.ihdr.color_type == ColorType::RGBA { - if let Some(r) = reduced_alpha_channel(&reduced, optimize_alpha) { - reduced = Cow::Owned(r); - } - } - - if should_reduce_bit_depth { - // Some conversions will allow us to perform bit depth reduction that - // wasn't possible before - if let Some(r) = reduce_bit_depth_8_or_less(&reduced, 1) { - reduced = Cow::Owned(r); - } - } - - match reduced { - Cow::Owned(r) => Some(r), - _ => None, - } + // Attempt to sort the palette + if opts.palette_reduction && !deadline.passed() { + if let Some(reduced) = sorted_palette(&png) { + png = Arc::new(reduced); + eval.try_image(png.clone()); + evaluation_added = true; + } + } + + // Attempt to convert from indexed to channels + // This may give a better result due to dropping the PLTE chunk + if opts.color_type_reduction && !deadline.passed() { + if let Some(reduced) = indexed_to_channels(&png, opts.grayscale_reduction) { + // This result should not be passed on to subsequent reductions + eval.try_image(Arc::new(reduced)); + evaluation_added = true; + } + } + + // Attempt to reduce to indexed + let mut indexed = None; + if opts.color_type_reduction && !deadline.passed() { + if let Some(reduced) = reduced_to_indexed(&png) { + // Make sure the palette gets sorted (but don't bother evaluating both results) + let new = Arc::new(sorted_palette(&reduced).unwrap_or(reduced)); + // For relatively small differences, enter this into the evaluator + // Otherwise we're confident enough for it to become the baseline + if png.data.len() - new.data.len() <= INDEXED_MAX_DIFF { + eval.try_image(new.clone()); + evaluation_added = true; + } else { + baseline = new.clone(); + reduction_occurred = true; + } + indexed = Some(new); + } + } + + // Attempt to reduce to a lower bit depth + if opts.bit_depth_reduction && !deadline.passed() { + // Try reducing the previous png, falling back to the indexed one if it exists + // This allows a grayscale depth reduction to be preferred over an indexed depth reduction + let reduced = reduced_bit_depth_8_or_less(&png, 1) + .or_else(|| indexed.and_then(|png| reduced_bit_depth_8_or_less(&png, 1))); + if let Some(reduced) = reduced { + eval.try_image(Arc::new(reduced)); + evaluation_added = true; + } + } + + if evaluation_added { + eval.set_baseline(baseline.clone()); + } + (baseline, reduction_occurred) } diff --git a/src/reduction/palette.rs b/src/reduction/palette.rs new file mode 100644 index 00000000..8c610d03 --- /dev/null +++ b/src/reduction/palette.rs @@ -0,0 +1,172 @@ +use crate::colors::{BitDepth, ColorType}; +use crate::headers::IhdrData; +use crate::png::PngImage; +use indexmap::IndexSet; +use rgb::RGBA8; + +/// Attempt to reduce the number of colors in the palette, returning the reduced image if successful +#[must_use] +pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option { + let palette = match &png.ihdr.color_type { + ColorType::Indexed { palette } if palette.len() > 1 => palette, + _ => return None, + }; + + let used = get_used_entries(png); + + let black = RGBA8::new(0, 0, 0, 255); + let mut condensed = IndexSet::with_capacity(palette.len()); + let mut palette_map = [0; 256]; + let mut did_change = false; + for (i, used) in used.iter().enumerate() { + if !used { + continue; + } + // There are invalid files that use pixel indices beyond palette size + let color = *palette.get(i).unwrap_or(&black); + palette_map[i] = add_color_to_set(color, &mut condensed, optimize_alpha); + if palette_map[i] as usize != i { + did_change = true; + } + } + + let data = if did_change { + // Reassign data bytes to new indices + let byte_map = palette_map_to_byte_map(png.ihdr.bit_depth, &palette_map); + png.data.iter().map(|b| byte_map[*b as usize]).collect() + } else if condensed.len() < palette.len() { + // Data is unchanged but palette will be truncated + png.data.clone() + } else { + // Nothing has changed + return None; + }; + + let palette: Vec<_> = condensed.into_iter().collect(); + + Some(PngImage { + ihdr: IhdrData { + color_type: ColorType::Indexed { palette }, + ..png.ihdr + }, + data, + }) +} + +fn add_color_to_set(mut color: RGBA8, set: &mut IndexSet, optimize_alpha: bool) -> u8 { + // If there are multiple fully transparent entries, reduce them into one + if optimize_alpha && color.a == 0 { + color.r = 0; + color.g = 0; + color.b = 0; + } + let (idx, _) = set.insert_full(color); + idx as u8 +} + +fn get_used_entries(png: &PngImage) -> [bool; 256] { + let mut used = [false; 256]; + match png.ihdr.bit_depth { + BitDepth::Eight => { + for &byte in &png.data { + used[byte as usize] = true; + } + } + BitDepth::Four => { + for &byte in &png.data { + used[(byte & 0x0F) as usize] = true; + used[(byte >> 4) as usize] = true; + } + } + BitDepth::Two => { + for &byte in &png.data { + used[(byte & 0x03) as usize] = true; + used[((byte >> 2) & 0x03) as usize] = true; + used[((byte >> 4) & 0x03) as usize] = true; + used[(byte >> 6) as usize] = true; + } + } + BitDepth::One => { + // Only two options, don't bother checking which are actually used + used[0] = true; + used[1] = true; + } + _ => unreachable!(), + }; + used +} + +fn palette_map_to_byte_map(bit_depth: BitDepth, palette_map: &[u8; 256]) -> [u8; 256] { + // Low bit-depths can be pre-computed for every byte value + match bit_depth { + BitDepth::Eight => *palette_map, + BitDepth::Four => { + let mut byte_map = [0_u8; 256]; + for byte in 0..256 { + byte_map[byte] = palette_map[byte & 0x0F] | (palette_map[byte >> 4] << 4); + } + byte_map + } + BitDepth::Two => { + let mut byte_map = [0_u8; 256]; + for byte in 0..256 { + byte_map[byte] = palette_map[byte & 0x03] + | (palette_map[(byte >> 2) & 0x03] << 2) + | (palette_map[(byte >> 4) & 0x03] << 4) + | (palette_map[byte >> 6] << 6); + } + byte_map + } + _ => unreachable!(), + } +} + +/// Attempt to sort the colors in the palette, returning the sorted image if successful +#[must_use] +pub fn sorted_palette(png: &PngImage) -> Option { + if png.ihdr.bit_depth == BitDepth::One { + // Don't bother trying to sort a 1-bit image + return None; + } + let palette = match &png.ihdr.color_type { + ColorType::Indexed { palette } => palette, + _ => return None, + }; + + let mut enumerated: Vec<_> = palette.iter().enumerate().collect(); + + // Sort the palette + enumerated.sort_by(|a, b| { + // Sort by ascending alpha and descending luma + let color_val = |color: &RGBA8| { + ((color.a as i32) << 18) + // These are coefficients for standard sRGB to luma conversion + - i32::from(color.r) * 299 + - i32::from(color.g) * 587 + - i32::from(color.b) * 114 + }; + color_val(a.1).cmp(&color_val(b.1)) + }); + + // Extract the new palette and determine if anything changed + let (old_map, palette): (Vec<_>, Vec) = enumerated.into_iter().unzip(); + if old_map.iter().enumerate().all(|(a, b)| a == *b) { + return None; + } + + // Construct the palette and byte maps and convert the data + let mut new_map = [0; 256]; + for (i, &v) in old_map.iter().enumerate() { + new_map[v] = i as u8; + } + let byte_map = palette_map_to_byte_map(png.ihdr.bit_depth, &new_map); + let data = png.data.iter().map(|&b| byte_map[b as usize]).collect(); + + Some(PngImage { + ihdr: IhdrData { + color_type: ColorType::Indexed { palette }, + ..png.ihdr + }, + data, + }) +} diff --git a/src/sanity_checks.rs b/src/sanity_checks.rs new file mode 100644 index 00000000..496b5dfd --- /dev/null +++ b/src/sanity_checks.rs @@ -0,0 +1,47 @@ +use image::{DynamicImage, GenericImageView, ImageFormat, Pixel}; +use log::{error, warn}; +use std::io::Cursor; + +/// Validate that the output png data still matches the original image +pub fn validate_output(output: &[u8], original_data: &[u8]) -> bool { + let (old_png, new_png) = rayon::join( + || load_png_image_from_memory(original_data), + || load_png_image_from_memory(output), + ); + + match (new_png, old_png) { + (Err(new_err), _) => { + error!("Failed to read output image for validation: {}", new_err); + false + } + (_, Err(old_err)) => { + // The original image might be invalid if, for example, there is a CRC error, + // and we set fix_errors to true. In that case, all we can do is check that the + // new image is decodable. + warn!("Failed to read input image for validation: {}", old_err); + true + } + (Ok(new_png), Ok(old_png)) => images_equal(&old_png, &new_png), + } +} + +/// Loads a PNG image from memory to a [DynamicImage] +fn load_png_image_from_memory(png_data: &[u8]) -> Result { + let mut reader = image::io::Reader::new(Cursor::new(png_data)); + reader.set_format(ImageFormat::Png); + reader.no_limits(); + reader.decode() +} + +/// Compares images pixel by pixel for equivalent content +fn images_equal(old_png: &DynamicImage, new_png: &DynamicImage) -> bool { + let a = old_png.pixels().filter(|x| { + let p = x.2.channels(); + !(p.len() == 4 && p[3] == 0) + }); + let b = new_png.pixels().filter(|x| { + let p = x.2.channels(); + !(p.len() == 4 && p[3] == 0) + }); + a.eq(b) +} diff --git a/tests/files/grayscale_16_should_be_grayscale_1.png b/tests/files/grayscale_16_should_be_grayscale_1.png new file mode 100644 index 00000000..66fe12e3 Binary files /dev/null and b/tests/files/grayscale_16_should_be_grayscale_1.png differ diff --git a/tests/files/grayscale_8_should_be_grayscale_4.png b/tests/files/grayscale_8_should_be_grayscale_4.png index c561432e..e5e62cb1 100644 Binary files a/tests/files/grayscale_8_should_be_grayscale_4.png and b/tests/files/grayscale_8_should_be_grayscale_4.png differ diff --git a/tests/files/grayscale_8_should_be_palette_8.png b/tests/files/grayscale_8_should_be_palette_8.png new file mode 100644 index 00000000..ff94dfb8 Binary files /dev/null and b/tests/files/grayscale_8_should_be_palette_8.png differ diff --git a/tests/files/palette_8_should_be_rgb.png b/tests/files/palette_8_should_be_rgb.png new file mode 100644 index 00000000..cb6b1678 Binary files /dev/null and b/tests/files/palette_8_should_be_rgb.png differ diff --git a/tests/files/palette_8_should_be_rgba.png b/tests/files/palette_8_should_be_rgba.png new file mode 100644 index 00000000..351a7a1b Binary files /dev/null and b/tests/files/palette_8_should_be_rgba.png differ diff --git a/tests/files/raw_api.png b/tests/files/raw_api.png new file mode 100644 index 00000000..3f9bdb1a Binary files /dev/null and b/tests/files/raw_api.png differ diff --git a/tests/files/strip_headers_all.png b/tests/files/strip_headers_all.png index d714a05d..6742de54 100644 Binary files a/tests/files/strip_headers_all.png and b/tests/files/strip_headers_all.png differ diff --git a/tests/files/strip_headers_list.png b/tests/files/strip_headers_list.png index d714a05d..6742de54 100644 Binary files a/tests/files/strip_headers_list.png and b/tests/files/strip_headers_list.png differ diff --git a/tests/files/strip_headers_none.png b/tests/files/strip_headers_none.png index d714a05d..6742de54 100644 Binary files a/tests/files/strip_headers_none.png and b/tests/files/strip_headers_none.png differ diff --git a/tests/files/strip_headers_safe.png b/tests/files/strip_headers_safe.png index d714a05d..6742de54 100644 Binary files a/tests/files/strip_headers_safe.png and b/tests/files/strip_headers_safe.png differ diff --git a/tests/filters.rs b/tests/filters.rs index e1dbad40..17e79b81 100644 --- a/tests/filters.rs +++ b/tests/filters.rs @@ -1,10 +1,16 @@ use indexmap::IndexSet; -use oxipng::{internal_tests::*, RowFilter}; -use oxipng::{InFile, OutFile}; +use oxipng::internal_tests::*; +use oxipng::*; use std::fs::remove_file; use std::path::Path; use std::path::PathBuf; +const GRAYSCALE: u8 = 0; +const RGB: u8 = 2; +const INDEXED: u8 = 3; +const GRAYSCALE_ALPHA: u8 = 4; +const RGBA: u8 = 6; + fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { let mut options = oxipng::Options { force: true, @@ -23,18 +29,18 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { fn test_it_converts( input: &str, filter: RowFilter, - color_type_in: ColorType, + color_type_in: u8, bit_depth_in: BitDepth, - color_type_out: ColorType, + color_type_out: u8, bit_depth_out: BitDepth, ) { let input = PathBuf::from(input); let (output, mut opts) = get_opts(&input); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); opts.filter = IndexSet::new(); opts.filter.insert(filter); - assert_eq!(png.raw.ihdr.color_type, color_type_in); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); match oxipng::optimize(&InFile::Path(input), &output, &opts) { @@ -44,7 +50,7 @@ fn test_it_converts( let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -52,12 +58,10 @@ fn test_it_converts( } }; - assert_eq!(png.raw.ihdr.color_type, color_type_out); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_out); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out); - if let Some(palette) = png.raw.palette.as_ref() { - assert!(palette.len() <= 1 << (png.raw.ihdr.bit_depth.as_u8() as usize)); - } else { - assert_ne!(png.raw.ihdr.color_type, ColorType::Indexed); + if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type { + assert!(palette.len() <= 1 << (png.raw.ihdr.bit_depth as u8)); } remove_file(output).ok(); @@ -68,9 +72,9 @@ fn filter_0_for_rgba_16() { test_it_converts( "tests/files/filter_0_for_rgba_16.png", RowFilter::None, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, ); } @@ -80,9 +84,9 @@ fn filter_1_for_rgba_16() { test_it_converts( "tests/files/filter_1_for_rgba_16.png", RowFilter::Sub, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, ); } @@ -92,9 +96,9 @@ fn filter_2_for_rgba_16() { test_it_converts( "tests/files/filter_2_for_rgba_16.png", RowFilter::Up, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, ); } @@ -104,9 +108,9 @@ fn filter_3_for_rgba_16() { test_it_converts( "tests/files/filter_3_for_rgba_16.png", RowFilter::Average, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, ); } @@ -116,9 +120,9 @@ fn filter_4_for_rgba_16() { test_it_converts( "tests/files/filter_4_for_rgba_16.png", RowFilter::Paeth, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, ); } @@ -128,9 +132,9 @@ fn filter_5_for_rgba_16() { test_it_converts( "tests/files/filter_5_for_rgba_16.png", RowFilter::MinSum, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, ); } @@ -140,9 +144,9 @@ fn filter_0_for_rgba_8() { test_it_converts( "tests/files/filter_0_for_rgba_8.png", RowFilter::None, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGBA, + RGBA, BitDepth::Eight, ); } @@ -152,9 +156,9 @@ fn filter_1_for_rgba_8() { test_it_converts( "tests/files/filter_1_for_rgba_8.png", RowFilter::Sub, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGBA, + RGBA, BitDepth::Eight, ); } @@ -164,9 +168,9 @@ fn filter_2_for_rgba_8() { test_it_converts( "tests/files/filter_2_for_rgba_8.png", RowFilter::Up, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGBA, + RGBA, BitDepth::Eight, ); } @@ -176,9 +180,9 @@ fn filter_3_for_rgba_8() { test_it_converts( "tests/files/filter_3_for_rgba_8.png", RowFilter::Average, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGBA, + RGBA, BitDepth::Eight, ); } @@ -188,9 +192,9 @@ fn filter_4_for_rgba_8() { test_it_converts( "tests/files/filter_4_for_rgba_8.png", RowFilter::Paeth, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGBA, + RGBA, BitDepth::Eight, ); } @@ -200,9 +204,9 @@ fn filter_5_for_rgba_8() { test_it_converts( "tests/files/filter_5_for_rgba_8.png", RowFilter::MinSum, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGBA, + RGBA, BitDepth::Eight, ); } @@ -212,9 +216,9 @@ fn filter_0_for_rgb_16() { test_it_converts( "tests/files/filter_0_for_rgb_16.png", RowFilter::None, - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Sixteen, ); } @@ -224,9 +228,9 @@ fn filter_1_for_rgb_16() { test_it_converts( "tests/files/filter_1_for_rgb_16.png", RowFilter::Sub, - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Sixteen, ); } @@ -236,9 +240,9 @@ fn filter_2_for_rgb_16() { test_it_converts( "tests/files/filter_2_for_rgb_16.png", RowFilter::Up, - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Sixteen, ); } @@ -248,9 +252,9 @@ fn filter_3_for_rgb_16() { test_it_converts( "tests/files/filter_3_for_rgb_16.png", RowFilter::Average, - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Sixteen, ); } @@ -260,9 +264,9 @@ fn filter_4_for_rgb_16() { test_it_converts( "tests/files/filter_4_for_rgb_16.png", RowFilter::Paeth, - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Sixteen, ); } @@ -272,9 +276,9 @@ fn filter_5_for_rgb_16() { test_it_converts( "tests/files/filter_5_for_rgb_16.png", RowFilter::MinSum, - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Sixteen, ); } @@ -284,9 +288,9 @@ fn filter_0_for_rgb_8() { test_it_converts( "tests/files/filter_0_for_rgb_8.png", RowFilter::None, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -296,9 +300,9 @@ fn filter_1_for_rgb_8() { test_it_converts( "tests/files/filter_1_for_rgb_8.png", RowFilter::Sub, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -308,9 +312,9 @@ fn filter_2_for_rgb_8() { test_it_converts( "tests/files/filter_2_for_rgb_8.png", RowFilter::Up, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -320,9 +324,9 @@ fn filter_3_for_rgb_8() { test_it_converts( "tests/files/filter_3_for_rgb_8.png", RowFilter::Average, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -332,9 +336,9 @@ fn filter_4_for_rgb_8() { test_it_converts( "tests/files/filter_4_for_rgb_8.png", RowFilter::Paeth, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -344,9 +348,9 @@ fn filter_5_for_rgb_8() { test_it_converts( "tests/files/filter_5_for_rgb_8.png", RowFilter::MinSum, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -356,9 +360,9 @@ fn filter_0_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_0_for_grayscale_alpha_16.png", RowFilter::None, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, ); } @@ -368,9 +372,9 @@ fn filter_1_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_1_for_grayscale_alpha_16.png", RowFilter::Sub, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, ); } @@ -380,9 +384,9 @@ fn filter_2_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_2_for_grayscale_alpha_16.png", RowFilter::Up, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, ); } @@ -392,9 +396,9 @@ fn filter_3_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_3_for_grayscale_alpha_16.png", RowFilter::Average, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, ); } @@ -404,9 +408,9 @@ fn filter_4_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_4_for_grayscale_alpha_16.png", RowFilter::Paeth, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, ); } @@ -416,9 +420,9 @@ fn filter_5_for_grayscale_alpha_16() { test_it_converts( "tests/files/filter_5_for_grayscale_alpha_16.png", RowFilter::MinSum, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, ); } @@ -428,9 +432,9 @@ fn filter_0_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_0_for_grayscale_alpha_8.png", RowFilter::None, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -440,9 +444,9 @@ fn filter_1_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_1_for_grayscale_alpha_8.png", RowFilter::Sub, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -452,9 +456,9 @@ fn filter_2_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_2_for_grayscale_alpha_8.png", RowFilter::Up, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -464,9 +468,9 @@ fn filter_3_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_3_for_grayscale_alpha_8.png", RowFilter::Average, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -476,9 +480,9 @@ fn filter_4_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_4_for_grayscale_alpha_8.png", RowFilter::Paeth, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -488,9 +492,9 @@ fn filter_5_for_grayscale_alpha_8() { test_it_converts( "tests/files/filter_5_for_grayscale_alpha_8.png", RowFilter::MinSum, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -500,9 +504,9 @@ fn filter_0_for_grayscale_16() { test_it_converts( "tests/files/filter_0_for_grayscale_16.png", RowFilter::None, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, ); } @@ -512,9 +516,9 @@ fn filter_1_for_grayscale_16() { test_it_converts( "tests/files/filter_1_for_grayscale_16.png", RowFilter::Sub, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, ); } @@ -524,9 +528,9 @@ fn filter_2_for_grayscale_16() { test_it_converts( "tests/files/filter_2_for_grayscale_16.png", RowFilter::Up, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, ); } @@ -536,9 +540,9 @@ fn filter_3_for_grayscale_16() { test_it_converts( "tests/files/filter_3_for_grayscale_16.png", RowFilter::Average, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, ); } @@ -548,9 +552,9 @@ fn filter_4_for_grayscale_16() { test_it_converts( "tests/files/filter_4_for_grayscale_16.png", RowFilter::Paeth, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, ); } @@ -560,9 +564,9 @@ fn filter_5_for_grayscale_16() { test_it_converts( "tests/files/filter_5_for_grayscale_16.png", RowFilter::MinSum, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, ); } @@ -572,9 +576,9 @@ fn filter_0_for_grayscale_8() { test_it_converts( "tests/files/filter_0_for_grayscale_8.png", RowFilter::None, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -584,9 +588,9 @@ fn filter_1_for_grayscale_8() { test_it_converts( "tests/files/filter_1_for_grayscale_8.png", RowFilter::Sub, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -596,9 +600,9 @@ fn filter_2_for_grayscale_8() { test_it_converts( "tests/files/filter_2_for_grayscale_8.png", RowFilter::Up, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -608,9 +612,9 @@ fn filter_3_for_grayscale_8() { test_it_converts( "tests/files/filter_3_for_grayscale_8.png", RowFilter::Average, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -620,9 +624,9 @@ fn filter_4_for_grayscale_8() { test_it_converts( "tests/files/filter_4_for_grayscale_8.png", RowFilter::Paeth, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -632,9 +636,9 @@ fn filter_5_for_grayscale_8() { test_it_converts( "tests/files/filter_5_for_grayscale_8.png", RowFilter::MinSum, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -644,9 +648,9 @@ fn filter_0_for_palette_4() { test_it_converts( "tests/files/filter_0_for_palette_4.png", RowFilter::None, - ColorType::Indexed, + INDEXED, BitDepth::Four, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -656,9 +660,9 @@ fn filter_1_for_palette_4() { test_it_converts( "tests/files/filter_1_for_palette_4.png", RowFilter::Sub, - ColorType::Indexed, + INDEXED, BitDepth::Four, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -668,9 +672,9 @@ fn filter_2_for_palette_4() { test_it_converts( "tests/files/filter_2_for_palette_4.png", RowFilter::Up, - ColorType::Indexed, + INDEXED, BitDepth::Four, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -680,9 +684,9 @@ fn filter_3_for_palette_4() { test_it_converts( "tests/files/filter_3_for_palette_4.png", RowFilter::Average, - ColorType::Indexed, + INDEXED, BitDepth::Four, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -692,9 +696,9 @@ fn filter_4_for_palette_4() { test_it_converts( "tests/files/filter_4_for_palette_4.png", RowFilter::Paeth, - ColorType::Indexed, + INDEXED, BitDepth::Four, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -704,9 +708,9 @@ fn filter_5_for_palette_4() { test_it_converts( "tests/files/filter_5_for_palette_4.png", RowFilter::MinSum, - ColorType::Indexed, + INDEXED, BitDepth::Four, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -716,9 +720,9 @@ fn filter_0_for_palette_2() { test_it_converts( "tests/files/filter_0_for_palette_2.png", RowFilter::None, - ColorType::Indexed, + INDEXED, BitDepth::Two, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -728,9 +732,9 @@ fn filter_1_for_palette_2() { test_it_converts( "tests/files/filter_1_for_palette_2.png", RowFilter::Sub, - ColorType::Indexed, + INDEXED, BitDepth::Two, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -740,9 +744,9 @@ fn filter_2_for_palette_2() { test_it_converts( "tests/files/filter_2_for_palette_2.png", RowFilter::Up, - ColorType::Indexed, + INDEXED, BitDepth::Two, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -752,9 +756,9 @@ fn filter_3_for_palette_2() { test_it_converts( "tests/files/filter_3_for_palette_2.png", RowFilter::Average, - ColorType::Indexed, + INDEXED, BitDepth::Two, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -764,9 +768,9 @@ fn filter_4_for_palette_2() { test_it_converts( "tests/files/filter_4_for_palette_2.png", RowFilter::Paeth, - ColorType::Indexed, + INDEXED, BitDepth::Two, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -776,9 +780,9 @@ fn filter_5_for_palette_2() { test_it_converts( "tests/files/filter_5_for_palette_2.png", RowFilter::MinSum, - ColorType::Indexed, + INDEXED, BitDepth::Two, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -788,9 +792,9 @@ fn filter_0_for_palette_1() { test_it_converts( "tests/files/filter_0_for_palette_1.png", RowFilter::None, - ColorType::Indexed, + INDEXED, BitDepth::One, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -800,9 +804,9 @@ fn filter_1_for_palette_1() { test_it_converts( "tests/files/filter_1_for_palette_1.png", RowFilter::Sub, - ColorType::Indexed, + INDEXED, BitDepth::One, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -812,9 +816,9 @@ fn filter_2_for_palette_1() { test_it_converts( "tests/files/filter_2_for_palette_1.png", RowFilter::Up, - ColorType::Indexed, + INDEXED, BitDepth::One, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -824,9 +828,9 @@ fn filter_3_for_palette_1() { test_it_converts( "tests/files/filter_3_for_palette_1.png", RowFilter::Average, - ColorType::Indexed, + INDEXED, BitDepth::One, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -836,9 +840,9 @@ fn filter_4_for_palette_1() { test_it_converts( "tests/files/filter_4_for_palette_1.png", RowFilter::Paeth, - ColorType::Indexed, + INDEXED, BitDepth::One, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -848,9 +852,9 @@ fn filter_5_for_palette_1() { test_it_converts( "tests/files/filter_5_for_palette_1.png", RowFilter::MinSum, - ColorType::Indexed, + INDEXED, BitDepth::One, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } diff --git a/tests/flags.rs b/tests/flags.rs index 38db85b7..7d146a31 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -1,6 +1,6 @@ -use indexmap::IndexSet; -use oxipng::{internal_tests::*, Interlacing, RowFilter}; -use oxipng::{InFile, OutFile}; +use indexmap::{indexset, IndexSet}; +use oxipng::internal_tests::*; +use oxipng::*; #[cfg(feature = "filetime")] use std::cell::RefCell; use std::fs::remove_file; @@ -11,6 +11,11 @@ use std::ops::Deref; use std::path::Path; use std::path::PathBuf; +const GRAYSCALE: u8 = 0; +const RGB: u8 = 2; +const INDEXED: u8 = 3; +const RGBA: u8 = 6; + fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { let mut options = oxipng::Options { force: true, @@ -27,37 +32,38 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { } /// Add callback to allow checks before the output file is deleted again +#[allow(clippy::too_many_arguments)] fn test_it_converts_callbacks( input: PathBuf, output: &OutFile, opts: &oxipng::Options, - color_type_in: ColorType, + color_type_in: u8, bit_depth_in: BitDepth, - color_type_out: ColorType, + color_type_out: u8, bit_depth_out: BitDepth, mut callback_pre: CBPRE, mut callback_post: CBPOST, ) where - CBPOST: FnMut(&Path) -> (), - CBPRE: FnMut(&Path) -> (), + CBPOST: FnMut(&Path), + CBPRE: FnMut(&Path), { - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); - assert_eq!(png.raw.ihdr.color_type, color_type_in); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); callback_pre(&input); - match oxipng::optimize(&InFile::Path(input), &output, &opts) { + match oxipng::optimize(&InFile::Path(input), output, opts) { Ok(_) => (), Err(x) => panic!("{}", x), }; let output = output.path().unwrap(); assert!(output.exists()); - callback_post(&output); + callback_post(output); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -65,7 +71,7 @@ fn test_it_converts_callbacks( } }; - assert_eq!(png.raw.ihdr.color_type, color_type_out); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_out); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out); remove_file(output).ok(); @@ -76,9 +82,9 @@ fn test_it_converts( input: PathBuf, output: &OutFile, opts: &oxipng::Options, - color_type_in: ColorType, + color_type_in: u8, bit_depth_in: BitDepth, - color_type_out: ColorType, + color_type_out: u8, bit_depth_out: BitDepth, ) { test_it_converts_callbacks( @@ -152,9 +158,9 @@ fn verbose_mode() { input, &output, &opts, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); }; @@ -174,48 +180,47 @@ fn verbose_mode() { }); let logs: Vec<_> = receiver.into_iter().collect(); - println!("logs={:?}", logs); - assert_eq!(logs.len(), 9); - let expected_logs = [ + let expected_prefixes = [ " 500x400 pixels, PNG format", - " 3x8 bits/pixel, RGB (non-interlaced)", + " 8-bit RGB, non-interlaced", " IDAT size = 113794 bytes", " File size = 114708 bytes", "Trying: 1 filters", - " zc = 11 f = None 149409 bytes", "Found better combination:", - " zc = 11 f = None 149409 bytes", - " IDAT size = 149409 bytes", + " zc = 11 f = None ", + " IDAT size = ", ]; - for (idx, expected_log) in expected_logs.into_iter().enumerate() { - if let Some(log) = logs.get(idx) { - if !log.starts_with(expected_log) { - panic!( - "logs[{}] = {:?} doesn't start with {:?}", - idx, log, expected_log - ); - } - } else { - panic!( - "Expected to find {} log entries, but got {}", - expected_logs.len(), - logs.len() - ); - } + assert_eq!(logs.len(), expected_prefixes.len()); + for (i, log) in logs.into_iter().enumerate() { + let expected_prefix = expected_prefixes[i]; + assert!( + log.starts_with(&expected_prefix), + "logs[{}] = {:?} doesn't start with {:?}", + i, + log, + expected_prefix + ); } } +fn count_chunk(png: &PngData, name: &[u8; 4]) -> usize { + png.aux_chunks + .iter() + .filter(|chunk| &chunk.name == name) + .count() +} + #[test] fn strip_headers_list() { let input = PathBuf::from("tests/files/strip_headers_list.png"); let (output, mut opts) = get_opts(&input); - opts.strip = Headers::Strip(vec!["iCCP".to_owned(), "tEXt".to_owned()]); + opts.strip = StripChunks::Strip(indexset![*b"iCCP", *b"tEXt"]); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - assert!(png.raw.aux_headers.contains_key(b"tEXt")); - assert!(png.raw.aux_headers.contains_key(b"iTXt")); - assert!(png.raw.aux_headers.contains_key(b"iCCP")); + assert_eq!(count_chunk(&png, b"tEXt"), 3); + assert_eq!(count_chunk(&png, b"iTXt"), 1); + assert_eq!(count_chunk(&png, b"iCCP"), 1); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -224,7 +229,7 @@ fn strip_headers_list() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -232,9 +237,9 @@ fn strip_headers_list() { } }; - assert!(!png.raw.aux_headers.contains_key(b"tEXt")); - assert!(png.raw.aux_headers.contains_key(b"iTXt")); - assert!(!png.raw.aux_headers.contains_key(b"iCCP")); + assert_eq!(count_chunk(&png, b"tEXt"), 0); + assert_eq!(count_chunk(&png, b"iTXt"), 1); + assert_eq!(count_chunk(&png, b"iCCP"), 0); remove_file(output).ok(); } @@ -243,13 +248,13 @@ fn strip_headers_list() { fn strip_headers_safe() { let input = PathBuf::from("tests/files/strip_headers_safe.png"); let (output, mut opts) = get_opts(&input); - opts.strip = Headers::Safe; + opts.strip = StripChunks::Safe; - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - assert!(png.raw.aux_headers.contains_key(b"tEXt")); - assert!(png.raw.aux_headers.contains_key(b"iTXt")); - assert!(png.raw.aux_headers.contains_key(b"iCCP")); + assert_eq!(count_chunk(&png, b"tEXt"), 3); + assert_eq!(count_chunk(&png, b"iTXt"), 1); + assert_eq!(count_chunk(&png, b"iCCP"), 1); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -258,7 +263,7 @@ fn strip_headers_safe() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -266,9 +271,9 @@ fn strip_headers_safe() { } }; - assert!(!png.raw.aux_headers.contains_key(b"tEXt")); - assert!(!png.raw.aux_headers.contains_key(b"iTXt")); - assert!(png.raw.aux_headers.contains_key(b"sRGB")); + assert_eq!(count_chunk(&png, b"tEXt"), 0); + assert_eq!(count_chunk(&png, b"iTXt"), 0); + assert_eq!(count_chunk(&png, b"sRGB"), 1); remove_file(output).ok(); } @@ -277,13 +282,13 @@ fn strip_headers_safe() { fn strip_headers_all() { let input = PathBuf::from("tests/files/strip_headers_all.png"); let (output, mut opts) = get_opts(&input); - opts.strip = Headers::All; + opts.strip = StripChunks::All; - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - assert!(png.raw.aux_headers.contains_key(b"tEXt")); - assert!(png.raw.aux_headers.contains_key(b"iTXt")); - assert!(png.raw.aux_headers.contains_key(b"iCCP")); + assert_eq!(count_chunk(&png, b"tEXt"), 3); + assert_eq!(count_chunk(&png, b"iTXt"), 1); + assert_eq!(count_chunk(&png, b"iCCP"), 1); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -292,7 +297,7 @@ fn strip_headers_all() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -300,9 +305,9 @@ fn strip_headers_all() { } }; - assert!(!png.raw.aux_headers.contains_key(b"tEXt")); - assert!(!png.raw.aux_headers.contains_key(b"iTXt")); - assert!(!png.raw.aux_headers.contains_key(b"iCCP")); + assert_eq!(count_chunk(&png, b"tEXt"), 0); + assert_eq!(count_chunk(&png, b"iTXt"), 0); + assert_eq!(count_chunk(&png, b"iCCP"), 0); remove_file(output).ok(); } @@ -311,13 +316,13 @@ fn strip_headers_all() { fn strip_headers_none() { let input = PathBuf::from("tests/files/strip_headers_none.png"); let (output, mut opts) = get_opts(&input); - opts.strip = Headers::None; + opts.strip = StripChunks::None; - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); - assert!(png.raw.aux_headers.contains_key(b"tEXt")); - assert!(png.raw.aux_headers.contains_key(b"iTXt")); - assert!(png.raw.aux_headers.contains_key(b"iCCP")); + assert_eq!(count_chunk(&png, b"tEXt"), 3); + assert_eq!(count_chunk(&png, b"iTXt"), 1); + assert_eq!(count_chunk(&png, b"iCCP"), 1); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -326,7 +331,7 @@ fn strip_headers_none() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -334,9 +339,9 @@ fn strip_headers_none() { } }; - assert!(png.raw.aux_headers.contains_key(b"tEXt")); - assert!(png.raw.aux_headers.contains_key(b"iTXt")); - assert!(png.raw.aux_headers.contains_key(b"iCCP")); + assert_eq!(count_chunk(&png, b"tEXt"), 3); + assert_eq!(count_chunk(&png, b"iTXt"), 1); + assert_eq!(count_chunk(&png, b"iCCP"), 1); remove_file(output).ok(); } @@ -347,7 +352,7 @@ fn interlacing_0_to_1() { let (output, mut opts) = get_opts(&input); opts.interlace = Some(Interlacing::Adam7); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.interlaced, Interlacing::None); @@ -358,7 +363,7 @@ fn interlacing_0_to_1() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -377,7 +382,7 @@ fn interlacing_1_to_0() { let (output, mut opts) = get_opts(&input); opts.interlace = Some(Interlacing::None); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7); @@ -388,7 +393,7 @@ fn interlacing_1_to_0() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -407,10 +412,10 @@ fn interlacing_0_to_1_small_files() { let (output, mut opts) = get_opts(&input); opts.interlace = Some(Interlacing::Adam7); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.interlaced, Interlacing::None); - assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); match oxipng::optimize(&InFile::Path(input), &output, &opts) { @@ -420,7 +425,7 @@ fn interlacing_0_to_1_small_files() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -429,8 +434,8 @@ fn interlacing_0_to_1_small_files() { }; assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7); - assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); - assert_eq!(png.raw.ihdr.bit_depth, BitDepth::One); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), RGB); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); remove_file(output).ok(); } @@ -441,10 +446,10 @@ fn interlacing_1_to_0_small_files() { let (output, mut opts) = get_opts(&input); opts.interlace = Some(Interlacing::None); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7); - assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); match oxipng::optimize(&InFile::Path(input), &output, &opts) { @@ -454,7 +459,7 @@ fn interlacing_1_to_0_small_files() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -463,8 +468,8 @@ fn interlacing_1_to_0_small_files() { }; assert_eq!(png.raw.ihdr.interlaced, Interlacing::None); - assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); - // the depth can't be asserted reliably, because on such small file different zlib implementations pick different depth as the best + assert_eq!(png.raw.ihdr.color_type.png_header_code(), RGB); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); remove_file(output).ok(); } @@ -478,7 +483,7 @@ fn interlaced_0_to_1_other_filter_mode() { filter.insert(RowFilter::Paeth); opts.filter = filter; - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.interlaced, Interlacing::None); @@ -489,7 +494,7 @@ fn interlaced_0_to_1_other_filter_mode() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -555,9 +560,9 @@ fn preserve_attrs() { input, &output, &opts, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, callback_pre, callback_post, @@ -572,9 +577,9 @@ fn fix_errors() { let (output, mut opts) = get_opts(&input); opts.fix_errors = true; - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); - assert_eq!(png.raw.ihdr.color_type, ColorType::RGBA); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), RGBA); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); match oxipng::optimize(&InFile::Path(input), &output, &opts) { @@ -584,7 +589,7 @@ fn fix_errors() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, false) { + let png = match PngData::new(output, &Options::default()) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -592,13 +597,30 @@ fn fix_errors() { } }; - assert_eq!(png.raw.ihdr.color_type, ColorType::Grayscale); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), GRAYSCALE); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); // Cannot check if pixels are equal because image crate cannot read corrupt (input) PNGs remove_file(output).ok(); } +#[test] +fn scale_16() { + let input = PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"); + let (output, mut opts) = get_opts(&input); + opts.scale_16 = true; + + test_it_converts( + input, + &output, + &opts, + RGB, + BitDepth::Sixteen, + RGB, + BitDepth::Eight, + ); +} + #[test] #[cfg(feature = "zopfli")] fn zopfli_mode() { @@ -612,9 +634,9 @@ fn zopfli_mode() { input, &output, &opts, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } diff --git a/tests/interlaced.rs b/tests/interlaced.rs index 747c1253..573c3182 100644 --- a/tests/interlaced.rs +++ b/tests/interlaced.rs @@ -1,10 +1,16 @@ use indexmap::IndexSet; -use oxipng::{internal_tests::*, Interlacing, RowFilter}; -use oxipng::{InFile, OutFile}; +use oxipng::internal_tests::*; +use oxipng::*; use std::fs::remove_file; use std::path::Path; use std::path::PathBuf; +const GRAYSCALE: u8 = 0; +const RGB: u8 = 2; +const INDEXED: u8 = 3; +const GRAYSCALE_ALPHA: u8 = 4; +const RGBA: u8 = 6; + fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { let mut options = oxipng::Options { force: true, @@ -22,16 +28,16 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { fn test_it_converts( input: &str, - color_type_in: ColorType, + color_type_in: u8, bit_depth_in: BitDepth, - color_type_out: ColorType, + color_type_out: u8, bit_depth_out: BitDepth, ) { let input = PathBuf::from(input); let (output, opts) = get_opts(&input); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); - assert_eq!(png.raw.ihdr.color_type, color_type_in); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7); @@ -42,7 +48,7 @@ fn test_it_converts( let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -50,7 +56,7 @@ fn test_it_converts( } }; - assert_eq!(png.raw.ihdr.color_type, color_type_out); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_out); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out); remove_file(output).ok(); @@ -60,9 +66,9 @@ fn test_it_converts( fn interlaced_rgba_16_should_be_rgba_16() { test_it_converts( "tests/files/interlaced_rgba_16_should_be_rgba_16.png", - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, ); } @@ -71,9 +77,9 @@ fn interlaced_rgba_16_should_be_rgba_16() { fn interlaced_rgba_16_should_be_rgba_8() { test_it_converts( "tests/files/interlaced_rgba_16_should_be_rgba_8.png", - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::RGBA, + RGBA, BitDepth::Eight, ); } @@ -82,9 +88,9 @@ fn interlaced_rgba_16_should_be_rgba_8() { fn interlaced_rgba_8_should_be_rgba_8() { test_it_converts( "tests/files/interlaced_rgba_8_should_be_rgba_8.png", - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGBA, + RGBA, BitDepth::Eight, ); } @@ -93,9 +99,9 @@ fn interlaced_rgba_8_should_be_rgba_8() { fn interlaced_rgba_16_should_be_rgb_16() { test_it_converts( "tests/files/interlaced_rgba_16_should_be_rgb_16.png", - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Sixteen, ); } @@ -104,9 +110,9 @@ fn interlaced_rgba_16_should_be_rgb_16() { fn interlaced_rgba_16_should_be_rgb_8() { test_it_converts( "tests/files/interlaced_rgba_16_should_be_rgb_8.png", - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -115,9 +121,9 @@ fn interlaced_rgba_16_should_be_rgb_8() { fn interlaced_rgba_8_should_be_rgb_8() { test_it_converts( "tests/files/interlaced_rgba_8_should_be_rgb_8.png", - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -126,9 +132,9 @@ fn interlaced_rgba_8_should_be_rgb_8() { fn interlaced_rgba_16_should_be_palette_8() { test_it_converts( "tests/files/interlaced_rgba_16_should_be_palette_8.png", - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); } @@ -137,9 +143,9 @@ fn interlaced_rgba_16_should_be_palette_8() { fn interlaced_rgba_8_should_be_palette_8() { test_it_converts( "tests/files/interlaced_rgba_8_should_be_palette_8.png", - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); } @@ -148,9 +154,9 @@ fn interlaced_rgba_8_should_be_palette_8() { fn interlaced_rgba_16_should_be_palette_4() { test_it_converts( "tests/files/interlaced_rgba_16_should_be_palette_4.png", - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -159,9 +165,9 @@ fn interlaced_rgba_16_should_be_palette_4() { fn interlaced_rgba_8_should_be_palette_4() { test_it_converts( "tests/files/interlaced_rgba_8_should_be_palette_4.png", - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -170,9 +176,9 @@ fn interlaced_rgba_8_should_be_palette_4() { fn interlaced_rgba_16_should_be_palette_2() { test_it_converts( "tests/files/interlaced_rgba_16_should_be_palette_2.png", - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -181,9 +187,9 @@ fn interlaced_rgba_16_should_be_palette_2() { fn interlaced_rgba_8_should_be_palette_2() { test_it_converts( "tests/files/interlaced_rgba_8_should_be_palette_2.png", - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -192,9 +198,9 @@ fn interlaced_rgba_8_should_be_palette_2() { fn interlaced_rgba_16_should_be_palette_1() { test_it_converts( "tests/files/interlaced_rgba_16_should_be_palette_1.png", - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -203,9 +209,9 @@ fn interlaced_rgba_16_should_be_palette_1() { fn interlaced_rgba_8_should_be_palette_1() { test_it_converts( "tests/files/interlaced_rgba_8_should_be_palette_1.png", - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -214,9 +220,9 @@ fn interlaced_rgba_8_should_be_palette_1() { fn interlaced_rgba_16_should_be_grayscale_alpha_16() { test_it_converts( "tests/files/interlaced_rgba_16_should_be_grayscale_alpha_16.png", - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, ); } @@ -225,9 +231,9 @@ fn interlaced_rgba_16_should_be_grayscale_alpha_16() { fn interlaced_rgba_16_should_be_grayscale_alpha_8() { test_it_converts( "tests/files/interlaced_rgba_16_should_be_grayscale_alpha_8.png", - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -236,9 +242,9 @@ fn interlaced_rgba_16_should_be_grayscale_alpha_8() { fn interlaced_rgba_8_should_be_grayscale_alpha_8() { test_it_converts( "tests/files/interlaced_rgba_8_should_be_grayscale_alpha_8.png", - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -247,9 +253,9 @@ fn interlaced_rgba_8_should_be_grayscale_alpha_8() { fn interlaced_rgba_16_should_be_grayscale_16() { test_it_converts( "tests/files/interlaced_rgba_16_should_be_grayscale_16.png", - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, ); } @@ -258,9 +264,9 @@ fn interlaced_rgba_16_should_be_grayscale_16() { fn interlaced_rgba_16_should_be_grayscale_8() { test_it_converts( "tests/files/interlaced_rgba_16_should_be_grayscale_8.png", - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -269,9 +275,9 @@ fn interlaced_rgba_16_should_be_grayscale_8() { fn interlaced_rgba_8_should_be_grayscale_8() { test_it_converts( "tests/files/interlaced_rgba_8_should_be_grayscale_8.png", - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -280,9 +286,9 @@ fn interlaced_rgba_8_should_be_grayscale_8() { fn interlaced_rgb_16_should_be_rgb_16() { test_it_converts( "tests/files/interlaced_rgb_16_should_be_rgb_16.png", - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Sixteen, ); } @@ -291,9 +297,9 @@ fn interlaced_rgb_16_should_be_rgb_16() { fn interlaced_rgb_16_should_be_rgb_8() { test_it_converts( "tests/files/interlaced_rgb_16_should_be_rgb_8.png", - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -302,9 +308,9 @@ fn interlaced_rgb_16_should_be_rgb_8() { fn interlaced_rgb_8_should_be_rgb_8() { test_it_converts( "tests/files/interlaced_rgb_8_should_be_rgb_8.png", - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -313,9 +319,9 @@ fn interlaced_rgb_8_should_be_rgb_8() { fn interlaced_rgb_16_should_be_palette_8() { test_it_converts( "tests/files/interlaced_rgb_16_should_be_palette_8.png", - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); } @@ -324,9 +330,9 @@ fn interlaced_rgb_16_should_be_palette_8() { fn interlaced_rgb_8_should_be_palette_8() { test_it_converts( "tests/files/interlaced_rgb_8_should_be_palette_8.png", - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); } @@ -335,9 +341,9 @@ fn interlaced_rgb_8_should_be_palette_8() { fn interlaced_rgb_16_should_be_palette_4() { test_it_converts( "tests/files/interlaced_rgb_16_should_be_palette_4.png", - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -346,9 +352,9 @@ fn interlaced_rgb_16_should_be_palette_4() { fn interlaced_rgb_8_should_be_palette_4() { test_it_converts( "tests/files/interlaced_rgb_8_should_be_palette_4.png", - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -357,9 +363,9 @@ fn interlaced_rgb_8_should_be_palette_4() { fn interlaced_rgb_16_should_be_palette_2() { test_it_converts( "tests/files/interlaced_rgb_16_should_be_palette_2.png", - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -368,9 +374,9 @@ fn interlaced_rgb_16_should_be_palette_2() { fn interlaced_rgb_8_should_be_palette_2() { test_it_converts( "tests/files/interlaced_rgb_8_should_be_palette_2.png", - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -379,9 +385,9 @@ fn interlaced_rgb_8_should_be_palette_2() { fn interlaced_rgb_16_should_be_palette_1() { test_it_converts( "tests/files/interlaced_rgb_16_should_be_palette_1.png", - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -390,9 +396,9 @@ fn interlaced_rgb_16_should_be_palette_1() { fn interlaced_rgb_8_should_be_palette_1() { test_it_converts( "tests/files/interlaced_rgb_8_should_be_palette_1.png", - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -401,9 +407,9 @@ fn interlaced_rgb_8_should_be_palette_1() { fn interlaced_rgb_16_should_be_grayscale_16() { test_it_converts( "tests/files/interlaced_rgb_16_should_be_grayscale_16.png", - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, ); } @@ -412,9 +418,9 @@ fn interlaced_rgb_16_should_be_grayscale_16() { fn interlaced_rgb_16_should_be_grayscale_8() { test_it_converts( "tests/files/interlaced_rgb_16_should_be_grayscale_8.png", - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -423,9 +429,9 @@ fn interlaced_rgb_16_should_be_grayscale_8() { fn interlaced_rgb_8_should_be_grayscale_8() { test_it_converts( "tests/files/interlaced_rgb_8_should_be_grayscale_8.png", - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -434,9 +440,9 @@ fn interlaced_rgb_8_should_be_grayscale_8() { fn interlaced_palette_8_should_be_palette_8() { test_it_converts( "tests/files/interlaced_palette_8_should_be_palette_8.png", - ColorType::Indexed, + INDEXED, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); } @@ -445,9 +451,9 @@ fn interlaced_palette_8_should_be_palette_8() { fn interlaced_palette_8_should_be_palette_4() { test_it_converts( "tests/files/interlaced_palette_8_should_be_palette_4.png", - ColorType::Indexed, + INDEXED, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -456,9 +462,9 @@ fn interlaced_palette_8_should_be_palette_4() { fn interlaced_palette_4_should_be_palette_4() { test_it_converts( "tests/files/interlaced_palette_4_should_be_palette_4.png", - ColorType::Indexed, + INDEXED, BitDepth::Four, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -467,9 +473,9 @@ fn interlaced_palette_4_should_be_palette_4() { fn interlaced_palette_8_should_be_palette_2() { test_it_converts( "tests/files/interlaced_palette_8_should_be_palette_2.png", - ColorType::Indexed, + INDEXED, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -478,9 +484,9 @@ fn interlaced_palette_8_should_be_palette_2() { fn interlaced_palette_4_should_be_palette_2() { test_it_converts( "tests/files/interlaced_palette_4_should_be_palette_2.png", - ColorType::Indexed, + INDEXED, BitDepth::Four, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -489,9 +495,9 @@ fn interlaced_palette_4_should_be_palette_2() { fn interlaced_palette_2_should_be_palette_2() { test_it_converts( "tests/files/interlaced_palette_2_should_be_palette_2.png", - ColorType::Indexed, + INDEXED, BitDepth::Two, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -500,9 +506,9 @@ fn interlaced_palette_2_should_be_palette_2() { fn interlaced_palette_8_should_be_palette_1() { test_it_converts( "tests/files/interlaced_palette_8_should_be_palette_1.png", - ColorType::Indexed, + INDEXED, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -511,9 +517,9 @@ fn interlaced_palette_8_should_be_palette_1() { fn interlaced_palette_4_should_be_palette_1() { test_it_converts( "tests/files/interlaced_palette_4_should_be_palette_1.png", - ColorType::Indexed, + INDEXED, BitDepth::Four, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -522,9 +528,9 @@ fn interlaced_palette_4_should_be_palette_1() { fn interlaced_palette_2_should_be_palette_1() { test_it_converts( "tests/files/interlaced_palette_2_should_be_palette_1.png", - ColorType::Indexed, + INDEXED, BitDepth::Two, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -533,20 +539,31 @@ fn interlaced_palette_2_should_be_palette_1() { fn interlaced_palette_1_should_be_palette_1() { test_it_converts( "tests/files/interlaced_palette_1_should_be_palette_1.png", - ColorType::Indexed, + INDEXED, BitDepth::One, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } +#[test] +fn interlaced_palette_8_should_be_grayscale_8() { + test_it_converts( + "tests/files/interlaced_palette_8_should_be_grayscale_8.png", + INDEXED, + BitDepth::Eight, + GRAYSCALE, + BitDepth::Eight, + ); +} + #[test] fn interlaced_grayscale_alpha_16_should_be_grayscale_alpha_16() { test_it_converts( "tests/files/interlaced_grayscale_alpha_16_should_be_grayscale_alpha_16.png", - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, ); } @@ -555,9 +572,9 @@ fn interlaced_grayscale_alpha_16_should_be_grayscale_alpha_16() { fn interlaced_grayscale_alpha_16_should_be_grayscale_alpha_8() { test_it_converts( "tests/files/interlaced_grayscale_alpha_16_should_be_grayscale_alpha_8.png", - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -566,9 +583,9 @@ fn interlaced_grayscale_alpha_16_should_be_grayscale_alpha_8() { fn interlaced_grayscale_alpha_8_should_be_grayscale_alpha_8() { test_it_converts( "tests/files/interlaced_grayscale_alpha_8_should_be_grayscale_alpha_8.png", - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -577,9 +594,9 @@ fn interlaced_grayscale_alpha_8_should_be_grayscale_alpha_8() { fn interlaced_grayscale_alpha_16_should_be_grayscale_16() { test_it_converts( "tests/files/interlaced_grayscale_alpha_16_should_be_grayscale_16.png", - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, ); } @@ -588,9 +605,9 @@ fn interlaced_grayscale_alpha_16_should_be_grayscale_16() { fn interlaced_grayscale_alpha_16_should_be_grayscale_8() { test_it_converts( "tests/files/interlaced_grayscale_alpha_16_should_be_grayscale_8.png", - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -599,9 +616,9 @@ fn interlaced_grayscale_alpha_16_should_be_grayscale_8() { fn interlaced_grayscale_alpha_8_should_be_grayscale_8() { test_it_converts( "tests/files/interlaced_grayscale_alpha_8_should_be_grayscale_8.png", - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -610,9 +627,9 @@ fn interlaced_grayscale_alpha_8_should_be_grayscale_8() { fn interlaced_grayscale_16_should_be_grayscale_16() { test_it_converts( "tests/files/interlaced_grayscale_16_should_be_grayscale_16.png", - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, ); } @@ -621,9 +638,9 @@ fn interlaced_grayscale_16_should_be_grayscale_16() { fn interlaced_grayscale_16_should_be_grayscale_8() { test_it_converts( "tests/files/interlaced_grayscale_16_should_be_grayscale_8.png", - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -632,9 +649,9 @@ fn interlaced_grayscale_16_should_be_grayscale_8() { fn interlaced_grayscale_8_should_be_grayscale_8() { test_it_converts( "tests/files/interlaced_grayscale_8_should_be_grayscale_8.png", - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -643,10 +660,10 @@ fn interlaced_grayscale_8_should_be_grayscale_8() { fn interlaced_small_files() { test_it_converts( "tests/files/interlaced_small_files.png", - ColorType::Indexed, + INDEXED, + BitDepth::Eight, + RGB, BitDepth::Eight, - ColorType::Indexed, - BitDepth::One, ); } @@ -654,9 +671,9 @@ fn interlaced_small_files() { fn interlaced_odd_width() { test_it_converts( "tests/files/interlaced_odd_width.png", - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } diff --git a/tests/interlacing.rs b/tests/interlacing.rs index e0841c88..2e971979 100644 --- a/tests/interlacing.rs +++ b/tests/interlacing.rs @@ -1,10 +1,13 @@ use indexmap::IndexSet; -use oxipng::{internal_tests::*, Interlacing, RowFilter}; -use oxipng::{InFile, OutFile}; +use oxipng::internal_tests::*; +use oxipng::*; use std::fs::remove_file; use std::path::Path; use std::path::PathBuf; +const RGB: u8 = 2; +const INDEXED: u8 = 3; + fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { let mut options = oxipng::Options { force: true, @@ -23,16 +26,16 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { fn test_it_converts( input: &str, interlace: Interlacing, - color_type_in: ColorType, + color_type_in: u8, bit_depth_in: BitDepth, - color_type_out: ColorType, + color_type_out: u8, bit_depth_out: BitDepth, ) { let input = PathBuf::from(input); let (output, mut opts) = get_opts(&input); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); opts.interlace = Some(interlace); - assert_eq!(png.raw.ihdr.color_type, color_type_in); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); assert_eq!( png.raw.ihdr.interlaced, @@ -50,7 +53,7 @@ fn test_it_converts( let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -58,7 +61,7 @@ fn test_it_converts( } }; - assert_eq!(png.raw.ihdr.color_type, color_type_out); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_out); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out); remove_file(output).ok(); @@ -69,9 +72,9 @@ fn deinterlace_rgb_16() { test_it_converts( "tests/files/interlaced_rgb_16_should_be_rgb_16.png", Interlacing::None, - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Sixteen, ); } @@ -81,9 +84,9 @@ fn deinterlace_rgb_8() { test_it_converts( "tests/files/interlaced_rgb_8_should_be_rgb_8.png", Interlacing::None, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -93,9 +96,9 @@ fn deinterlace_palette_8() { test_it_converts( "tests/files/interlaced_palette_8_should_be_palette_8.png", Interlacing::None, - ColorType::Indexed, + INDEXED, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); } @@ -105,9 +108,9 @@ fn deinterlace_palette_4() { test_it_converts( "tests/files/interlaced_palette_4_should_be_palette_4.png", Interlacing::None, - ColorType::Indexed, + INDEXED, BitDepth::Four, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -117,9 +120,9 @@ fn deinterlace_palette_2() { test_it_converts( "tests/files/interlaced_palette_2_should_be_palette_2.png", Interlacing::None, - ColorType::Indexed, + INDEXED, BitDepth::Two, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -129,9 +132,9 @@ fn deinterlace_palette_1() { test_it_converts( "tests/files/interlaced_palette_1_should_be_palette_1.png", Interlacing::None, - ColorType::Indexed, + INDEXED, BitDepth::One, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -141,9 +144,9 @@ fn interlace_rgb_16() { test_it_converts( "tests/files/rgb_16_should_be_rgb_16.png", Interlacing::Adam7, - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Sixteen, ); } @@ -153,9 +156,9 @@ fn interlace_rgb_8() { test_it_converts( "tests/files/rgb_8_should_be_rgb_8.png", Interlacing::Adam7, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -165,9 +168,9 @@ fn interlace_palette_8() { test_it_converts( "tests/files/palette_8_should_be_palette_8.png", Interlacing::Adam7, - ColorType::Indexed, + INDEXED, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); } @@ -177,9 +180,9 @@ fn interlace_palette_4() { test_it_converts( "tests/files/palette_4_should_be_palette_4.png", Interlacing::Adam7, - ColorType::Indexed, + INDEXED, BitDepth::Four, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -189,9 +192,9 @@ fn interlace_palette_2() { test_it_converts( "tests/files/palette_2_should_be_palette_2.png", Interlacing::Adam7, - ColorType::Indexed, + INDEXED, BitDepth::Two, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -201,9 +204,9 @@ fn interlace_palette_1() { test_it_converts( "tests/files/palette_1_should_be_palette_1.png", Interlacing::Adam7, - ColorType::Indexed, + INDEXED, BitDepth::One, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } diff --git a/tests/lib.rs b/tests/lib.rs index 8f5e28d1..a32de8b1 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,6 +1,4 @@ -use oxipng::Headers; -use oxipng::OutFile; -use std::default::Default; +use oxipng::*; use std::fs; use std::fs::File; use std::io::prelude::*; @@ -11,9 +9,7 @@ fn optimize_from_memory() { let mut in_file_buf: Vec = Vec::new(); in_file.read_to_end(&mut in_file_buf).unwrap(); - let opts: oxipng::Options = Default::default(); - - let result = oxipng::optimize_from_memory(&in_file_buf, &opts); + let result = oxipng::optimize_from_memory(&in_file_buf, &Options::default()); assert!(result.is_ok()); } @@ -23,9 +19,7 @@ fn optimize_from_memory_corrupted() { let mut in_file_buf: Vec = Vec::new(); in_file.read_to_end(&mut in_file_buf).unwrap(); - let opts: oxipng::Options = Default::default(); - - let result = oxipng::optimize_from_memory(&in_file_buf, &opts); + let result = oxipng::optimize_from_memory(&in_file_buf, &Options::default()); assert!(result.is_err()); } @@ -35,44 +29,36 @@ fn optimize_from_memory_apng() { let mut in_file_buf: Vec = Vec::new(); in_file.read_to_end(&mut in_file_buf).unwrap(); - let opts: oxipng::Options = Default::default(); - - let result = oxipng::optimize_from_memory(&in_file_buf, &opts); + let result = oxipng::optimize_from_memory(&in_file_buf, &Options::default()); assert!(result.is_err()); } #[test] fn optimize() { - let opts: oxipng::Options = Default::default(); - let result = oxipng::optimize( &"tests/files/fully_optimized.png".into(), &OutFile::Path(None), - &opts, + &Options::default(), ); assert!(result.is_ok()); } #[test] fn optimize_corrupted() { - let opts: oxipng::Options = Default::default(); - let result = oxipng::optimize( &"tests/files/corrupted_header.png".into(), &OutFile::Path(None), - &opts, + &Options::default(), ); assert!(result.is_err()); } #[test] fn optimize_apng() { - let opts: oxipng::Options = Default::default(); - let result = oxipng::optimize( &"tests/files/apng_file.png".into(), &OutFile::Path(None), - &opts, + &Options::default(), ); assert!(result.is_err()); } @@ -80,12 +66,12 @@ fn optimize_apng() { #[test] fn optimize_srgb_icc() { let file = fs::read("tests/files/badsrgb.png").unwrap(); - let mut opts: oxipng::Options = Default::default(); + let mut opts = Options::default(); let result = oxipng::optimize_from_memory(&file, &opts); assert!(result.unwrap().len() > 1000); - opts.strip = Headers::Safe; + opts.strip = StripChunks::Safe; let result = oxipng::optimize_from_memory(&file, &opts); assert!(result.unwrap().len() < 1000); } diff --git a/tests/raw.rs b/tests/raw.rs new file mode 100644 index 00000000..c3e16fb2 --- /dev/null +++ b/tests/raw.rs @@ -0,0 +1,97 @@ +use oxipng::internal_tests::*; +use oxipng::*; +use std::path::PathBuf; +use std::sync::Arc; + +fn get_opts() -> Options { + Options { + force: true, + filter: indexset! { RowFilter::None }, + ..Default::default() + } +} + +fn test_it_converts(input: &str) { + let input = PathBuf::from(input); + let opts = get_opts(); + + let original_data = PngData::read_file(&PathBuf::from(input)).unwrap(); + let image = PngData::from_slice(&original_data, &opts).unwrap(); + let png = Arc::try_unwrap(image.raw).unwrap(); + + let num_chunks = image.aux_chunks.len(); + assert!(num_chunks > 0); + + let mut raw = RawImage::new( + png.ihdr.width, + png.ihdr.height, + png.ihdr.color_type, + png.ihdr.bit_depth, + png.data, + ) + .unwrap(); + + for chunk in image.aux_chunks { + raw.add_png_chunk(chunk.name, chunk.data); + } + + let output = raw.create_optimized_png(&opts).unwrap(); + + let new = PngData::from_slice(&output, &opts).unwrap(); + assert!(new.aux_chunks.len() == num_chunks); + + #[cfg(feature = "sanity-checks")] + assert!(validate_output(&output, &original_data)); +} + +#[test] +fn from_file() { + test_it_converts("tests/files/raw_api.png"); +} + +#[test] +fn custom_indexed() { + let opts = get_opts(); + + let raw = RawImage::new( + 4, + 4, + ColorType::Indexed { + palette: vec![ + RGBA8::new(255, 255, 255, 255), + RGBA8::new(255, 0, 0, 255), + RGBA8::new(0, 255, 0, 255), + RGBA8::new(0, 0, 255, 255), + ], + }, + BitDepth::Eight, + vec![0, 0, 1, 1, 0, 0, 1, 1, 2, 2, 3, 3, 2, 2, 3, 3], + ) + .unwrap(); + + raw.create_optimized_png(&opts).unwrap(); +} + +#[test] +fn invalid_depth() { + RawImage::new( + 2, + 2, + ColorType::RGBA, + BitDepth::Four, + vec![0, 0, 1, 1, 0, 0, 1, 1, 2, 2, 3, 3, 2, 2, 3, 3], + ) + .expect_err("Expected invalid depth for color type"); +} + +#[test] +fn incorrect_length() { + RawImage::new( + 2, + 2, + ColorType::RGBA, + BitDepth::Eight, + vec![0, 0, 1, 1, 0, 0, 1, 1], + ) + .expect_err("Expected incorrect data length"); +} diff --git a/tests/reduction.rs b/tests/reduction.rs index f3afdb88..8a1a1e34 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -1,10 +1,16 @@ use indexmap::IndexSet; -use oxipng::{internal_tests::*, Interlacing, RowFilter}; -use oxipng::{InFile, OutFile}; +use oxipng::internal_tests::*; +use oxipng::*; use std::fs::remove_file; use std::path::Path; use std::path::PathBuf; +const GRAYSCALE: u8 = 0; +const RGB: u8 = 2; +const INDEXED: u8 = 3; +const GRAYSCALE_ALPHA: u8 = 4; +const RGBA: u8 = 6; + fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { let mut options = oxipng::Options { force: true, @@ -23,17 +29,17 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { fn test_it_converts( input: &str, optimize_alpha: bool, - color_type_in: ColorType, + color_type_in: u8, bit_depth_in: BitDepth, - color_type_out: ColorType, + color_type_out: u8, bit_depth_out: BitDepth, ) { let input = PathBuf::from(input); let (output, mut opts) = get_opts(&input); opts.optimize_alpha = optimize_alpha; - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); - assert_eq!(png.raw.ihdr.color_type, color_type_in); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in, "test file is broken"); assert_eq!(png.raw.ihdr.interlaced, Interlacing::None); @@ -44,7 +50,7 @@ fn test_it_converts( let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -52,7 +58,7 @@ fn test_it_converts( } }; - assert_eq!(png.raw.ihdr.color_type, color_type_out); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_out); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out); remove_file(output).ok(); @@ -63,9 +69,9 @@ fn rgba_16_should_be_rgba_16() { test_it_converts( "tests/files/rgba_16_should_be_rgba_16.png", false, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, ); } @@ -75,9 +81,9 @@ fn rgba_16_should_be_rgba_8() { test_it_converts( "tests/files/rgba_16_should_be_rgba_8.png", false, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::RGBA, + RGBA, BitDepth::Eight, ); } @@ -87,9 +93,9 @@ fn rgba_8_should_be_rgba_8() { test_it_converts( "tests/files/rgba_8_should_be_rgba_8.png", false, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGBA, + RGBA, BitDepth::Eight, ); } @@ -99,9 +105,9 @@ fn rgba_16_should_be_rgb_16() { test_it_converts( "tests/files/rgba_16_should_be_rgb_16.png", false, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Sixteen, ); } @@ -111,9 +117,9 @@ fn rgba_16_should_be_rgb_8() { test_it_converts( "tests/files/rgba_16_should_be_rgb_8.png", false, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -123,9 +129,9 @@ fn rgba_8_should_be_rgb_8() { test_it_converts( "tests/files/rgba_8_should_be_rgb_8.png", false, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -135,9 +141,9 @@ fn rgba_16_should_be_rgb_trns_16() { test_it_converts( "tests/files/rgba_16_should_be_rgb_trns_16.png", true, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Sixteen, ); } @@ -147,9 +153,9 @@ fn rgba_8_should_be_rgb_trns_8() { test_it_converts( "tests/files/rgba_8_should_be_rgb_trns_8.png", true, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -159,9 +165,9 @@ fn rgba_16_should_be_palette_8() { test_it_converts( "tests/files/rgba_16_should_be_palette_8.png", false, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); } @@ -171,9 +177,9 @@ fn rgba_8_should_be_palette_8() { test_it_converts( "tests/files/rgba_8_should_be_palette_8.png", false, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); } @@ -183,9 +189,9 @@ fn rgba_16_should_be_palette_4() { test_it_converts( "tests/files/rgba_16_should_be_palette_4.png", false, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -195,9 +201,9 @@ fn rgba_8_should_be_palette_4() { test_it_converts( "tests/files/rgba_8_should_be_palette_4.png", false, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -207,9 +213,9 @@ fn rgba_16_should_be_palette_2() { test_it_converts( "tests/files/rgba_16_should_be_palette_2.png", false, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -219,9 +225,9 @@ fn rgba_8_should_be_palette_2() { test_it_converts( "tests/files/rgba_8_should_be_palette_2.png", false, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -231,9 +237,9 @@ fn rgba_16_should_be_palette_1() { test_it_converts( "tests/files/rgba_16_should_be_palette_1.png", false, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -243,9 +249,9 @@ fn rgba_8_should_be_palette_1() { test_it_converts( "tests/files/rgba_8_should_be_palette_1.png", false, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -255,9 +261,9 @@ fn rgba_16_should_be_grayscale_alpha_16() { test_it_converts( "tests/files/rgba_16_should_be_grayscale_alpha_16.png", false, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, ); } @@ -267,9 +273,9 @@ fn rgba_16_should_be_grayscale_alpha_8() { test_it_converts( "tests/files/rgba_16_should_be_grayscale_alpha_8.png", false, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -279,9 +285,9 @@ fn rgba_8_should_be_grayscale_alpha_8() { test_it_converts( "tests/files/rgba_8_should_be_grayscale_alpha_8.png", false, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -291,9 +297,9 @@ fn rgba_16_should_be_grayscale_16() { test_it_converts( "tests/files/rgba_16_should_be_grayscale_16.png", false, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, ); } @@ -303,9 +309,9 @@ fn rgba_16_should_be_grayscale_8() { test_it_converts( "tests/files/rgba_16_should_be_grayscale_8.png", false, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -315,9 +321,9 @@ fn rgba_8_should_be_grayscale_8() { test_it_converts( "tests/files/rgba_8_should_be_grayscale_8.png", false, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -327,9 +333,9 @@ fn rgb_16_should_be_rgb_16() { test_it_converts( "tests/files/rgb_16_should_be_rgb_16.png", false, - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Sixteen, ); } @@ -339,9 +345,9 @@ fn rgb_16_should_be_rgb_8() { test_it_converts( "tests/files/rgb_16_should_be_rgb_8.png", false, - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -351,9 +357,9 @@ fn rgb_8_should_be_rgb_8() { test_it_converts( "tests/files/rgb_8_should_be_rgb_8.png", false, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -363,9 +369,9 @@ fn rgb_16_should_be_palette_8() { test_it_converts( "tests/files/rgb_16_should_be_palette_8.png", false, - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); } @@ -375,9 +381,9 @@ fn rgb_8_should_be_palette_8() { test_it_converts( "tests/files/rgb_8_should_be_palette_8.png", false, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); } @@ -387,9 +393,9 @@ fn rgb_16_should_be_palette_4() { test_it_converts( "tests/files/rgb_16_should_be_palette_4.png", false, - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -399,9 +405,9 @@ fn rgb_8_should_be_palette_4() { test_it_converts( "tests/files/rgb_8_should_be_palette_4.png", false, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -411,9 +417,9 @@ fn rgb_16_should_be_palette_2() { test_it_converts( "tests/files/rgb_16_should_be_palette_2.png", false, - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -423,9 +429,9 @@ fn rgb_8_should_be_palette_2() { test_it_converts( "tests/files/rgb_8_should_be_palette_2.png", false, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -435,9 +441,9 @@ fn rgb_16_should_be_palette_1() { test_it_converts( "tests/files/rgb_16_should_be_palette_1.png", false, - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -447,9 +453,9 @@ fn rgb_8_should_be_palette_1() { test_it_converts( "tests/files/rgb_8_should_be_palette_1.png", false, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -459,9 +465,9 @@ fn rgb_16_should_be_grayscale_16() { test_it_converts( "tests/files/rgb_16_should_be_grayscale_16.png", false, - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, ); } @@ -471,9 +477,9 @@ fn rgb_16_should_be_grayscale_8() { test_it_converts( "tests/files/rgb_16_should_be_grayscale_8.png", false, - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -483,9 +489,9 @@ fn rgb_8_should_be_grayscale_8() { test_it_converts( "tests/files/rgb_8_should_be_grayscale_8.png", false, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -495,9 +501,9 @@ fn palette_8_should_be_palette_8() { test_it_converts( "tests/files/palette_8_should_be_palette_8.png", false, - ColorType::Indexed, + INDEXED, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); } @@ -507,9 +513,9 @@ fn palette_8_should_be_palette_4() { test_it_converts( "tests/files/palette_8_should_be_palette_4.png", false, - ColorType::Indexed, + INDEXED, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -519,9 +525,9 @@ fn palette_4_should_be_palette_4() { test_it_converts( "tests/files/palette_4_should_be_palette_4.png", false, - ColorType::Indexed, + INDEXED, BitDepth::Four, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -531,9 +537,9 @@ fn palette_8_should_be_palette_2() { test_it_converts( "tests/files/palette_8_should_be_palette_2.png", false, - ColorType::Indexed, + INDEXED, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -543,21 +549,57 @@ fn palette_4_should_be_palette_2() { test_it_converts( "tests/files/palette_4_should_be_palette_2.png", false, - ColorType::Indexed, + INDEXED, BitDepth::Four, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } +#[test] +fn palette_8_should_be_grayscale_8() { + test_it_converts( + "tests/files/palette_8_should_be_grayscale_8.png", + false, + INDEXED, + BitDepth::Eight, + GRAYSCALE, + BitDepth::Eight, + ); +} + +#[test] +fn palette_8_should_be_rgb() { + test_it_converts( + "tests/files/palette_8_should_be_rgb.png", + false, + INDEXED, + BitDepth::Eight, + RGB, + BitDepth::Eight, + ); +} + +#[test] +fn palette_8_should_be_rgba() { + test_it_converts( + "tests/files/palette_8_should_be_rgba.png", + false, + INDEXED, + BitDepth::Eight, + RGBA, + BitDepth::Eight, + ); +} + #[test] fn palette_2_should_be_palette_2() { test_it_converts( "tests/files/palette_2_should_be_palette_2.png", false, - ColorType::Indexed, + INDEXED, BitDepth::Two, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -567,9 +609,9 @@ fn palette_8_should_be_palette_1() { test_it_converts( "tests/files/palette_8_should_be_palette_1.png", false, - ColorType::Indexed, + INDEXED, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -579,9 +621,9 @@ fn palette_4_should_be_palette_1() { test_it_converts( "tests/files/palette_4_should_be_palette_1.png", false, - ColorType::Indexed, + INDEXED, BitDepth::Four, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -591,9 +633,9 @@ fn palette_2_should_be_palette_1() { test_it_converts( "tests/files/palette_2_should_be_palette_1.png", false, - ColorType::Indexed, + INDEXED, BitDepth::Two, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -603,9 +645,9 @@ fn palette_1_should_be_palette_1() { test_it_converts( "tests/files/palette_1_should_be_palette_1.png", false, - ColorType::Indexed, + INDEXED, BitDepth::One, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -615,9 +657,9 @@ fn grayscale_alpha_16_should_be_grayscale_alpha_16() { test_it_converts( "tests/files/grayscale_alpha_16_should_be_grayscale_alpha_16.png", false, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, ); } @@ -627,9 +669,9 @@ fn grayscale_alpha_16_should_be_grayscale_alpha_8() { test_it_converts( "tests/files/grayscale_alpha_16_should_be_grayscale_alpha_8.png", false, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -639,9 +681,9 @@ fn grayscale_alpha_8_should_be_grayscale_alpha_8() { test_it_converts( "tests/files/grayscale_alpha_8_should_be_grayscale_alpha_8.png", false, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -651,9 +693,9 @@ fn grayscale_alpha_16_should_be_grayscale_16() { test_it_converts( "tests/files/grayscale_alpha_16_should_be_grayscale_16.png", false, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, ); } @@ -663,9 +705,9 @@ fn grayscale_alpha_16_should_be_grayscale_8() { test_it_converts( "tests/files/grayscale_alpha_16_should_be_grayscale_8.png", false, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -675,9 +717,9 @@ fn grayscale_alpha_8_should_be_grayscale_8() { test_it_converts( "tests/files/grayscale_alpha_8_should_be_grayscale_8.png", false, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -687,9 +729,9 @@ fn grayscale_16_should_be_grayscale_16() { test_it_converts( "tests/files/grayscale_16_should_be_grayscale_16.png", false, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, ); } @@ -699,21 +741,33 @@ fn grayscale_16_should_be_grayscale_8() { test_it_converts( "tests/files/grayscale_16_should_be_grayscale_8.png", false, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } +#[test] +fn grayscale_16_should_be_grayscale_1() { + test_it_converts( + "tests/files/grayscale_16_should_be_grayscale_1.png", + false, + GRAYSCALE, + BitDepth::Sixteen, + GRAYSCALE, + BitDepth::One, + ); +} + #[test] fn grayscale_8_should_be_grayscale_8() { test_it_converts( "tests/files/grayscale_8_should_be_grayscale_8.png", false, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -723,9 +777,9 @@ fn grayscale_8_should_be_grayscale_4() { test_it_converts( "tests/files/grayscale_8_should_be_grayscale_4.png", false, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Four, ); } @@ -735,9 +789,9 @@ fn grayscale_8_should_be_grayscale_2() { test_it_converts( "tests/files/grayscale_8_should_be_grayscale_2.png", false, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Two, ); } @@ -747,9 +801,9 @@ fn grayscale_4_should_be_grayscale_2() { test_it_converts( "tests/files/grayscale_4_should_be_grayscale_2.png", false, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Four, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Two, ); } @@ -759,9 +813,9 @@ fn grayscale_8_should_be_grayscale_1() { test_it_converts( "tests/files/grayscale_8_should_be_grayscale_1.png", false, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::One, ); } @@ -771,9 +825,9 @@ fn grayscale_4_should_be_grayscale_1() { test_it_converts( "tests/files/grayscale_4_should_be_grayscale_1.png", false, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Four, - ColorType::Grayscale, + GRAYSCALE, BitDepth::One, ); } @@ -783,9 +837,57 @@ fn grayscale_2_should_be_grayscale_1() { test_it_converts( "tests/files/grayscale_2_should_be_grayscale_1.png", false, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Two, - ColorType::Grayscale, + GRAYSCALE, + BitDepth::One, + ); +} + +#[test] +fn grayscale_8_should_be_palette_8() { + test_it_converts( + "tests/files/grayscale_8_should_be_palette_8.png", + false, + GRAYSCALE, + BitDepth::Eight, + INDEXED, + BitDepth::Eight, + ); +} + +#[test] +fn grayscale_8_should_be_palette_4() { + test_it_converts( + "tests/files/grayscale_8_should_be_palette_4.png", + false, + GRAYSCALE, + BitDepth::Eight, + INDEXED, + BitDepth::Four, + ); +} + +#[test] +fn grayscale_8_should_be_palette_2() { + test_it_converts( + "tests/files/grayscale_8_should_be_palette_2.png", + false, + GRAYSCALE, + BitDepth::Eight, + INDEXED, + BitDepth::Two, + ); +} + +#[test] +fn grayscale_8_should_be_palette_1() { + test_it_converts( + "tests/files/grayscale_8_should_be_palette_1.png", + false, + GRAYSCALE, + BitDepth::Eight, + INDEXED, BitDepth::One, ); } @@ -795,9 +897,9 @@ fn grayscale_alpha_16_should_be_grayscale_trns_16() { test_it_converts( "tests/files/grayscale_alpha_16_should_be_grayscale_trns_16.png", true, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Sixteen, ); } @@ -807,42 +909,23 @@ fn grayscale_alpha_8_should_be_grayscale_trns_8() { test_it_converts( "tests/files/grayscale_alpha_8_should_be_grayscale_trns_8.png", true, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } #[test] fn small_files() { - let input = PathBuf::from("tests/files/small_files.png"); - let (output, opts) = get_opts(&input); - - let png = PngData::new(&input, opts.fix_errors).unwrap(); - - assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); - assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); - - match oxipng::optimize(&InFile::Path(input), &output, &opts) { - Ok(_) => (), - Err(x) => panic!("{}", x), - }; - let output = output.path().unwrap(); - assert!(output.exists()); - - let png = match PngData::new(output, opts.fix_errors) { - Ok(x) => x, - Err(x) => { - remove_file(&output).ok(); - panic!("{}", x) - } - }; - - assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); - // depth varies depending on zlib implementation used - - remove_file(output).ok(); + test_it_converts( + "tests/files/small_files.png", + false, + INDEXED, + BitDepth::Eight, + RGB, + BitDepth::Eight, + ); } #[test] @@ -850,11 +933,13 @@ fn palette_should_be_reduced_with_dupes() { let input = PathBuf::from("tests/files/palette_should_be_reduced_with_dupes.png"); let (output, opts) = get_opts(&input); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); - assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); - assert_eq!(png.raw.palette.as_ref().unwrap().len(), 43); + if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type { + assert_eq!(palette.len(), 43); + } match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -863,17 +948,19 @@ fn palette_should_be_reduced_with_dupes() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { - remove_file(&output).ok(); + remove_file(output).ok(); panic!("{}", x) } }; - assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); - assert_eq!(png.raw.palette.as_ref().unwrap().len(), 35); + if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type { + assert_eq!(palette.len(), 35); + } remove_file(output).ok(); } @@ -883,11 +970,13 @@ fn palette_should_be_reduced_with_unused() { let input = PathBuf::from("tests/files/palette_should_be_reduced_with_unused.png"); let (output, opts) = get_opts(&input); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); - assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); - assert_eq!(png.raw.palette.as_ref().unwrap().len(), 35); + if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type { + assert_eq!(palette.len(), 35); + } match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -896,17 +985,19 @@ fn palette_should_be_reduced_with_unused() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { - remove_file(&output).ok(); + remove_file(output).ok(); panic!("{}", x) } }; - assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); - assert_eq!(png.raw.palette.as_ref().unwrap().len(), 33); + if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type { + assert_eq!(palette.len(), 33); + } remove_file(output).ok(); } @@ -916,11 +1007,13 @@ fn palette_should_be_reduced_with_both() { let input = PathBuf::from("tests/files/palette_should_be_reduced_with_both.png"); let (output, opts) = get_opts(&input); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); - assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); - assert_eq!(png.raw.palette.as_ref().unwrap().len(), 43); + if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type { + assert_eq!(palette.len(), 43); + } match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -929,17 +1022,19 @@ fn palette_should_be_reduced_with_both() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { - remove_file(&output).ok(); + remove_file(output).ok(); panic!("{}", x) } }; - assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); - assert_eq!(png.raw.palette.as_ref().unwrap().len(), 33); + if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type { + assert_eq!(palette.len(), 33); + } remove_file(output).ok(); } @@ -949,9 +1044,9 @@ fn rgba_16_reduce_alpha() { test_it_converts( "tests/files/rgba_16_reduce_alpha.png", true, - ColorType::RGBA, + RGBA, BitDepth::Sixteen, - ColorType::RGBA, + RGBA, BitDepth::Eight, ); } @@ -961,9 +1056,9 @@ fn rgba_8_reduce_alpha() { test_it_converts( "tests/files/rgba_8_reduce_alpha.png", true, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGBA, + RGBA, BitDepth::Eight, ); } @@ -973,9 +1068,9 @@ fn grayscale_alpha_16_reduce_alpha() { test_it_converts( "tests/files/grayscale_alpha_16_reduce_alpha.png", true, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Sixteen, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -985,9 +1080,9 @@ fn grayscale_alpha_8_reduce_alpha() { test_it_converts( "tests/files/grayscale_alpha_8_reduce_alpha.png", true, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } diff --git a/tests/regression.rs b/tests/regression.rs index e88cd077..c84a25dd 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -1,10 +1,16 @@ use indexmap::IndexSet; -use oxipng::{internal_tests::*, Interlacing, RowFilter}; -use oxipng::{InFile, OutFile}; +use oxipng::internal_tests::*; +use oxipng::*; use std::fs::remove_file; use std::path::Path; use std::path::PathBuf; +const GRAYSCALE: u8 = 0; +const RGB: u8 = 2; +const INDEXED: u8 = 3; +const GRAYSCALE_ALPHA: u8 = 4; +const RGBA: u8 = 6; + fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { let mut options = oxipng::Options { force: true, @@ -23,17 +29,18 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { fn test_it_converts( input: &str, custom: Option<(OutFile, oxipng::Options)>, - color_type_in: ColorType, + color_type_in: u8, bit_depth_in: BitDepth, - color_type_out: ColorType, + color_type_out: u8, bit_depth_out: BitDepth, ) { let input = PathBuf::from(input); let (output, opts) = custom.unwrap_or_else(|| get_opts(&input)); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!( - png.raw.ihdr.color_type, color_type_in, + png.raw.ihdr.color_type.png_header_code(), + color_type_in, "test file is broken" ); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in, "test file is broken"); @@ -45,7 +52,7 @@ fn test_it_converts( let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -54,23 +61,16 @@ fn test_it_converts( }; assert_eq!( - png.raw.ihdr.color_type, color_type_out, + png.raw.ihdr.color_type.png_header_code(), + color_type_out, "optimized to wrong color type" ); assert_eq!( png.raw.ihdr.bit_depth, bit_depth_out, "optimized to wrong bit depth" ); - if let Some(palette) = png.raw.palette.as_ref() { - let mut max_palette_size = 1 << (png.raw.ihdr.bit_depth.as_u8() as usize); - // Ensure bKGD color is valid - if let Some(&idx) = png.raw.aux_headers.get(b"bKGD").and_then(|b| b.first()) { - assert!(palette.len() > idx as usize); - max_palette_size = max_palette_size.max(idx as usize + 1); - } - assert!(palette.len() <= max_palette_size); - } else { - assert_ne!(png.raw.ihdr.color_type, ColorType::Indexed); + if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type { + assert!(palette.len() <= 1 << (png.raw.ihdr.bit_depth as u8)); } remove_file(output).ok(); @@ -81,9 +81,9 @@ fn issue_29() { test_it_converts( "tests/files/issue-29.png", None, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -94,7 +94,7 @@ fn issue_42() { let (output, mut opts) = get_opts(&input); opts.interlace = Some(Interlacing::Adam7); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); assert_eq!(png.raw.ihdr.interlaced, Interlacing::None); assert_eq!(png.raw.ihdr.color_type, ColorType::GrayscaleAlpha); @@ -107,7 +107,7 @@ fn issue_42() { let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -127,9 +127,9 @@ fn issue_52_01() { test_it_converts( "tests/files/issue-52-01.png", None, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); } @@ -139,9 +139,9 @@ fn issue_52_02() { test_it_converts( "tests/files/issue-52-02.png", None, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + RGBA, BitDepth::Eight, ); } @@ -151,9 +151,9 @@ fn issue_52_03() { test_it_converts( "tests/files/issue-52-03.png", None, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + RGBA, BitDepth::Eight, ); } @@ -163,10 +163,10 @@ fn issue_52_04() { test_it_converts( "tests/files/issue-52-04.png", None, - ColorType::RGBA, + RGBA, + BitDepth::Eight, + RGB, BitDepth::Eight, - ColorType::Indexed, - BitDepth::One, ); } @@ -175,9 +175,9 @@ fn issue_52_05() { test_it_converts( "tests/files/issue-52-05.png", None, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -187,10 +187,10 @@ fn issue_52_06() { test_it_converts( "tests/files/issue-52-06.png", None, - ColorType::RGBA, + RGBA, + BitDepth::Eight, + RGBA, BitDepth::Eight, - ColorType::Indexed, - BitDepth::Two, ); } @@ -199,9 +199,9 @@ fn issue_56() { test_it_converts( "tests/files/issue-56.png", None, - ColorType::Indexed, + INDEXED, BitDepth::Four, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -211,9 +211,9 @@ fn issue_58() { test_it_converts( "tests/files/issue-58.png", None, - ColorType::Indexed, + INDEXED, BitDepth::Four, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -223,9 +223,9 @@ fn issue_59() { test_it_converts( "tests/files/issue-59.png", None, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGBA, + INDEXED, BitDepth::Eight, ); } @@ -235,9 +235,9 @@ fn issue_60() { test_it_converts( "tests/files/issue-60.png", None, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -247,9 +247,9 @@ fn issue_80() { test_it_converts( "tests/files/issue-80.png", None, - ColorType::Indexed, + INDEXED, BitDepth::Two, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -259,9 +259,9 @@ fn issue_82() { test_it_converts( "tests/files/issue-82.png", None, - ColorType::Indexed, + INDEXED, BitDepth::Four, - ColorType::Indexed, + INDEXED, BitDepth::Four, ); } @@ -271,9 +271,9 @@ fn issue_89() { test_it_converts( "tests/files/issue-89.png", None, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -283,9 +283,9 @@ fn issue_92_filter_0() { test_it_converts( "tests/files/issue-92.png", None, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -300,9 +300,9 @@ fn issue_92_filter_5() { test_it_converts( input, Some((output, opts)), - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -316,9 +316,9 @@ fn issue_113() { test_it_converts( input, Some((output, opts)), - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -326,14 +326,7 @@ fn issue_113() { #[test] fn issue_129() { let input = "tests/files/issue-129.png"; - test_it_converts( - input, - None, - ColorType::RGB, - BitDepth::Eight, - ColorType::Indexed, - BitDepth::Eight, - ); + test_it_converts(input, None, RGB, BitDepth::Eight, INDEXED, BitDepth::Eight); } #[test] @@ -344,9 +337,9 @@ fn issue_133() { test_it_converts( input, Some((output, opts)), - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGBA, + RGBA, BitDepth::Eight, ); } @@ -356,9 +349,9 @@ fn issue_140() { test_it_converts( "tests/files/issue-140.png", None, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Two, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Two, ); } @@ -368,9 +361,9 @@ fn issue_141() { test_it_converts( "tests/files/issue-141.png", None, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -380,9 +373,9 @@ fn issue_153() { test_it_converts( "tests/files/issue-153.png", None, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); } @@ -392,9 +385,9 @@ fn issue_159() { test_it_converts( "tests/files/issue-159.png", None, - ColorType::Indexed, + INDEXED, BitDepth::One, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -404,9 +397,9 @@ fn issue_171() { test_it_converts( "tests/files/issue-171.png", None, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -416,9 +409,9 @@ fn issue_175() { test_it_converts( "tests/files/issue-175.png", None, - ColorType::Grayscale, + GRAYSCALE, BitDepth::One, - ColorType::Grayscale, + GRAYSCALE, BitDepth::One, ); } @@ -432,9 +425,9 @@ fn issue_182() { test_it_converts( input, Some((output, opts)), - ColorType::Grayscale, + GRAYSCALE, BitDepth::One, - ColorType::Grayscale, + GRAYSCALE, BitDepth::One, ); } @@ -444,9 +437,9 @@ fn issue_195() { test_it_converts( "tests/files/issue-195.png", None, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); } @@ -456,9 +449,9 @@ fn issue_426_01() { test_it_converts( "tests/files/issue-426-01.png", None, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::One, ); } @@ -468,9 +461,9 @@ fn issue_426_02() { test_it_converts( "tests/files/issue-426-02.png", None, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::One, ); } diff --git a/tests/strategies.rs b/tests/strategies.rs index aa5d80f4..8b4b9478 100644 --- a/tests/strategies.rs +++ b/tests/strategies.rs @@ -1,10 +1,15 @@ use indexmap::IndexSet; -use oxipng::{internal_tests::*, RowFilter}; -use oxipng::{InFile, OutFile}; +use oxipng::internal_tests::*; +use oxipng::*; use std::fs::remove_file; use std::path::Path; use std::path::PathBuf; +const GRAYSCALE: u8 = 0; +const RGB: u8 = 2; +const INDEXED: u8 = 3; +const RGBA: u8 = 6; + fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { let mut options = oxipng::Options { force: true, @@ -23,18 +28,18 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { fn test_it_converts( input: &str, filter: RowFilter, - color_type_in: ColorType, + color_type_in: u8, bit_depth_in: BitDepth, - color_type_out: ColorType, + color_type_out: u8, bit_depth_out: BitDepth, ) { let input = PathBuf::from(input); let (output, mut opts) = get_opts(&input); - let png = PngData::new(&input, opts.fix_errors).unwrap(); + let png = PngData::new(&input, &opts).unwrap(); opts.filter = IndexSet::new(); opts.filter.insert(filter); - assert_eq!(png.raw.ihdr.color_type, color_type_in); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); match oxipng::optimize(&InFile::Path(input), &output, &opts) { @@ -44,7 +49,7 @@ fn test_it_converts( let output = output.path().unwrap(); assert!(output.exists()); - let png = match PngData::new(output, opts.fix_errors) { + let png = match PngData::new(output, &opts) { Ok(x) => x, Err(x) => { remove_file(output).ok(); @@ -52,12 +57,10 @@ fn test_it_converts( } }; - assert_eq!(png.raw.ihdr.color_type, color_type_out); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_out); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out); - if let Some(palette) = png.raw.palette.as_ref() { - assert!(palette.len() <= 1 << (png.raw.ihdr.bit_depth.as_u8() as usize)); - } else { - assert_ne!(png.raw.ihdr.color_type, ColorType::Indexed); + if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type { + assert!(palette.len() <= 1 << (png.raw.ihdr.bit_depth as u8)); } remove_file(output).ok(); @@ -68,9 +71,9 @@ fn filter_minsum() { test_it_converts( "tests/files/rgb_16_should_be_rgb_16.png", RowFilter::MinSum, - ColorType::RGB, + RGB, BitDepth::Sixteen, - ColorType::RGB, + RGB, BitDepth::Sixteen, ); } @@ -80,9 +83,9 @@ fn filter_entropy() { test_it_converts( "tests/files/rgb_8_should_be_rgb_8.png", RowFilter::Entropy, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -92,9 +95,9 @@ fn filter_bigrams() { test_it_converts( "tests/files/rgba_8_should_be_rgba_8.png", RowFilter::Bigrams, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGBA, + RGBA, BitDepth::Eight, ); } @@ -104,9 +107,9 @@ fn filter_bigent() { test_it_converts( "tests/files/grayscale_8_should_be_grayscale_8.png", RowFilter::BigEnt, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, - ColorType::Grayscale, + GRAYSCALE, BitDepth::Eight, ); } @@ -116,9 +119,9 @@ fn filter_brute() { test_it_converts( "tests/files/palette_8_should_be_palette_8.png", RowFilter::Brute, - ColorType::Indexed, + INDEXED, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); }