From a234c39e41177d7b1810d7bb0fa6a3f84b19590a Mon Sep 17 00:00:00 2001 From: Josh Holmer Date: Sat, 22 Apr 2023 22:25:06 -0400 Subject: [PATCH 01/18] Fix new annoyingly pedantic clippy warnings --- Cargo.lock | 10 ---------- src/reduction/mod.rs | 12 ++++++------ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 49d2b1ad..9b8d1e67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -265,15 +265,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c397ca3ea05ad509c4ec451fea28b4771236a376ca1c69fd5143aae0cf8f93c4" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "libc" version = "0.2.139" @@ -387,7 +378,6 @@ dependencies = [ "filetime", "image", "indexmap", - "itertools", "libdeflater", "log", "rayon", diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index 9bedcf62..defd0eab 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -158,16 +158,16 @@ fn palette_map_to_byte_map(png: &PngImage, palette_map: &[Option; 256]) -> O } 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); + 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); + 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); } } _ => {} From 110eae7c1997e8d335bb6b07bf6d604fdbfb217e Mon Sep 17 00:00:00 2001 From: XhmikosR Date: Fri, 7 Apr 2023 08:54:30 +0300 Subject: [PATCH 02/18] Update .editorconfig --- .editorconfig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 From be19ed592d3963e819707e4a9859abd9e43393f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= <7822554+AlexTMjugador@users.noreply.github.com> Date: Mon, 24 Apr 2023 19:24:23 +0200 Subject: [PATCH 03/18] Make dependency on `image` optional (#498) * Make dependency on `image` optional After PR https://github.com/shssoichiro/oxipng/pull/481 was merged, the `image` dependency became unused when building with debug assertions disabled, as it is only used to implement output sanity checks when such assertions are enabled. The `image` crate transitively pulls a significant amount of dependencies, so it's useful for OxiPNG users to get rid of them when not needed. [Cargo does not allow specifying dependencies that are only pulled when debug assertions are enabled](https://github.com/rust-lang/cargo/issues/7634), so the next best way to give users some flexibility is to gate those debug assertions behind a feature flag. These changes add a `sanity-checks` feature flag that controls whether the `image` crate and the related sanity checks are compiled in. This feature is enabled by default to keep debug builds useful to catch problems during development. * Fix Clippy lints * Run tests with new sanity-checks feature enabled --- .github/workflows/oxipng.yml | 2 +- Cargo.toml | 6 +++ src/lib.rs | 94 +++++++++++++++++++----------------- src/rayon.rs | 1 + tests/flags.rs | 9 ++-- tests/reduction.rs | 8 +-- 6 files changed, 68 insertions(+), 52 deletions(-) diff --git a/.github/workflows/oxipng.yml b/.github/workflows/oxipng.yml index dfdb48bf..495efd22 100644 --- a/.github/workflows/oxipng.yml +++ b/.github/workflows/oxipng.yml @@ -78,7 +78,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.toml b/Cargo.toml index 8f4a214d..443f81d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,10 @@ name = "oxipng" path = "src/main.rs" required-features = ["binary"] +[[bench]] +name = "zopfli" +required-features = ["zopfli"] + [dependencies] zopfli = { version = "0.7.1", optional = true } rgb = "0.8.33" @@ -53,6 +57,7 @@ optional = true version = "2.1.0" [dependencies.image] +optional = true default-features = false features = ["png"] version = "0.24.3" @@ -65,6 +70,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/src/lib.rs b/src/lib.rs index 614246ce..0f5b637f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,12 +31,11 @@ use crate::evaluate::Evaluator; 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, 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; @@ -659,7 +658,8 @@ fn optimize_png( ); } - debug_assert!(validate_output(&output, original_data)); + #[cfg(feature = "sanity-checks")] + debug_assert!(sanity_checks::validate_output(&output, original_data)); Ok(output) } @@ -1044,46 +1044,54 @@ 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), - ); +#[cfg(feature = "sanity-checks")] +mod sanity_checks { + use super::*; + use image::{DynamicImage, GenericImageView, ImageFormat, Pixel}; + use log::error; + use std::io::Cursor; - match (new_png, old_png) { - (Err(new_err), _) => { - error!("Failed to read output image for validation: {}", new_err); - false + /// Validate that the output png data still matches the original image + pub(super) 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), } - (_, 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) } } - -/// 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/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/tests/flags.rs b/tests/flags.rs index 38db85b7..fc7ace9e 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -27,6 +27,7 @@ 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, @@ -38,8 +39,8 @@ fn test_it_converts_callbacks( 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(); @@ -48,14 +49,14 @@ fn test_it_converts_callbacks( 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) { Ok(x) => x, diff --git a/tests/reduction.rs b/tests/reduction.rs index f3afdb88..d88cc58c 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -834,7 +834,7 @@ fn small_files() { let png = match PngData::new(output, opts.fix_errors) { Ok(x) => x, Err(x) => { - remove_file(&output).ok(); + remove_file(output).ok(); panic!("{}", x) } }; @@ -866,7 +866,7 @@ fn palette_should_be_reduced_with_dupes() { let png = match PngData::new(output, opts.fix_errors) { Ok(x) => x, Err(x) => { - remove_file(&output).ok(); + remove_file(output).ok(); panic!("{}", x) } }; @@ -899,7 +899,7 @@ fn palette_should_be_reduced_with_unused() { let png = match PngData::new(output, opts.fix_errors) { Ok(x) => x, Err(x) => { - remove_file(&output).ok(); + remove_file(output).ok(); panic!("{}", x) } }; @@ -932,7 +932,7 @@ fn palette_should_be_reduced_with_both() { let png = match PngData::new(output, opts.fix_errors) { Ok(x) => x, Err(x) => { - remove_file(&output).ok(); + remove_file(output).ok(); panic!("{}", x) } }; From e88b9be12b68a8daf3dbc940333e3394ddb685bc Mon Sep 17 00:00:00 2001 From: XhmikosR Date: Fri, 7 Apr 2023 08:48:18 +0300 Subject: [PATCH 04/18] Update CI * add `workflow_dispatch` for manual running * add `fail-fast` false in matrix * specify `persist-credentials: false` for actions/checkout * update actions to the latest versions * move cache before installing the toolchain * cache more stuff * limit deploy to shssoichiro/oxipng repository * reindent --- .github/workflows/deploy.yml | 176 ++++++++++++++++++----------------- .github/workflows/oxipng.yml | 27 ++++-- 2 files changed, 108 insertions(+), 95 deletions(-) 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 495efd22..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 From 36af4198ed37713ae1376157e0e0e7f1ddd1d11c Mon Sep 17 00:00:00 2001 From: XhmikosR Date: Fri, 7 Apr 2023 08:52:07 +0300 Subject: [PATCH 05/18] Enforce LF On Windows `autocrlf` is `true` by default. This makes sure LF is used everywhere. --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattributes 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 From c97572be3e10fac85361c0ee800e59517c901c1c Mon Sep 17 00:00:00 2001 From: andrews05 Date: Fri, 28 Apr 2023 11:49:54 +1200 Subject: [PATCH 06/18] ColorType Refactor (#500) * Refactor ColorType Move transparency and palette data into the ColorType * Fixup tests * Make more use of helper functions * Change BitDepth to u8 representation with TryFrom * Fix clippy lints * Don't use unstable language features * Restore documentation on transparency/palette --- src/colors.rs | 120 +++++++------- src/headers.rs | 63 ++++++-- src/interlace.rs | 30 ++-- src/lib.rs | 6 +- src/png/mod.rs | 147 ++++++++--------- src/png/scan_lines.rs | 8 +- src/reduction/alpha.rs | 65 ++++---- src/reduction/bit_depth.rs | 45 +++--- src/reduction/color.rs | 48 +++--- src/reduction/mod.rs | 48 +++--- tests/filters.rs | 284 ++++++++++++++++---------------- tests/flags.rs | 41 ++--- tests/interlaced.rs | 234 ++++++++++++++------------- tests/interlacing.rs | 59 +++---- tests/reduction.rs | 322 ++++++++++++++++++++----------------- tests/regression.rs | 143 ++++++++-------- tests/strategies.rs | 39 ++--- 17 files changed, 866 insertions(+), 836 deletions(-) diff --git a/src/colors.rs b/src/colors.rs index e339092b..32999849 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -1,14 +1,26 @@ +use rgb::{RGB16, RGBA8}; use std::fmt; -#[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 @@ -22,9 +34,9 @@ impl fmt::Display for ColorType { f, "{}", match *self { - ColorType::Grayscale => "Grayscale", - ColorType::RGB => "RGB", - ColorType::Indexed => "Indexed", + ColorType::Grayscale { .. } => "Grayscale", + ColorType::RGB { .. } => "RGB", + ColorType::Indexed { .. } => "Indexed", ColorType::GrayscaleAlpha => "Grayscale + Alpha", ColorType::RGBA => "RGB + Alpha", } @@ -35,85 +47,71 @@ impl fmt::Display for ColorType { 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 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 fn is_rgb(&self) -> bool { + matches!(self, ColorType::RGB { .. } | ColorType::RGBA) + } + + #[inline] + pub fn has_alpha(&self) -> bool { + matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA) + } } +#[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 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 fmt::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"), - } + write!(f, "{}", *self as u8) } } diff --git a/src/headers.rs b/src/headers.rs index 7fdeef1e..a3e2b4f9 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -4,10 +4,11 @@ use crate::error::PngError; use crate::interlace::Interlacing; use crate::PngResult; use indexmap::IndexSet; +use rgb::{RGB16, RGBA8}; use std::io; use std::io::{Cursor, Read}; -#[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 @@ -30,8 +31,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 +42,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 { @@ -143,27 +144,36 @@ pub fn parse_next_header<'a>( Ok(Some(RawHeader { name, data })) } -pub fn parse_ihdr_header(byte_data: &[u8]) -> PngResult { +pub fn parse_ihdr_header( + 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")), - }, + bit_depth: byte_data[8].try_into()?, width: read_be_u32(&mut rdr).map_err(|_| PngError::TruncatedData)?, height: read_be_u32(&mut rdr).map_err(|_| PngError::TruncatedData)?, compression: byte_data[10], @@ -172,6 +182,25 @@ pub fn parse_ihdr_header(byte_data: &[u8]) -> PngResult { }) } +/// 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>(rdr: &mut Cursor) -> Result { let mut int_buf = [0; 4]; diff --git a/src/interlace.rs b/src/interlace.rs index 8ac0fdc7..88c146ff 100644 --- a/src/interlace.rs +++ b/src/interlace.rs @@ -45,11 +45,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 +87,11 @@ 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 +102,18 @@ 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 +126,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 +161,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 +170,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 +188,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 0f5b637f..d88746b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,7 @@ extern crate rayon; mod rayon; use crate::atomicmin::AtomicMin; -use crate::colors::BitDepth; +use crate::colors::{BitDepth, ColorType}; use crate::deflate::{crc32, inflate}; use crate::evaluate::Evaluator; use crate::png::PngData; @@ -569,7 +569,7 @@ fn optimize_png( if filters.is_empty() { // Pick a filter automatically - if png.raw.ihdr.bit_depth.as_u8() >= 8 { + 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 { @@ -814,7 +814,7 @@ impl Deadline { /// Display the format of the image data fn report_format(prefix: &str, png: &PngImage) { - if let Some(ref palette) = png.palette { + if let ColorType::Indexed { palette } = &png.ihdr.color_type { debug!( "{}{} bits/pixel, {} colors in palette ({})", prefix, diff --git a/src/png/mod.rs b/src/png/mod.rs index f3d712d0..32870581 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -1,4 +1,4 @@ -use crate::colors::ColorType; +use crate::colors::{BitDepth, ColorType}; use crate::deflate; use crate::error::PngError; use crate::filters::*; @@ -8,7 +8,6 @@ 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,11 +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>, } @@ -51,8 +45,6 @@ pub struct PngData { pub filtered: Vec, } -type PaletteWithTrns = (Option>, Option>); - impl PngData { /// Create a new `PngData` struct by opening a file #[inline] @@ -116,7 +108,11 @@ impl PngData { Some(ihdr) => ihdr, None => return Err(PngError::ChunkMissing("IHDR")), }; - let ihdr_header = parse_ihdr_header(&ihdr)?; + let ihdr_header = parse_ihdr_header( + &ihdr, + aux_headers.remove(b"PLTE"), + aux_headers.remove(b"tRNS"), + )?; let raw_data = deflate::inflate(idat_headers.as_ref(), ihdr_header.raw_data_size())?; // Reject files with incorrect width/height or truncated data @@ -124,17 +120,9 @@ impl PngData { 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, data: raw_data, - palette, - transparency_pixel, aux_headers, }; let unfiltered = raw.unfilter_image()?; @@ -146,31 +134,6 @@ impl PngData { }) } - /// 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)) - } - } - /// Format the `PngData` struct into a valid PNG bytestream pub fn output(&self) -> Vec { // PNG header @@ -181,7 +144,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(); @@ -198,40 +161,49 @@ impl PngData { { write_png_block(key, header, &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); + let mut max_palette_size = 1 << (self.raw.ihdr.bit_depth as u8); + // 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); + } + for px in palette.iter().take(max_palette_size) { + palette_data.extend_from_slice(px.rgb().as_slice()); + } + 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(); + 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 @@ -274,8 +246,18 @@ 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, + } } /// Return an iterator over the scanlines of the image @@ -287,7 +269,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 +291,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(); 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/reduction/alpha.rs b/src/reduction/alpha.rs index cf8c22ad..4b9fd439 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,18 @@ 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(), + ihdr: png.ihdr.clone(), aux_headers: png.aux_headers.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 +59,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,13 +67,27 @@ 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]), }; } + // 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)), + }, + }; + let mut aux_headers = png.aux_headers.clone(); // sBIT contains information about alpha channel's original depth, // and alpha has just been removed @@ -102,7 +103,5 @@ pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option Option { if png.ihdr.bit_depth != BitDepth::Sixteen { - if png.ihdr.color_type == ColorType::Indexed || png.ihdr.color_type == ColorType::Grayscale - { + if png.channels_per_pixel() == 1 { return reduce_bit_depth_8_or_less(png, minimum_bits); } return None; @@ -23,11 +21,10 @@ 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(), }) } @@ -35,14 +32,14 @@ pub fn reduce_bit_depth(png: &PngImage, minimum_bits: usize) -> Option #[must_use] pub fn reduce_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; + let bit_depth = png.ihdr.bit_depth as usize; if minimum_bits >= bit_depth || bit_depth > 8 { return None; } // Calculate the current number of pixels per byte let ppb = 8 / bit_depth; - if png.ihdr.color_type == ColorType::Indexed { + if let ColorType::Indexed { .. } = png.ihdr.color_type { for line in png.scan_lines(false) { let line_max = line .data @@ -129,12 +126,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 +138,25 @@ 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..e8d5e73f 100644 --- a/src/reduction/color.rs +++ b/src/reduction/color.rs @@ -2,7 +2,7 @@ use crate::colors::{BitDepth, ColorType}; use crate::headers::IhdrData; use crate::png::PngImage; use indexmap::IndexMap; -use rgb::{FromSlice, RGB8, RGBA, RGBA8}; +use rgb::{ComponentMap, FromSlice, RGBA, RGBA8}; use rustc_hash::FxHasher; use std::hash::{BuildHasherDefault, Hash}; @@ -41,12 +41,9 @@ pub fn reduce_to_palette(png: &PngImage) -> Option { 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 { + let ok = if let ColorType::RGB { transparent_color } = png.ihdr.color_type { + // Convert the RGB16 transparency to RGB8 + let transparency_pixel = transparent_color.map(|t| t.map(|c| c as u8)); reduce_scanline_to_palette( png.data.as_rgb().iter().cloned().map(|px| { px.alpha(if Some(px) != transparency_pixel { @@ -132,20 +129,20 @@ pub fn reduce_to_palette(png: &PngImage) -> Option { Some(PngImage { data: raw_data, ihdr: IhdrData { - color_type: ColorType::Indexed, + color_type: ColorType::Indexed { + palette: palette_vec, + }, ..png.ihdr }, aux_headers, - transparency_pixel: None, - palette: Some(palette_vec), }) } #[must_use] pub fn reduce_rgb_to_grayscale(png: &PngImage) -> Option { 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,16 +155,6 @@ 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 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() { @@ -180,17 +167,22 @@ pub fn reduce_rgb_to_grayscale(png: &PngImage) -> Option { } } + 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, + }; + 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, }) } diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index defd0eab..8154189c 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -18,10 +18,11 @@ pub(crate) use crate::bit_depth::reduce_bit_depth; /// 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 { + let palette = match &png.ihdr.color_type { + ColorType::Indexed { palette } => palette, // Can't reduce if there is no palette - return None; - } + _ => 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 @@ -31,8 +32,6 @@ pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option 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 => { @@ -109,11 +108,15 @@ pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option } } - do_palette_reduction(png, &palette_map) + do_palette_reduction(png, palette, &palette_map) } #[must_use] -fn do_palette_reduction(png: &PngImage, palette_map: &[Option; 256]) -> Option { +fn do_palette_reduction( + png: &PngImage, + palette: &[RGBA8], + palette_map: &[Option; 256], +) -> Option { let byte_map = palette_map_to_byte_map(png, palette_map)?; // Reassign data bytes to new indices @@ -131,12 +134,12 @@ fn do_palette_reduction(png: &PngImage, palette_map: &[Option; 256]) -> Opti Some(PngImage { ihdr: IhdrData { - color_type: ColorType::Indexed, + color_type: ColorType::Indexed { + palette: reordered_palette(palette, palette_map), + }, ..png.ihdr }, data: raw_data, - transparency_pixel: None, - palette: Some(reordered_palette(png.palette.as_ref()?, palette_map)), aux_headers, }) } @@ -187,22 +190,20 @@ fn reordered_palette(palette: &[RGBA8], palette_map: &[Option; 256]) -> Vec< new_palette } -/// Attempt to reduce the color type of the image -/// Returns true if the color type was reduced, false otherwise +/// Attempt to reduce the color type of the image, returning the reduced image if successful pub fn reduce_color_type( png: &PngImage, grayscale_reduction: bool, optimize_alpha: bool, ) -> Option { - let mut should_reduce_bit_depth = false; + let was_single_channel = png.channels_per_pixel() == 1; 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) { + // Go down one step at a time - maybe not the most efficient, but it's safe + // Attempt to reduce RGB to grayscale + if grayscale_reduction && reduced.ihdr.color_type.is_rgb() { if let Some(r) = reduce_rgb_to_grayscale(&reduced) { reduced = Cow::Owned(r); - should_reduce_bit_depth = reduced.ihdr.color_type == ColorType::Grayscale; } } @@ -210,17 +211,13 @@ pub fn reduce_color_type( 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 - ) { + // Attempt to reduce to palette, if not already a single channel + if reduced.channels_per_pixel() != 1 { 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) { @@ -236,9 +233,8 @@ pub fn reduce_color_type( } } - if should_reduce_bit_depth { - // Some conversions will allow us to perform bit depth reduction that - // wasn't possible before + // Some conversions will allow us to perform bit depth reduction that wasn't possible before + if !was_single_channel && reduced.channels_per_pixel() == 1 { if let Some(r) = reduce_bit_depth_8_or_less(&reduced, 1) { reduced = Cow::Owned(r); } diff --git a/tests/filters.rs b/tests/filters.rs index e1dbad40..065c1259 100644 --- a/tests/filters.rs +++ b/tests/filters.rs @@ -5,6 +5,12 @@ 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,9 +29,9 @@ 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); @@ -34,7 +40,7 @@ fn test_it_converts( let png = PngData::new(&input, opts.fix_errors).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) { @@ -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 fc7ace9e..d31f70e2 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -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, @@ -32,9 +37,9 @@ 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, @@ -44,7 +49,7 @@ fn test_it_converts_callbacks( { let png = PngData::new(&input, opts.fix_errors).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); @@ -66,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(); @@ -77,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( @@ -153,9 +158,9 @@ fn verbose_mode() { input, &output, &opts, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); }; @@ -411,7 +416,7 @@ fn interlacing_0_to_1_small_files() { let png = PngData::new(&input, opts.fix_errors).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) { @@ -430,7 +435,7 @@ 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.color_type.png_header_code(), INDEXED); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::One); remove_file(output).ok(); @@ -445,7 +450,7 @@ fn interlacing_1_to_0_small_files() { let png = PngData::new(&input, opts.fix_errors).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) { @@ -464,7 +469,7 @@ fn interlacing_1_to_0_small_files() { }; 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); // the depth can't be asserted reliably, because on such small file different zlib implementations pick different depth as the best remove_file(output).ok(); @@ -556,9 +561,9 @@ fn preserve_attrs() { input, &output, &opts, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, callback_pre, callback_post, @@ -575,7 +580,7 @@ fn fix_errors() { let png = PngData::new(&input, opts.fix_errors).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) { @@ -593,7 +598,7 @@ 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 @@ -613,9 +618,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..ff884b1a 100644 --- a/tests/interlaced.rs +++ b/tests/interlaced.rs @@ -5,6 +5,12 @@ 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(); - 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); @@ -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,9 +539,9 @@ 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, ); } @@ -544,9 +550,9 @@ fn interlaced_palette_1_should_be_palette_1() { 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 +561,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 +572,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 +583,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 +594,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 +605,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 +616,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 +627,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 +638,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,9 +649,9 @@ 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, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -654,9 +660,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..10f558e6 100644 --- a/tests/interlacing.rs +++ b/tests/interlacing.rs @@ -5,6 +5,9 @@ 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(); 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, @@ -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/reduction.rs b/tests/reduction.rs index d88cc58c..040375af 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -5,6 +5,12 @@ 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,9 +29,9 @@ 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); @@ -33,7 +39,7 @@ fn test_it_converts( opts.optimize_alpha = optimize_alpha; let png = PngData::new(&input, opts.fix_errors).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); @@ -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,9 +549,9 @@ 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, ); } @@ -555,9 +561,9 @@ 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 +573,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 +585,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 +597,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 +609,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 +621,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 +633,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 +645,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 +657,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 +669,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 +681,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 +693,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,9 +705,9 @@ 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, ); } @@ -711,9 +717,9 @@ 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 +729,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 +741,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 +753,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 +765,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 +777,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 +789,9 @@ 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, ); } @@ -795,9 +801,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,9 +813,9 @@ 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, ); } @@ -821,7 +827,7 @@ fn small_files() { let png = PngData::new(&input, opts.fix_errors).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); match oxipng::optimize(&InFile::Path(input), &output, &opts) { @@ -839,7 +845,7 @@ fn small_files() { } }; - assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED); // depth varies depending on zlib implementation used remove_file(output).ok(); @@ -852,9 +858,11 @@ fn palette_should_be_reduced_with_dupes() { let png = PngData::new(&input, opts.fix_errors).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(_) => (), @@ -871,9 +879,11 @@ fn palette_should_be_reduced_with_dupes() { } }; - 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(); } @@ -885,9 +895,11 @@ fn palette_should_be_reduced_with_unused() { let png = PngData::new(&input, opts.fix_errors).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(_) => (), @@ -904,9 +916,11 @@ fn palette_should_be_reduced_with_unused() { } }; - 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(); } @@ -918,9 +932,11 @@ fn palette_should_be_reduced_with_both() { let png = PngData::new(&input, opts.fix_errors).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(_) => (), @@ -937,9 +953,11 @@ fn palette_should_be_reduced_with_both() { } }; - 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 +967,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 +979,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 +991,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 +1003,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..06833b25 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -5,6 +5,12 @@ 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,9 +29,9 @@ 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); @@ -33,7 +39,8 @@ fn test_it_converts( let png = PngData::new(&input, opts.fix_errors).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"); @@ -54,23 +61,22 @@ 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); + if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type { + let mut max_palette_size = 1 << (png.raw.ihdr.bit_depth as u8); // 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); } remove_file(output).ok(); @@ -81,9 +87,9 @@ fn issue_29() { test_it_converts( "tests/files/issue-29.png", None, - ColorType::RGB, + RGB, BitDepth::Eight, - ColorType::RGB, + RGB, BitDepth::Eight, ); } @@ -127,9 +133,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 +145,9 @@ fn issue_52_02() { test_it_converts( "tests/files/issue-52-02.png", None, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); } @@ -151,9 +157,9 @@ fn issue_52_03() { test_it_converts( "tests/files/issue-52-03.png", None, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Eight, ); } @@ -163,9 +169,9 @@ fn issue_52_04() { test_it_converts( "tests/files/issue-52-04.png", None, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::One, ); } @@ -175,9 +181,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,9 +193,9 @@ fn issue_52_06() { test_it_converts( "tests/files/issue-52-06.png", None, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::Indexed, + INDEXED, BitDepth::Two, ); } @@ -199,9 +205,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 +217,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 +229,9 @@ fn issue_59() { test_it_converts( "tests/files/issue-59.png", None, - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGBA, + RGBA, BitDepth::Eight, ); } @@ -235,9 +241,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 +253,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 +265,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 +277,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 +289,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 +306,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 +322,9 @@ fn issue_113() { test_it_converts( input, Some((output, opts)), - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::GrayscaleAlpha, + GRAYSCALE_ALPHA, BitDepth::Eight, ); } @@ -326,14 +332,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 +343,9 @@ fn issue_133() { test_it_converts( input, Some((output, opts)), - ColorType::RGBA, + RGBA, BitDepth::Eight, - ColorType::RGBA, + RGBA, BitDepth::Eight, ); } @@ -356,9 +355,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 +367,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 +379,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 +391,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 +403,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 +415,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 +431,9 @@ fn issue_182() { test_it_converts( input, Some((output, opts)), - ColorType::Grayscale, + GRAYSCALE, BitDepth::One, - ColorType::Grayscale, + GRAYSCALE, BitDepth::One, ); } @@ -444,9 +443,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 +455,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 +467,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..415014fe 100644 --- a/tests/strategies.rs +++ b/tests/strategies.rs @@ -5,6 +5,11 @@ 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,9 +28,9 @@ 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); @@ -34,7 +39,7 @@ fn test_it_converts( let png = PngData::new(&input, opts.fix_errors).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) { @@ -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, ); } From 798a1209269c1991da9cf86f504a5a0d92ca28e2 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 15 Jan 2023 20:55:38 +1300 Subject: [PATCH 07/18] Include PLTE/tRNS size in evaluations --- src/evaluate.rs | 4 ++-- src/png/mod.rs | 21 +++++++++++++++++++++ tests/regression.rs | 8 ++++---- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/evaluate.rs b/src/evaluate.rs index 23cc9832..ee92b17a 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -30,7 +30,7 @@ pub struct Candidate { impl Candidate { fn cmp_key(&self) -> impl Ord { ( - self.image.idat_data.len(), + self.image.estimated_output_size(), self.image.raw.data.len(), self.image.raw.ihdr.bit_depth, self.filter, @@ -134,7 +134,6 @@ impl Evaluator { if let Ok(idat_data) = deflate::deflate(&filtered, compression, &best_candidate_size) { - best_candidate_size.set_min(idat_data.len()); let new = Candidate { image: PngData { idat_data, @@ -145,6 +144,7 @@ impl Evaluator { is_reduction, nth, }; + best_candidate_size.set_min(new.image.estimated_output_size()); #[cfg(feature = "parallel")] { diff --git a/src/png/mod.rs b/src/png/mod.rs index 32870581..dd37f69c 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -134,6 +134,27 @@ impl PngData { }) } + /// Return an estimate of the output size + pub fn estimated_output_size(&self) -> usize { + // Add the size of the PLTE and tRNS chunks to the compressed idat size + // This can help with evaluation of very small data + let size = self.idat_data.len(); + size + match &self.raw.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, + } + } + /// Format the `PngData` struct into a valid PNG bytestream pub fn output(&self) -> Vec { // PNG header diff --git a/tests/regression.rs b/tests/regression.rs index 06833b25..a161a3bf 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -147,7 +147,7 @@ fn issue_52_02() { None, RGBA, BitDepth::Eight, - INDEXED, + RGBA, BitDepth::Eight, ); } @@ -159,7 +159,7 @@ fn issue_52_03() { None, RGBA, BitDepth::Eight, - INDEXED, + RGBA, BitDepth::Eight, ); } @@ -195,8 +195,8 @@ fn issue_52_06() { None, RGBA, BitDepth::Eight, - INDEXED, - BitDepth::Two, + RGBA, + BitDepth::Eight, ); } From a26d225d812dd602e28679dec8da59e747b13163 Mon Sep 17 00:00:00 2001 From: andrews05 Date: Sun, 7 May 2023 02:45:31 +1200 Subject: [PATCH 08/18] More flexible verbosity, improve verbose test (#501) * More flexible verbosity, improve verbose test * Tweak reporting format * Add evaluation reporting at trace level --- src/colors.rs | 26 +++++++++++------------- src/evaluate.rs | 25 +++++++++++++++++++---- src/filters.rs | 14 ++++++------- src/interlace.rs | 13 ++++++------ src/lib.rs | 53 ++++++++++++++++++++---------------------------- src/main.rs | 7 ++++--- tests/flags.rs | 36 +++++++++++++------------------- 7 files changed, 86 insertions(+), 88 deletions(-) diff --git a/src/colors.rs b/src/colors.rs index 32999849..77ee9818 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -1,5 +1,5 @@ use rgb::{RGB16, RGBA8}; -use std::fmt; +use std::{fmt, fmt::Display}; use crate::PngError; @@ -27,20 +27,18 @@ pub enum ColorType { 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), + } } } @@ -109,9 +107,9 @@ impl TryFrom for BitDepth { } } -impl fmt::Display for BitDepth { +impl Display for BitDepth { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", *self as u8) + Display::fmt(&(*self as u8).to_string(), f) } } diff --git a/src/evaluate.rs b/src/evaluate.rs index ee92b17a..833f41c6 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -9,9 +9,11 @@ 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; @@ -131,9 +133,8 @@ impl Evaluator { return; } let filtered = image.filter_image(filter, optimize_alpha); - if let Ok(idat_data) = - deflate::deflate(&filtered, compression, &best_candidate_size) - { + let idat_data = deflate::deflate(&filtered, compression, &best_candidate_size); + if let Ok(idat_data) = idat_data { let new = Candidate { image: PngData { idat_data, @@ -144,7 +145,15 @@ impl Evaluator { is_reduction, nth, }; - best_candidate_size.set_min(new.image.estimated_output_size()); + let size = new.image.estimated_output_size(); + best_candidate_size.set_min(size); + trace!( + "Eval: {}-bit {:20} {:8} {} bytes", + image.ihdr.bit_depth, + image.ihdr.color_type, + filter, + size + ); #[cfg(feature = "parallel")] { @@ -158,6 +167,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..1fec39e6 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,7 +45,8 @@ impl Display for RowFilter { Self::Bigrams => "Bigrams", Self::BigEnt => "BigEnt", Self::Brute => "Brute", - } + }, + f, ) } } diff --git a/src/interlace.rs b/src/interlace.rs index 88c146ff..0fd63f21 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, ) } } diff --git a/src/lib.rs b/src/lib.rs index d88746b7..d331c6dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,13 +25,13 @@ extern crate rayon; mod rayon; use crate::atomicmin::AtomicMin; -use crate::colors::{BitDepth, ColorType}; +use crate::colors::BitDepth; use crate::deflate::{crc32, inflate}; use crate::evaluate::Evaluator; use crate::png::PngData; use crate::png::PngImage; use crate::reduction::*; -use log::{debug, info, warn}; +use log::{debug, info, trace, warn}; use rayon::prelude::*; use std::fmt; use std::fs::{copy, File, Metadata}; @@ -330,7 +330,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; } @@ -532,7 +532,7 @@ fn optimize_png( } if !filters.is_empty() { - debug!("Evaluating: {} filters", filters.len()); + trace!("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()); @@ -615,7 +615,7 @@ fn optimize_png( png.idat_data = idat_data; debug!("Found better combination:"); debug!( - " zc = {} f = {} {} bytes", + " zc = {} f = {:8} {} bytes", opts.compression, opts.filter, png.idat_data.len() @@ -746,16 +746,20 @@ fn perform_trial( match new_idat { Ok(n) => { let bytes = n.len(); - debug!( - " zc = {} f = {} {} bytes", - trial.compression, trial.filter, bytes + trace!( + " zc = {} f = {:8} {} bytes", + trial.compression, + trial.filter, + bytes ); Some((trial, n)) } Err(PngError::DeflatedDataTooLong(bytes)) => { - debug!( - " zc = {} f = {} >{} bytes", - trial.compression, trial.filter, bytes, + trace!( + " zc = {} f = {:8} >{} bytes", + trial.compression, + trial.filter, + bytes, ); None } @@ -814,24 +818,10 @@ impl Deadline { /// Display the format of the image data fn report_format(prefix: &str, png: &PngImage) { - if let ColorType::Indexed { palette } = &png.ihdr.color_type { - 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` @@ -1032,9 +1022,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!( diff --git a/src/main.rs b/src/main.rs index b484fac6..3676e85e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ #![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; @@ -154,9 +154,10 @@ fn main() { ) .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 +382,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(); diff --git a/tests/flags.rs b/tests/flags.rs index d31f70e2..3690f21a 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -180,34 +180,26 @@ 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 + ); } } From 2f622fc7bd610c3e8a0d2471c5d46c63f32cd626 Mon Sep 17 00:00:00 2001 From: andrews05 Date: Mon, 8 May 2023 09:46:58 +1200 Subject: [PATCH 09/18] Minor reduction improvements (#502) * Fix bKGD conversion from gray to palette * Allow grayscale reduction from 16 to 4 or less * Refactor reduction evaluation sequence * Separate palette into new file --- benches/reductions.rs | 58 ++-- src/colors.rs | 9 + src/lib.rs | 82 +---- src/reduction/bit_depth.rs | 12 +- src/reduction/color.rs | 48 ++- src/reduction/mod.rs | 294 +++++------------- src/reduction/palette.rs | 180 +++++++++++ .../grayscale_16_should_be_grayscale_1.png | Bin 0 -> 2483 bytes tests/reduction.rs | 12 + tests/regression.rs | 4 +- 10 files changed, 357 insertions(+), 342 deletions(-) create mode 100644 src/reduction/palette.rs create mode 100644 tests/files/grayscale_16_should_be_grayscale_1.png diff --git a/benches/reductions.rs b/benches/reductions.rs index be9b6e06..dc97effb 100644 --- a/benches/reductions.rs +++ b/benches/reductions.rs @@ -12,7 +12,7 @@ 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(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_16_to_8(&png.raw)); } #[bench] @@ -22,7 +22,7 @@ fn reductions_8_to_4_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).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] @@ -32,7 +32,7 @@ fn reductions_8_to_2_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).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] @@ -42,7 +42,7 @@ fn reductions_8_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).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] @@ -52,7 +52,7 @@ fn reductions_4_to_2_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).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] @@ -62,7 +62,7 @@ fn reductions_4_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).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] @@ -72,7 +72,7 @@ fn reductions_2_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).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] @@ -82,7 +82,7 @@ fn reductions_grayscale_8_to_4_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).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] @@ -92,7 +92,7 @@ fn reductions_grayscale_8_to_2_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).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] @@ -102,7 +102,7 @@ fn reductions_grayscale_8_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).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] @@ -112,7 +112,7 @@ fn reductions_grayscale_4_to_2_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).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] @@ -122,7 +122,7 @@ fn reductions_grayscale_4_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).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] @@ -132,7 +132,7 @@ fn reductions_grayscale_2_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).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] @@ -140,7 +140,7 @@ 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(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| alpha::reduced_alpha_channel(&png.raw, false)); } #[bench] @@ -148,7 +148,7 @@ 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(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| alpha::reduced_alpha_channel(&png.raw, false)); } #[bench] @@ -158,7 +158,7 @@ fn reductions_rgba_to_grayscale_alpha_16(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_rgb_to_grayscale(&png.raw)); } #[bench] @@ -168,7 +168,7 @@ fn reductions_rgba_to_grayscale_alpha_8(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_rgb_to_grayscale(&png.raw)); } #[bench] @@ -178,7 +178,10 @@ fn reductions_rgba_to_grayscale_16(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| { + color::reduce_rgb_to_grayscale(&png.raw) + .and_then(|r| alpha::reduced_alpha_channel(&r, false)) + }); } #[bench] @@ -188,7 +191,10 @@ fn reductions_rgba_to_grayscale_8(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| { + color::reduce_rgb_to_grayscale(&png.raw) + .and_then(|r| alpha::reduced_alpha_channel(&r, false)) + }); } #[bench] @@ -198,7 +204,7 @@ fn reductions_rgb_to_grayscale_16(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_rgb_to_grayscale(&png.raw)); } #[bench] @@ -206,7 +212,7 @@ 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(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_rgb_to_grayscale(&png.raw)); } #[bench] @@ -214,7 +220,7 @@ 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(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_to_palette(&png.raw)); } #[bench] @@ -222,7 +228,7 @@ 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(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_to_palette(&png.raw)); } #[bench] @@ -232,7 +238,7 @@ fn reductions_palette_duplicate_reduction(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduced_palette(&png.raw, false)); + b.iter(|| palette::optimized_palette(&png.raw, false)); } #[bench] @@ -242,7 +248,7 @@ fn reductions_palette_unused_reduction(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduced_palette(&png.raw, false)); + b.iter(|| palette::optimized_palette(&png.raw, false)); } #[bench] @@ -252,7 +258,7 @@ fn reductions_palette_full_reduction(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduced_palette(&png.raw, false)); + b.iter(|| palette::optimized_palette(&png.raw, false)); } #[bench] diff --git a/src/colors.rs b/src/colors.rs index 77ee9818..59a0c360 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -74,6 +74,15 @@ impl ColorType { pub fn has_alpha(&self) -> bool { matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA) } + + #[inline] + pub 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)] diff --git a/src/lib.rs b/src/lib.rs index d331c6dc..99a0d6fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,6 @@ extern crate rayon; mod rayon; use crate::atomicmin::AtomicMin; -use crate::colors::BitDepth; use crate::deflate::{crc32, inflate}; use crate::evaluate::Evaluator; use crate::png::PngData; @@ -477,23 +476,6 @@ fn optimize_png( 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 @@ -505,7 +487,9 @@ fn optimize_png( eval_compression, false, ); - perform_reductions(png.raw.clone(), opts, &deadline, &eval); + let (baseline, mut reduction_occurred) = + perform_reductions(png.raw.clone(), opts, &deadline, &eval); + png.raw = baseline; let mut eval_filter = if let Some(result) = eval.get_best_candidate() { *png = result.image; if result.is_reduction { @@ -664,66 +648,6 @@ fn optimize_png( Ok(output) } -fn perform_reductions( - 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()); - 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); - } -} - /// Execute a compression trial fn perform_trial( filtered: &[u8], diff --git a/src/reduction/bit_depth.rs b/src/reduction/bit_depth.rs index 948de473..9213c88c 100644 --- a/src/reduction/bit_depth.rs +++ b/src/reduction/bit_depth.rs @@ -2,13 +2,10 @@ use crate::colors::{BitDepth, ColorType}; use crate::headers::IhdrData; use crate::png::PngImage; -/// Attempt to reduce the bit depth of the image, returning the reduced image if successful +/// Attempt to reduce a 16-bit image to 8-bit, returning the reduced image if successful #[must_use] -pub fn reduce_bit_depth(png: &PngImage, minimum_bits: usize) -> Option { +pub fn reduced_bit_depth_16_to_8(png: &PngImage) -> Option { if png.ihdr.bit_depth != BitDepth::Sixteen { - if png.channels_per_pixel() == 1 { - return reduce_bit_depth_8_or_less(png, minimum_bits); - } return None; } @@ -29,11 +26,12 @@ pub fn reduce_bit_depth(png: &PngImage, minimum_bits: usize) -> Option }) } +/// Attempt to reduce an 8/4/2-bit image to a lower bit depth, 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 reduced_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Option { assert!((1..8).contains(&minimum_bits)); let bit_depth = png.ihdr.bit_depth as usize; - if minimum_bits >= bit_depth || bit_depth > 8 { + if minimum_bits >= bit_depth || bit_depth > 8 || png.channels_per_pixel() != 1 { return None; } // Calculate the current number of pixels per byte diff --git a/src/reduction/color.rs b/src/reduction/color.rs index e8d5e73f..6eb9700a 100644 --- a/src/reduction/color.rs +++ b/src/reduction/color.rs @@ -35,7 +35,7 @@ where #[must_use] pub fn reduce_to_palette(png: &PngImage) -> Option { - if png.ihdr.bit_depth != BitDepth::Eight { + if png.ihdr.bit_depth != BitDepth::Eight || png.channels_per_pixel() == 1 { return None; } let mut raw_data = Vec::with_capacity(png.data.len()); @@ -98,22 +98,36 @@ pub fn reduce_to_palette(png: &PngImage) -> Option { 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; - } - // 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 + let bg = if png.ihdr.color_type.is_rgb() && bkgd_header.len() == 6 { + // In bKGD 16-bit values are used even for 8-bit images + Some(RGBA8::new( + bkgd_header[1], + bkgd_header[3], + bkgd_header[5], + 255, + )) + } else if png.ihdr.color_type == ColorType::GrayscaleAlpha && bkgd_header.len() == 2 { + Some(RGBA8::new( + bkgd_header[1], + bkgd_header[1], + bkgd_header[1], + 255, + )) } else { - return None; // No space in palette to store the bg as an index + None }; - aux_headers.insert(*b"bKGD", vec![entry]); + if let Some(bg) = bg { + 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") { @@ -140,6 +154,10 @@ pub fn reduce_to_palette(png: &PngImage) -> Option { #[must_use] pub fn reduce_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.bytes_per_channel(); let bpp = png.channels_per_pixel() * byte_depth; diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index 8154189c..e6888216 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -1,247 +1,115 @@ -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 { - let palette = match &png.ihdr.color_type { - ColorType::Indexed { palette } => palette, - // 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; - } - - let mut palette_map = [None; 256]; - let mut used = [false; 256]; - { - // 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!(), - } - - 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)); - } - } - - 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()), - } + // 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; } } - do_palette_reduction(png, palette, &palette_map) -} - -#[must_use] -fn do_palette_reduction( - png: &PngImage, - palette: &[RGBA8], - 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]); + // 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 } } - Some(PngImage { - ihdr: IhdrData { - color_type: ColorType::Indexed { - palette: reordered_palette(palette, palette_map), - }, - ..png.ihdr - }, - data: raw_data, - 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; + // 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) { + png = Arc::new(reduced); + reduction_occurred = true; } } - new_palette -} -/// Attempt to reduce the color type of the image, returning the reduced image if successful -pub fn reduce_color_type( - png: &PngImage, - grayscale_reduction: bool, - optimize_alpha: bool, -) -> Option { - let was_single_channel = png.channels_per_pixel() == 1; - let mut reduced = Cow::Borrowed(png); - - // Go down one step at a time - maybe not the most efficient, but it's safe // Attempt to reduce RGB to grayscale - if grayscale_reduction && reduced.ihdr.color_type.is_rgb() { - if let Some(r) = reduce_rgb_to_grayscale(&reduced) { - reduced = Cow::Owned(r); + // This is just removal of bytes and does not need to be evaluated + if opts.color_type_reduction && !deadline.passed() { + if let Some(reduced) = reduce_rgb_to_grayscale(&png) { + png = Arc::new(reduced); + reduction_occurred = true; } } - // 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); - } - } + // 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 to reduce to palette, if not already a single channel - if reduced.channels_per_pixel() != 1 { - if let Some(r) = reduce_to_palette(&reduced) { - reduced = Cow::Owned(r); - - // 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 alpha removal + if opts.color_type_reduction && !deadline.passed() { + if let Some(reduced) = reduced_alpha_channel(&png, opts.optimize_alpha) { + png = Arc::new(reduced); + // If the reduction requires a tRNS chunk, enter this into the evaluator + // Otherwise it is just removal of bytes and should become the baseline + if png.ihdr.color_type.has_trns() { + eval.try_image(png.clone()); + evaluation_added = true; + } else { + baseline = png.clone(); + reduction_occurred = true; } } } - // 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); + // Attempt to reduce the palette size + if opts.palette_reduction && !deadline.passed() { + if let Some(reduced) = optimized_palette(&png, opts.optimize_alpha) { + png = Arc::new(reduced); + eval.try_image(png.clone()); + evaluation_added = true; } } - // Some conversions will allow us to perform bit depth reduction that wasn't possible before - if !was_single_channel && reduced.channels_per_pixel() == 1 { - if let Some(r) = reduce_bit_depth_8_or_less(&reduced, 1) { - reduced = Cow::Owned(r); + // Attempt to reduce to palette + if opts.color_type_reduction && !deadline.passed() { + if let Some(reduced) = reduce_to_palette(&png) { + png = Arc::new(reduced); + // Make sure the palette gets sorted (ideally, this should be done within reduce_to_palette) + if let Some(reduced) = optimized_palette(&png, opts.optimize_alpha) { + png = Arc::new(reduced); + } + eval.try_image(png.clone()); + evaluation_added = true; } } - match reduced { - Cow::Owned(r) => Some(r), - _ => None, + // Attempt to reduce to a lower bit depth + if opts.bit_depth_reduction && !deadline.passed() { + if let Some(reduced) = reduced_bit_depth_8_or_less(&png, 1) { + png = Arc::new(reduced); + eval.try_image(png.clone()); + 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..081d43db --- /dev/null +++ b/src/reduction/palette.rs @@ -0,0 +1,180 @@ +use crate::colors::{BitDepth, ColorType}; +use crate::headers::IhdrData; +use crate::png::PngImage; +use indexmap::map::{Entry::*, IndexMap}; +use rgb::RGBA8; + +/// Attempt to shrink and sort the palette, returning the optimized image if successful +#[must_use] +pub fn optimized_palette(png: &PngImage, optimize_alpha: bool) -> Option { + let palette = match &png.ihdr.color_type { + ColorType::Indexed { palette } => palette, + // 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; + } + + let mut palette_map = [None; 256]; + let mut used = [false; 256]; + { + // 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!(), + } + + 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)); + } + } + + 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()), + } + } + } + + do_palette_reduction(png, palette, &palette_map) +} + +#[must_use] +fn do_palette_reduction( + png: &PngImage, + palette: &[RGBA8], + 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 { + palette: reordered_palette(palette, palette_map), + }, + ..png.ihdr + }, + data: raw_data, + 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 +} 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 0000000000000000000000000000000000000000..66fe12e3c8fb10bd19e2602e4d8955a43931533f GIT binary patch literal 2483 zcmd5+Z%i9y7=Mof?KBBbl0Iu?fjG94z_)C3Z#(Au(iNB?L|UGvD`mE#>I6AHDXy zd+vFEzvs{UJkPz>e6W6tzC;fI*wWbGYykjUforf3DuA(JTPpy$4ZPbUd0YpWcEN9A zJA^jQ6!r%Z0NBC|6#bmU;$gop5Msi$1{uLX8L0^aE<>ajYYiS(Gwu*X4!4*{6KSyP zaU8da9bTrzxnH5fw_3wdNeVIq(cRr`>NcAMv6G-`YHA3QCTQ9S5yntYKw`tjKxk*e zNYajz3$=@UP~wFEj_k5+!ZE4VU_g#iS0YX>%&&P0gp{;kf&|(jC=*F!qQPWS@vUQp z+28|767PM0FSL`O$%m1M(S{;m9D+{}pAWL_oSm}CL8YtKGD@JE`7r16IC(!82*K>_ zRV1m9)H@lOt!vE6^~|)LG$uR-#vyX7B#3T7@Y!W2e)Nhrivn&onN75fP@_}^TtWx$ z?QyaaXQxS6SqlCwZqmw-G(#)&G>nQZyXO=(WigVJk+QmJlA)@YDwurAc><$iyI^Z} zuo9c}ZZ?utMzYdPnHkE;RFV}G&7gxoR@PzD?4aT?EnG(uxrk>_B`BR8lUWEbf=HQuupmc5!L;YCpBu29pr)zsF1r>1W!R2zY2 zEp7G^gDq>}WZU)d7?WQKyPn>@wGMtbzUMrRUY83z1!W!3Vh;lilrZ=MSP96JWdPc+ z(v;kVJ*i-FAcg|)4jzF^#AsRG0p16#ii{aLuuWZ!xYRpQ*sf?Jow0$_#V9Im_{#O8w?0J;7}gyH zRb%_^Fuki))vV*As|y$NL2>?(ncvSXSyw*T@s8tsrXCJXe`tHJESug5Tzad1Xe5g% zi0&I()`e67Y3`aU-l(is)hAjtw%o6VW^$qZoICVr&)JRbxM4uSg~_ip^?f=2Ij@mD i-uahD9Is@U>d4lah4S91c;F@~Y2&_w&X4PkocsrUJHIpl literal 0 HcmV?d00001 diff --git a/tests/reduction.rs b/tests/reduction.rs index 040375af..a7fe5e39 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -712,6 +712,18 @@ fn grayscale_16_should_be_grayscale_8() { ); } +#[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( diff --git a/tests/regression.rs b/tests/regression.rs index a161a3bf..c24c8373 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -171,8 +171,8 @@ fn issue_52_04() { None, RGBA, BitDepth::Eight, - INDEXED, - BitDepth::One, + RGB, + BitDepth::Eight, ); } From a7be8751dce82e6d42936718134a1083b875768c Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 7 May 2023 20:26:28 +1200 Subject: [PATCH 10/18] Don't force output due to interlacing flag --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 99a0d6fc..2b27c3c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -847,7 +847,7 @@ fn srgb_rendering_intent(mut iccp: &[u8]) -> Option { /// 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<()> { From 9a500941d80eb2a5a82cb951728e158a22c98570 Mon Sep 17 00:00:00 2001 From: andrews05 Date: Tue, 16 May 2023 00:37:49 +1200 Subject: [PATCH 11/18] Raw API (#482) --- src/colors.rs | 6 +- src/error.rs | 11 + src/evaluate.rs | 2 +- src/filters.rs | 9 +- src/headers.rs | 6 - src/lib.rs | 438 +++++++++++++++++++++++----------------- src/sanity_checks.rs | 47 +++++ tests/files/raw_api.png | Bin 0 -> 114708 bytes tests/raw.rs | 97 +++++++++ 9 files changed, 415 insertions(+), 201 deletions(-) create mode 100644 src/sanity_checks.rs create mode 100644 tests/files/raw_api.png create mode 100644 tests/raw.rs diff --git a/src/colors.rs b/src/colors.rs index 59a0c360..22f24f19 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -56,7 +56,7 @@ impl ColorType { } #[inline] - pub fn channels_per_pixel(&self) -> u8 { + pub(crate) fn channels_per_pixel(&self) -> u8 { match self { ColorType::Grayscale { .. } | ColorType::Indexed { .. } => 1, ColorType::GrayscaleAlpha => 2, @@ -66,12 +66,12 @@ impl ColorType { } #[inline] - pub fn is_rgb(&self) -> bool { + pub(crate) fn is_rgb(&self) -> bool { matches!(self, ColorType::RGB { .. } | ColorType::RGBA) } #[inline] - pub fn has_alpha(&self) -> bool { + pub(crate) fn has_alpha(&self) -> bool { matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA) } 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 833f41c6..15763a2c 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -81,7 +81,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; diff --git a/src/filters.rs b/src/filters.rs index 1fec39e6..a2136139 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -53,10 +53,11 @@ impl Display for RowFilter { 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 a3e2b4f9..6511f604 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -19,10 +19,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, } @@ -176,8 +172,6 @@ pub fn parse_ihdr_header( bit_depth: byte_data[8].try_into()?, 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], interlaced: interlaced.try_into()?, }) } diff --git a/src/lib.rs b/src/lib.rs index 2b27c3c0..c3927d45 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ mod rayon; use crate::atomicmin::AtomicMin; use crate::deflate::{crc32, inflate}; use crate::evaluate::Evaluator; +use crate::headers::IhdrData; use crate::png::PngData; use crate::png::PngImage; use crate::reduction::*; @@ -40,12 +41,14 @@ 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::interlace::Interlacing; pub use indexmap::{indexset, IndexMap, IndexSet}; +pub use rgb::{RGB16, RGBA8}; mod atomicmin; mod colors; @@ -57,6 +60,8 @@ 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)] @@ -67,6 +72,8 @@ pub mod internal_tests { pub use crate::headers::*; pub use crate::png::*; pub use crate::reduction::*; + #[cfg(feature = "sanity-checks")] + pub use crate::sanity_checks::*; } #[derive(Clone, Debug)] @@ -307,6 +314,88 @@ impl Default for Options { } } +#[derive(Debug)] +/// A raw image definition which can be used to create an optimized png +pub struct RawImage { + png: Arc, +} + +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_headers: IndexMap::new(), + }), + }) + } + + /// Add a png chunk, such as "iTXt", to be included in the output + pub fn add_png_chunk(&mut self, chunk_type: [u8; 4], data: Vec) { + // We can guarantee this will succeed - failure indicates a bug + let png = Arc::get_mut(&mut self.png).unwrap(); + png.aux_headers.insert(chunk_type, data); + } + + /// Add an ICC profile for the image + pub fn add_icc_profile(&mut self, data: &[u8]) { + // Compress with default compression level + if let Ok(mut compressed) = deflate::deflate(data, 11, &AtomicMin::new(None)) { + let mut iccp = Vec::with_capacity(compressed.len() + 13); + iccp.extend(b"icc"); // Profile name - generally unused, can be anything + iccp.extend([0, 0]); // Null separator, zlib compression method + iccp.append(&mut compressed); + self.add_png_chunk(*b"iCCP", 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 png = optimize_raw(Arc::clone(&self.png), opts, deadline, None) + .ok_or_else(|| PngError::new("Failed to optimize input data"))?; + 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. @@ -474,141 +563,10 @@ fn optimize_png( // Do this first so that reductions can ignore certain chunks such as bKGD perform_strip(png, opts); - let stripped_png = png.clone(); - // 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.raw.clone(), opts, &deadline, &eval); - png.raw = baseline; - 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 { - None - }; - - if reduction_occurred { - report_format("Reducing image to ", &png.raw); - } - - 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() { - trace!("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 = {:8} {} 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; + if let Some(new_png) = optimize_raw(png.raw.clone(), opts, deadline, Some(idat_original_size)) { + png.raw = new_png.raw; + png.idat_data = new_png.idat_data; } let output = png.output(); @@ -643,11 +601,169 @@ fn optimize_png( } #[cfg(feature = "sanity-checks")] - debug_assert!(sanity_checks::validate_output(&output, original_data)); + assert!(sanity_checks::validate_output(&output, original_data)); Ok(output) } +/// Perform optimization on the input image data using the options provided +fn optimize_raw( + mut png: Arc, + opts: &Options, + deadline: Arc, + max_idat_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 = Arc::clone(&result.image.raw); + reduction_occurred = true; + } + } + + if reduction_occurred { + 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.image.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 eval_result = eval_result?; + + let trial = TrialOptions { + filter: eval_result.filter, + compression: match opts.deflate { + Deflaters::Libdeflater { compression } => compression, + _ => 0, + }, + }; + if trial.compression > 0 && trial.compression <= eval_compression { + // No further compression required + let idat_data = eval_result.image.idat_data; + if opts.force || idat_data.len() < max_idat_size.unwrap_or(usize::MAX) { + Some((trial, idat_data)) + } else { + None + } + } else { + debug!("Trying: {}", trial.filter); + let best_size = AtomicMin::new(if opts.force { None } else { max_idat_size }); + perform_trial(&eval_result.image.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.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 best_size = AtomicMin::new(if opts.force { None } else { max_idat_size }); + 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.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 { + debug!("Found better combination:"); + debug!( + " zc = {} f = {:8} {} bytes", + opts.compression, + opts.filter, + idat_data.len() + ); + return Some(PngData { + raw: png, + // The filtered data has not been retained here, but we don't need to return it + filtered: vec![], + idat_data, + }); + } + } 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 idat_data = &result.image.idat_data; + if idat_data.len() < max_idat_size.unwrap_or(usize::MAX) { + debug!("Found better combination:"); + debug!( + " zc = {} f = {:8} {} bytes", + eval_compression, + result.filter, + idat_data.len() + ); + return Some(result.image); + } + } + + None +} + /// Execute a compression trial fn perform_trial( filtered: &[u8], @@ -958,55 +1074,3 @@ fn copy_times(input_path_meta: &Metadata, out_path: &Path) -> PngResult<()> { )) }) } - -#[cfg(feature = "sanity-checks")] -mod sanity_checks { - use super::*; - use image::{DynamicImage, GenericImageView, ImageFormat, Pixel}; - use log::error; - use std::io::Cursor; - - /// Validate that the output png data still matches the original image - pub(super) 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/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/raw_api.png b/tests/files/raw_api.png new file mode 100644 index 0000000000000000000000000000000000000000..3f9bdb1a260418aa9fff6c1d5a529e2de10e2a95 GIT binary patch literal 114708 zcmaI6b981w(=QrxV%y2YHYT=h+kRqmV%xTD+qR7-P9~Z=?|07k-TTKmcdxbAuC7(} ztLpA*)$T||c?kqKTsROA5CkbnQDqPi(5HWIJPg=B&e{TG!oLrWtC*&%ii5c;z{uGQ zM99>^*o;`p&d9<{+04k)(`mwt4+I4Kr(@{?+?%Q4A0ikMkiNqRY(sd&k&nt0imaGR3;5+LUD z;Q0q&XXa`|>|tkX@51B3Px@cDJpbtbuo+2-|4YQxhM)9*Ii)G5NG#&uY(~t^z(H@q z%*;&8$<4sb&c({YPDjkb#LUjf#LdXeM9;*`!_36P!bJSPAJTu;oK4Mnltsn=x2=CA zeo{+US4SR3MgRc70AOWsaJFD%=H}*RWMW}tVWIygLGR*e?`q^hZ|_3(-w2{+E+)=a zj;>Y?_Qd}|G%|K@bLA)f=js2df}P|4z}ma~?`HbfV2mC{j*QFtEP|LgDn5!gl5)6tAk+04bk&DrE%$C;D;$CM+Fh_jiItAn$ugM;mVx1yq@ zgR6s!rGq1}h{%7n8u3p#BNHq8|1hclOG{3UN6OyC)yUq&OiGlW^dAm`m6a(EHwPy- zw-|@0Fb9V?GqV^wmxw5v2pb186Pq}vh`12|jUyUlr%E`ak$0#w^Sv%*Dp`4^Qm>?92bsHT}Q)BJzLrh4G&= zjQ{E0|4(=SH|t*v{fGRY;Qd$lpYS)c{}=Pl|APDB3E~q3v^z*jR7llh^SVbT+D^}% zkZmm6=7z@~AYp%`d%O9msbD;lKNgd2dr^gF|OSLs*?p{Ca-Rv|wMC7U?YsH~cvVqeycR8N(i>ZZIWRq|c!D}XcT_49ckXtmQ zJlBKAp1(eO{dyy{kUiIGRKAvI_B?UNgpdE?M zf3?5L0C!h3ey=b`!$Z4tm^=3$!S(K6Ls^y~(eXEDePLEDAzUtn$n$fzUE_yb)}Cuv zrt#Cm$_t2fCd*r#!8o1^%pW{aA6o=`#((O?1;TkTk)fPF@J^w140~d^QTj`2jJEg(dg+&8Hi)4A2S}A5Or@B=*KafKaz!SL0ZP-71Y9$8Hp( zPi-e-n0t>7Cq1bn`~@9lAEdeuu+LT{?p7G`9>9|bNnj_^1w5M1Qjqi>+28ckKp3HT ze$+;9jX*Ya2@rK4%_fN&QQkToh%=p#n|iVW`DNF|v77L-PI4MFdutJ4%_q6XhNQ;u z6Lawy-+UI<5-|MffdDEz4w1)o?OZ>u0q@8RKYU$p2~u0miljHB`R9CIu>!R(cHo}r zew^B#(;M>r7q|XX&H%xugYF^F<7y*z?9TUfwCk-mLgH}`)0DH_eA_5FzN#K1(3qVj zaWe&jmsI-J@plx+^OxYWKLm#B3%4ZzJ?!nL0u_lARH^GD1hr^XccR?f>*DUnCfuv} zY@=7`FuhkEaR65%+I6WLT}asT89VV;T0j57kQ06v!-6NI>qqUU-uHZaH(wys{KZg= z7ryG6#5?V`d;}DkU6W$dEbXR8SjX5Ow)aX6?#V#5SOC=0 zIFf{17Ri@!Uw%Bdx2c|tIJZ`b(*7{6s#QBfV%D?AHYNU;tFta-|A}@JwU~D-z82W| z#Eh6Apce0|I>>)m1!3T&c?bAv3?AS2{=EY;W2R{U`H0fs5ip$!Ir@szem=m`MOeR` zzDI}jSsE?L=pZVK{w>YOmjICE%4PaWWFTg&cu!dkUPozuRI+LpNJe*%na>kXSV*GBpfR2%H$K

*yUtAz-H_5c^!SY)EXQ9LR5lV!_l?l{*~onpI1? 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 JPDHLkV1lbh_d5Up literal 0 HcmV?d00001 diff --git a/tests/raw.rs b/tests/raw.rs new file mode 100644 index 00000000..03802444 --- /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 png = PngData::from_slice(&original_data, opts.fix_errors).unwrap(); + let png = Arc::try_unwrap(png.raw).unwrap(); + + let num_headers = png.aux_headers.len(); + assert!(num_headers > 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_type, data) in png.aux_headers { + raw.add_png_chunk(chunk_type, data); + } + + let output = raw.create_optimized_png(&opts).unwrap(); + + let new = PngData::from_slice(&output, opts.fix_errors).unwrap(); + assert!(new.raw.aux_headers.len() == num_headers); + + #[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"); +} From d8b7ebaf47287b15f27567b041c3a2accff85383 Mon Sep 17 00:00:00 2001 From: andrews05 Date: Wed, 17 May 2023 12:53:05 +1200 Subject: [PATCH 12/18] Further small reduction improvements (#504) --- benches/reductions.rs | 52 ++- src/colors.rs | 10 +- src/lib.rs | 51 +-- src/png/mod.rs | 22 +- src/reduction/color.rs | 235 ++++++++------ src/reduction/mod.rs | 62 +++- src/reduction/palette.rs | 301 ++++++++++-------- .../grayscale_8_should_be_grayscale_4.png | Bin 14680 -> 67607 bytes .../files/grayscale_8_should_be_palette_8.png | Bin 0 -> 42482 bytes tests/files/palette_8_should_be_rgb.png | Bin 0 -> 195 bytes tests/files/palette_8_should_be_rgba.png | Bin 0 -> 1715 bytes tests/flags.rs | 8 +- tests/interlaced.rs | 15 +- tests/reduction.rs | 119 +++++-- 14 files changed, 533 insertions(+), 342 deletions(-) create mode 100644 tests/files/grayscale_8_should_be_palette_8.png create mode 100644 tests/files/palette_8_should_be_rgb.png create mode 100644 tests/files/palette_8_should_be_rgba.png diff --git a/benches/reductions.rs b/benches/reductions.rs index dc97effb..28803a38 100644 --- a/benches/reductions.rs +++ b/benches/reductions.rs @@ -158,7 +158,7 @@ fn reductions_rgba_to_grayscale_alpha_16(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| color::reduce_rgb_to_grayscale(&png.raw)); + b.iter(|| color::reduced_rgb_to_grayscale(&png.raw)); } #[bench] @@ -168,7 +168,7 @@ fn reductions_rgba_to_grayscale_alpha_8(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| color::reduce_rgb_to_grayscale(&png.raw)); + b.iter(|| color::reduced_rgb_to_grayscale(&png.raw)); } #[bench] @@ -179,7 +179,7 @@ fn reductions_rgba_to_grayscale_16(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - color::reduce_rgb_to_grayscale(&png.raw) + color::reduced_rgb_to_grayscale(&png.raw) .and_then(|r| alpha::reduced_alpha_channel(&r, false)) }); } @@ -192,7 +192,7 @@ fn reductions_rgba_to_grayscale_8(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - color::reduce_rgb_to_grayscale(&png.raw) + color::reduced_rgb_to_grayscale(&png.raw) .and_then(|r| alpha::reduced_alpha_channel(&r, false)) }); } @@ -204,7 +204,7 @@ fn reductions_rgb_to_grayscale_16(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| color::reduce_rgb_to_grayscale(&png.raw)); + b.iter(|| color::reduced_rgb_to_grayscale(&png.raw)); } #[bench] @@ -212,7 +212,7 @@ 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(); - b.iter(|| color::reduce_rgb_to_grayscale(&png.raw)); + b.iter(|| color::reduced_rgb_to_grayscale(&png.raw)); } #[bench] @@ -220,7 +220,7 @@ 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(); - b.iter(|| color::reduce_to_palette(&png.raw)); + b.iter(|| color::reduced_to_indexed(&png.raw)); } #[bench] @@ -228,7 +228,27 @@ 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(); - b.iter(|| color::reduce_to_palette(&png.raw)); + 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, false).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, false).unwrap(); + + b.iter(|| color::indexed_to_channels(&png.raw)); } #[bench] @@ -238,7 +258,7 @@ fn reductions_palette_duplicate_reduction(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| palette::optimized_palette(&png.raw, false)); + b.iter(|| palette::reduced_palette(&png.raw, false)); } #[bench] @@ -248,7 +268,7 @@ fn reductions_palette_unused_reduction(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| palette::optimized_palette(&png.raw, false)); + b.iter(|| palette::reduced_palette(&png.raw, false)); } #[bench] @@ -258,7 +278,17 @@ fn reductions_palette_full_reduction(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| palette::optimized_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, false).unwrap(); + + b.iter(|| palette::sorted_palette(&png.raw)); } #[bench] diff --git a/src/colors.rs b/src/colors.rs index 22f24f19..59cd589e 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -70,13 +70,21 @@ impl ColorType { matches!(self, ColorType::RGB { .. } | ColorType::RGBA) } + #[inline] + pub(crate) fn is_grayscale(&self) -> bool { + matches!( + self, + ColorType::Grayscale { .. } | ColorType::GrayscaleAlpha + ) + } + #[inline] pub(crate) fn has_alpha(&self) -> bool { matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA) } #[inline] - pub fn has_trns(&self) -> bool { + 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(), diff --git a/src/lib.rs b/src/lib.rs index c3927d45..4c43e287 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -564,7 +564,12 @@ fn optimize_png( // Do this first so that reductions can ignore certain chunks such as bKGD perform_strip(png, opts); - if let Some(new_png) = optimize_raw(png.raw.clone(), opts, deadline, Some(idat_original_size)) { + let max_size = if opts.force { + None + } else { + Some(png.estimated_output_size()) + }; + if let Some(new_png) = optimize_raw(png.raw.clone(), opts, deadline, max_size) { png.raw = new_png.raw; png.idat_data = new_png.idat_data; } @@ -611,7 +616,7 @@ fn optimize_raw( mut png: Arc, opts: &Options, deadline: Arc, - max_idat_size: Option, + max_size: Option, ) -> Option { // Must use normal (lazy) compression, as faster ones (greedy) are not representative let eval_compression = 5; @@ -673,15 +678,10 @@ fn optimize_raw( }; if trial.compression > 0 && trial.compression <= eval_compression { // No further compression required - let idat_data = eval_result.image.idat_data; - if opts.force || idat_data.len() < max_idat_size.unwrap_or(usize::MAX) { - Some((trial, idat_data)) - } else { - None - } + Some((trial, eval_result.image.idat_data)) } else { debug!("Trying: {}", trial.filter); - let best_size = AtomicMin::new(if opts.force { None } else { max_idat_size }); + let best_size = AtomicMin::new(max_size); perform_trial(&eval_result.image.filtered, opts, trial, &best_size) } } else { @@ -712,7 +712,7 @@ fn optimize_raw( debug!("Trying: {} filters", results.len()); - let best_size = AtomicMin::new(if opts.force { None } else { max_idat_size }); + let best_size = AtomicMin::new(max_size); let results_iter = results.into_par_iter().with_max_len(1); let best = results_iter.filter_map(|trial| { if deadline.passed() { @@ -730,34 +730,37 @@ fn optimize_raw( }) }; - if let Some((opts, idat_data)) = best { - debug!("Found better combination:"); - debug!( - " zc = {} f = {:8} {} bytes", - opts.compression, - opts.filter, - idat_data.len() - ); - return Some(PngData { + if let Some((trial, idat_data)) = best { + let image = PngData { raw: png, // The filtered data has not been retained here, but we don't need to return it filtered: vec![], idat_data, - }); + }; + if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) { + debug!("Found better combination:"); + debug!( + " zc = {} f = {:8} {} bytes", + trial.compression, + trial.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 idat_data = &result.image.idat_data; - if idat_data.len() < max_idat_size.unwrap_or(usize::MAX) { + let image = result.image; + if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) { debug!("Found better combination:"); debug!( " zc = {} f = {:8} {} bytes", eval_compression, result.filter, - idat_data.len() + image.idat_data.len() ); - return Some(result.image); + return Some(image); } } diff --git a/src/png/mod.rs b/src/png/mod.rs index dd37f69c..c25c52a9 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -186,28 +186,12 @@ impl PngData { match &self.raw.ihdr.color_type { ColorType::Indexed { palette } => { let mut palette_data = Vec::with_capacity(palette.len() * 3); - let mut max_palette_size = 1 << (self.raw.ihdr.bit_depth as u8); - // 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); - } - for px in palette.iter().take(max_palette_size) { + for px in palette { palette_data.extend_from_slice(px.rgb().as_slice()); } 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(); + 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); } } diff --git a/src/reduction/color.rs b/src/reduction/color.rs index 6eb9700a..177ae1d6 100644 --- a/src/reduction/color.rs +++ b/src/reduction/color.rs @@ -1,103 +1,88 @@ use crate::colors::{BitDepth, ColorType}; use crate::headers::IhdrData; use crate::png::PngImage; -use indexmap::IndexMap; -use rgb::{ComponentMap, FromSlice, RGBA, RGBA8}; +use indexmap::IndexSet; +use rgb::alt::Gray; +use rgb::{ComponentMap, ComponentSlice, FromSlice, RGB, RGBA, RGBA8}; 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 { - if png.ihdr.bit_depth != BitDepth::Eight || png.channels_per_pixel() == 1 { +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 ok = if let ColorType::RGB { transparent_color } = png.ihdr.color_type { - // Convert the RGB16 transparency to RGB8 - let transparency_pixel = transparent_color.map(|t| t.map(|c| c as u8)); - 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 mut 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, - ) + .collect() + } + 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, }; - 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 let Some(bkgd_header) = aux_headers.remove(b"bKGD") { let bg = if png.ihdr.color_type.is_rgb() && bkgd_header.len() == 6 { // In bKGD 16-bit values are used even for 8-bit images Some(RGBA8::new( @@ -106,7 +91,7 @@ pub fn reduce_to_palette(png: &PngImage) -> Option { bkgd_header[5], 255, )) - } else if png.ihdr.color_type == ColorType::GrayscaleAlpha && bkgd_header.len() == 2 { + } else if png.ihdr.color_type.is_grayscale() && bkgd_header.len() == 2 { Some(RGBA8::new( bkgd_header[1], bkgd_header[1], @@ -117,16 +102,15 @@ pub fn reduce_to_palette(png: &PngImage) -> Option { None }; if let Some(bg) = bg { - 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]); + let idx = palette.iter().position(|&px| px == bg).or_else(|| { + if palette.len() < 256 { + palette.push(bg); + Some(palette.len() - 1) + } else { + None // No space in palette to store the bg as an index + } + })?; + aux_headers.insert(*b"bKGD", vec![idx as u8]); } } @@ -135,17 +119,10 @@ pub fn reduce_to_palette(png: &PngImage) -> Option { 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; - } - Some(PngImage { data: raw_data, ihdr: IhdrData { - color_type: ColorType::Indexed { - palette: palette_vec, - }, + color_type: ColorType::Indexed { palette }, ..png.ihdr }, aux_headers, @@ -153,7 +130,7 @@ pub fn reduce_to_palette(png: &PngImage) -> Option { } #[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; } @@ -204,3 +181,67 @@ pub fn reduce_rgb_to_grayscale(png: &PngImage) -> Option { aux_headers, }) } + +/// Attempt to convert indexed to a different color type, returning the resulting image if successful +#[must_use] +pub fn indexed_to_channels(png: &PngImage) -> 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 = palette.iter().all(|c| c.r == c.g && c.g == c.b); + 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]); + } + + // Update bKGD if it exists + let mut aux_headers = png.aux_headers.clone(); + if let Some(idx) = aux_headers.remove(b"bKGD").and_then(|b| b.first().cloned()) { + if let Some(color) = palette.get(idx as usize) { + let bkgd = if is_gray { + vec![0, color.r] + } else { + vec![0, color.r, 0, color.g, 0, color.b] + }; + aux_headers.insert(*b"bKGD", bkgd); + } + } + + Some(PngImage { + ihdr: IhdrData { + color_type, + ..png.ihdr + }, + data, + aux_headers, + }) +} diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index e6888216..f5539096 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -51,7 +51,16 @@ pub(crate) fn perform_reductions( // Attempt to reduce RGB to grayscale // This is just removal of bytes and does not need to be evaluated if opts.color_type_reduction && !deadline.passed() { - if let Some(reduced) = reduce_rgb_to_grayscale(&png) { + 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; } @@ -65,9 +74,9 @@ pub(crate) fn perform_reductions( if opts.color_type_reduction && !deadline.passed() { if let Some(reduced) = reduced_alpha_channel(&png, opts.optimize_alpha) { png = Arc::new(reduced); - // If the reduction requires a tRNS chunk, enter this into the evaluator - // Otherwise it is just removal of bytes and should become the baseline - if png.ihdr.color_type.has_trns() { + // 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 { @@ -77,33 +86,52 @@ pub(crate) fn perform_reductions( } } - // Attempt to reduce the palette size + // Attempt to sort the palette if opts.palette_reduction && !deadline.passed() { - if let Some(reduced) = optimized_palette(&png, opts.optimize_alpha) { + if let Some(reduced) = sorted_palette(&png) { png = Arc::new(reduced); eval.try_image(png.clone()); evaluation_added = true; } } - // Attempt to reduce to palette + // 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) = reduce_to_palette(&png) { - png = Arc::new(reduced); - // Make sure the palette gets sorted (ideally, this should be done within reduce_to_palette) - if let Some(reduced) = optimized_palette(&png, opts.optimize_alpha) { - png = Arc::new(reduced); - } - eval.try_image(png.clone()); + if let Some(reduced) = indexed_to_channels(&png) { + // 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() { - if let Some(reduced) = reduced_bit_depth_8_or_less(&png, 1) { - png = Arc::new(reduced); - eval.try_image(png.clone()); + // 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; } } diff --git a/src/reduction/palette.rs b/src/reduction/palette.rs index 081d43db..83c14e24 100644 --- a/src/reduction/palette.rs +++ b/src/reduction/palette.rs @@ -1,180 +1,201 @@ use crate::colors::{BitDepth, ColorType}; use crate::headers::IhdrData; use crate::png::PngImage; -use indexmap::map::{Entry::*, IndexMap}; +use indexmap::IndexSet; use rgb::RGBA8; -/// Attempt to shrink and sort the palette, returning the optimized image if successful +/// Attempt to reduce the number of colors in the palette, returning the reduced image if successful #[must_use] -pub fn optimized_palette(png: &PngImage, optimize_alpha: bool) -> Option { +pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option { let palette = match &png.ihdr.color_type { - ColorType::Indexed { palette } => palette, - // Can't reduce if there is no palette + ColorType::Indexed { palette } if palette.len() > 1 => 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; - } - let mut palette_map = [None; 256]; - let mut used = [false; 256]; - { - // 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!(), + 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; } - - 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)); - } - } - - 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()), - } + // 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; } } - do_palette_reduction(png, palette, &palette_map) -} - -#[must_use] -fn do_palette_reduction( - png: &PngImage, - palette: &[RGBA8], - 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(); - + // Update bKGD if it exists, ensuring it comes last in the palette if otherwise unused 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]); + if let Some(idx) = aux_headers.remove(b"bKGD").and_then(|b| b.first().cloned()) { + if let Some(&color) = palette.get(idx as usize) { + let idx = add_color_to_set(color, &mut condensed, optimize_alpha); + aux_headers.insert(*b"bKGD", vec![idx]); } } + 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: reordered_palette(palette, palette_map), - }, + color_type: ColorType::Indexed { palette }, ..png.ihdr }, - data: raw_data, + data, 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; +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 +} - let mut byte_map = [0_u8; 256]; - - // low bit-depths can be pre-computed for every byte value +fn get_used_entries(png: &PngImage) -> [bool; 256] { + let mut used = [false; 256]; match png.ihdr.bit_depth { BitDepth::Eight => { - for byte in 0..=255usize { - byte_map[byte] = palette_map[byte].unwrap_or(0) + for &byte in &png.data { + used[byte as usize] = true; } } 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); + for &byte in &png.data { + used[(byte & 0x0F) as usize] = true; + used[(byte >> 4) as usize] = true; } } 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); + 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; } } - _ => {} - } - - 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; + BitDepth::One => { + // Only two options, don't bother checking which are actually used + used[0] = true; + used[1] = true; } - } - new_palette + _ => 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(); + + // If the background is the last entry in the palette we should make sure it stays last + // Otherwise an entry that's unused by the idat could prevent reduction to a lower depth + let mut aux_headers = png.aux_headers.clone(); + let bkgd_idx = aux_headers.remove(b"bKGD").and_then(|b| b.first().cloned()); + let bkgd_last = match bkgd_idx { + Some(idx) if idx as usize + 1 == palette.len() => enumerated.pop(), + _ => None, + }; + + // 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)) + }); + + if let Some(bkgd) = bkgd_last { + enumerated.push(bkgd); + } + + // 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(); + + // Update bKGD if it exists + if let Some(idx) = bkgd_idx.map(|idx| new_map[idx as usize]) { + aux_headers.insert(*b"bKGD", vec![idx]); + } + + Some(PngImage { + ihdr: IhdrData { + color_type: ColorType::Indexed { palette }, + ..png.ihdr + }, + data, + aux_headers, + }) } diff --git a/tests/files/grayscale_8_should_be_grayscale_4.png b/tests/files/grayscale_8_should_be_grayscale_4.png index c561432ec2313d8fccfe51a6bb6df4358a7d9e76..e5e62cb1d5ed420c586146a954b7f28838f6f12b 100644 GIT binary patch literal 67607 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

W07_%>0UVZiB1&)=DTNXXq2 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 literal 14680 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 cdk9k4cHBl$Q4woccYS27z4uy&y-(`Y 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 J`_RPV{{U34bz=Yk literal 0 HcmV?d00001 diff --git a/tests/flags.rs b/tests/flags.rs index 3690f21a..ebb2108e 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -427,8 +427,8 @@ fn interlacing_0_to_1_small_files() { }; assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7); - assert_eq!(png.raw.ihdr.color_type.png_header_code(), 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(); } @@ -461,8 +461,8 @@ fn interlacing_1_to_0_small_files() { }; assert_eq!(png.raw.ihdr.interlaced, Interlacing::None); - assert_eq!(png.raw.ihdr.color_type.png_header_code(), 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(); } diff --git a/tests/interlaced.rs b/tests/interlaced.rs index ff884b1a..52540a85 100644 --- a/tests/interlaced.rs +++ b/tests/interlaced.rs @@ -546,6 +546,17 @@ fn interlaced_palette_1_should_be_palette_1() { ); } +#[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( @@ -651,8 +662,8 @@ fn interlaced_small_files() { "tests/files/interlaced_small_files.png", INDEXED, BitDepth::Eight, - INDEXED, - BitDepth::One, + RGB, + BitDepth::Eight, ); } diff --git a/tests/reduction.rs b/tests/reduction.rs index a7fe5e39..3d6577eb 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -556,6 +556,42 @@ fn palette_4_should_be_palette_2() { ); } +#[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( @@ -808,6 +844,54 @@ fn grayscale_2_should_be_grayscale_1() { ); } +#[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, + ); +} + #[test] fn grayscale_alpha_16_should_be_grayscale_trns_16() { test_it_converts( @@ -834,33 +918,14 @@ fn grayscale_alpha_8_should_be_grayscale_trns_8() { #[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.png_header_code(), 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.png_header_code(), 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] From a5832706bd643bd15e72827fcb46a7d95d5a3ecc Mon Sep 17 00:00:00 2001 From: XhmikosR Date: Fri, 7 Apr 2023 08:49:37 +0300 Subject: [PATCH 13/18] Create dependabot.yml for action updates --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml 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 From ea5f1884be78f694f72a14f619c384df77b0e220 Mon Sep 17 00:00:00 2001 From: andrews05 Date: Mon, 22 May 2023 07:34:23 +1200 Subject: [PATCH 14/18] Refactor aux chunk handling (#505) --- benches/deflate.rs | 16 +- benches/filters.rs | 63 +++--- benches/interlacing.rs | 23 ++- benches/reductions.rs | 61 +++--- benches/strategies.rs | 13 +- benches/zopfli.rs | 11 +- src/colors.rs | 8 - src/deflate/deflater.rs | 7 +- src/deflate/mod.rs | 30 +++ src/evaluate.rs | 31 ++- src/headers.rs | 150 +++++++++++--- src/interlace.rs | 2 - src/lib.rs | 312 +++++++++++------------------ src/main.rs | 63 +++--- src/png/mod.rs | 143 ++++++------- src/reduction/alpha.rs | 10 - src/reduction/bit_depth.rs | 45 ++--- src/reduction/color.rs | 70 +------ src/reduction/palette.rs | 29 --- tests/files/strip_headers_all.png | Bin 117347 -> 117445 bytes tests/files/strip_headers_list.png | Bin 117347 -> 117445 bytes tests/files/strip_headers_none.png | Bin 117347 -> 117445 bytes tests/files/strip_headers_safe.png | Bin 117347 -> 117445 bytes tests/filters.rs | 8 +- tests/flags.rs | 113 ++++++----- tests/interlaced.rs | 8 +- tests/interlacing.rs | 8 +- tests/lib.rs | 32 +-- tests/raw.rs | 16 +- tests/reduction.rs | 20 +- tests/regression.rs | 22 +- tests/strategies.rs | 8 +- 32 files changed, 621 insertions(+), 701 deletions(-) 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 28803a38..16aae41f 100644 --- a/benches/reductions.rs +++ b/benches/reductions.rs @@ -4,13 +4,14 @@ 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::reduced_bit_depth_16_to_8(&png.raw)); } @@ -20,7 +21,7 @@ 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::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -30,7 +31,7 @@ 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::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -40,7 +41,7 @@ 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::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -50,7 +51,7 @@ 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::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -60,7 +61,7 @@ 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::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -70,7 +71,7 @@ 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::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -80,7 +81,7 @@ 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::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -90,7 +91,7 @@ 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::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -100,7 +101,7 @@ 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::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -110,7 +111,7 @@ 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::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -120,7 +121,7 @@ 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::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -130,7 +131,7 @@ 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::reduced_bit_depth_8_or_less(&png.raw, 1)); } @@ -138,7 +139,7 @@ fn reductions_grayscale_2_to_1_bits(b: &mut Bencher) { #[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(|| alpha::reduced_alpha_channel(&png.raw, false)); } @@ -146,7 +147,7 @@ fn reductions_rgba_to_rgb_16(b: &mut Bencher) { #[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(|| alpha::reduced_alpha_channel(&png.raw, false)); } @@ -156,7 +157,7 @@ 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(|| color::reduced_rgb_to_grayscale(&png.raw)); } @@ -166,7 +167,7 @@ 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(|| color::reduced_rgb_to_grayscale(&png.raw)); } @@ -176,7 +177,7 @@ 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(|| { color::reduced_rgb_to_grayscale(&png.raw) @@ -189,7 +190,7 @@ 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(|| { color::reduced_rgb_to_grayscale(&png.raw) @@ -202,7 +203,7 @@ 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(|| color::reduced_rgb_to_grayscale(&png.raw)); } @@ -210,7 +211,7 @@ fn reductions_rgb_to_grayscale_16(b: &mut Bencher) { #[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(|| color::reduced_rgb_to_grayscale(&png.raw)); } @@ -218,7 +219,7 @@ fn reductions_rgb_to_grayscale_8(b: &mut Bencher) { #[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(|| color::reduced_to_indexed(&png.raw)); } @@ -226,7 +227,7 @@ fn reductions_rgba_to_palette_8(b: &mut Bencher) { #[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(|| color::reduced_to_indexed(&png.raw)); } @@ -236,7 +237,7 @@ 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, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| color::reduced_to_indexed(&png.raw)); } @@ -246,7 +247,7 @@ 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, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| color::indexed_to_channels(&png.raw)); } @@ -256,7 +257,7 @@ 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(|| palette::reduced_palette(&png.raw, false)); } @@ -266,7 +267,7 @@ 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(|| palette::reduced_palette(&png.raw, false)); } @@ -276,7 +277,7 @@ 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(|| palette::reduced_palette(&png.raw, false)); } @@ -286,7 +287,7 @@ 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, false).unwrap(); + let png = PngData::new(&input, &Options::default()).unwrap(); b.iter(|| palette::sorted_palette(&png.raw)); } @@ -294,7 +295,7 @@ fn reductions_palette_sort(b: &mut Bencher) { #[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 59cd589e..744ec720 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -70,14 +70,6 @@ impl ColorType { matches!(self, ColorType::RGB { .. } | ColorType::RGBA) } - #[inline] - pub(crate) fn is_grayscale(&self) -> bool { - matches!( - self, - ColorType::Grayscale { .. } | ColorType::GrayscaleAlpha - ) - } - #[inline] pub(crate) fn has_alpha(&self) -> bool { matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA) 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/evaluate.rs b/src/evaluate.rs index 15763a2c..8c05fde7 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -4,7 +4,6 @@ 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; @@ -22,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 @@ -32,9 +33,9 @@ pub struct Candidate { impl Candidate { fn cmp_key(&self) -> impl Ord { ( - self.image.estimated_output_size(), - 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, ) @@ -135,17 +136,7 @@ impl Evaluator { let filtered = image.filter_image(filter, optimize_alpha); let idat_data = deflate::deflate(&filtered, compression, &best_candidate_size); if let Ok(idat_data) = idat_data { - let new = Candidate { - image: PngData { - idat_data, - filtered, - raw: Arc::clone(&image), - }, - filter, - is_reduction, - nth, - }; - let size = new.image.estimated_output_size(); + let size = idat_data.len() + image.key_chunks_size(); best_candidate_size.set_min(size); trace!( "Eval: {}-bit {:20} {:8} {} bytes", @@ -154,6 +145,14 @@ impl Evaluator { filter, size ); + let new = Candidate { + image: image.clone(), + idat_data, + filtered, + filter, + is_reduction, + nth, + }; #[cfg(feature = "parallel")] { diff --git a/src/headers.rs b/src/headers.rs index 6511f604..274173d7 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -1,12 +1,13 @@ 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 log::warn; use rgb::{RGB16, RGBA8}; -use std::io; -use std::io::{Cursor, Read}; #[derive(Debug, Clone)] /// Headers from the IHDR chunk of the image @@ -62,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]; @@ -85,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 @@ -117,37 +138,34 @@ 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( +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 { @@ -170,8 +188,8 @@ pub fn parse_ihdr_header( _ => return Err(PngError::new("Unexpected color type in header")), }, bit_depth: byte_data[8].try_into()?, - width: read_be_u32(&mut rdr).map_err(|_| PngError::TruncatedData)?, - height: read_be_u32(&mut rdr).map_err(|_| PngError::TruncatedData)?, + width: read_be_u32(&byte_data[0..4]), + height: read_be_u32(&byte_data[4..8]), interlaced: interlaced.try_into()?, }) } @@ -196,8 +214,74 @@ fn palette_to_rgba( } #[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)) +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 0fd63f21..0293dce1 100644 --- a/src/interlace.rs +++ b/src/interlace.rs @@ -90,7 +90,6 @@ pub fn interlace_image(png: &PngImage) -> PngImage { interlaced: Interlacing::Adam7, ..png.ihdr }, - aux_headers: png.aux_headers.clone(), } } @@ -105,7 +104,6 @@ pub fn deinterlace_image(png: &PngImage) -> PngImage { interlaced: Interlacing::None, ..png.ihdr }, - aux_headers: png.aux_headers.clone(), } } diff --git a/src/lib.rs b/src/lib.rs index 4c43e287..46886f33 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,9 +25,8 @@ extern crate rayon; mod rayon; use crate::atomicmin::AtomicMin; -use crate::deflate::{crc32, inflate}; use crate::evaluate::Evaluator; -use crate::headers::IhdrData; +use crate::headers::*; use crate::png::PngData; use crate::png::PngImage; use crate::reduction::*; @@ -45,9 +44,9 @@ 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; @@ -67,9 +66,7 @@ mod sanity_checks; #[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")] @@ -189,10 +186,10 @@ pub struct Options { /// /// Default: `true` pub idat_recoding: bool, - /// Which headers to strip from the PNG file, if any + /// 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` @@ -306,7 +303,7 @@ impl Default for Options { palette_reduction: true, grayscale_reduction: true, idat_recoding: true, - strip: Headers::None, + strip: StripChunks::None, deflate: Deflaters::Libdeflater { compression: 11 }, fast_evaluation: true, timeout: None, @@ -318,6 +315,7 @@ impl Default for Options { /// A raw image definition which can be used to create an optimized png pub struct RawImage { png: Arc, + aux_chunks: Vec, } impl RawImage { @@ -363,35 +361,40 @@ impl RawImage { interlaced: Interlacing::None, }, data, - aux_headers: IndexMap::new(), }), + aux_chunks: Vec::new(), }) } /// Add a png chunk, such as "iTXt", to be included in the output - pub fn add_png_chunk(&mut self, chunk_type: [u8; 4], data: Vec) { - // We can guarantee this will succeed - failure indicates a bug - let png = Arc::get_mut(&mut self.png).unwrap(); - png.aux_headers.insert(chunk_type, data); + 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 default compression level - if let Ok(mut compressed) = deflate::deflate(data, 11, &AtomicMin::new(None)) { - let mut iccp = Vec::with_capacity(compressed.len() + 13); - iccp.extend(b"icc"); // Profile name - generally unused, can be anything - iccp.extend([0, 0]); // Null separator, zlib compression method - iccp.append(&mut compressed); - self.add_png_chunk(*b"iCCP", iccp); + // 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 png = optimize_raw(Arc::clone(&self.png), opts, deadline, None) + 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()) } } @@ -434,7 +437,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"); @@ -522,7 +525,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)?; @@ -535,13 +538,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( @@ -553,27 +550,27 @@ 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 max_size = if opts.force { None } else { Some(png.estimated_output_size()) }; - if let Some(new_png) = optimize_raw(png.raw.clone(), opts, deadline, max_size) { + 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; } + postprocess_chunks(png, opts, &raw.ihdr); + let output = png.output(); if idat_original_size >= png.idat_data.len() { @@ -635,7 +632,7 @@ fn optimize_raw( let mut eval_result = eval.get_best_candidate(); if let Some(ref result) = eval_result { if result.is_reduction { - png = Arc::clone(&result.image.raw); + png = result.image.clone(); reduction_occurred = true; } } @@ -647,7 +644,7 @@ fn optimize_raw( 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 { + 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() { @@ -659,7 +656,7 @@ fn optimize_raw( 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.image.idat_data.len()); + eval.set_best_size(result.idat_data.len()); } eval.try_image(png.clone()); if let Some(result) = eval.get_best_candidate() { @@ -667,22 +664,18 @@ fn optimize_raw( } } // We should have a result here - fail if not (e.g. deadline passed) - let eval_result = eval_result?; + let result = eval_result?; - let trial = TrialOptions { - filter: eval_result.filter, - compression: match opts.deflate { - Deflaters::Libdeflater { compression } => compression, - _ => 0, - }, - }; - if trial.compression > 0 && trial.compression <= eval_compression { - // No further compression required - Some((trial, eval_result.image.idat_data)) - } else { - debug!("Trying: {}", trial.filter); - let best_size = AtomicMin::new(max_size); - perform_trial(&eval_result.image.filtered, opts, trial, &best_size) + 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 @@ -698,28 +691,16 @@ fn optimize_raw( } } - 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()); + debug!("Trying: {} filters", filters.len()); let best_size = AtomicMin::new(max_size); - let results_iter = results.into_par_iter().with_max_len(1); - let best = results_iter.filter_map(|trial| { + 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(trial.filter, opts.optimize_alpha); - perform_trial(filtered, opts, trial, &best_size) + 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) { @@ -730,19 +711,18 @@ fn optimize_raw( }) }; - if let Some((trial, idat_data)) = best { + if let Some((filter, idat_data)) = best { let image = PngData { raw: png, - // The filtered data has not been retained here, but we don't need to return it - filtered: vec![], 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", - trial.compression, - trial.filter, + opts.deflate, + filter, image.idat_data.len() ); return Some(image); @@ -751,7 +731,11 @@ fn optimize_raw( } 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 = result.image; + 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!( @@ -771,37 +755,26 @@ fn optimize_raw( 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(); +) -> 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", - trial.compression, - trial.filter, + opts.deflate, + filter, bytes ); - Some((trial, n)) + Some((filter, new_idat)) } Err(PngError::DeflatedDataTooLong(bytes)) => { trace!( " zc = {} f = {:8} >{} bytes", - trial.compression, - trial.filter, + opts.deflate, + filter, bytes, ); None @@ -867,100 +840,59 @@ fn report_format(prefix: &str, png: &PngImage) { ); } -/// 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 + }); } } diff --git a/src/main.rs b/src/main.rs index 3676e85e..585f7086 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,9 +17,9 @@ 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")] @@ -496,40 +496,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") { @@ -556,6 +560,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 c25c52a9..49f7a89d 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -4,8 +4,8 @@ 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 rustc_hash::FxHashMap; @@ -30,8 +30,6 @@ pub struct PngImage { pub ihdr: IhdrData, /// The uncompressed, unfiltered data from the IDAT chunk pub data: Vec, - /// 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 @@ -41,17 +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, } 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> { @@ -80,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)?; @@ -88,71 +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, - aux_headers.remove(b"PLTE"), - aux_headers.remove(b"tRNS"), + let ihdr = parse_ihdr_chunk( + &ihdr_chunk, + key_chunks.remove(b"PLTE"), + key_chunks.remove(b"tRNS"), )?; - let raw_data = deflate::inflate(idat_headers.as_ref(), ihdr_header.raw_data_size())?; + 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 mut raw = PngImage { - ihdr: ihdr_header, + ihdr, data: raw_data, - 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, }) } - /// Return an estimate of the output size + /// Return an estimate of the output size which can help with evaluation of very small data pub fn estimated_output_size(&self) -> usize { - // Add the size of the PLTE and tRNS chunks to the compressed idat size - // This can help with evaluation of very small data - let size = self.idat_data.len(); - size + match &self.raw.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, - } + self.idat_data.len() + self.raw.key_chunks_size() } /// Format the `PngData` struct into a valid PNG bytestream @@ -173,14 +165,13 @@ 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 and transparency match &self.raw.ihdr.color_type { @@ -210,14 +201,13 @@ impl PngData { } _ => {} } - // 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); @@ -265,6 +255,24 @@ impl PngImage { } } + /// 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 #[inline] pub fn scan_lines(&self, has_filter: bool) -> ScanLines<'_> { @@ -451,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/reduction/alpha.rs b/src/reduction/alpha.rs index 4b9fd439..3045ddf6 100644 --- a/src/reduction/alpha.rs +++ b/src/reduction/alpha.rs @@ -25,7 +25,6 @@ pub fn cleaned_alpha_channel(png: &PngImage) -> Option { Some(PngImage { data: reduced, ihdr: png.ihdr.clone(), - aux_headers: png.aux_headers.clone(), }) } @@ -88,20 +87,11 @@ pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option Option { bit_depth: BitDepth::Eight, ..png.ihdr }, - aux_headers: png.aux_headers.clone(), }) } @@ -37,37 +36,22 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> O // Calculate the current number of pixels per byte let ppb = 8 / bit_depth; - if let ColorType::Indexed { .. } = png.ihdr.color_type { - 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 { @@ -155,6 +139,5 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> O bit_depth: (minimum_bits as u8).try_into().unwrap(), ..png.ihdr }, - aux_headers: png.aux_headers.clone(), }) } diff --git a/src/reduction/color.rs b/src/reduction/color.rs index 177ae1d6..630619d2 100644 --- a/src/reduction/color.rs +++ b/src/reduction/color.rs @@ -3,7 +3,7 @@ use crate::headers::IhdrData; use crate::png::PngImage; use indexmap::IndexSet; use rgb::alt::Gray; -use rgb::{ComponentMap, ComponentSlice, FromSlice, RGB, RGBA, RGBA8}; +use rgb::{ComponentMap, ComponentSlice, FromSlice, RGB, RGBA}; use rustc_hash::FxHasher; use std::hash::{BuildHasherDefault, Hash}; @@ -41,7 +41,7 @@ pub fn reduced_to_indexed(png: &PngImage) -> Option { } let mut raw_data = Vec::with_capacity(png.data.len() / png.channels_per_pixel()); - let mut palette: Vec<_> = match png.ihdr.color_type { + 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 @@ -81,51 +81,12 @@ pub fn reduced_to_indexed(png: &PngImage) -> Option { _ => return None, }; - let mut aux_headers = png.aux_headers.clone(); - if let Some(bkgd_header) = aux_headers.remove(b"bKGD") { - let bg = if png.ihdr.color_type.is_rgb() && bkgd_header.len() == 6 { - // In bKGD 16-bit values are used even for 8-bit images - Some(RGBA8::new( - bkgd_header[1], - bkgd_header[3], - bkgd_header[5], - 255, - )) - } else if png.ihdr.color_type.is_grayscale() && bkgd_header.len() == 2 { - Some(RGBA8::new( - bkgd_header[1], - bkgd_header[1], - bkgd_header[1], - 255, - )) - } else { - None - }; - if let Some(bg) = bg { - let idx = palette.iter().position(|&px| px == bg).or_else(|| { - if palette.len() < 256 { - palette.push(bg); - Some(palette.len() - 1) - } else { - None // No space in palette to store the bg as an index - } - })?; - aux_headers.insert(*b"bKGD", vec![idx as u8]); - } - } - - 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()); - } - Some(PngImage { data: raw_data, ihdr: IhdrData { color_type: ColorType::Indexed { palette }, ..png.ihdr }, - aux_headers, }) } @@ -150,18 +111,6 @@ pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option { reduced.extend_from_slice(&pixel[last_color..]); } - 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()); - } - } - let color_type = match png.ihdr.color_type { ColorType::RGB { transparent_color } => ColorType::Grayscale { // Copy the transparent component if it is also gray @@ -178,7 +127,6 @@ pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option { color_type, ..png.ihdr }, - aux_headers, }) } @@ -223,25 +171,11 @@ pub fn indexed_to_channels(png: &PngImage) -> Option { data.extend_from_slice(&color.as_slice()[ch_start..=ch_end]); } - // Update bKGD if it exists - let mut aux_headers = png.aux_headers.clone(); - if let Some(idx) = aux_headers.remove(b"bKGD").and_then(|b| b.first().cloned()) { - if let Some(color) = palette.get(idx as usize) { - let bkgd = if is_gray { - vec![0, color.r] - } else { - vec![0, color.r, 0, color.g, 0, color.b] - }; - aux_headers.insert(*b"bKGD", bkgd); - } - } - Some(PngImage { ihdr: IhdrData { color_type, ..png.ihdr }, data, - aux_headers, }) } diff --git a/src/reduction/palette.rs b/src/reduction/palette.rs index 83c14e24..8c610d03 100644 --- a/src/reduction/palette.rs +++ b/src/reduction/palette.rs @@ -30,15 +30,6 @@ pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option } } - // Update bKGD if it exists, ensuring it comes last in the palette if otherwise unused - let mut aux_headers = png.aux_headers.clone(); - if let Some(idx) = aux_headers.remove(b"bKGD").and_then(|b| b.first().cloned()) { - if let Some(&color) = palette.get(idx as usize) { - let idx = add_color_to_set(color, &mut condensed, optimize_alpha); - aux_headers.insert(*b"bKGD", vec![idx]); - } - } - 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); @@ -59,7 +50,6 @@ pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option ..png.ihdr }, data, - aux_headers, }) } @@ -145,15 +135,6 @@ pub fn sorted_palette(png: &PngImage) -> Option { let mut enumerated: Vec<_> = palette.iter().enumerate().collect(); - // If the background is the last entry in the palette we should make sure it stays last - // Otherwise an entry that's unused by the idat could prevent reduction to a lower depth - let mut aux_headers = png.aux_headers.clone(); - let bkgd_idx = aux_headers.remove(b"bKGD").and_then(|b| b.first().cloned()); - let bkgd_last = match bkgd_idx { - Some(idx) if idx as usize + 1 == palette.len() => enumerated.pop(), - _ => None, - }; - // Sort the palette enumerated.sort_by(|a, b| { // Sort by ascending alpha and descending luma @@ -167,10 +148,6 @@ pub fn sorted_palette(png: &PngImage) -> Option { color_val(a.1).cmp(&color_val(b.1)) }); - if let Some(bkgd) = bkgd_last { - enumerated.push(bkgd); - } - // 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) { @@ -185,17 +162,11 @@ pub fn sorted_palette(png: &PngImage) -> Option { 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(); - // Update bKGD if it exists - if let Some(idx) = bkgd_idx.map(|idx| new_map[idx as usize]) { - aux_headers.insert(*b"bKGD", vec![idx]); - } - Some(PngImage { ihdr: IhdrData { color_type: ColorType::Indexed { palette }, ..png.ihdr }, data, - aux_headers, }) } diff --git a/tests/files/strip_headers_all.png b/tests/files/strip_headers_all.png index d714a05d305858ceb92cdbfd6010d39c377699ad..6742de54312ee276d18763533983a4e3c2fe998b 100644 GIT binary patch delta 115 zcmaDnh5hJM_6^gy5*SoVTq8Tv9^JM zm4SiMl>7V)3`iPs^HVa@DuEh|fT~S(4J|?pEUZiotV~QG8m?adU)Ma9Yx`6##&i1t D?+YV? delta 18 acmX>)mHqJ)_6^gyntyU_|H;L8WTv9^JM zm4SiMl>7V)3`iPs^HVa@DuEh|fT~S(4J|?pEUZiotV~QG8m?adU)Ma9Yx`6##&i1t D?+YV? delta 18 acmX>)mHqJ)_6^gyntyU_|H;L8WTv9^JM zm4SiMl>7V)3`iPs^HVa@DuEh|fT~S(4J|?pEUZiotV~QG8m?adU)Ma9Yx`6##&i1t D?+YV? delta 18 acmX>)mHqJ)_6^gyntyU_|H;L8WTv9^JM zm4SiMl>7V)3`iPs^HVa@DuEh|fT~S(4J|?pEUZiotV~QG8m?adU)Ma9Yx`6##&i1t D?+YV? delta 18 acmX>)mHqJ)_6^gyntyU_|H;L8W x, Err(x) => { remove_file(output).ok(); diff --git a/tests/flags.rs b/tests/flags.rs index ebb2108e..f3fb17d2 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; @@ -47,7 +47,7 @@ fn test_it_converts_callbacks( 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.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); @@ -63,7 +63,7 @@ fn test_it_converts_callbacks( 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(); @@ -203,17 +203,24 @@ fn verbose_mode() { } } +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(_) => (), @@ -222,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(); @@ -230,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(); } @@ -241,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(_) => (), @@ -256,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(); @@ -264,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(); } @@ -275,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(_) => (), @@ -290,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(); @@ -298,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(); } @@ -309,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(_) => (), @@ -324,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(); @@ -332,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(); } @@ -345,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); @@ -356,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(); @@ -375,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); @@ -386,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(); @@ -405,7 +412,7 @@ 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.png_header_code(), INDEXED); @@ -418,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(); @@ -439,7 +446,7 @@ 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.png_header_code(), INDEXED); @@ -452,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(); @@ -476,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); @@ -487,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(); @@ -570,7 +577,7 @@ 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.png_header_code(), RGBA); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); @@ -582,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(); diff --git a/tests/interlaced.rs b/tests/interlaced.rs index 52540a85..573c3182 100644 --- a/tests/interlaced.rs +++ b/tests/interlaced.rs @@ -1,6 +1,6 @@ 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; @@ -35,7 +35,7 @@ fn test_it_converts( ) { 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.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); @@ -48,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(); diff --git a/tests/interlacing.rs b/tests/interlacing.rs index 10f558e6..2e971979 100644 --- a/tests/interlacing.rs +++ b/tests/interlacing.rs @@ -1,6 +1,6 @@ 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; @@ -33,7 +33,7 @@ fn test_it_converts( ) { 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.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); @@ -53,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(); 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 index 03802444..c3e16fb2 100644 --- a/tests/raw.rs +++ b/tests/raw.rs @@ -16,11 +16,11 @@ fn test_it_converts(input: &str) { let opts = get_opts(); let original_data = PngData::read_file(&PathBuf::from(input)).unwrap(); - let png = PngData::from_slice(&original_data, opts.fix_errors).unwrap(); - let png = Arc::try_unwrap(png.raw).unwrap(); + let image = PngData::from_slice(&original_data, &opts).unwrap(); + let png = Arc::try_unwrap(image.raw).unwrap(); - let num_headers = png.aux_headers.len(); - assert!(num_headers > 0); + let num_chunks = image.aux_chunks.len(); + assert!(num_chunks > 0); let mut raw = RawImage::new( png.ihdr.width, @@ -31,14 +31,14 @@ fn test_it_converts(input: &str) { ) .unwrap(); - for (chunk_type, data) in png.aux_headers { - raw.add_png_chunk(chunk_type, data); + 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.fix_errors).unwrap(); - assert!(new.raw.aux_headers.len() == num_headers); + 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)); diff --git a/tests/reduction.rs b/tests/reduction.rs index 3d6577eb..8a1a1e34 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -1,6 +1,6 @@ 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; @@ -37,7 +37,7 @@ fn test_it_converts( 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.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in, "test file is broken"); @@ -50,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(); @@ -933,7 +933,7 @@ 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.png_header_code(), INDEXED); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); @@ -948,7 +948,7 @@ 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(); @@ -970,7 +970,7 @@ 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.png_header_code(), INDEXED); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); @@ -985,7 +985,7 @@ 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(); @@ -1007,7 +1007,7 @@ 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.png_header_code(), INDEXED); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); @@ -1022,7 +1022,7 @@ 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(); diff --git a/tests/regression.rs b/tests/regression.rs index c24c8373..c84a25dd 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -1,6 +1,6 @@ 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; @@ -36,7 +36,7 @@ fn test_it_converts( ) { 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.png_header_code(), @@ -52,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(); @@ -70,13 +70,7 @@ fn test_it_converts( "optimized to wrong bit depth" ); if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type { - let mut max_palette_size = 1 << (png.raw.ihdr.bit_depth as u8); - // 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); + assert!(palette.len() <= 1 << (png.raw.ihdr.bit_depth as u8)); } remove_file(output).ok(); @@ -100,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); @@ -113,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(); @@ -231,7 +225,7 @@ fn issue_59() { None, RGBA, BitDepth::Eight, - RGBA, + INDEXED, BitDepth::Eight, ); } diff --git a/tests/strategies.rs b/tests/strategies.rs index 415014fe..8b4b9478 100644 --- a/tests/strategies.rs +++ b/tests/strategies.rs @@ -1,6 +1,6 @@ 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; @@ -36,7 +36,7 @@ fn test_it_converts( 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.png_header_code(), color_type_in); @@ -49,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(); From c1222368b457cf44d736af026802424e09d855cc Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 22 May 2023 08:11:36 +1200 Subject: [PATCH 15/18] Deinterlace by default --- src/lib.rs | 4 ++-- src/main.rs | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 46886f33..82856624 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -159,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, @@ -296,7 +296,7 @@ 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, diff --git a/src/main.rs b/src/main.rs index 585f7086..a1032d7e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -144,13 +144,14 @@ 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("verbose") @@ -394,7 +395,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") { From 88b930b5b196f44ce103903acf12d2bbe1eda1ec Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 22 May 2023 09:25:01 +1200 Subject: [PATCH 16/18] Add scale16 option to force 16-bit reduction --- benches/reductions.rs | 10 +++++++++- src/lib.rs | 5 +++++ src/main.rs | 9 +++++++++ src/reduction/bit_depth.rs | 38 +++++++++++++++++++++++++++++++++++++- src/reduction/mod.rs | 2 +- tests/flags.rs | 17 +++++++++++++++++ 6 files changed, 78 insertions(+), 3 deletions(-) diff --git a/benches/reductions.rs b/benches/reductions.rs index 16aae41f..45c14bee 100644 --- a/benches/reductions.rs +++ b/benches/reductions.rs @@ -13,7 +13,15 @@ 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, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduced_bit_depth_16_to_8(&png.raw)); + 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] diff --git a/src/lib.rs b/src/lib.rs index 82856624..bbb9919e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -186,6 +186,10 @@ pub struct Options { /// /// Default: `true` pub idat_recoding: bool, + /// 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` @@ -303,6 +307,7 @@ impl Default for Options { palette_reduction: true, grayscale_reduction: true, idat_recoding: true, + scale_16: false, strip: StripChunks::None, deflate: Deflaters::Libdeflater { compression: 11 }, fast_evaluation: true, diff --git a/src/main.rs b/src/main.rs index a1032d7e..71c66fa3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -153,6 +153,11 @@ fn main() { .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 (use multiple times to increase verbosity)") @@ -446,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; } diff --git a/src/reduction/bit_depth.rs b/src/reduction/bit_depth.rs index d6dc68ae..7a4a1ca5 100644 --- a/src/reduction/bit_depth.rs +++ b/src/reduction/bit_depth.rs @@ -4,11 +4,15 @@ use crate::png::PngImage; /// Attempt to reduce a 16-bit image to 8-bit, returning the reduced image if successful #[must_use] -pub fn reduced_bit_depth_16_to_8(png: &PngImage) -> Option { +pub fn reduced_bit_depth_16_to_8(png: &PngImage, force_scale: bool) -> Option { if png.ihdr.bit_depth != BitDepth::Sixteen { 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 @@ -25,6 +29,38 @@ pub fn reduced_bit_depth_16_to_8(png: &PngImage) -> Option { }) } +/// Forcibly reduce a 16-bit image to 8-bit by scaling, returning the reduced image if successful +#[must_use] +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 { diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index f5539096..5dbd6079 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -42,7 +42,7 @@ pub(crate) fn perform_reductions( // 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) { + if let Some(reduced) = reduced_bit_depth_16_to_8(&png, opts.scale_16) { png = Arc::new(reduced); reduction_occurred = true; } diff --git a/tests/flags.rs b/tests/flags.rs index f3fb17d2..7d146a31 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -604,6 +604,23 @@ fn fix_errors() { 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() { From 12761bbfb12a08a4aaeb73174dd99ef9f6aec89f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= <7822554+AlexTMjugador@users.noreply.github.com> Date: Sun, 28 May 2023 19:45:46 +0200 Subject: [PATCH 17/18] Update Zopfli and several other depenencies (#512) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update Zopfli and several other depenencies I've just published a new Zopfli release, v0.7.3, which includes several new features and internal refactors. Performance should be a tad bit better, but I didn't test it throughly, so YMMV. Perhaps more importantly for OxiPNG, its dependency tree is smaller, and Gzip-exclusive compression code can be excluded at compile time thanks to new feature switches. As I mentioned on https://github.com/shssoichiro/oxipng/pull/495#issuecomment-1552669505, I plan on delivering more significant Zopfli performance improvements at some point, but for now I think it's good to give the new release more real-world usage and testing 😄 While at it, I've upgraded other dependencies that are not performance-critical to their latest semver-compatible versions. This excludes `libdeflater` on purpose, as its performance characteristics are said to be somewhat different. * Update Zopfli to v0.7.4 v0.7.3 was superseeded shortly after v0.7.3 was released to address a last minute change to the new API it introduced. OxiPNG is not affected by this, but I think it's good practice to update Zopfli anyway. --- Cargo.lock | 196 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 18 ++--- 2 files changed, 106 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9b8d1e67..6dc42816 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,12 +8,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - [[package]] name = "atty" version = "0.2.14" @@ -51,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" @@ -63,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" @@ -75,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", @@ -103,21 +97,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" -[[package]] -name = "crc" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53757d12b596c16c78b83458d732a5d1a17ab3f53f2f7412f6fb57cc8a140ab3" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" - [[package]] name = "crc32fast" version = "1.3.2" @@ -129,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", @@ -139,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", @@ -150,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", @@ -163,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", @@ -190,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", @@ -206,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" @@ -236,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", @@ -250,26 +238,20 @@ 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", "rayon", ] -[[package]] -name = "iter-read" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c397ca3ea05ad509c4ec451fea28b4771236a376ca1c69fd5143aae0cf8f93c4" - [[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" @@ -300,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]] @@ -358,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" @@ -391,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", ] @@ -409,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", @@ -419,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", @@ -440,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", ] @@ -470,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 = "stderrlog" @@ -515,18 +505,19 @@ 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", ] [[package]] name = "typed-arena" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0685c84d5d54d1c26f7d3eb96cd41550adb97baed141a761cf335d3d33bcd0ae" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "wild" @@ -570,9 +561,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", @@ -585,45 +585,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" @@ -636,14 +636,12 @@ dependencies = [ [[package]] name = "zopfli" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1e0d16c30236860686a8f03d36b384dc2fc0675a8916367d2f9a1ecd795eab6" +checksum = "4e0650ae6a051326d798eb099b632f1afb0d323d25ee4ec82ffb0779512084d5" dependencies = [ - "adler32", - "byteorder", - "crc", - "iter-read", + "crc32fast", "log", + "simd-adler32", "typed-arena", ] diff --git a/Cargo.toml b/Cargo.toml index 443f81d1..cf3ab8dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,30 +27,30 @@ name = "zopfli" required-features = ["zopfli"] [dependencies] -zopfli = { version = "0.7.1", optional = true } -rgb = "0.8.33" -indexmap = "1.9.1" +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.20" +version = "3.2.25" [dependencies.wild] optional = true @@ -60,7 +60,7 @@ version = "2.1.0" optional = true default-features = false features = ["png"] -version = "0.24.3" +version = "0.24.6" [build-dependencies] rustc_version = "0.4.0" From 86fccf082a92f4fc2e394a964b4126ae6fa983ac Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 23 May 2023 16:38:07 +1200 Subject: [PATCH 18/18] Fix grayscale_reduction option --- benches/reductions.rs | 2 +- src/reduction/color.rs | 8 ++++++-- src/reduction/mod.rs | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/benches/reductions.rs b/benches/reductions.rs index 45c14bee..46fb8f49 100644 --- a/benches/reductions.rs +++ b/benches/reductions.rs @@ -257,7 +257,7 @@ fn reductions_palette_8_to_grayscale_8(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| color::indexed_to_channels(&png.raw)); + b.iter(|| color::indexed_to_channels(&png.raw, true)); } #[bench] diff --git a/src/reduction/color.rs b/src/reduction/color.rs index 630619d2..457c4422 100644 --- a/src/reduction/color.rs +++ b/src/reduction/color.rs @@ -132,7 +132,7 @@ pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option { /// Attempt to convert indexed to a different color type, returning the resulting image if successful #[must_use] -pub fn indexed_to_channels(png: &PngImage) -> Option { +pub fn indexed_to_channels(png: &PngImage, allow_grayscale: bool) -> Option { if png.ihdr.bit_depth != BitDepth::Eight { return None; } @@ -142,7 +142,11 @@ pub fn indexed_to_channels(png: &PngImage) -> Option { }; // Determine which channels are required - let is_gray = palette.iter().all(|c| c.r == c.g && c.g == c.b); + 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, diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index 5dbd6079..e07f57db 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -50,7 +50,7 @@ pub(crate) fn perform_reductions( // Attempt to reduce RGB to grayscale // This is just removal of bytes and does not need to be evaluated - if opts.color_type_reduction && !deadline.passed() { + 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; @@ -98,7 +98,7 @@ pub(crate) fn perform_reductions( // 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) { + 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;