Compare commits

..

No commits in common. "master" and "v9.1.1" have entirely different histories.

303 changed files with 2862 additions and 3856 deletions

View file

@ -1,2 +1,9 @@
[alias]
xtask = "run --manifest-path xtask/Cargo.toml --"
# Remove this if targeting AArch64 from an AArch64 Linux box
[target.'cfg(all(target_os = "linux", target_arch = "aarch64"))']
runner = 'qemu-aarch64'
[target.aarch64-unknown-linux-gnu]
linker = 'aarch64-linux-gnu-gcc'
[target.aarch64-unknown-linux-musl]
linker = 'aarch64-linux-musl-gcc'

View file

@ -1,7 +0,0 @@
.editorconfig
.gitignore
/.git/
/.github/
/.pre-commit-hooks.yaml
/scripts/
/target/

View file

@ -1,24 +1,6 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
commit-message:
prefix: "chore"
include: "scope"
labels:
- "dependencies"
- "rust"
groups:
rust-dependencies:
patterns:
- "*"
update-types:
- "minor"
- "patch"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
interval: monthly

View file

@ -3,12 +3,7 @@ name: deploy
on:
push:
tags:
- v*.*.*
concurrency:
# If we ever end up with two concurrent jobs for releasing the same tag, the former should be cancelled
group: release-${{ github.ref }}
cancel-in-progress: true
- 'v*.*.*'
permissions:
actions: read
@ -25,10 +20,6 @@ jobs:
if: ${{ !github.event.repository.fork }}
strategy:
# Execute one job at a time to avoid potential race conditions in the
# GitHub release management APIs. See:
# https://github.com/softprops/action-gh-release/issues/445#issuecomment-2407940052
max-parallel: 1
matrix:
target:
- x86_64-unknown-linux-gnu
@ -42,32 +33,18 @@ jobs:
steps:
- name: Checkout source
uses: actions/checkout@v6
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: nightly
cache-bin: false
cache-shared-key: cache
- name: Install cargo-deb
if: endsWith(matrix.target, '-linux-gnu')
uses: taiki-e/install-action@v2
with:
tool: cargo-deb
uses: actions/checkout@v4
- name: Get the Oxipng version
id: oxipngMeta
run: >
echo "version=$(cargo metadata --format-version 1 --no-deps |
jq -r '.packages[] | select(.name == "oxipng").version')" >> "$GITHUB_OUTPUT"
run: echo "version=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "oxipng").version')"
>> "$GITHUB_OUTPUT"
- name: Retrieve ${{ matrix.target }} binary
uses: dawidd6/action-download-artifact@v21
uses: dawidd6/action-download-artifact@v3
with:
workflow: oxipng.yml
commit: ${{ github.sha }}
commit: ${{ env.GITHUB_SHA }}
name: Oxipng binary (${{ matrix.target }})
path: target
@ -94,12 +71,12 @@ jobs:
# so make the binary world-executable to meet user
# expectations set by preceding releases.
# Related issue:
# https://github.com/oxipng/oxipng/issues/575
# https://github.com/shssoichiro/oxipng/issues/575
chmod ugo+x "$ARCHIVE_NAME"/oxipng
tar -vczf "${ARCHIVE_NAME}.tar.gz" "$ARCHIVE_NAME"/*;;
esac
- name: Install AArch64 libc components
- name: Install QEMU and AArch64 cross compiler
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get -yq update
@ -108,27 +85,20 @@ jobs:
# properly
sudo apt-get -yq install libc6-arm64-cross libgcc-s1-arm64-cross
- name: Build Debian packages
- name: Build deb archives
if: endsWith(matrix.target, '-linux-gnu')
env:
# The *-arm64-cross packages above install AArch64 libraries in
# /usr/<arch>/lib instead of /usr/lib/<arch>, as expected by
# cargo-deb and dpkg-shlibdeps to find such shared libraries.
# Make both of them visible to such commands by adding that directory
# to the dynamic linker's library search path. See:
# - <https://man7.org/linux/man-pages/man1/dpkg-shlibdeps.1.html> ("Errors" section)
# - <https://github.com/kornelski/cargo-deb/issues/21>
LD_LIBRARY_PATH: /usr/aarch64-linux-gnu/lib
run: |
mkdir -p "target/${{ matrix.target }}/release"
mv target/oxipng "target/${{ matrix.target }}/release"
mv target/debug/assets "target/${{ matrix.target }}/release"
cargo install --locked cargo-deb
cargo deb --target "${{ matrix.target }}" --no-build --no-strip
- name: Create release notes
run: tail -n +3 CHANGELOG.md | sed -e '/^$/,$d' > RELEASE_NOTES.txt
- name: Create release
uses: softprops/action-gh-release@v3
uses: softprops/action-gh-release@v2
with:
name: v${{ steps.oxipngMeta.outputs.version }}
body_path: RELEASE_NOTES.txt

View file

@ -1,60 +0,0 @@
name: docker
on:
push:
branches:
- master
tags:
- v*.*.*
pull_request:
types:
- opened
- synchronize
workflow_dispatch:
inputs:
use_cache:
description: "Use build cache"
required: true
type: boolean
default: true
concurrency:
# If we ever end up with two concurrent Docker build jobs for the same commit
# and same versioning context, the former should be cancelled
group: docker-${{ github.sha }}-${{ github.ref_type }}-${{ github.head_ref }}
cancel-in-progress: true
jobs:
build:
uses: docker/github-builder/.github/workflows/build.yml@v1
permissions:
contents: read
packages: write
id-token: write
attestations: write
with:
output: image
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
cache: ${{ github.event_name != 'workflow_dispatch' || inputs.use_cache }}
# We don't sign the image because ghcr.io doesn't support OCI Referrers API yet, and the fallback relies on pushing new tags on each build
# https://github.com/docker/github-builder/issues/109#issuecomment-3885082486
# https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#backwards-compatibility
sign: false
sbom: true
meta-images: ghcr.io/${{ github.repository }}
meta-tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{raw}}
set-meta-annotations: true
set-meta-labels: true
labels: |
org.opencontainers.image.title=Oxipng
annotations: |
org.opencontainers.image.title=Oxipng
secrets:
registry-auths: |
- registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

View file

@ -8,11 +8,6 @@ on:
- synchronize
workflow_dispatch:
concurrency:
# If we ever end up with two concurrent CI jobs for the same commit, the former should be cancelled
group: ci-${{ github.sha }}
cancel-in-progress: true
jobs:
ci:
name: CI
@ -38,83 +33,94 @@ jobs:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-24.04
os: ubuntu-22.04
target-apt-arch: amd64
- target: x86_64-unknown-linux-musl
os: ubuntu-24.04
os: ubuntu-22.04
target-apt-arch: amd64
- target: aarch64-unknown-linux-gnu
os: ubuntu-24.04-arm
os: ubuntu-22.04
target-apt-arch: arm64
- target: aarch64-unknown-linux-musl
os: ubuntu-24.04-arm
os: ubuntu-22.04
target-apt-arch: arm64
- target: x86_64-pc-windows-msvc
os: windows-latest
- target: i686-pc-windows-msvc
os: windows-latest
- target: x86_64-apple-darwin
os: macos-15
os: macos-12
- target: aarch64-apple-darwin
os: macos-15
os: macos-14 # ARM64 runner
env:
CARGO_BUILD_TARGET: ${{ matrix.target }}
RUSTFLAGS: -Zlocation-detail=none
steps:
- name: Checkout source
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Ubuntu multiarch
if: startsWith(matrix.os, 'ubuntu') && matrix.target-apt-arch != 'amd64'
run: |
readonly DISTRO_CODENAME=jammy
sudo dpkg --add-architecture "${{ matrix.target-apt-arch }}"
sudo sed -i "s/^deb http/deb [arch=$(dpkg-architecture -q DEB_HOST_ARCH)] http/" /etc/apt/sources.list
sudo sed -i "s/^deb mirror/deb [arch=$(dpkg-architecture -q DEB_HOST_ARCH)] mirror/" /etc/apt/sources.list
for suite in '' '-updates' '-backports' '-security'; do
echo "deb [arch=${{ matrix.target-apt-arch }}] http://ports.ubuntu.com/ $DISTRO_CODENAME$suite main universe multiverse" | \
sudo tee -a /etc/apt/sources.list >/dev/null
done
- name: Install musl development files
if: endsWith(matrix.target, '-musl')
run: |
sudo apt-get -yq update
sudo apt-get -yq install musl-tools musl-dev:${{ matrix.target-apt-arch }}
- name: Install QEMU and AArch64 cross compiler
if: startsWith(matrix.target, 'aarch64-unknown-linux')
run: |
sudo apt-get -yq update
# libc6 must be present to run executables dynamically linked
# against glibc for the target architecture
sudo apt-get -yq install qemu-user gcc-aarch64-linux-gnu libc6:arm64
- name: Cache Cargo artifacts
uses: Swatinem/rust-cache@v2
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: nightly
target: ${{ env.CARGO_BUILD_TARGET }}
components: clippy, rustfmt, rust-src
cache-bin: false
cache-shared-key: cache
targets: ${{ env.CARGO_BUILD_TARGET }}
components: clippy, rustfmt
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Install cargo-hack
if: matrix.target == 'x86_64-unknown-linux-gnu'
uses: taiki-e/install-action@cargo-hack
- name: Install clippy-sarif
if: matrix.target == 'x86_64-unknown-linux-gnu'
uses: taiki-e/install-action@v2
with:
tool: clippy-sarif
- name: Install sarif-fmt
if: matrix.target == 'x86_64-unknown-linux-gnu'
uses: taiki-e/install-action@v2
with:
tool: sarif-fmt
- name: Run rustfmt
if: matrix.target == 'x86_64-unknown-linux-gnu'
run: cargo fmt --check
- name: Run Clippy for all feature combinations
- name: Run Clippy (no default features)
if: matrix.target == 'x86_64-unknown-linux-gnu'
run: >
set -o pipefail;
cargo hack clippy --no-deps --all-targets --feature-powerset \
--exclude-features sanity-checks,system-libdeflate \
--message-format=json -- -D warnings \
| clippy-sarif
| tee clippy-results.sarif
| sarif-fmt
uses: giraffate/clippy-action@v1
with:
clippy_flags: --no-deps --all-targets --no-default-features -- -D warnings
reporter: github-check
fail_on_error: true
- name: Run Clippy (all features)
if: matrix.target == 'x86_64-unknown-linux-gnu'
uses: giraffate/clippy-action@v1
with:
clippy_flags: --no-deps --all-targets --all-features -- -D warnings
reporter: github-check
fail_on_error: true
- name: Run tests
run: |
@ -129,26 +135,16 @@ jobs:
run: cargo doc --release --no-deps
- name: Build CLI binary
run: cargo build --release -Zbuild-std
env:
RUSTFLAGS: "-Zlocation-detail=none -Zunstable-options -Cpanic=immediate-abort"
run: cargo build --release
- name: Upload CLI binary as artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: Oxipng binary (${{ matrix.target }})
path: |
target/${{ env.CARGO_BUILD_TARGET }}/release/oxipng
target/${{ env.CARGO_BUILD_TARGET }}/release/oxipng.exe
- name: Upload analysis results to GitHub
uses: github/codeql-action/upload-sarif@v4
if: always() && matrix.target == 'x86_64-unknown-linux-gnu'
continue-on-error: true
with:
sarif_file: clippy-results.sarif
category: clippy
msrv-check:
name: MSRV check
@ -160,16 +156,15 @@ jobs:
steps:
- name: Checkout source
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Cache Cargo artifacts
uses: Swatinem/rust-cache@v2
- name: Install MSRV Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.85.1
cache-bin: false
cache-shared-key: cache
uses: dtolnay/rust-toolchain@1.74.0
- name: Install nextest
uses: taiki-e/install-action@nextest

8
.gitignore vendored
View file

@ -1,6 +1,6 @@
.DS_Store
target
*.bk
.DS_Store
*.out.png
/.idea/
/node_modules/
/target/
/.idea
/node_modules

View file

@ -1,65 +1,3 @@
## Version 10.1.1
- [Performance] Improve Bigrams performance, giving notably faster results at lower levels.
- [Improvement] Change `--np` flag to also prevent conversion to indexed from other color types.
- [Improvement] Improve support for recompressing ICC profiles with high compression ratios.
- [Misc] Add warning when `--ziwi` exceeds `--zi`.
- [Build] Further reduce size of binaries.
## Version 10.1.0
- [Feature] Add `--json` option for machine-readable output.
- [Improvement] New default output with file counter and summary. (Use an extra `-v` flag to get the same output as before.)
- [Bugfix] Fix fast mode sometimes giving suboptimal results for small, indexed images.
## Version 10.0.0
- [Breaking] CLI: Change short zopfli flag from `-Z` to `-z`.
- [Breaking] CLI: Change `--pretend`/`-P` to `--dry-run`/`-d`.
- [Breaking] CLI: Change `--interlace` options from `1`/`0` to `on`/`off`.
- [Breaking] API: Change `Options.interlace: Option<Interlacing>` to `Options.interlace: Option<bool>`.
- [Breaking] API: Change `Options.filter: IndexSet<RowFilter>` to `Options.filters: IndexSet<FilterStrategy>`.
- [Breaking] API: Change `Options.deflate: Deflaters` to `Options.deflater: Deflater`.
- [Breaking] API: Change `Deflaters::Zopfli { .. }` to `Deflater::Zopfli(ZopfliOptions { .. })`.
- [Breaking] API: Change `optimize()` to return `(usize, usize)`, with original and optimized sizes.
- [Feature] Allow zopfli iterations higher than 255.
- [Feature] Add `--ziwi` option for zopfli iterations without improvement.
- [Feature] Add `--max-raw-size` option to skip images that are too large.
- [Feature] Add `--brute-level` and `--brute-lines` options for advanced control of Brute filter.
- [Improvement] Reduce memory usage for `fast` mode.
- [Improvement] Slightly improve compression with Brute filter at levels 5 and 6.
- [Misc] Change `--preserve` option to no longer preserve last access time.
- [Misc] Make help output colored.
## Version 9.1.5
- [Feature] Add `--sequential` option to process files sequentially rather than in parallel.
- [Performance] Update to latest Zopfli with greatly improved performance.
- [Improvement] Reduce memory usage.
- [Bugfix] Correct handling of grayscale conversion when ICC profile is present.
## Version 9.1.4
- [Improvement] Improve optimization of APNG files (reductions still not supported yet).
- [Improvement] Improve reductions for small images and ensure consistent results for repeat runs.
- [Build] Add feature `system-libdeflate` to use the system-installed version of libdeflate.
- [Misc] Strip C2PA metadata by default.
## Version 9.1.3
- [Feature] Add `--zi` option to control the number of Zopfli iterations.
- [Improvement] Allow setting compression level to 0.
- [Performance] Improve filtering performance for some images.
- [Build] Move man page generation to an xtask.
## Version 9.1.2
- [Bugfix] Fix `--nx` still applying deinterlacing by default.
- [Bugfix] Fix wildcard matching being case-sensitive on Windows.
- [Bugfix] Fix optimized APNGs not being compatible with some programs.
- [Build] Fix feature `sanity-checks` not working without `parallel`.
- [Misc] Resolve ambiguity between optional dependencies and crate features.
## Version 9.1.1
- [Build] Change man page generation path to resolve issue with cargo publish.
@ -199,51 +137,51 @@
### Version 3.0.0
- [Breaking] Bump minimum Rust version to 1.41.0
- [Breaking] Use IndexMap/IndexSet to provide more consistent performance ([#202](https://github.com/oxipng/oxipng/pull/202))
- [Breaking] Use IndexMap/IndexSet to provide more consistent performance ([#202](https://github.com/shssoichiro/oxipng/pull/202))
- This changes some public-facing types.
`IndexMap` and `IndexSet` are reexported
at the crate root to aid migration.
- [Breaking] Remove fields from the `Options` struct which were never used ([#211](https://github.com/oxipng/oxipng/pull/211/files#diff-b4aea3e418ccdb71239b96952d9cddb6L217), [#212](https://github.com/oxipng/oxipng/pull/212/files#diff-b4aea3e418ccdb71239b96952d9cddb6L134))
- [Breaking] Refactor zlib-specific options in the `Options` struct ([#210](https://github.com/oxipng/oxipng/pull/210/files))
- [Feature] Add libdeflater as an option ([#203](https://github.com/oxipng/oxipng/pull/203))
- [Feature] Use standard `log` library ([#218](https://github.com/oxipng/oxipng/pull/218))
- [Feature] Add `-o max` setting which will always reference the highest compression preset ([#224](https://github.com/oxipng/oxipng/pull/224))
- [Breaking] Remove fields from the `Options` struct which were never used ([#211](https://github.com/shssoichiro/oxipng/pull/211/files#diff-b4aea3e418ccdb71239b96952d9cddb6L217), [#212](https://github.com/shssoichiro/oxipng/pull/212/files#diff-b4aea3e418ccdb71239b96952d9cddb6L134))
- [Breaking] Refactor zlib-specific options in the `Options` struct ([#210](https://github.com/shssoichiro/oxipng/pull/210/files))
- [Feature] Add libdeflater as an option ([#203](https://github.com/shssoichiro/oxipng/pull/203))
- [Feature] Use standard `log` library ([#218](https://github.com/shssoichiro/oxipng/pull/218))
- [Feature] Add `-o max` setting which will always reference the highest compression preset ([#224](https://github.com/shssoichiro/oxipng/pull/224))
- [Deprecated] `-o 4` was found to be equivalent to `-o 3` and is deprecated.
It will likely be removed in a future release.
For now it remains equivalent to `-o 3`. ([#224](https://github.com/oxipng/oxipng/pull/224))
- [Bugfix] Ensure output is deterministic ([#199](https://github.com/oxipng/oxipng/pull/199))
For now it remains equivalent to `-o 3`. ([#224](https://github.com/shssoichiro/oxipng/pull/224))
- [Bugfix] Ensure output is deterministic ([#199](https://github.com/shssoichiro/oxipng/pull/199))
- Update `image` crate to 0.23
- Update `itertools` crate to 0.9
- Various performance and internal improvements
### Version 2.3.0
- Allow disabling all alpha optimizations ([#181](https://github.com/oxipng/oxipng/pull/181))
- Fix interlacing issues on tiny images ([#182](https://github.com/oxipng/oxipng/pull/182))
- Reduce memory usage in filtering ([#191](https://github.com/oxipng/oxipng/pull/191))
- Implement palette sorting to improve compression ([#193](https://github.com/oxipng/oxipng/pull/193))
- Disable alpha optimizations by default ([#187](https://github.com/oxipng/oxipng/pull/187))
- Add support for WASM ([#194](https://github.com/oxipng/oxipng/pull/194))
- Allow disabling all alpha optimizations ([#181](https://github.com/shssoichiro/oxipng/pull/181))
- Fix interlacing issues on tiny images ([#182](https://github.com/shssoichiro/oxipng/pull/182))
- Reduce memory usage in filtering ([#191](https://github.com/shssoichiro/oxipng/pull/191))
- Implement palette sorting to improve compression ([#193](https://github.com/shssoichiro/oxipng/pull/193))
- Disable alpha optimizations by default ([#187](https://github.com/shssoichiro/oxipng/pull/187))
- Add support for WASM ([#194](https://github.com/shssoichiro/oxipng/pull/194))
### Version 2.2.2
- Fix grayscale bit-depth reduction ([#171](https://github.com/oxipng/oxipng/pull/171))
- Fix typos and incorrect log message ([#172](https://github.com/oxipng/oxipng/pull/172))
- Make metadata order deterministic ([#174](https://github.com/oxipng/oxipng/pull/174))
- Fix 32-bit builds ([#176](https://github.com/oxipng/oxipng/pull/176))
- Enable LTO in release builds ([#177](https://github.com/oxipng/oxipng/pull/177))
- Use deterministic compression strategy ([#179](https://github.com/oxipng/oxipng/pull/179))
- Fix decoding interlaced images with height or width <= 2 ([#175](https://github.com/oxipng/oxipng/pull/175))
- Preallocate memory in reduced_alpha_to_up ([#180](https://github.com/oxipng/oxipng/pull/180))
- Fix grayscale bit-depth reduction ([#171](https://github.com/shssoichiro/oxipng/pull/171))
- Fix typos and incorrect log message ([#172](https://github.com/shssoichiro/oxipng/pull/172))
- Make metadata order deterministic ([#174](https://github.com/shssoichiro/oxipng/pull/174))
- Fix 32-bit builds ([#176](https://github.com/shssoichiro/oxipng/pull/176))
- Enable LTO in release builds ([#177](https://github.com/shssoichiro/oxipng/pull/177))
- Use deterministic compression strategy ([#179](https://github.com/shssoichiro/oxipng/pull/179))
- Fix decoding interlaced images with height or width <= 2 ([#175](https://github.com/shssoichiro/oxipng/pull/175))
- Preallocate memory in reduced_alpha_to_up ([#180](https://github.com/shssoichiro/oxipng/pull/180))
- Update `bit-vec` crate to 0.6
### Version 2.2.1
- Fix compression of very large files ([#167](https://github.com/oxipng/oxipng/pull/167)) ([#168](https://github.com/oxipng/oxipng/pull/168))
- Fix compression of very large files ([#167](https://github.com/shssoichiro/oxipng/pull/167)) ([#168](https://github.com/shssoichiro/oxipng/pull/168))
### Version 2.2.0
- Various internal improvements ([#154](https://github.com/oxipng/oxipng/pull/154)) ([#158](https://github.com/oxipng/oxipng/pull/158)) ([#160](https://github.com/oxipng/oxipng/pull/160)) ([#161](https://github.com/oxipng/oxipng/pull/161)) ([#162](https://github.com/oxipng/oxipng/pull/162)) ([#163](https://github.com/oxipng/oxipng/pull/163))
- Various internal improvements ([#154](https://github.com/shssoichiro/oxipng/pull/154)) ([#158](https://github.com/shssoichiro/oxipng/pull/158)) ([#160](https://github.com/shssoichiro/oxipng/pull/160)) ([#161](https://github.com/shssoichiro/oxipng/pull/161)) ([#162](https://github.com/shssoichiro/oxipng/pull/162)) ([#163](https://github.com/shssoichiro/oxipng/pull/163))
- Update `image` crate to 0.21.0
- Update `itertools` crate to 0.8.0
- Update `zopfli` crate to 0.4.0
@ -252,24 +190,24 @@
### Version 2.1.8
- Fix non-standard sBIT headers in other code locations ([#153](https://github.com/oxipng/oxipng/issues/153))
- Fix non-standard sBIT headers in other code locations ([#153](https://github.com/shssoichiro/oxipng/issues/153))
### Version 2.1.7
- 80x faster palette reduction ([#150](https://github.com/oxipng/oxipng/pull/150))
- Optimize RGB to palette conversion ([#148](https://github.com/oxipng/oxipng/pull/148))
- Various microoptimizations ([#146](https://github.com/oxipng/oxipng/pull/146))
- Introduce third-party safe wrapper around cloudflare-zlib ([#149](https://github.com/oxipng/oxipng/pull/149))
- 80x faster palette reduction ([#150](https://github.com/shssoichiro/oxipng/pull/150))
- Optimize RGB to palette conversion ([#148](https://github.com/shssoichiro/oxipng/pull/148))
- Various microoptimizations ([#146](https://github.com/shssoichiro/oxipng/pull/146))
- Introduce third-party safe wrapper around cloudflare-zlib ([#149](https://github.com/shssoichiro/oxipng/pull/149))
### Version 2.1.6
- Identify and drop useless sRGB profiles ([#143](https://github.com/oxipng/oxipng/pull/143))
- Alpha heuristic improvements ([#144](https://github.com/oxipng/oxipng/pull/144))
- Identify and drop useless sRGB profiles ([#143](https://github.com/shssoichiro/oxipng/pull/143))
- Alpha heuristic improvements ([#144](https://github.com/shssoichiro/oxipng/pull/144))
- Bump `miniz_oxide` and `cloudflare-zlib-sys` to 0.2.0
### Version 2.1.5
- Fix issue where some images will incorrectly reduce bit depth ([#140](https://github.com/oxipng/oxipng/issues/140))
- Fix issue where some images will incorrectly reduce bit depth ([#140](https://github.com/shssoichiro/oxipng/issues/140))
### Version 2.1.4
@ -282,22 +220,22 @@
### Version 2.1.2
- Fix issue with PNG to Indexed reduction on images with transparency pixel ([#129](https://github.com/oxipng/oxipng/issues/129))
- Fix issue with PNG to Indexed reduction on images with transparency pixel ([#129](https://github.com/shssoichiro/oxipng/issues/129))
### Version 2.1.1
- More fixes for alpha optimization on interlaced images ([#133](https://github.com/oxipng/oxipng/issues/133))
- More fixes for alpha optimization on interlaced images ([#133](https://github.com/shssoichiro/oxipng/issues/133))
### Version 2.1.0
- [SEMVER_MINOR] Bump minimum Rust version to 1.27.0
- [SEMVER_MINOR] Reenable faster Cloudflare zlib compression on platforms that support it
- Fix memory leak with Cloudflare zlib ([#126](https://github.com/oxipng/oxipng/issues/126))
- Fix memory leak with Cloudflare zlib ([#126](https://github.com/shssoichiro/oxipng/issues/126))
- Minor fixes and cleanup
### Version 2.0.2
- Fix an issue in alpha optimization on interlaced images ([#113](https://github.com/oxipng/oxipng/issues/113))
- Fix an issue in alpha optimization on interlaced images ([#113](https://github.com/shssoichiro/oxipng/issues/113))
### Version 2.0.1
@ -334,12 +272,12 @@
### Version 1.0.1
- Bump rayon to 1.0 ([#99](https://github.com/oxipng/oxipng/pull/99) @cuviper)
- Bump rayon to 1.0 ([#99](https://github.com/shssoichiro/oxipng/pull/99) @cuviper)
- Bump minor versions of other dependencies for binary distribution
### Version 1.0.0
- Remove the C dependency on miniz, and replace it with a Rust version ([#57](https://github.com/oxipng/oxipng/issues/57))
- Remove the C dependency on miniz, and replace it with a Rust version ([#57](https://github.com/shssoichiro/oxipng/issues/57))
- This improves decompression speed by 15%. Compression speed is not affected.
- [SEMVER_MAJOR] This also obsoletes the `-zm` command line option and the `memory` key on the `Options` struct.
- Presets will be updated automatically. This means that presets 3 and higher will run significantly more quickly.
@ -366,30 +304,30 @@
### Version 0.18.3
- Return exit code of 1 if an error occurred while processing a file using the CLI app ([#93](https://github.com/oxipng/oxipng/issues/93))
- Return exit code of 1 if an error occurred while processing a file using the CLI app ([#93](https://github.com/shssoichiro/oxipng/issues/93))
### Version 0.18.2
- Bump `image` to 0.18
- Fix unfiltering of scan lines in interlaced images ([#92](https://github.com/oxipng/oxipng/issues/92))
- Fix unfiltering of scan lines in interlaced images ([#92](https://github.com/shssoichiro/oxipng/issues/92))
### Version 0.18.1
- Bump `rayon` to 0.9
- Fix failure to optimize on certain grayscale images ([#89](https://github.com/oxipng/oxipng/issues/89))
- Fix failure to optimize on certain grayscale images ([#89](https://github.com/shssoichiro/oxipng/issues/89))
### Version 0.18.0
- Bump `itertools` to 0.7
- Bump `image` to 0.17
- [SEMVER_MAJOR] Bump minimum rustc version to 1.20.0
- Fix parsing of glob paths on Windows ([#90](https://github.com/oxipng/oxipng/issues/90))
- Fix parsing of glob paths on Windows ([#90](https://github.com/shssoichiro/oxipng/issues/90))
### Version 0.17.2
- Bump `image` to 0.16
- Quickly pass over files that do not have a PNG header ([#85](https://github.com/oxipng/oxipng/issues/85) @emielbeinema)
- Return an error instead of crashing on APNG files ([#83](https://github.com/oxipng/oxipng/issues/83) @emielbeinema)
- Quickly pass over files that do not have a PNG header ([#85](https://github.com/shssoichiro/oxipng/issues/85) @emielbeinema)
- Return an error instead of crashing on APNG files ([#83](https://github.com/shssoichiro/oxipng/issues/83) @emielbeinema)
### Version 0.17.1
@ -405,14 +343,14 @@
trials for optimizing the alpha channel, using the previously mentioned fast heuristic.
This option will make optimization of images with transparency somewhat slower,
but may improve compression.
- Fixed a bug in reducing palettes for images with bit depth of two ([#80](https://github.com/oxipng/oxipng/issues/80))
- Fixed another bug in reducing palettes for images with bit depth less than eight ([#82](https://github.com/oxipng/oxipng/issues/82))
- Fixed a bug in reducing palettes for images with bit depth of two ([#80](https://github.com/shssoichiro/oxipng/issues/80))
- Fixed another bug in reducing palettes for images with bit depth less than eight ([#82](https://github.com/shssoichiro/oxipng/issues/82))
- Code cleanup
- Bump `image` to 0.15
### Version 0.16.3
- Fix command-line help text ([#70](https://github.com/oxipng/oxipng/issues/70))
- Fix command-line help text ([#70](https://github.com/shssoichiro/oxipng/issues/70))
### Version 0.16.2
@ -430,18 +368,18 @@
### Version 0.15.2
- Bump `image` to 0.13 ([#65](https://github.com/oxipng/oxipng/pull/65))
- Bump `image` to 0.13 ([#65](https://github.com/shssoichiro/oxipng/pull/65))
- Bump `rayon` to 0.7
- Bump `itertools` to 0.6
### Version 0.15.1
- Ignore color reductions that would increase file size ([#61](https://github.com/oxipng/oxipng/issues/61))
- Ignore color reductions that would increase file size ([#61](https://github.com/shssoichiro/oxipng/issues/61))
### Version 0.15.0
- [SEMVER_MINOR] Check images for correctness before writing result ([#60](https://github.com/oxipng/oxipng/issues/60))
- Fix invalid output when reducing image to a different color type but file size does not improve ([#60](https://github.com/oxipng/oxipng/issues/60))
- [SEMVER_MINOR] Check images for correctness before writing result ([#60](https://github.com/shssoichiro/oxipng/issues/60))
- Fix invalid output when reducing image to a different color type but file size does not improve ([#60](https://github.com/shssoichiro/oxipng/issues/60))
- Don't write new file if moving from interlaced to non-interlaced if new file would be larger
### Version 0.14.4
@ -475,14 +413,14 @@
### Version 0.13.0
- Fix bug in certain PNG headers when reducing color type ([#52](https://github.com/oxipng/oxipng/issues/52))
- Fix bug in certain PNG headers when reducing color type ([#52](https://github.com/shssoichiro/oxipng/issues/52))
- [SEMVER_MAJOR] Reduction functions now take `&mut PngData` and return a `bool` indicating whether the image was reduced
- [SMEVER_MAJOR] Bump minimum required rustc version to 1.12.0
### Version 0.12.0
- Performance optimizations
- Fix processing filenames that contain commas (@aliceatlas [#50](https://github.com/oxipng/oxipng/pull/50))
- Fix processing filenames that contain commas (@aliceatlas [#50](https://github.com/shssoichiro/oxipng/pull/50))
- [SEMVER_MINOR] Add zopfli option (-Z), disabled by default. Gives about 10% better compression, but is currently 50-100x slower.
### Version 0.11.0
@ -507,7 +445,7 @@
### Version 0.8.2
- Fix issue where images smaller than 4px width would crash on interlacing ([#42](https://github.com/oxipng/oxipng/issues/42))
- Fix issue where images smaller than 4px width would crash on interlacing ([#42](https://github.com/shssoichiro/oxipng/issues/42))
### Version 0.8.1
@ -523,7 +461,7 @@
- Minor compression improvement on interlaced images
- Performance optimizations
- [SEMVER_MINOR] Move default Options into a Default impl
- [SEMVER_MINOR] Add option for setting number of threads ([#39](https://github.com/oxipng/oxipng/issues/39))
- [SEMVER_MINOR] Add option for setting number of threads ([#39](https://github.com/shssoichiro/oxipng/issues/39))
### Version 0.6.0
@ -534,47 +472,47 @@
### Version 0.5.0
- [SEMVER_MINOR] Palette entries can now reduced, on by default ([#11](https://github.com/oxipng/oxipng/issues/11))
- [SEMVER_MINOR] Palette entries can now reduced, on by default ([#11](https://github.com/shssoichiro/oxipng/issues/11))
- Don't report that we are in pretend mode if verbosity is set to none
- Add cargo bench suite ([#7](https://github.com/oxipng/oxipng/issues/7))
- Add cargo bench suite ([#7](https://github.com/shssoichiro/oxipng/issues/7))
### Version 0.4.0
- Performance optimizations
- [SEMVER_MAJOR] `-s` automatically infers `--strip safe` ([#31](https://github.com/oxipng/oxipng/issues/31))
- [SEMVER_MAJOR] `-s` automatically infers `--strip safe` ([#31](https://github.com/shssoichiro/oxipng/issues/31))
- Update byteorder and clap crates
- Fix issue where interlaced images incorrectly applied filters on the first line of a pass
### Version 0.3.0
- Properly decode interlaced images
- [SEMVER_MINOR] Allow converting between progressive and interlaced images ([#3](https://github.com/oxipng/oxipng/issues/3))
- [SEMVER_MINOR] Allow converting between progressive and interlaced images ([#3](https://github.com/shssoichiro/oxipng/issues/3))
- Fix a bug that would cause oxipng to crash on very small images
### Version 0.2.2
- Limit number of threads to 1.5x number of cores
- Significantly improve memory usage, especially with high optimization levels. ([#32](https://github.com/oxipng/oxipng/issues/32))
- Refactor output code ([#19](https://github.com/oxipng/oxipng/issues/19))
- Significantly improve memory usage, especially with high optimization levels. ([#32](https://github.com/shssoichiro/oxipng/issues/32))
- Refactor output code ([#19](https://github.com/shssoichiro/oxipng/issues/19))
### Version 0.2.1
- Add rustdoc for public methods and structs
- Improve filter mode 5 heuristic ([#16](https://github.com/oxipng/oxipng/issues/16))
- Add tests for edge-case images with subtitles ([#29](https://github.com/oxipng/oxipng/issues/29))
- Improve filter mode 5 heuristic ([#16](https://github.com/shssoichiro/oxipng/issues/16))
- Add tests for edge-case images with subtitles ([#29](https://github.com/shssoichiro/oxipng/issues/29))
### Version 0.2.0
- Fix program version that is displayed when running `oxipng -V`
- Ensure `--quiet` mode is actually quiet (@SethDusek [#20](https://github.com/oxipng/oxipng/pull/20))
- Ensure `--quiet` mode is actually quiet (@SethDusek [#20](https://github.com/shssoichiro/oxipng/pull/20))
- Write status/debug information to stderr instead of stdout
- Use heuristics to determine best combination for `-o1` ([#21](https://github.com/oxipng/oxipng/issues/21))
- Use heuristics to determine best combination for `-o1` ([#21](https://github.com/shssoichiro/oxipng/issues/21))
- [SEMVER_MAJOR] Allow 'safe', 'all', or comma-separated list as options for `--strip`
- [SEMVER_MINOR] Add `-s` alias for `--strip`
### Version 0.1.1
- Fix `oxipng *` writing all input files to one output file ([#15](https://github.com/oxipng/oxipng/issues/15))
- Fix `oxipng *` writing all input files to one output file ([#15](https://github.com/shssoichiro/oxipng/issues/15))
### Version 0.1.0

584
Cargo.lock generated
View file

@ -1,74 +1,78 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
version = 3
[[package]]
name = "adler2"
version = "2.0.1"
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "anstream"
version = "1.0.0"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys",
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
"windows-sys 0.52.0",
]
[[package]]
name = "autocfg"
version = "1.5.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
[[package]]
name = "bitflags"
version = "2.11.1"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
[[package]]
name = "bitvec"
@ -82,54 +86,44 @@ dependencies = [
"wyz",
]
[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "bytemuck"
version = "1.25.0"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.2.60"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
dependencies = [
"find-msvc-tools",
"shlex",
]
checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41"
[[package]]
name = "cfg-if"
version = "1.0.4"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.6.1"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
dependencies = [
"anstream",
"anstyle",
@ -140,30 +134,49 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "1.1.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "clap_mangen"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1dd95b5ebb5c1c54581dd6346f3ed6a79a3eef95dd372fc2ac13d535535300e"
dependencies = [
"clap",
"roff",
]
[[package]]
name = "colorchoice"
version = "1.0.5"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "crc32fast"
version = "1.5.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
name = "crossbeam-channel"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
@ -180,30 +193,30 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
name = "either"
version = "1.15.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
[[package]]
name = "env_filter"
version = "1.0.1"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea"
dependencies = [
"log",
]
[[package]]
name = "env_logger"
version = "0.11.10"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9"
dependencies = [
"anstream",
"anstyle",
@ -213,40 +226,46 @@ dependencies = [
[[package]]
name = "equivalent"
version = "1.0.2"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.14"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"libc",
"windows-sys",
"windows-sys 0.52.0",
]
[[package]]
name = "fdeflate"
version = "0.3.7"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645"
dependencies = [
"simd-adler32",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
name = "filetime"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"windows-sys 0.52.0",
]
[[package]]
name = "flate2"
version = "1.1.9"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
dependencies = [
"crc32fast",
"miniz_oxide",
@ -260,199 +279,129 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "glob"
version = "0.3.3"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "hashbrown"
version = "0.17.0"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
[[package]]
name = "image"
version = "0.25.9"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"byteorder",
"num-traits",
"png",
]
[[package]]
name = "indexmap"
version = "2.14.0"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown",
"rayon",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.185"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "libdeflate-sys"
version = "1.25.2"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72753e0008ea87963d2f0770042d0df7abe51fafbb8dcaf618ac440f2f1fec0a"
checksum = "669ea17f9257bcb48c09c7ee4bef3957777504acffac557263e20c11001977bc"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "libdeflater"
version = "1.25.2"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1ee41cf6fb1bb6030dfb59ffb7bc01ab26aade44142084c87f0fc7a1658fe71"
checksum = "8dfd6424f7010ee0a3416f1d796d0450e3ad3ac237a237644f728277c4ded016"
dependencies = [
"libdeflate-sys",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "log"
version = "0.4.30"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
dependencies = [
"adler2",
"adler",
"simd-adler32",
]
[[package]]
name = "moxcms"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "num-traits"
version = "0.2.19"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "oxipng"
version = "10.1.1"
version = "9.1.1"
dependencies = [
"bitvec",
"clap",
"clap_mangen",
"crossbeam-channel",
"env_logger",
"filetime",
"glob",
"image",
"indexmap",
"libdeflater",
"log",
"parse-size",
"rayon",
"rgb",
"rustc-hash",
"serde_json",
"rustc_version",
"zopfli",
]
[[package]]
name = "parse-size"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b"
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "png"
version = "0.18.1"
version = "0.17.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pxfm"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "radium"
version = "0.7.0"
@ -461,9 +410,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rayon"
version = "1.12.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
@ -471,95 +420,77 @@ dependencies = [
[[package]]
name = "rayon-core"
version = "1.13.0"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "rgb"
version = "0.8.53"
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "rgb"
version = "0.8.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8"
dependencies = [
"bytemuck",
]
[[package]]
name = "rustc-hash"
version = "2.1.2"
name = "roff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "1.1.4"
version = "0.38.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
dependencies = [
"bitflags",
"bitflags 2.5.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
"windows-sys 0.52.0",
]
[[package]]
name = "serde"
version = "1.0.228"
name = "semver"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
[[package]]
name = "simd-adler32"
version = "0.3.9"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "strsim"
@ -567,17 +498,6 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tap"
version = "1.0.1"
@ -586,41 +506,165 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "terminal_size"
version = "0.4.4"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
dependencies = [
"rustix",
"windows-sys",
"windows-sys 0.48.0",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
name = "typed-arena"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "windows-sys"
version = "0.61.2"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-link",
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.5",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
dependencies = [
"windows_aarch64_gnullvm 0.52.5",
"windows_aarch64_msvc 0.52.5",
"windows_i686_gnu 0.52.5",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.5",
"windows_x86_64_gnu 0.52.5",
"windows_x86_64_gnullvm 0.52.5",
"windows_x86_64_msvc 0.52.5",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]]
name = "wyz"
version = "0.5.1"
@ -630,20 +674,14 @@ dependencies = [
"tap",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zopfli"
version = "0.8.3"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
checksum = "5c1f48f3508a3a3f2faee01629564400bc12260f6214a056d06a3aaaa6ef0736"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
"typed-arena",
]

View file

@ -2,9 +2,8 @@
authors = ["Joshua Holmer <jholmer.in@gmail.com>"]
categories = ["command-line-utilities", "compression"]
description = "A lossless PNG compression optimizer"
keywords = ["png", "image-compression", "optimization", "multi-threading", "lossless"]
documentation = "https://docs.rs/oxipng"
edition = "2024"
edition = "2021"
exclude = [
".editorconfig",
".gitattributes",
@ -14,17 +13,16 @@ exclude = [
"Dockerfile",
"scripts/*",
"tests/*",
"xtask/*",
]
homepage = "https://github.com/oxipng/oxipng"
homepage = "https://github.com/shssoichiro/oxipng"
license = "MIT"
name = "oxipng"
repository = "https://github.com/oxipng/oxipng"
version = "10.1.1"
rust-version = "1.85.1"
repository = "https://github.com/shssoichiro/oxipng"
version = "9.1.1"
rust-version = "1.74.0"
[badges]
travis-ci = { repository = "oxipng/oxipng", branch = "master" }
travis-ci = { repository = "shssoichiro/oxipng", branch = "master" }
maintenance = { status = "actively-developed" }
[[bin]]
@ -38,33 +36,58 @@ name = "zopfli"
required-features = ["zopfli"]
[dependencies]
zopfli = { version = "0.8.0", optional = true, default-features = false, features = ["std", "zlib"] }
rgb = "0.8.37"
indexmap = "2.2.6"
libdeflater = "1.20.0"
log = "0.4.21"
bitvec = "1.0.1"
clap = { version = "4.6.0", optional = true, features = ["wrap_help"] }
env_logger = { version = "0.11.10", optional = true, default-features = false, features = ["auto-color"] }
image = { version = "0.25.9", optional = true, default-features = false, features = ["png"] }
indexmap = "2.14.0"
libdeflater = "1.25.2"
log = "0.4.30"
parse-size = { version = "1.1.0", optional = true }
rayon = { version = "1.11.0", optional = true }
rgb = "0.8.53"
rustc-hash = "2.1.2"
zopfli = { version = "0.8.3", optional = true, default-features = false, features = ["std", "zlib"] }
rustc-hash = "1.1.0"
[target.'cfg(windows)'.dependencies]
glob = { version = "0.3.3", optional = true }
[dependencies.env_logger]
optional = true
default-features = false
features = ["auto-color"]
version = "0.11.3"
[dev-dependencies]
serde_json = "1.0.150"
[dependencies.crossbeam-channel]
optional = true
version = "0.5.12"
[dependencies.filetime]
optional = true
version = "0.2.23"
[dependencies.rayon]
optional = true
version = "1.10.0"
[dependencies.clap]
optional = true
version = "4.5.4"
features = ["wrap_help"]
[target.'cfg(windows)'.dependencies.glob]
optional = true
version = "0.3.1"
[dependencies.image]
optional = true
default-features = false
features = ["png"]
version = "0.25.1"
[build-dependencies]
clap = "4.5.4"
clap_mangen = "0.2.20"
rustc_version = "0.4.0"
[features]
binary = ["dep:clap", "dep:glob", "dep:env_logger", "dep:parse-size"]
default = ["binary", "parallel", "zopfli"]
parallel = ["dep:rayon", "indexmap/rayon"]
binary = ["clap", "glob", "env_logger"]
default = ["binary", "filetime", "parallel", "zopfli"]
parallel = ["rayon", "indexmap/rayon", "crossbeam-channel"]
freestanding = ["libdeflater/freestanding"]
sanity-checks = ["dep:image"]
zopfli = ["dep:zopfli"]
system-libdeflate = ["libdeflater/dynamic"]
sanity-checks = ["image"]
[lib]
name = "oxipng"
@ -81,41 +104,7 @@ panic = "abort"
[package.metadata.deb]
assets = [
["target/release/oxipng", "usr/bin/", "755"],
["target/xtask/mangen/manpages/oxipng.1", "usr/share/man/man1/", "644"],
["target/release/assets/oxipng.1", "usr/share/man/man1/", "644"],
["README.md", "usr/share/doc/oxipng/", "644"],
["CHANGELOG.md", "usr/share/doc/oxipng/", "644"],
]
[lints.rust]
missing_copy_implementations = "deny"
missing_debug_implementations = "deny"
trivial_casts = "warn"
trivial_numeric_casts = "warn"
unused_import_braces = "warn"
[lints.clippy]
cloned_instead_of_copied = "warn"
expl_impl_clone_on_copy = "warn"
float_cmp_const = "warn"
if_not_else = "warn"
ignored_unit_patterns = "warn"
linkedlist = "warn"
manual_let_else = "warn"
map_flatten = "warn"
map_unwrap_or = "warn"
match_same_arms = "warn"
mem_forget = "warn"
missing_const_for_fn = "warn"
mut_mut = "warn"
mutex_integer = "warn"
needless_continue = "warn"
option_if_let_else = "warn"
path_buf_push_overwrite = "warn"
range_plus_one = "warn"
redundant_clone = "warn"
redundant_closure_for_method_calls = "warn"
semicolon_if_nothing_returned = "warn"
unnecessary_semicolon = "warn"
unseparated_literal_suffix = "warn"
use_self = "warn"
useless_let_if_seq = "warn"

View file

@ -1,46 +1,20 @@
# syntax=docker/dockerfile:1
# check=error=true
FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
FROM rust:alpine as base
FROM --platform=$BUILDPLATFORM rust:1.85.1-alpine AS base
COPY . /src
RUN apk update && \
apk add \
RUN rustup update 1.74 && rustup default 1.74
RUN apk update \
&& apk add \
gcc \
g++ \
clang
g++
COPY --from=xx / /
RUN cd /src && cargo build --release
ARG TARGETPLATFORM
RUN xx-info env
FROM alpine as tool
RUN xx-apk add \
gcc \
musl-dev \
libdeflate
COPY --from=base /src/target/release/oxipng /usr/local/bin
WORKDIR /src
COPY . .
RUN --mount=type=cache,target=/root/.cargo/git/db \
--mount=type=cache,target=/root/.cargo/registry/cache \
--mount=type=cache,target=/root/.cargo/registry/index \
xx-cargo build --release && \
xx-verify /src/target/$(xx-cargo --print-target-triple)/release/oxipng && \
cp /src/target/$(xx-cargo --print-target-triple)/release/oxipng /src/target/oxipng
FROM scratch AS tool
LABEL org.opencontainers.image.title="Oxipng"
LABEL org.opencontainers.image.description="Multithreaded PNG optimizer written in Rust"
LABEL org.opencontainers.image.authors="Joshua Holmer <jholmer.in@gmail.com>"
LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.source="https://github.com/oxipng/oxipng"
COPY --from=base /src/target/oxipng /usr/local/bin/oxipng
WORKDIR /work
ENTRYPOINT [ "oxipng" ]
CMD [ "--help" ]

View file

@ -1,4 +1,4 @@
oxipng 10.1.0
oxipng 9.1.1
Losslessly improve compression of PNG files
Usage: oxipng [OPTIONS] <files>...
@ -13,14 +13,14 @@ Options:
compression. Lower levels are faster, higher levels provide better compression, though
with increasingly diminishing returns.
0 => --zc 5 --fast (filter chosen heuristically)
1 => --zc 10 --fast (filter chosen heuristically)
2 => --zc 11 -f 0,1,6,7 --fast
3 => --zc 11 -f 0,7,8,9 --brute-level 1 --brute-lines 3
4 => --zc 12 -f 0,7,8,9 --brute-level 1 --brute-lines 4
5 => --zc 12 -f 0,1,2,5,6,7,8,9 --brute-level 4 --brute-lines 4
6 => --zc 12 -f 0-9 --brute-level 5 --brute-lines 8
max => (stable alias for the maximum level)
0 => --zc 5 --fast (1 trial, determined heuristically)
1 => --zc 10 --fast (1 trial, determined heuristically)
2 => --zc 11 -f 0,1,6,7 --fast (4 fast trials, 1 main trial)
3 => --zc 11 -f 0,7,8,9 (4 trials)
4 => --zc 12 -f 0,7,8,9 (4 trials)
5 => --zc 12 -f 0,1,2,5,6,7,8,9 (8 trials)
6 => --zc 12 -f 0-9 (10 trials)
max => (stable alias for the max level)
Manually specifying a compression option (zc, f, etc.) will override the optimization
preset, regardless of the order you write the arguments.
@ -45,7 +45,7 @@ Options:
-p, --preserve
Preserve file permissions and timestamps if possible
-d, --dry-run
-P, --pretend
Do not write any files, only show compression results
-s
@ -61,14 +61,10 @@ Options:
CAUTION: 'all' will convert APNGs to standard PNGs.
Please note that regardless of any options set, some chunks will necessarily be stripped
when invalidated by the optimization:
bKGD, sBIT, hIST: Stripped if the color type or bit depth changes.
iDOT: Always stripped.
caBX: Stripped if it contains C2PA metadata. If explicitly retained by `--keep`,
optimization will be aborted.
Note that 'bKGD', 'sBIT' and 'hIST' will be forcibly stripped if the color type or bit
depth is changed, regardless of any options set.
The default when --strip is not passed is to keep all chunks that remain valid.
The default when --strip is not passed is to keep all metadata.
--keep <list>
Strip all metadata chunks except those in the comma-separated list. The special value
@ -83,17 +79,17 @@ Options:
but take care as while this is “visually lossless”, it is technically a lossy
transformation and may be unsuitable for some applications.
-i, --interlace <mode>
Set the PNG interlacing mode, where <mode> is one of:
-i, --interlace <type>
Set the PNG interlacing type, where <type> is one of:
off => Remove interlacing from all images that are processed
on => Apply Adam7 interlacing on all images that are processed
keep => Keep the existing interlacing mode of each image
0 => Remove interlacing from all images that are processed
1 => Apply Adam7 interlacing on all images that are processed
keep => Keep the existing interlacing type of each image
Note that interlacing can add 25-50% to the size of an optimized image. Only use it if you
believe the benefits outweigh the costs for your use case.
[default: off]
[default: 0]
--scale16
Forcibly reduce images with 16 bits per channel to 8 bits per channel. This is a lossy
@ -105,13 +101,10 @@ Options:
losslessly.
-v, --verbose...
Show per-file info (use multiple times for more detail)
Run in verbose mode (use twice to increase verbosity)
-q, --quiet
Suppress all output messages
-j, --json
Print results as JSON
Run in quiet mode
-f, --filters <list>
Perform compression trials with each of the given filter types. You can specify a
@ -126,9 +119,9 @@ Options:
Heuristic strategies (try to find the best delta filter for each line)
5 => MinSum Minimum sum of absolute differences
6 => Entropy Smallest Shannon entropy
6 => Entropy Highest Shannon entropy
7 => Bigrams Lowest count of distinct bigrams
8 => BigEnt Smallest Shannon entropy of bigrams
8 => BigEnt Highest Shannon entropy of bigrams
9 => Brute Smallest compressed size (slow)
The default value depends on the optimization level preset.
@ -139,7 +132,7 @@ Options:
CPU cores.
--zc <level>
Deflate compression level (0-12) for main compression trials. The levels here are defined
Deflate compression level (1-12) for main compression trials. The levels here are defined
by the libdeflate compression library.
The default value depends on the optimization level preset.
@ -166,33 +159,15 @@ Options:
--fix
Do not perform checksum validation of PNG chunks. This may allow some files with errors to
be processed successfully. The output will always have correct checksums.
be processed successfully.
--force
Write the output even if it is larger than the input
-z, --zopfli
-Z, --zopfli
Use the much slower but stronger Zopfli compressor for main compression trials.
Recommended use is with '-o max' and '--fast'.
--zi <iterations>
Set the number of iterations to use for Zopfli compression. Using fewer iterations may
speed up compression for large files. This option requires '--zopfli' to be set.
[default: 15]
--ziwi <iterations>
Stop Zopfli compression after this number of iterations without improvement. Use this in
conjunction with a high value for '--zi' to achieve better compression in reasonable time.
--brute-level <level>
Set the libdeflate compression level to use with the Brute filter strategy. Sane values
are 1-5. Higher values are not necessarily better.
--brute-lines <lines>
Set the number of lines to compress at once with the Brute filter strategy. Sane values
are 2-16. Higher values are not necessarily better.
--timeout <secs>
Maximum amount of time, in seconds, to spend on optimizations. Oxipng will check the
timeout before each transformation or compression trial, and will stop trying to optimize
@ -200,25 +175,8 @@ Options:
are already in progress, so it is currently of limited effectiveness for large files with
high compression levels.
--max-raw-size <bytes>
Maximum size to allow for the input image. If the raw, decompressed image data (or the
file size) of the image exceeds this size, it will be skipped. This is useful for limiting
memory usage or avoiding long processing times on large images. The value may be specified
with a unit suffix such as k, KB, m, MB, etc.
The decompressed size of an image is roughly equal to width * height * bit-depth / 8. E.g.
a 1920x1080 image with 24-bit color depth would be roughly 6MB.
-t, --threads <num>
Set the maximum number of threads to use. Oxipng uses multithreading to evaluate multiple
optimizations on the same file in parallel as well as process multiple files in parallel.
You can set this to a lower value if you need to limit memory or CPU usage.
[default: num logical CPUs]
--sequential
Process multiple files sequentially rather than in parallel. Use this if you need
determinism in the processing order. Note this is not necessary if using '--threads 1'.
Set number of threads to use [default: num CPU cores]
-h, --help
Print help (see a summary with '-h')

204
README.md
View file

@ -1,46 +1,41 @@
# Oxipng
[![Build Status](https://github.com/oxipng/oxipng/workflows/oxipng/badge.svg)](https://github.com/oxipng/oxipng/actions?query=branch%3Amaster)
[![Build Status](https://github.com/shssoichiro/oxipng/workflows/oxipng/badge.svg)](https://github.com/shssoichiro/oxipng/actions?query=branch%3Amaster)
[![Version](https://img.shields.io/crates/v/oxipng.svg)](https://crates.io/crates/oxipng)
[![License](https://img.shields.io/crates/l/oxipng.svg)](https://github.com/oxipng/oxipng/blob/master/LICENSE)
[![License](https://img.shields.io/crates/l/oxipng.svg)](https://github.com/shssoichiro/oxipng/blob/master/LICENSE)
[![Docs](https://docs.rs/oxipng/badge.svg)](https://docs.rs/oxipng)
## Overview
Oxipng is a multithreaded lossless PNG/APNG compression optimizer. It can be used via a command-line
interface or as a library in other Rust programs. It is fast and highly effective.
interface or as a library in other Rust programs.
## Installing
Oxipng for Windows can be downloaded via the
[Releases](https://github.com/oxipng/oxipng/releases) section on its GitHub page. Recently,
however, Oxipng has also been made available through package managers. Check the list below for
up-to-date options.
Oxipng for Windows can be downloaded from the [Releases](https://github.com/shssoichiro/oxipng/releases) link on the GitHub page.
For MacOS or Linux, it is recommended to install from your distro's package repository, provided
Oxipng is available there in a not too outdated version for your use case.
Oxipng is known to be packaged for the environments listed below.
For MacOS or Linux, it is recommended to install from your distro's package
repository, if possible. Oxipng is known to be packaged for the environments listed below.
[![Packaging status](https://repology.org/badge/vertical-allrepos/oxipng.svg?exclude_unsupported=1&columns=3&exclude_sources=modules,site)](https://repology.org/project/oxipng/versions)
Alternatively, Oxipng can be installed from Cargo, via the following command:
Alternatively, oxipng can be installed from Cargo, via the following command:
```
cargo install oxipng
```
Oxipng can also be built from source using the latest stable or nightly Rust.
This is primarily useful for developing on Oxipng.
Oxipng can be built from source using the latest stable or nightly Rust.
This is primarily useful for developing on oxipng.
```
git clone https://github.com/oxipng/oxipng.git
git clone https://github.com/shssoichiro/oxipng.git
cd oxipng
cargo build --release
cp target/release/oxipng /usr/local/bin
```
The current minimum supported Rust version is **1.85.1**.
The current minimum supported Rust version is **1.74.0**.
Oxipng follows Semantic Versioning.
@ -56,7 +51,7 @@ The most commonly used options are as follows:
- Optimization: `-o 0` through `-o 6` (or `-o max`), lower is faster, higher is better compression.
The default (`-o 2`) is quite fast and provides good compression. Higher levels can be notably
better* but generally have increasingly diminishing returns.
better but generally have increasingly diminishing returns.
- Strip: Used to remove metadata info from processed images. Used via `--strip [safe,all]`.
Can save a few kilobytes if you don't need the metadata. "Safe" removes only metadata that
will never affect rendering of the image. "All" removes all metadata that is not critical
@ -71,19 +66,7 @@ More advanced options can be found by running `oxipng --help`, or viewed [here](
Some options have both short (`-a`) and long (`--alpha`) forms. Which form you use is just a
matter of preference. Multiple short options can be combined together, e.g.:
`-savvo6` is equivalent to to `--strip safe --alpha --verbose --verbose --opt 6`.
All options are case-sensitive.
\* Note that oxipng is not a brute-force optimizer. This means that while higher optimization levels
are almost always better or equal to lower levels, this is not guaranteed and it is possible in
rare circumstances that a lower level may give a marginally smaller output. Similarly, using Zopfli
compression (`-z`) is not guaranteed to always be better than default compression.
## APNG support
Oxipng currently only supports limited optimization of animated PNGs (APNGs). It can perform
alpha-optimization, refiltering and recompression of all frames, but all transformations will be
disabled. For best results, it is recommended to use another tool such as
[apngopt](https://sourceforge.net/projects/apng/files/APNG_Optimizer/) before running Oxipng.
Note that all options are case-sensitive.
## Git integration via [pre-commit]
@ -92,59 +75,83 @@ preamble to an already existing one:
```yaml
repos:
- repo: https://github.com/oxipng/oxipng
rev: v10.0.0
- repo: https://github.com/shssoichiro/oxipng
rev: v9.0.0
hooks:
- id: oxipng
args: ["-o", "4", "--strip", "safe", "--alpha"]
```
[pre-commit]: https://pre-commit.com/
## Docker
## Git integration via [Trunk]
A Docker image is availlable at [`ghcr.io/oxipng/oxipng`](https://github.com/oxipng/oxipng/pkgs/container/oxipng) for `linux/amd64` and `linux/arm64`.
[Trunk] is an extendable superlinter which can be used to run `oxipng` to automatically optimize `png`s when committing them into a git repo, or to gate any `png`s being added to a git repo on whether they are optimized. The [trunk] oxipng integration is [here](https://github.com/trunk-io/plugins/tree/main/linters/oxipng).
You can use it the following way:
To enable oxipng via [trunk]:
```bash
docker run --rm -v $(pwd):/work ghcr.io/oxipng/oxipng -o 4 /work/file.png
# to get the latest version:
trunk check enable oxipng
# to get a specific version:
trunk check enable oxipng@9.0.0
```
Some older images are also available at [`ghcr.io/shssoichiro/oxipng`](https://github.com/users/shssoichiro/packages/container/package/oxipng).
or modify `.trunk/trunk.yaml` in your repo to contain:
```
lint:
enabled:
- oxipng@9.0.0
```
Then just run:
```bash
# to optimize a png:
trunk fmt <file>
# to check if a png is already optimized:
trunk check <file>
```
You can setup [trunk] to [manage your git hooks](https://docs.trunk.io/docs/actions-git-hooks) and automatically optimize any `png`s you commit to git, _when_ you `git commit`. To enable this, run:
```bash
trunk actions enable trunk-fmt-pre-commit
```
[trunk]: https://docs.trunk.io
## Library Usage
Although originally intended to be used as an executable, Oxipng can also be used as a library in
other Rust projects. To do so, simply add Oxipng as a dependency in your Cargo.toml. You should then
have access to all of the library functions [documented here](https://docs.rs/oxipng). The simplest
method of usage involves creating an [Options
struct](https://docs.rs/oxipng/latest/oxipng/struct.Options.html) and passing it, along with an
input filename, into the [optimize function](https://docs.rs/oxipng/latest/oxipng/fn.optimize.html).
Although originally intended to be used as an executable, oxipng can also be used as a library in
other Rust projects. To do so, simply add oxipng as a dependency in your Cargo.toml,
then `extern crate oxipng` in your project. You should then have access to all of the library
functions [documented here](https://docs.rs/oxipng). The simplest
method of usage involves creating an
[Options struct](https://docs.rs/oxipng/latest/oxipng/struct.Options.html) and
passing it, along with an input filename, into the
[optimize function](https://docs.rs/oxipng/latest/oxipng/fn.optimize.html).
It is recommended to disable the "binary" feature when including Oxipng as a library. Currently, there is
It is recommended to disable the "binary" feature when including oxipng as a library. Currently, there is
no simple way to just disable one feature in Cargo, it has to be done by disabling default features
and specifying the desired ones, for example:
`oxipng = { version = "10.0", features = ["parallel", "zopfli", "filetime"], default-features = false }`
## Software using Oxipng
- [ImageOptim](https://imageoptim.com): Mac app and web service for optimizing images
- [Squoosh](https://squoosh.app): Web app for optimizing images
- [FileOptimizer](https://nikkhokkho.sourceforge.io/?page=FileOptimizer): Windows app for optimizing files
- [Curtail](https://github.com/Huluti/Curtail): Linux app for optimizing images
- [pyoxipng](https://pypi.org/project/pyoxipng/): Python wrapper for Oxipng
- [jSquash](https://github.com/jamsinclair/jSquash): Collection of WebAssembly image codecs
- [Trunk](https://trunk.io): Developer experience toolkit for managing code
`oxipng = { version = "9.0", features = ["parallel", "zopfli", "filetime"], default-features = false }`
## History
Oxipng began in 2015 as a rewrite of the OptiPNG project. The core goal was to implement
multithreading, which would have been very difficult to do within the existing C codebase of OptiPNG.
Oxipng began as a complete rewrite of the OptiPNG project,
which was assumed to be dead as no commit had been made to it since March 2014.
(OptiPNG has since released a new version, after Oxipng was first released.)
The name has been changed to avoid confusion and potential legal issues.
The core goal of rewriting OptiPNG was to implement multithreading,
which would be very difficult to do within the existing C codebase of OptiPNG.
This also served as an opportunity to choose a more modern, safer language (Rust).
However, Oxipng has evolved considerably since then. While some of the options remain similar to
OptiPNG, the architecture and capabilities are now quite different. It is not a drop-in
replacement - if you are migrating from OptiPNG, please check the [help](MANUAL.txt) before use.
Note that, while similar, Oxipng is not a drop-in replacement for OptiPNG.
If you are migrating from OptiPNG, please check the [help](MANUAL.txt) before using.
## Contributing
@ -154,11 +161,78 @@ to submit a fix with the bug report, it is preferred that you do so via pull req
however you do not need to be a Rust developer to contribute.
Other contributions (such as improving documentation or translations) are also welcome via GitHub.
## Benchmarks
An independent benchmark is linked here with permission by the author:\
[oxipng and friends: A comparison of PNG optimization tools](https://op111.net/posts/2025/09/png-compression-oxipng-optipng-fileoptimizer-cwebp/)
## License
Oxipng is open-source software, distributed under the MIT license.
## Benchmarks
Tested OxiPNG 9.0.0 (commit `c16519b38b0519988db625913be919d4f0e42f5d`, compiled
on `rustc 1.74.0-nightly (7b4d9e155 2023-09-28)`) against OptiPNG version 0.7.7,
as packaged by Debian unstable, on a Linux 6.5.0-2-amd64 kernel, Intel Core
i7-12700 CPU (8 performance cores, 4 efficiency cores, 20 threads), DDR5-5200
RAM in dual channel configuration.
```
Benchmark 1: ./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 59.6 ms ± 7.7 ms [User: 77.4 ms, System: 3.6 ms]
Range (min … max): 53.3 ms … 89.9 ms 32 runs
Benchmark 2: optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 132.4 ms ± 0.8 ms [User: 132.5 ms, System: 0.6 ms]
Range (min … max): 131.8 ms … 134.4 ms 22 runs
Summary
./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png ran
2.22 ± 0.29 times faster than optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
Benchmark 1: ./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 88.7 ms ± 4.3 ms [User: 270.3 ms, System: 11.0 ms]
Range (min … max): 86.8 ms … 109.4 ms 26 runs
Benchmark 2: optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 444.9 ms ± 0.3 ms [User: 444.8 ms, System: 0.7 ms]
Range (min … max): 444.4 ms … 445.6 ms 10 runs
Summary
./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png ran
5.01 ± 0.25 times faster than optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
```
<details>
<summary>Older benchmark</summary>
Tested oxipng 5.0.0 (compiled on rustc 1.55.0-nightly (7a16cfcff 2021-07-11)) against OptiPNG version 0.7.7 on AMD Ryzen 7 4800H with Radeon Graphics with 16 logical cores
```
Benchmark #1: ./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 128.8 ms ± 14.2 ms [User: 296.0 ms, System: 14.3 ms]
Range (min … max): 98.8 ms … 152.3 ms 21 runs
Benchmark #2: optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 254.2 ms ± 16.0 ms [User: 252.8 ms, System: 1.2 ms]
Range (min … max): 208.4 ms … 263.8 ms 14 runs
Summary
'./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png' ran
1.97 ± 0.25 times faster than 'optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png'
Benchmark #1: ./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 141.4 ms ± 14.9 ms [User: 611.7 ms, System: 21.1 ms]
Range (min … max): 100.2 ms … 160.4 ms 23 runs
Benchmark #2: optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 730.0 ms ± 25.9 ms [User: 728.0 ms, System: 1.2 ms]
Range (min … max): 713.3 ms … 768.2 ms 10 runs
Summary
'./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png' ran
5.16 ± 0.58 times faster than 'optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png'
```
</details>

View file

@ -1,13 +0,0 @@
# Security Policy
## Supported Versions
Only the latest version will be supported with security updates. We will not maintain old branches.
For this reason, you should attempt to always use the latest released version.
## Reporting a Vulnerability
To privately report a vulnerability in oxipng, please visit [our advisories page](https://github.com/oxipng/oxipng/security/advisories) and use the button to report a vulnerability.
We will review it as soon as possible.
For vulnerabilities in dependencies, please open a pull request updating the dependency to a patched version.

View file

@ -13,7 +13,10 @@ 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, &Options::default()).unwrap();
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw.data.as_ref(), 12, &min)
});
}
#[bench]
@ -21,7 +24,10 @@ 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, &Options::default()).unwrap();
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw.data.as_ref(), 12, &min)
});
}
#[bench]
@ -31,7 +37,10 @@ fn deflate_4_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw.data.as_ref(), 12, &min)
});
}
#[bench]
@ -41,7 +50,10 @@ fn deflate_2_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw.data.as_ref(), 12, &min)
});
}
#[bench]
@ -51,7 +63,10 @@ fn deflate_1_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw.data.as_ref(), 12, &min)
});
}
#[bench]

View file

@ -13,7 +13,9 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
b.iter(|| {
png.raw.filter_image(RowFilter::None, false);
});
}
#[bench]
@ -21,7 +23,9 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
b.iter(|| {
png.raw.filter_image(RowFilter::None, false);
});
}
#[bench]
@ -31,7 +35,9 @@ fn filters_4_bits_filter_0(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
b.iter(|| {
png.raw.filter_image(RowFilter::None, false);
});
}
#[bench]
@ -41,7 +47,9 @@ fn filters_2_bits_filter_0(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
b.iter(|| {
png.raw.filter_image(RowFilter::None, false);
});
}
#[bench]
@ -51,7 +59,9 @@ fn filters_1_bits_filter_0(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
b.iter(|| {
png.raw.filter_image(RowFilter::None, false);
});
}
#[bench]
@ -59,7 +69,9 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Sub, false);
});
}
#[bench]
@ -67,7 +79,9 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Sub, false);
});
}
#[bench]
@ -77,7 +91,9 @@ fn filters_4_bits_filter_1(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Sub, false);
});
}
#[bench]
@ -87,7 +103,9 @@ fn filters_2_bits_filter_1(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Sub, false);
});
}
#[bench]
@ -97,7 +115,9 @@ fn filters_1_bits_filter_1(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Sub, false);
});
}
#[bench]
@ -105,7 +125,9 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Up, false);
});
}
#[bench]
@ -113,7 +135,9 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Up, false);
});
}
#[bench]
@ -123,7 +147,9 @@ fn filters_4_bits_filter_2(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Up, false);
});
}
#[bench]
@ -133,7 +159,9 @@ fn filters_2_bits_filter_2(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Up, false);
});
}
#[bench]
@ -143,7 +171,9 @@ fn filters_1_bits_filter_2(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Up, false);
});
}
#[bench]
@ -151,7 +181,9 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Average, false);
});
}
#[bench]
@ -159,7 +191,9 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Average, false);
});
}
#[bench]
@ -169,7 +203,9 @@ fn filters_4_bits_filter_3(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Average, false);
});
}
#[bench]
@ -179,7 +215,9 @@ fn filters_2_bits_filter_3(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Average, false);
});
}
#[bench]
@ -189,7 +227,9 @@ fn filters_1_bits_filter_3(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Average, false);
});
}
#[bench]
@ -197,7 +237,9 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Paeth, false);
});
}
#[bench]
@ -205,7 +247,9 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Paeth, false);
});
}
#[bench]
@ -215,7 +259,9 @@ fn filters_4_bits_filter_4(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Paeth, false);
});
}
#[bench]
@ -225,7 +271,9 @@ fn filters_2_bits_filter_4(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Paeth, false);
});
}
#[bench]
@ -235,7 +283,9 @@ fn filters_1_bits_filter_4(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Paeth, false);
});
}
#[bench]
@ -243,7 +293,9 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
b.iter(|| {
png.raw.filter_image(RowFilter::MinSum, false);
});
}
#[bench]
@ -251,7 +303,9 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
b.iter(|| {
png.raw.filter_image(RowFilter::MinSum, false);
});
}
#[bench]
@ -261,7 +315,9 @@ fn filters_4_bits_filter_5(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
b.iter(|| {
png.raw.filter_image(RowFilter::MinSum, false);
});
}
#[bench]
@ -271,7 +327,9 @@ fn filters_2_bits_filter_5(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
b.iter(|| {
png.raw.filter_image(RowFilter::MinSum, false);
});
}
#[bench]
@ -281,5 +339,7 @@ fn filters_1_bits_filter_5(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
b.iter(|| {
png.raw.filter_image(RowFilter::MinSum, false);
});
}

View file

@ -13,7 +13,7 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(true));
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
}
#[bench]
@ -21,7 +21,7 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(true));
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
}
#[bench]
@ -31,7 +31,7 @@ fn interlacing_4_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(true));
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
}
#[bench]
@ -41,7 +41,7 @@ fn interlacing_2_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(true));
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
}
#[bench]
@ -51,7 +51,7 @@ fn interlacing_1_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(true));
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
}
#[bench]
@ -61,7 +61,7 @@ fn deinterlacing_16_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(false));
b.iter(|| png.raw.change_interlacing(Interlacing::None));
}
#[bench]
@ -71,7 +71,7 @@ fn deinterlacing_8_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(false));
b.iter(|| png.raw.change_interlacing(Interlacing::None));
}
#[bench]
@ -81,7 +81,7 @@ fn deinterlacing_4_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(false));
b.iter(|| png.raw.change_interlacing(Interlacing::None));
}
#[bench]
@ -91,7 +91,7 @@ fn deinterlacing_2_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(false));
b.iter(|| png.raw.change_interlacing(Interlacing::None));
}
#[bench]
@ -101,5 +101,5 @@ fn deinterlacing_1_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(false));
b.iter(|| png.raw.change_interlacing(Interlacing::None));
}

View file

@ -54,6 +54,36 @@ fn reductions_8_to_1_bits(b: &mut Bencher) {
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
}
#[bench]
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, &Options::default()).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
}
#[bench]
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, &Options::default()).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
}
#[bench]
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, &Options::default()).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
}
#[bench]
fn reductions_grayscale_8_to_4_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
@ -84,12 +114,42 @@ fn reductions_grayscale_8_to_1_bits(b: &mut Bencher) {
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
}
#[bench]
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, &Options::default()).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
}
#[bench]
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, &Options::default()).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
}
#[bench]
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, &Options::default()).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
}
#[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, &Options::default()).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
b.iter(|| alpha::reduced_alpha_channel(&png.raw, false));
}
#[bench]
@ -97,15 +157,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, &Options::default()).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
}
#[bench]
fn reductions_rgba_to_rgb_trns_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_rgb_trns_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
b.iter(|| alpha::reduced_alpha_channel(&png.raw, false));
}
#[bench]
@ -128,6 +180,32 @@ fn reductions_rgba_to_grayscale_alpha_8(b: &mut Bencher) {
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
}
#[bench]
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, &Options::default()).unwrap();
b.iter(|| {
color::reduced_rgb_to_grayscale(&png.raw)
.and_then(|r| alpha::reduced_alpha_channel(&r, false))
});
}
#[bench]
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, &Options::default()).unwrap();
b.iter(|| {
color::reduced_rgb_to_grayscale(&png.raw)
.and_then(|r| alpha::reduced_alpha_channel(&r, false))
});
}
#[bench]
fn reductions_rgb_to_grayscale_16(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
@ -162,36 +240,6 @@ fn reductions_rgb_to_palette_8(b: &mut Bencher) {
b.iter(|| color::reduced_to_indexed(&png.raw, true));
}
#[bench]
fn reductions_grayscale_alpha_to_grayscale_16(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/grayscale_alpha_16_should_be_grayscale_16.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
}
#[bench]
fn reductions_grayscale_alpha_to_grayscale_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/grayscale_alpha_8_should_be_grayscale_8.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
}
#[bench]
fn reductions_grayscale_alpha_to_grayscale_trns_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/grayscale_alpha_8_should_be_grayscale_trns_8.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
}
#[bench]
fn reductions_grayscale_8_to_palette_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
@ -209,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, true, false));
b.iter(|| color::indexed_to_channels(&png.raw, true));
}
#[bench]

View file

@ -13,7 +13,9 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
b.iter(|| {
png.raw.filter_image(RowFilter::MinSum, false);
});
}
#[bench]
@ -21,7 +23,9 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::Entropy, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Entropy, false);
});
}
#[bench]
@ -29,7 +33,9 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::Bigrams, false));
b.iter(|| {
png.raw.filter_image(RowFilter::Bigrams, false);
});
}
#[bench]
@ -37,7 +43,9 @@ 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, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::BigEnt, false));
b.iter(|| {
png.raw.filter_image(RowFilter::BigEnt, false);
});
}
#[bench]
@ -46,12 +54,6 @@ fn filters_brute(b: &mut Bencher) {
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(
FilterStrategy::Brute {
num_lines: 4,
level: 1,
},
false,
)
png.raw.filter_image(RowFilter::Brute, false);
});
}

View file

@ -3,18 +3,21 @@
extern crate oxipng;
extern crate test;
use std::path::PathBuf;
use std::{num::NonZeroU8, path::PathBuf};
use oxipng::{internal_tests::*, *};
use test::Bencher;
// SAFETY: trivially safe. Stopgap solution until const unwrap is stabilized.
const DEFAULT_ZOPFLI_ITERATIONS: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(15) };
#[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, &Options::default()).unwrap();
b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
});
}
@ -24,7 +27,7 @@ fn zopfli_8_bits_strategy_0(b: &mut Bencher) {
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
});
}
@ -36,7 +39,7 @@ fn zopfli_4_bits_strategy_0(b: &mut Bencher) {
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
});
}
@ -48,7 +51,7 @@ fn zopfli_2_bits_strategy_0(b: &mut Bencher) {
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
});
}
@ -60,6 +63,6 @@ fn zopfli_1_bits_strategy_0(b: &mut Bencher) {
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
});
}

40
build.rs Normal file
View file

@ -0,0 +1,40 @@
use std::{
env,
fs::File,
io::{BufWriter, Error},
path::Path,
};
use clap_mangen::Man;
include!("src/cli.rs");
fn build_manpages(outdir: &Path) -> Result<(), Error> {
let app = build_command();
let file = Path::new(&outdir).join("oxipng.1");
let mut file = BufWriter::new(File::create(file)?);
Man::new(app).render(&mut file)?;
Ok(())
}
fn main() -> Result<(), Error> {
println!("cargo:rerun-if-changed=src/cli.rs");
println!("cargo:rerun-if-changed=src/display_chunks.rs");
// Create `target/<debug|release>/assets/` folder.
let outdir = match env::var_os("OUT_DIR") {
None => return Ok(()),
Some(outdir) => outdir,
};
let out_path = PathBuf::from(outdir);
let mut path = out_path.ancestors().nth(3).unwrap().to_owned();
path.push("assets");
std::fs::create_dir_all(&path).unwrap();
build_manpages(&path)?;
Ok(())
}

View file

@ -1,6 +1,5 @@
#!/bin/sh -eu
#!/bin/bash
cargo build
cargo xtask mangen
./target/debug/oxipng -V > MANUAL.txt
#Redirect all streams to prevent detection of the terminal width and force an internal default of 100

View file

@ -1,72 +0,0 @@
use crate::{
PngResult,
error::PngError,
headers::{read_be_u16, read_be_u32},
};
#[derive(Debug, Clone)]
/// Animated PNG frame
pub struct Frame {
/// Width of the frame
pub width: u32,
/// Height of the frame
pub height: u32,
/// X offset of the frame
pub x_offset: u32,
/// Y offset of the frame
pub y_offset: u32,
/// Frame delay numerator
pub delay_num: u16,
/// Frame delay denominator
pub delay_den: u16,
/// Frame disposal operation
pub dispose_op: u8,
/// Frame blend operation
pub blend_op: u8,
/// Frame data, from fdAT chunks
pub data: Vec<u8>,
}
impl Frame {
/// Construct a new Frame from the data in a fcTL chunk
pub fn from_fctl_data(byte_data: &[u8]) -> PngResult<Self> {
if byte_data.len() < 26 {
return Err(PngError::TruncatedData);
}
Ok(Self {
width: read_be_u32(&byte_data[4..8]),
height: read_be_u32(&byte_data[8..12]),
x_offset: read_be_u32(&byte_data[12..16]),
y_offset: read_be_u32(&byte_data[16..20]),
delay_num: read_be_u16(&byte_data[20..22]),
delay_den: read_be_u16(&byte_data[22..24]),
dispose_op: byte_data[24],
blend_op: byte_data[25],
data: vec![],
})
}
/// Construct the data for a fcTL chunk using the given sequence number
#[must_use]
pub fn fctl_data(&self, sequence_number: u32) -> Vec<u8> {
let mut byte_data = Vec::with_capacity(26);
byte_data.extend_from_slice(&sequence_number.to_be_bytes());
byte_data.extend_from_slice(&self.width.to_be_bytes());
byte_data.extend_from_slice(&self.height.to_be_bytes());
byte_data.extend_from_slice(&self.x_offset.to_be_bytes());
byte_data.extend_from_slice(&self.y_offset.to_be_bytes());
byte_data.extend_from_slice(&self.delay_num.to_be_bytes());
byte_data.extend_from_slice(&self.delay_den.to_be_bytes());
byte_data.extend_from_slice(&[self.dispose_op, self.blend_op]);
byte_data
}
/// Construct the data for a fdAT chunk using the given sequence number
#[must_use]
pub fn fdat_data(&self, sequence_number: u32) -> Vec<u8> {
let mut byte_data = Vec::with_capacity(4 + self.data.len());
byte_data.extend_from_slice(&sequence_number.to_be_bytes());
byte_data.extend_from_slice(&self.data);
byte_data
}
}

View file

@ -6,7 +6,6 @@ pub struct AtomicMin {
}
impl AtomicMin {
#[must_use]
pub fn new(init: Option<usize>) -> Self {
Self {
val: AtomicUsize::new(init.unwrap_or(usize::MAX)),
@ -15,7 +14,16 @@ impl AtomicMin {
pub fn get(&self) -> Option<usize> {
let val = self.val.load(SeqCst);
if val == usize::MAX { None } else { Some(val) }
if val == usize::MAX {
None
} else {
Some(val)
}
}
/// Unset value is usize_max
pub fn as_atomic_usize(&self) -> &AtomicUsize {
&self.val
}
/// Try a new value, returning true if it is the new minimum

View file

@ -1,18 +1,9 @@
use std::{num::NonZeroU64, path::PathBuf};
use std::path::PathBuf;
use clap::builder::Styles;
use clap::builder::styling::{AnsiColor, Effects};
use clap::{Arg, ArgAction, Command, builder::ArgPredicate, value_parser};
use parse_size::parse_size;
use clap::{value_parser, Arg, ArgAction, Command};
include!("display_chunks.rs");
const STYLES: Styles = Styles::styled()
.header(AnsiColor::Green.on_default().effects(Effects::BOLD))
.usage(AnsiColor::Green.on_default().effects(Effects::BOLD))
.literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD))
.placeholder(AnsiColor::Cyan.on_default());
pub fn build_command() -> Command {
// Note: clap 'wrap_help' is enabled to automatically wrap lines according to terminal width.
// To keep things tidy though, short help descriptions should be no more than 54 characters,
@ -23,7 +14,6 @@ pub fn build_command() -> Command {
.version(env!("CARGO_PKG_VERSION"))
.author("Joshua Holmer <jholmer.in@gmail.com>")
.about("Losslessly improve compression of PNG files")
.styles(STYLES)
.arg(
Arg::new("files")
.help("File(s) to compress (use '-' for stdin)")
@ -41,14 +31,14 @@ Set the optimization level preset. The default level 2 is quite fast and provide
compression. Lower levels are faster, higher levels provide better compression, though \
with increasingly diminishing returns.
0 => --zc 5 --fast (filter chosen heuristically)
1 => --zc 10 --fast (filter chosen heuristically)
2 => --zc 11 -f 0,1,6,7 --fast
3 => --zc 11 -f 0,7,8,9 --brute-level 1 --brute-lines 3
4 => --zc 12 -f 0,7,8,9 --brute-level 1 --brute-lines 4
5 => --zc 12 -f 0,1,2,5,6,7,8,9 --brute-level 4 --brute-lines 4
6 => --zc 12 -f 0-9 --brute-level 5 --brute-lines 8
max => (stable alias for the maximum level)
0 => --zc 5 --fast (1 trial, determined heuristically)
1 => --zc 10 --fast (1 trial, determined heuristically)
2 => --zc 11 -f 0,1,6,7 --fast (4 fast trials, 1 main trial)
3 => --zc 11 -f 0,7,8,9 (4 trials)
4 => --zc 12 -f 0,7,8,9 (4 trials)
5 => --zc 12 -f 0,1,2,5,6,7,8,9 (8 trials)
6 => --zc 12 -f 0-9 (10 trials)
max => (stable alias for the max level)
Manually specifying a compression option (zc, f, etc.) will override the optimization \
preset, regardless of the order you write the arguments.")
@ -59,6 +49,14 @@ preset, regardless of the order you write the arguments.")
.value_parser(["0", "1", "2", "3", "4", "5", "6", "max"])
.hide_possible_values(true),
)
.arg(
Arg::new("backup")
.help("Back up modified files")
.short('b')
.long("backup")
.hide(true)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("recursive")
.help("Recurse input directories, optimizing all PNG files")
@ -107,10 +105,10 @@ Note that this will not preserve the directory structure of the input files when
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("dry-run")
Arg::new("pretend")
.help("Do not write any files, only show compression results")
.short('d')
.long("dry-run")
.short('P')
.long("pretend")
.action(ArgAction::SetTrue),
)
.arg(
@ -133,14 +131,10 @@ Strip metadata chunks, where <mode> is one of:
CAUTION: 'all' will convert APNGs to standard PNGs.
Please note that regardless of any options set, some chunks will necessarily be stripped \
when invalidated by the optimization:
bKGD, sBIT, hIST: Stripped if the color type or bit depth changes.
iDOT: Always stripped.
caBX: Stripped if it contains C2PA metadata. If explicitly retained by `--keep`, \
optimization will be aborted.
Note that 'bKGD', 'sBIT' and 'hIST' will be forcibly stripped if the color type or bit \
depth is changed, regardless of any options set.
The default when --strip is not passed is to keep all chunks that remain valid.",
The default when --strip is not passed is to keep all metadata.",
DISPLAY_CHUNKS
.iter()
.map(|c| String::from_utf8_lossy(c))
@ -178,22 +172,21 @@ transformation and may be unsuitable for some applications.")
)
.arg(
Arg::new("interlace")
.help("Set PNG interlacing (off, on, keep)")
.help("Set PNG interlacing type (0, 1, keep)")
.long_help("\
Set the PNG interlacing mode, where <mode> is one of:
Set the PNG interlacing type, where <type> is one of:
off => Remove interlacing from all images that are processed
on => Apply Adam7 interlacing on all images that are processed
keep => Keep the existing interlacing mode of each image
0 => Remove interlacing from all images that are processed
1 => Apply Adam7 interlacing on all images that are processed
keep => Keep the existing interlacing type of each image
Note that interlacing can add 25-50% to the size of an optimized image. Only use it if you \
believe the benefits outweigh the costs for your use case.")
.short('i')
.long("interlace")
.value_name("mode")
.value_parser(["off", "on", "keep", "0", "1"])
.default_value("off")
.default_value_if("no-reductions", ArgPredicate::IsPresent, "keep")
.value_name("type")
.default_value("0")
.value_parser(["0", "1", "keep"])
.hide_possible_values(true),
)
.arg(
@ -212,7 +205,7 @@ losslessly.")
)
.arg(
Arg::new("verbose")
.help("Show per-file info (use multiple times for more detail)")
.help("Run in verbose mode (use twice to increase verbosity)")
.short('v')
.long("verbose")
.action(ArgAction::Count)
@ -220,20 +213,12 @@ losslessly.")
)
.arg(
Arg::new("quiet")
.help("Suppress all output messages")
.help("Run in quiet mode")
.short('q')
.long("quiet")
.action(ArgAction::SetTrue)
.conflicts_with("verbose"),
)
.arg(
Arg::new("json")
.help("Print results as JSON")
.short('j')
.long("json")
.action(ArgAction::SetTrue)
.conflicts_with("stdout"),
)
.arg(
Arg::new("filters")
.help("Filters to try (0-9; see '--help' for details)")
@ -250,9 +235,9 @@ PNG delta filters (apply the same filter to every line)
Heuristic strategies (try to find the best delta filter for each line)
5 => MinSum Minimum sum of absolute differences
6 => Entropy Smallest Shannon entropy
6 => Entropy Highest Shannon entropy
7 => Bigrams Lowest count of distinct bigrams
8 => BigEnt Smallest Shannon entropy of bigrams
8 => BigEnt Highest Shannon entropy of bigrams
9 => Brute Smallest compressed size (slow)
The default value depends on the optimization level preset.")
@ -272,15 +257,15 @@ CPU cores.")
)
.arg(
Arg::new("compression")
.help("Deflate compression level (0-12)")
.help("Deflate compression level (1-12)")
.long_help("\
Deflate compression level (0-12) for main compression trials. The levels here are defined \
Deflate compression level (1-12) for main compression trials. The levels here are defined \
by the libdeflate compression library.
The default value depends on the optimization level preset.")
.long("zc")
.value_name("level")
.value_parser(0..=12)
.value_parser(1..=12)
.conflicts_with("zopfli"),
)
.arg(
@ -298,8 +283,6 @@ The default value depends on the optimization level preset.")
.arg(
Arg::new("no-palette-reduction")
.help("Do not change color palette")
.long_help("\
Do not convert to indexed and do not modify an existing color palette.")
.long("np")
.action(ArgAction::SetTrue),
)
@ -332,7 +315,7 @@ compressed chunks (such as iCCP) will also be disabled. Note that the combinatio
.help("Disable checksum validation")
.long_help("\
Do not perform checksum validation of PNG chunks. This may allow some files with errors to \
be processed successfully. The output will always have correct checksums.")
be processed successfully.")
.long("fix")
.action(ArgAction::SetTrue),
)
@ -348,54 +331,10 @@ be processed successfully. The output will always have correct checksums.")
.long_help("\
Use the much slower but stronger Zopfli compressor for main compression trials. \
Recommended use is with '-o max' and '--fast'.")
.short('z')
.short_alias('Z') // Kept for backwards compatibility
.short('Z')
.long("zopfli")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("iterations")
.help("Number of Zopfli iterations")
.long_help("\
Set the number of iterations to use for Zopfli compression. Using fewer iterations may \
speed up compression for large files. This option requires '--zopfli' to be set.")
.long("zi")
.value_name("iterations")
.default_value("15")
.value_parser(value_parser!(NonZeroU64))
.requires("zopfli"),
)
.arg(
Arg::new("iterations-without-improvement")
.hide_short_help(true)
.long_help("\
Stop Zopfli compression after this number of iterations without improvement. Use this in \
conjunction with a high value for '--zi' to achieve better compression in reasonable time.")
.long("ziwi")
.value_name("iterations")
.value_parser(value_parser!(NonZeroU64))
.requires("zopfli"),
)
.arg(
Arg::new("brute-level")
.hide_short_help(true)
.long_help("\
Set the libdeflate compression level to use with the Brute filter strategy. Sane values \
are 1-5. Higher values are not necessarily better.")
.long("brute-level")
.value_name("level")
.value_parser(1..=12),
)
.arg(
Arg::new("brute-lines")
.hide_short_help(true)
.long_help("\
Set the number of lines to compress at once with the Brute filter strategy. Sane values \
are 2-16. Higher values are not necessarily better.")
.long("brute-lines")
.value_name("lines")
.value_parser(value_parser!(usize)),
)
.arg(
Arg::new("timeout")
.help("Maximum amount of time to spend on optimizations")
@ -405,46 +344,16 @@ timeout before each transformation or compression trial, and will stop trying to
the file if the timeout is exceeded. Note that this does not cut short any operations that \
are already in progress, so it is currently of limited effectiveness for large files with \
high compression levels.")
.long("timeout")
.value_name("secs")
.long("timeout")
.value_parser(value_parser!(u64)),
)
.arg(
Arg::new("max-size")
.help("Skip image if the decompressed size exceeds this limit")
.long_help("\
Maximum size to allow for the input image. If the raw, decompressed image data (or the \
file size) of the image exceeds this size, it will be skipped. This is useful for limiting \
memory usage or avoiding long processing times on large images. The value may be specified \
with a unit suffix such as k, KB, m, MB, etc.
The decompressed size of an image is roughly equal to width * height * bit-depth / 8. E.g. \
a 1920x1080 image with 24-bit color depth would be roughly 6MB.")
.long("max-raw-size")
.value_name("bytes")
.value_parser(|s: &str| parse_size(s)),
)
.arg(
Arg::new("threads")
.help("Number of threads to use [default: num logical CPUs]")
.long_help("\
Set the maximum number of threads to use. Oxipng uses multithreading to evaluate multiple \
optimizations on the same file in parallel as well as process multiple files in parallel. \
You can set this to a lower value if you need to limit memory or CPU usage.
[default: num logical CPUs]")
.short('t')
.help("Set number of threads to use [default: num CPU cores]")
.long("threads")
.short('t')
.value_name("num")
.value_parser(value_parser!(usize)),
)
.arg(
Arg::new("parallel-files")
.help("Process multiple files sequentially")
.long_help("\
Process multiple files sequentially rather than in parallel. Use this if you need \
determinism in the processing order. Note this is not necessary if using '--threads 1'.")
.long("sequential")
.action(ArgAction::SetFalse),
)
}

View file

@ -32,11 +32,13 @@ impl Display for ColorType {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Grayscale { .. } => write!(f, "Grayscale"),
Self::RGB { .. } => write!(f, "RGB"),
Self::Indexed { palette } => write!(f, "Indexed ({} colors)", palette.len()),
Self::GrayscaleAlpha => write!(f, "Grayscale + Alpha"),
Self::RGBA => write!(f, "RGB + Alpha"),
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),
}
}
}
@ -44,47 +46,49 @@ impl Display for ColorType {
impl ColorType {
/// Get the code used by the PNG specification to denote this color type
#[inline]
#[must_use]
pub const fn png_header_code(&self) -> u8 {
pub fn png_header_code(&self) -> u8 {
match self {
Self::Grayscale { .. } => 0,
Self::RGB { .. } => 2,
Self::Indexed { .. } => 3,
Self::GrayscaleAlpha => 4,
Self::RGBA => 6,
ColorType::Grayscale { .. } => 0,
ColorType::RGB { .. } => 2,
ColorType::Indexed { .. } => 3,
ColorType::GrayscaleAlpha => 4,
ColorType::RGBA => 6,
}
}
#[inline]
pub(crate) const fn channels_per_pixel(&self) -> u8 {
pub(crate) fn channels_per_pixel(&self) -> u8 {
match self {
Self::Grayscale { .. } | Self::Indexed { .. } => 1,
Self::GrayscaleAlpha => 2,
Self::RGB { .. } => 3,
Self::RGBA => 4,
ColorType::Grayscale { .. } | ColorType::Indexed { .. } => 1,
ColorType::GrayscaleAlpha => 2,
ColorType::RGB { .. } => 3,
ColorType::RGBA => 4,
}
}
#[inline]
pub(crate) const fn is_rgb(&self) -> bool {
matches!(self, Self::RGB { .. } | Self::RGBA)
pub(crate) fn is_rgb(&self) -> bool {
matches!(self, ColorType::RGB { .. } | ColorType::RGBA)
}
#[inline]
pub(crate) const fn is_gray(&self) -> bool {
matches!(self, Self::Grayscale { .. } | Self::GrayscaleAlpha)
pub(crate) fn is_gray(&self) -> bool {
matches!(
self,
ColorType::Grayscale { .. } | ColorType::GrayscaleAlpha
)
}
#[inline]
pub(crate) const fn has_alpha(&self) -> bool {
matches!(self, Self::GrayscaleAlpha | Self::RGBA)
pub(crate) fn has_alpha(&self) -> bool {
matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA)
}
#[inline]
pub(crate) const fn has_trns(&self) -> bool {
pub(crate) fn has_trns(&self) -> bool {
match self {
Self::Grayscale { transparent_shade } => transparent_shade.is_some(),
Self::RGB { transparent_color } => transparent_color.is_some(),
ColorType::Grayscale { transparent_shade } => transparent_shade.is_some(),
ColorType::RGB { transparent_color } => transparent_color.is_some(),
_ => false,
}
}
@ -116,7 +120,7 @@ impl TryFrom<u8> for BitDepth {
4 => Ok(Self::Four),
8 => Ok(Self::Eight),
16 => Ok(Self::Sixteen),
_ => Err(PngError::InvalidData),
_ => Err(PngError::new("Unexpected bit depth")),
}
}
}

View file

@ -1,10 +1,12 @@
use libdeflater::*;
use crate::{PngError, PngResult};
use crate::{atomicmin::AtomicMin, PngError, PngResult};
pub fn deflate(data: &[u8], level: u8, max_size: Option<usize>) -> PngResult<Vec<u8>> {
pub fn deflate(data: &[u8], level: u8, max_size: &AtomicMin) -> PngResult<Vec<u8>> {
let mut compressor = Compressor::new(CompressionLvl::new(level.into()).unwrap());
let capacity = max_size.unwrap_or_else(|| compressor.zlib_compress_bound(data.len()));
let capacity = max_size
.get()
.unwrap_or_else(|| compressor.zlib_compress_bound(data.len()));
let mut dest = vec![0; capacity];
let len = compressor
.zlib_compress(data, &mut dest)
@ -22,13 +24,12 @@ pub fn inflate(data: &[u8], out_size: usize) -> PngResult<Vec<u8>> {
.zlib_decompress(data, &mut dest)
.map_err(|err| match err {
DecompressionError::BadData => PngError::InvalidData,
DecompressionError::InsufficientSpace => PngError::InflatedDataTooLong(out_size),
DecompressionError::InsufficientSpace => PngError::new("inflated data too long"),
})?;
dest.truncate(len);
Ok(dest)
}
#[must_use]
pub fn crc32(data: &[u8]) -> u32 {
let mut crc = Crc::new();
crc.update(data);

View file

@ -1,38 +1,42 @@
mod deflater;
#[cfg(feature = "zopfli")]
use std::num::NonZeroU8;
use std::{fmt, fmt::Display};
pub use deflater::{crc32, deflate, inflate};
use crate::{PngError, PngResult};
use crate::{AtomicMin, PngError, PngResult};
#[cfg(feature = "zopfli")]
mod zopfli_oxipng;
#[cfg(feature = "zopfli")]
pub use zopfli::Options as ZopfliOptions;
#[cfg(feature = "zopfli")]
pub use zopfli_oxipng::deflate as zopfli_deflate;
/// DEFLATE algorithms supported by oxipng (for use in [`Options`][crate::Options])
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Deflater {
/// DEFLATE algorithms supported by oxipng
pub enum Deflaters {
/// Use libdeflater.
Libdeflater {
/// Which compression level to use on the file (0-12)
/// Which compression level to use on the file (1-12)
compression: u8,
},
#[cfg(feature = "zopfli")]
/// Use the better but slower Zopfli implementation
Zopfli(ZopfliOptions),
Zopfli {
/// The number of compression iterations to do. 15 iterations are fine
/// for small files, but bigger files will need to be compressed with
/// less iterations, or else they will be too slow.
iterations: NonZeroU8,
},
}
impl Deflater {
pub(crate) fn deflate(self, data: &[u8], max_size: Option<usize>) -> PngResult<Vec<u8>> {
impl Deflaters {
pub(crate) fn deflate(self, data: &[u8], max_size: &AtomicMin) -> PngResult<Vec<u8>> {
let compressed = match self {
Self::Libdeflater { compression } => deflate(data, compression, max_size)?,
#[cfg(feature = "zopfli")]
Self::Zopfli(options) => zopfli_deflate(data, options)?,
Self::Zopfli { iterations } => zopfli_deflate(data, iterations)?,
};
if let Some(max) = max_size {
if let Some(max) = max_size.get() {
if compressed.len() > max {
return Err(PngError::DeflatedDataTooLong(max));
}
@ -41,13 +45,13 @@ impl Deflater {
}
}
impl Display for Deflater {
impl Display for Deflaters {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Libdeflater { compression } => write!(f, "zc = {compression}"),
Self::Libdeflater { compression } => Display::fmt(compression, f),
#[cfg(feature = "zopfli")]
Self::Zopfli(options) => write!(f, "zopfli, zi = {}", options.iteration_count),
Self::Zopfli { .. } => Display::fmt("zopfli", f),
}
}
}

View file

@ -1,13 +1,17 @@
use std::num::NonZeroU8;
use crate::{PngError, PngResult};
pub fn deflate(data: &[u8], options: zopfli::Options) -> PngResult<Vec<u8>> {
pub fn deflate(data: &[u8], iterations: NonZeroU8) -> PngResult<Vec<u8>> {
let mut output = Vec::with_capacity(data.len());
// Since Rust v1.74, passing &[u8] directly into zopfli causes a regression in compressed size
// for some files. Wrapping the slice in another Read implementer such as Box fixes it for now.
match zopfli::compress(options, zopfli::Format::Zlib, Box::new(data), &mut output) {
Ok(()) => (),
let options = zopfli::Options {
iteration_count: iterations.into(),
..Default::default()
};
match zopfli::compress(options, zopfli::Format::Zlib, data, &mut output) {
Ok(_) => (),
Err(_) => return Err(PngError::new("Failed to compress in zopfli")),
}
};
output.shrink_to_fit();
Ok(output)
}

View file

@ -2,22 +2,18 @@ use std::{error::Error, fmt};
use crate::colors::{BitDepth, ColorType};
#[derive(Debug)]
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum PngError {
APNGOutOfOrder,
C2PAMetadataPreventsChanges,
ChunkMissing(&'static str),
CRCMismatch([u8; 4]),
DeflatedDataTooLong(usize),
IncorrectDataLength(usize, usize),
InflatedDataTooLong(usize),
InvalidData,
InvalidDepthForType(BitDepth, ColorType),
TimedOut,
NotPNG,
ReadFailed(String, std::io::Error),
APNGNotSupported,
InvalidData,
TruncatedData,
WriteFailed(String, std::io::Error),
ChunkMissing(&'static str),
InvalidDepthForType(BitDepth, ColorType),
IncorrectDataLength(usize, usize),
Other(Box<str>),
}
@ -28,42 +24,31 @@ impl fmt::Display for PngError {
#[cold]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::APNGOutOfOrder => f.write_str("APNG chunks are out of order"),
Self::C2PAMetadataPreventsChanges => f.write_str(
"The image contains C2PA manifest that would be invalidated by any file changes",
),
Self::ChunkMissing(s) => write!(f, "Chunk {s} missing or empty"),
Self::CRCMismatch(ref c) => write!(
f,
"CRC mismatch in {} chunk; May be recoverable by using --fix",
String::from_utf8_lossy(c)
),
Self::DeflatedDataTooLong(_) => f.write_str("Deflated data too long"),
Self::IncorrectDataLength(l1, l2) => write!(
f,
"Data length {l1} does not match the expected length {l2}"
),
Self::InflatedDataTooLong(max) => write!(
f,
"Inflated data would exceed the maximum size ({max} bytes)"
),
Self::InvalidData => f.write_str("Invalid data found; unable to read PNG file"),
Self::InvalidDepthForType(d, ref c) => {
write!(f, "Invalid bit depth {d} for color type {c}")
PngError::DeflatedDataTooLong(_) => f.write_str("deflated data too long"),
PngError::TimedOut => f.write_str("timed out"),
PngError::NotPNG => f.write_str("Invalid header detected; Not a PNG file"),
PngError::InvalidData => f.write_str("Invalid data found; unable to read PNG file"),
PngError::TruncatedData => {
f.write_str("Missing data in the file; the file is truncated")
}
Self::NotPNG => f.write_str("Invalid header detected; Not a PNG file"),
Self::ReadFailed(ref s, ref e) => write!(f, "Failed to read from {s}: {e}"),
Self::TruncatedData => f.write_str("Missing data in the file; the file is truncated"),
Self::WriteFailed(ref s, ref e) => write!(f, "Failed to write to {s}: {e}"),
Self::Other(ref s) => f.write_str(s),
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),
}
}
}
impl PngError {
#[cold]
#[must_use]
pub fn new(description: &str) -> Self {
Self::Other(description.into())
pub fn new(description: &str) -> PngError {
PngError::Other(description.into())
}
}

View file

@ -4,44 +4,37 @@
#[cfg(not(feature = "parallel"))]
use std::cell::RefCell;
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering::*},
Arc,
};
use deflate::Deflater;
#[cfg(feature = "parallel")]
use crossbeam_channel::{unbounded, Receiver, Sender};
use indexmap::IndexSet;
use log::trace;
use rayon::prelude::*;
#[cfg(feature = "parallel")]
use std::sync::mpsc::{Receiver, Sender, channel};
#[cfg(not(feature = "parallel"))]
use crate::rayon;
use crate::{
Deadline, PngError, atomicmin::AtomicMin, deflate, filters::FilterStrategy, png::PngImage,
};
use crate::{atomicmin::AtomicMin, deflate, filters::RowFilter, png::PngImage, Deadline, PngError};
pub(crate) struct Candidate {
pub struct Candidate {
pub image: Arc<PngImage>,
pub idat_data: Option<Vec<u8>>,
pub estimated_output_size: usize,
/// The input filter, which is retained for printing and for APNG frames.
pub filter: FilterStrategy,
/// The filter returned by the filter function, which may be Predefined.
/// Use this for the next round to avoid recomputing the filter.
pub filter_used: FilterStrategy,
/// For determining tie-breaker
pub idat_data: Vec<u8>,
pub filtered: Vec<u8>,
pub filter: RowFilter,
// first wins tie-breaker
nth: usize,
}
impl Candidate {
fn cmp_key(&self) -> impl Ord + use<> {
fn cmp_key(&self) -> impl Ord {
(
self.estimated_output_size,
self.idat_data.len() + self.image.key_chunks_size(),
self.image.data.len(),
self.filter.clone(),
// Prefer the later image added (e.g. baseline, which is always added last)
usize::MAX - self.nth,
self.image.ihdr.bit_depth,
self.filter,
self.nth,
)
}
}
@ -49,10 +42,9 @@ impl Candidate {
/// Collect image versions and pick one that compresses best
pub(crate) struct Evaluator {
deadline: Arc<Deadline>,
filters: IndexSet<FilterStrategy>,
deflater: Deflater,
filters: IndexSet<RowFilter>,
compression: u8,
optimize_alpha: bool,
final_round: bool,
nth: AtomicUsize,
executed: Arc<AtomicUsize>,
best_candidate_size: Arc<AtomicMin>,
@ -67,19 +59,17 @@ pub(crate) struct Evaluator {
impl Evaluator {
pub fn new(
deadline: Arc<Deadline>,
filters: IndexSet<FilterStrategy>,
deflater: Deflater,
filters: IndexSet<RowFilter>,
compression: u8,
optimize_alpha: bool,
final_round: bool,
) -> Self {
#[cfg(feature = "parallel")]
let eval_channel = channel();
let eval_channel = unbounded();
Self {
deadline,
filters,
deflater,
compression,
optimize_alpha,
final_round,
nth: AtomicUsize::new(0),
executed: Arc::new(AtomicUsize::new(0)),
best_candidate_size: Arc::new(AtomicMin::new(None)),
@ -118,7 +108,7 @@ impl Evaluator {
/// Check if the image is smaller than others
pub fn try_image(&self, image: Arc<PngImage>) {
let description = image.ihdr.color_type.to_string();
let description = format!("{}", image.ihdr.color_type);
self.try_image_with_description(image, &description);
}
@ -128,9 +118,8 @@ impl Evaluator {
// These clones are only cheap refcounts
let deadline = self.deadline.clone();
let filters = self.filters.clone();
let deflater = self.deflater;
let compression = self.compression;
let optimize_alpha = self.optimize_alpha;
let final_round = self.final_round;
let executed = self.executed.clone();
let best_candidate_size = self.best_candidate_size.clone();
let description = description.to_string();
@ -146,37 +135,29 @@ impl Evaluator {
// which are dangerous to do in side Rayon's loop.
// Instead, only update (atomic) best size in real time,
// and the best result later without need for locks.
filters_iter.for_each(|filter| {
filters_iter.for_each(|&filter| {
if deadline.passed() {
return;
}
let (filtered, filter_used) = image.filter_image(filter.clone(), optimize_alpha);
let idat_data = deflater.deflate(&filtered, best_candidate_size.get());
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 estimated_output_size = image.estimated_output_size(&idat_data);
let size = idat_data.len() + image.key_chunks_size();
best_candidate_size.set_min(size);
trace!(
"Eval: {}-bit {:23} {:8} {} bytes",
image.ihdr.bit_depth, description, filter, estimated_output_size
image.ihdr.bit_depth,
description,
filter,
size
);
// Skip if it exceeds best known size. (This is important to ensure
// the evaluator returns no result when all candidates are too large.)
if let Some(max) = best_candidate_size.get() {
if estimated_output_size > max {
return;
}
}
// We only need to retain the IDAT data in the final round
let new = Candidate {
image: image.clone(),
idat_data: if final_round { Some(idat_data) } else { None },
estimated_output_size,
filter: filter.clone(),
filter_used,
idat_data,
filtered,
filter,
nth,
};
best_candidate_size.set_min(estimated_output_size);
#[cfg(feature = "parallel")]
{
@ -193,7 +174,10 @@ impl Evaluator {
} else if let Err(PngError::DeflatedDataTooLong(size)) = idat_data {
trace!(
"Eval: {}-bit {:23} {:8} >{} bytes",
image.ihdr.bit_depth, description, filter, size
image.ihdr.bit_depth,
description,
filter,
size
);
}
});

View file

@ -1,67 +1,29 @@
use std::{fmt, fmt::Display, mem::transmute};
/// Filtering strategy for use in [`Options`][crate::Options]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
pub enum FilterStrategy {
/// Same filter for all rows
Basic(RowFilter),
/// Minimum sum of absolute differences
MinSum,
/// Shannon entropy
Entropy,
/// Count of distinct bigrams
Bigrams,
/// Shannon entropy of bigrams
BigEnt,
/// Deflate compression
Brute {
/// The number of lines to compress at once
num_lines: usize,
/// The compression level to use (1-12)
level: u8,
},
/// Predefined filter for each row
Predefined(Vec<RowFilter>),
}
use crate::error::PngError;
impl FilterStrategy {
pub const NONE: Self = Self::Basic(RowFilter::None);
pub const SUB: Self = Self::Basic(RowFilter::Sub);
pub const UP: Self = Self::Basic(RowFilter::Up);
pub const AVERAGE: Self = Self::Basic(RowFilter::Average);
pub const PAETH: Self = Self::Basic(RowFilter::Paeth);
}
impl Display for FilterStrategy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Basic(filter) => filter.fmt(f),
Self::MinSum => "MinSum".fmt(f),
Self::Entropy => "Entropy".fmt(f),
Self::Bigrams => "Bigrams".fmt(f),
Self::BigEnt => "BigEnt".fmt(f),
Self::Brute { .. } => "Brute".fmt(f),
Self::Predefined(_) => "Predefined".fmt(f),
}
}
}
/// PNG delta filters
#[repr(u8)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
pub enum RowFilter {
// Standard filter types
None,
Sub,
Up,
Average,
Paeth,
// Heuristic strategies
MinSum,
Entropy,
Bigrams,
BigEnt,
Brute,
}
impl TryFrom<u8> for RowFilter {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
if value > 4 {
if value > Self::LAST {
return Err(());
}
unsafe { transmute(value as i8) }
@ -77,6 +39,11 @@ impl Display for RowFilter {
Self::Up => "Up",
Self::Average => "Average",
Self::Paeth => "Paeth",
Self::MinSum => "MinSum",
Self::Entropy => "Entropy",
Self::Bigrams => "Bigrams",
Self::BigEnt => "BigEnt",
Self::Brute => "Brute",
},
f,
)
@ -84,7 +51,9 @@ impl Display for RowFilter {
}
impl RowFilter {
pub(crate) const ALL: [Self; 5] = [Self::None, Self::Sub, Self::Up, Self::Average, Self::Paeth];
pub const LAST: u8 = Self::Brute as u8;
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(crate) fn filter_line(
@ -127,20 +96,25 @@ impl RowFilter {
}
Self::Average => {
for (i, byte) in data.iter().enumerate() {
buf.push(byte.wrapping_sub(i.checked_sub(bpp).map_or_else(
|| prev_line[i] >> 1,
|x| ((u16::from(data[x]) + u16::from(prev_line[i])) >> 1) as u8,
)));
buf.push(match i.checked_sub(bpp) {
Some(x) => byte.wrapping_sub(
((u16::from(data[x]) + u16::from(prev_line[i])) >> 1) as u8,
),
None => byte.wrapping_sub(prev_line[i] >> 1),
});
}
}
Self::Paeth => {
for (i, byte) in data.iter().enumerate() {
buf.push(byte.wrapping_sub(i.checked_sub(bpp).map_or_else(
|| prev_line[i],
|x| paeth_predictor(data[x], prev_line[i], prev_line[x]),
)));
buf.push(match i.checked_sub(bpp) {
Some(x) => {
byte.wrapping_sub(paeth_predictor(data[x], prev_line[i], prev_line[x]))
}
None => byte.wrapping_sub(prev_line[i]),
});
}
}
_ => unreachable!(),
}
}
@ -151,8 +125,8 @@ impl RowFilter {
return;
}
let mut pixels: Vec<_> = data.chunks_exact_mut(bpp).collect();
let prev_pixels: Vec<_> = prev_line.chunks_exact(bpp).collect();
let mut pixels: Vec<_> = data.chunks_mut(bpp).collect();
let prev_pixels: Vec<_> = prev_line.chunks(bpp).collect();
for i in 0..pixels.len() {
if pixels[i].iter().skip(color_bytes).all(|b| *b == 0) {
// If the first pixel in the row is transparent, find the next non-transparent pixel and pretend
@ -164,28 +138,10 @@ impl RowFilter {
.unwrap_or(i),
_ => i - 1,
};
// These assertions help eliminate a few bounds checks in the slice accesses below
assert!(prev < pixels.len());
assert!(i < prev_pixels.len());
match self {
Self::None => unreachable!(),
Self::Sub => {
// The code below is roughly equivalent to pixels[i][0..color_bytes].copy_from_slice(&pixels[prev][0..color_bytes]),
// if such a thing was possible to do without violating Rust aliasing rules. See:
// https://users.rust-lang.org/t/problem-borrowing-two-elements-of-vec-mutably/21446/2
if prev < i {
let (pixels_head, pixels_tail) = pixels.split_at_mut(prev + 1);
pixels_tail[i - prev - 1][0..color_bytes]
.copy_from_slice(&pixels_head[prev][0..color_bytes]);
} else if prev > i {
let (pixels_head, pixels_tail) = pixels.split_at_mut(i + 1);
pixels_head[i][0..color_bytes]
.copy_from_slice(&pixels_tail[prev - i - 1][0..color_bytes]);
} else {
// If prev == i, we'd be copying the pixels onto themselves, which is useless
for j in 0..color_bytes {
pixels[i][j] = pixels[prev][j];
}
}
Self::Up => {
@ -214,6 +170,7 @@ impl RowFilter {
};
}
}
_ => unreachable!(),
}
}
}
@ -225,7 +182,7 @@ impl RowFilter {
data: &[u8],
prev_line: &[u8],
buf: &mut Vec<u8>,
) {
) -> Result<(), PngError> {
buf.clear();
buf.reserve(data.len());
assert!(data.len() >= bpp);
@ -237,7 +194,10 @@ impl RowFilter {
Self::Sub => {
for (i, &cur) in data.iter().enumerate() {
let prev_byte = i.checked_sub(bpp).and_then(|x| buf.get(x).copied());
buf.push(prev_byte.map_or(cur, |b| cur.wrapping_add(b)));
buf.push(match prev_byte {
Some(b) => cur.wrapping_add(b),
None => cur,
});
}
}
Self::Up => {
@ -250,10 +210,10 @@ impl RowFilter {
Self::Average => {
for (i, (&cur, &last)) in data.iter().zip(prev_line).enumerate() {
let prev_byte = i.checked_sub(bpp).and_then(|x| buf.get(x).copied());
buf.push(cur.wrapping_add(prev_byte.map_or_else(
|| last >> 1,
|b| ((u16::from(b) + u16::from(last)) >> 1) as u8,
)));
buf.push(match prev_byte {
Some(b) => cur.wrapping_add(((u16::from(b) + u16::from(last)) >> 1) as u8),
None => cur.wrapping_add(last >> 1),
});
}
}
Self::Paeth => {
@ -271,7 +231,9 @@ impl RowFilter {
);
}
}
_ => return Err(PngError::InvalidData),
}
Ok(())
}
}

View file

@ -1,13 +1,14 @@
use indexmap::IndexSet;
use log::{debug, trace, warn};
use log::warn;
use rgb::{RGB16, RGBA8};
use crate::{
Deflater, Options, PngResult,
colors::{BitDepth, ColorType},
deflate::{crc32, inflate},
display_chunks::DISPLAY_CHUNKS,
error::PngError,
interlace::Interlacing,
AtomicMin, Deflaters, PngResult,
};
#[derive(Debug, Clone)]
@ -21,30 +22,32 @@ pub struct IhdrData {
pub color_type: ColorType,
/// The bit depth of the image
pub bit_depth: BitDepth,
/// Whether the image is interlaced
pub interlaced: bool,
/// The interlacing mode of the image
pub interlaced: Interlacing,
}
impl IhdrData {
/// Bits per pixel
#[must_use]
#[inline]
pub const fn bpp(&self) -> usize {
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
#[must_use]
pub const fn raw_data_size(&self) -> usize {
pub fn raw_data_size(&self) -> usize {
let w = self.width as usize;
let h = self.height as usize;
let bpp = self.bpp();
const fn bitmap_size(bpp: usize, w: usize, h: usize) -> usize {
(w * bpp).div_ceil(8) * h
fn bitmap_size(bpp: usize, w: usize, h: usize) -> usize {
((w * bpp + 7) / 8) * h
}
if self.interlaced {
if self.interlaced == Interlacing::None {
bitmap_size(bpp, w, h) + h
} else {
let mut size = bitmap_size(bpp, (w + 7) >> 3, (h + 7) >> 3) + ((h + 7) >> 3);
if w > 4 {
size += bitmap_size(bpp, (w + 3) >> 3, (h + 7) >> 3) + ((h + 7) >> 3);
@ -58,8 +61,6 @@ impl IhdrData {
size += bitmap_size(bpp, w >> 1, (h + 1) >> 1) + ((h + 1) >> 1);
}
size + bitmap_size(bpp, w, h >> 1) + (h >> 1)
} else {
bitmap_size(bpp, w, h) + h
}
}
}
@ -70,12 +71,10 @@ pub struct Chunk {
pub data: Vec<u8>,
}
/// [`Options`][crate::Options] to use when stripping chunks (metadata)
#[derive(Debug, PartialEq, Eq, Clone)]
/// Options to use when stripping chunks
pub enum StripChunks {
/// None
///
/// ...except caBX chunk if it contains a C2PA.org signature.
None,
/// Remove specific chunks
Strip(IndexSet<[u8; 4]>),
@ -90,11 +89,11 @@ pub enum StripChunks {
impl StripChunks {
pub(crate) fn keep(&self, name: &[u8; 4]) -> bool {
match &self {
Self::None => true,
Self::Keep(names) => names.contains(name),
Self::Strip(names) => !names.contains(name),
Self::Safe => DISPLAY_CHUNKS.contains(name),
Self::All => false,
StripChunks::None => true,
StripChunks::Keep(names) => names.contains(name),
StripChunks::Strip(names) => !names.contains(name),
StripChunks::Safe => DISPLAY_CHUNKS.contains(name),
StripChunks::All => false,
}
}
}
@ -112,36 +111,6 @@ pub struct RawChunk<'a> {
pub data: &'a [u8],
}
impl RawChunk<'_> {
// Is it a chunk for C2PA/CAI JUMBF metadata
pub(crate) fn is_c2pa(&self) -> bool {
if self.name == *b"caBX" {
if let Some((b"jumb", data)) = parse_jumbf_box(self.data) {
if let Some((b"jumd", data)) = parse_jumbf_box(data) {
if data.get(..4) == Some(b"c2pa") {
return true;
}
}
}
}
false
}
}
fn parse_jumbf_box(data: &[u8]) -> Option<(&[u8], &[u8])> {
if data.len() < 8 {
return None;
}
let (len, rest) = data.split_at(4);
let len = read_be_u32(len) as usize;
if len < 8 || len > data.len() {
return None;
}
let (box_name, data) = rest.split_at(4);
let data = data.get(..len - 8)?;
Some((box_name, data))
}
pub fn parse_next_chunk<'a>(
byte_data: &'a [u8],
byte_offset: &mut usize,
@ -152,27 +121,37 @@ pub fn parse_next_chunk<'a>(
.get(*byte_offset..*byte_offset + 4)
.ok_or(PngError::TruncatedData)?,
);
if byte_data.len() < *byte_offset + 12 + length as usize {
return Err(PngError::TruncatedData);
}
*byte_offset += 4;
let chunk_start = *byte_offset;
let chunk_name = &byte_data[chunk_start..chunk_start + 4];
let chunk_name = byte_data
.get(chunk_start..chunk_start + 4)
.ok_or(PngError::TruncatedData)?;
if chunk_name == b"IEND" {
// End of data
return Ok(None);
}
*byte_offset += 4;
let data = &byte_data[*byte_offset..*byte_offset + length as usize];
let data = byte_data
.get(*byte_offset..*byte_offset + length as usize)
.ok_or(PngError::TruncatedData)?;
*byte_offset += length as usize;
let crc = read_be_u32(&byte_data[*byte_offset..*byte_offset + 4]);
let crc = read_be_u32(
byte_data
.get(*byte_offset..*byte_offset + 4)
.ok_or(PngError::TruncatedData)?,
);
*byte_offset += 4;
let chunk_bytes = &byte_data[chunk_start..chunk_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(chunk_bytes) != crc {
return Err(PngError::CRCMismatch(chunk_name.try_into().unwrap()));
return Err(PngError::new(&format!(
"CRC Mismatch in {} chunk; May be recoverable by using --fix",
String::from_utf8_lossy(chunk_name)
)));
}
let name: [u8; 4] = chunk_name.try_into().unwrap();
@ -191,13 +170,13 @@ pub fn parse_ihdr_chunk(
0 => ColorType::Grayscale {
transparent_shade: trns_data
.filter(|t| t.len() >= 2)
.map(|t| read_be_u16(&t[0..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: read_be_u16(&t[0..2]),
g: read_be_u16(&t[2..4]),
b: read_be_u16(&t[4..6]),
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 {
@ -205,16 +184,12 @@ pub fn parse_ihdr_chunk(
},
4 => ColorType::GrayscaleAlpha,
6 => ColorType::RGBA,
_ => return Err(PngError::InvalidData),
_ => return Err(PngError::new("Unexpected color type in header")),
},
bit_depth: byte_data[8].try_into()?,
width: read_be_u32(&byte_data[0..4]),
height: read_be_u32(&byte_data[4..8]),
interlaced: match interlaced {
0 => false,
1 => true,
_ => return Err(PngError::InvalidData),
},
interlaced: interlaced.try_into()?,
})
}
@ -223,9 +198,9 @@ fn palette_to_rgba(
palette_data: Option<Vec<u8>>,
trns_data: Option<Vec<u8>>,
) -> Result<Vec<RGBA8>, PngError> {
let palette_data = palette_data.ok_or(PngError::ChunkMissing("PLTE"))?;
let palette_data = palette_data.ok_or_else(|| PngError::new("no palette in indexed image"))?;
let mut palette: Vec<_> = palette_data
.chunks_exact(3)
.chunks(3)
.map(|color| RGBA8::new(color[0], color[1], color[2], 255))
.collect();
@ -238,17 +213,12 @@ fn palette_to_rgba(
}
#[inline]
pub fn read_be_u16(bytes: &[u8]) -> u16 {
u16::from_be_bytes(bytes.try_into().unwrap())
}
#[inline]
pub fn read_be_u32(bytes: &[u8]) -> u32 {
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, max_size: Option<usize>) -> Option<Vec<u8>> {
pub fn extract_icc(iccp: &Chunk) -> Option<Vec<u8>> {
// Skip (useless) profile name
let mut data = iccp.data.as_slice();
loop {
@ -263,24 +233,21 @@ pub fn extract_icc(iccp: &Chunk, max_size: Option<usize>) -> Option<Vec<u8>> {
if compression_method != 0 {
return None; // The profile is supposed to be compressed (method 0)
}
// Libdeflate works with a fixed size buffer. Since the decompressed size is unknown we have to
// guess the required buffer size. We allow a fairly generous 10x factor with a minimum of 1000.
let mut out_size = (compressed_data.len() * 10).max(1000);
// For sanity, impose a default limit of 1MB.
out_size = out_size.min(max_size.unwrap_or(1_000_000));
match inflate(compressed_data, out_size) {
// 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}");
warn!("Failed to decompress icc: {}", e);
None
}
}
}
/// Make an iCCP chunk by compressing the ICC profile
pub fn make_iccp(icc: &[u8], deflater: Deflater, max_size: Option<usize>) -> PngResult<Chunk> {
let mut compressed = deflater.deflate(icc, max_size)?;
/// Construct an iCCP chunk by compressing the ICC profile
pub fn construct_iccp(icc: &[u8], deflater: Deflaters) -> PngResult<Chunk> {
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
@ -317,109 +284,3 @@ pub fn srgb_rendering_intent(icc_data: &[u8]) -> Option<u8> {
_ => None,
}
}
/// Process aux chunks and potentially adjust options before optimizing
pub fn preprocess_chunks(aux_chunks: &mut Vec<Chunk>, opts: &mut Options) {
let has_srgb = aux_chunks.iter().any(|c| &c.name == b"sRGB");
// Grayscale conversion should not be performed if the image is not in the sRGB colorspace
// An sRGB profile would need to be stripped on conversion, so disallow if stripping is disabled
let mut allow_grayscale = !has_srgb || opts.strip != StripChunks::None;
if let Some(iccp_idx) = aux_chunks.iter().position(|c| &c.name == b"iCCP") {
allow_grayscale = false;
// 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 && has_srgb {
// Files aren't supposed to have both chunks, so we chose to honor sRGB
trace!("Removing iCCP chunk due to conflict with sRGB chunk");
aux_chunks.remove(iccp_idx);
allow_grayscale = true;
} else if let Some(icc) = extract_icc(&aux_chunks[iccp_idx], opts.max_decompressed_size) {
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
if let Some(intent) = intent {
trace!("Replacing iCCP chunk with equivalent sRGB chunk");
aux_chunks[iccp_idx] = Chunk {
name: *b"sRGB",
data: vec![intent],
};
allow_grayscale = true;
} else if opts.idat_recoding {
// Try recompressing the profile
let cur_len = aux_chunks[iccp_idx].data.len();
if let Ok(iccp) = make_iccp(&icc, opts.deflater, Some(cur_len - 1)) {
debug!(
"Recompressed iCCP chunk: {} ({} bytes decrease)",
iccp.data.len(),
cur_len - iccp.data.len()
);
aux_chunks[iccp_idx] = iccp;
}
}
}
}
if !allow_grayscale && opts.grayscale_reduction {
debug!("Disabling grayscale reduction due to presence of sRGB or iCCP chunk");
opts.grayscale_reduction = false;
}
// Check for APNG by presence of acTL chunk
if aux_chunks.iter().any(|c| &c.name == b"acTL") {
warn!("APNG detected, disabling all reductions");
opts.interlace = None;
opts.bit_depth_reduction = false;
opts.color_type_reduction = false;
opts.palette_reduction = false;
opts.grayscale_reduction = false;
}
}
/// Perform cleanup of certain aux chunks after optimization has been completed
pub fn postprocess_chunks(aux_chunks: &mut Vec<Chunk>, ihdr: &IhdrData, orig_ihdr: &IhdrData) {
// 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
if orig_ihdr.bit_depth != ihdr.bit_depth || orig_ihdr.color_type != ihdr.color_type {
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
});
}
// Remove any sRGB or iCCP chunks if the image was converted to or from grayscale
if orig_ihdr.color_type.is_gray() != ihdr.color_type.is_gray() {
aux_chunks.retain(|c| {
let invalid = &c.name == b"sRGB" || &c.name == b"iCCP";
if invalid {
trace!(
"Removing {} chunk as it no longer matches the color type",
std::str::from_utf8(&c.name).unwrap()
);
}
!invalid
});
}
// Remove iDOT which will necessarily be invalid after successful optimization
aux_chunks.retain(|c| {
let invalid = &c.name == b"iDOT";
if invalid {
trace!(
"Removing {} chunk as it no longer matches the IDAT",
std::str::from_utf8(&c.name).unwrap()
);
}
!invalid
});
}

View file

@ -1,6 +1,39 @@
use std::{fmt, fmt::Display};
use bitvec::prelude::*;
use crate::{headers::IhdrData, png::PngImage};
use crate::{headers::IhdrData, png::PngImage, PngError};
#[repr(u8)]
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Interlacing {
None,
Adam7,
}
impl TryFrom<u8> for Interlacing {
type Error = PngError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::None),
1 => Ok(Self::Adam7),
_ => Err(PngError::new("Unexpected interlacing in header")),
}
}
}
impl Display for Interlacing {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(
match self {
Self::None => "non-interlaced",
Self::Adam7 => "interlaced",
},
f,
)
}
}
#[must_use]
pub fn interlace_image(png: &PngImage) -> PngImage {
@ -53,7 +86,7 @@ pub fn interlace_image(png: &PngImage) -> PngImage {
data: output,
ihdr: IhdrData {
color_type: png.ihdr.color_type.clone(),
interlaced: true,
interlaced: Interlacing::Adam7,
..png.ihdr
},
}
@ -67,7 +100,7 @@ pub fn deinterlace_image(png: &PngImage) -> PngImage {
},
ihdr: IhdrData {
color_type: png.ihdr.color_type.clone(),
interlaced: false,
interlaced: Interlacing::None,
..png.ihdr
},
}
@ -85,8 +118,10 @@ fn deinterlace_bits(png: &PngImage) -> Vec<u8> {
let mut current_y: usize = pass_constants.y_shift as usize;
for line in png.scan_lines(false) {
let bit_vec = line.data.view_bits::<Msb0>();
let bits_in_line = (png.ihdr.width - u32::from(pass_constants.x_shift))
.div_ceil(u32::from(pass_constants.x_step)) as usize
let bits_in_line = ((png.ihdr.width - u32::from(pass_constants.x_shift)
+ u32::from(pass_constants.x_step)
- 1)
/ u32::from(pass_constants.x_step)) as usize
* bits_per_pixel;
for (i, bit) in bit_vec.iter().by_vals().enumerate() {
// Avoid moving padded 0's into new image
@ -149,7 +184,7 @@ fn deinterlace_bytes(png: &PngImage) -> Vec<u8> {
lines.concat()
}
const 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;
}

View file

@ -1,4 +1,23 @@
#![cfg_attr(not(feature = "zopfli"), allow(unreachable_patterns))]
#![warn(trivial_casts, trivial_numeric_casts, unused_import_braces)]
#![deny(missing_debug_implementations, missing_copy_implementations)]
#![warn(clippy::expl_impl_clone_on_copy)]
#![warn(clippy::float_cmp_const)]
#![warn(clippy::linkedlist)]
#![warn(clippy::map_flatten)]
#![warn(clippy::match_same_arms)]
#![warn(clippy::mem_forget)]
#![warn(clippy::mut_mut)]
#![warn(clippy::mutex_integer)]
#![warn(clippy::needless_continue)]
#![warn(clippy::path_buf_push_overwrite)]
#![warn(clippy::range_plus_one)]
#![allow(clippy::cognitive_complexity)]
#![allow(clippy::upper_case_acronyms)]
#![cfg_attr(
not(feature = "zopfli"),
allow(irrefutable_let_patterns),
allow(unreachable_patterns)
)]
#[cfg(feature = "parallel")]
extern crate rayon;
@ -7,39 +26,39 @@ extern crate rayon;
mod rayon;
use std::{
fs::File,
io::{BufWriter, Read, Write, stdin, stdout},
path::PathBuf,
borrow::Cow,
fs::{File, Metadata},
io::{stdin, stdout, BufWriter, Read, Write},
path::Path,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
Arc,
},
time::{Duration, Instant},
};
pub use indexmap::{IndexSet, indexset};
pub use indexmap::{indexset, IndexSet};
use log::{debug, info, trace, warn};
use rayon::prelude::*;
pub use rgb::{RGB16, RGBA8};
#[cfg(feature = "zopfli")]
pub use crate::deflate::ZopfliOptions;
pub use crate::{
colors::{BitDepth, ColorType},
deflate::Deflater,
error::PngError,
filters::{FilterStrategy, RowFilter},
headers::StripChunks,
options::{InFile, Options, OutFile},
};
use crate::{
evaluate::{Candidate, Evaluator},
atomicmin::AtomicMin,
evaluate::Evaluator,
headers::*,
png::{PngData, PngImage},
reduction::*,
};
pub use crate::{
colors::{BitDepth, ColorType},
deflate::Deflaters,
error::PngError,
filters::RowFilter,
headers::StripChunks,
interlace::Interlacing,
options::{InFile, Options, OutFile},
};
mod apng;
mod atomicmin;
mod colors;
mod deflate;
@ -60,11 +79,10 @@ mod sanity_checks;
pub mod internal_tests {
#[cfg(feature = "sanity-checks")]
pub use crate::sanity_checks::*;
pub use crate::{deflate::*, png::*, reduction::*};
pub use crate::{atomicmin::*, deflate::*, png::*, reduction::*};
}
pub type PngResult<T> = Result<T, PngError>;
pub type OptimizationResult = PngResult<(usize, usize)>;
#[derive(Debug)]
/// A raw image definition which can be used to create an optimized png
@ -87,7 +105,7 @@ impl RawImage {
color_type: ColorType,
bit_depth: BitDepth,
data: Vec<u8>,
) -> PngResult<Self> {
) -> Result<Self, PngError> {
// Validate bit depth
let valid_depth = match color_type {
ColorType::Grayscale { .. } => true,
@ -100,7 +118,7 @@ impl RawImage {
// Validate data length
let bpp = bit_depth as usize * color_type.channels_per_pixel() as usize;
let row_bytes = (bpp * width as usize).div_ceil(8);
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));
@ -113,7 +131,7 @@ impl RawImage {
height,
color_type,
bit_depth,
interlaced: false,
interlaced: Interlacing::None,
},
data,
}),
@ -129,56 +147,71 @@ impl RawImage {
/// Add an ICC profile for the image
pub fn add_icc_profile(&mut self, data: &[u8]) {
// Compress with fastest compression level - will be recompressed during optimization
let deflater = Deflater::Libdeflater { compression: 1 };
if let Ok(iccp) = make_iccp(data, deflater, None) {
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<Vec<u8>> {
let mut opts = opts.to_owned();
let mut aux_chunks: Vec<_> = self
let deadline = Arc::new(Deadline::new(opts.timeout));
let mut png = optimize_raw(self.png.clone(), opts, deadline.clone(), 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();
preprocess_chunks(&mut aux_chunks, &mut opts);
let deadline = Arc::new(Deadline::new(opts.timeout));
let Some(result) = optimize_raw(self.png.clone(), &opts, deadline, None) else {
return Err(PngError::new("Failed to optimize input data"));
};
let mut png = PngData {
raw: result.image,
idat_data: result.idat_data.unwrap(),
aux_chunks,
frames: Vec::new(),
};
postprocess_chunks(&mut png.aux_chunks, &png.raw.ihdr, &self.png.ihdr);
postprocess_chunks(&mut png, opts, deadline, &self.png.ihdr);
Ok(png.output())
}
}
/// Perform optimization on the input file using the options provided
///
/// Returns the original and optimized file sizes
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> OptimizationResult {
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<()> {
// Read in the file and try to decode as PNG.
info!("Processing: {input}");
info!("Processing: {}", input);
let deadline = Arc::new(Deadline::new(opts.timeout));
// grab metadata before even opening input file to preserve atime
let opt_metadata_preserved;
let in_data = match *input {
InFile::Path(ref input_path) => PngData::read_file(input_path)?,
InFile::Path(ref input_path) => {
if matches!(
output,
OutFile::Path {
preserve_attrs: true,
..
}
) {
opt_metadata_preserved = input_path
.metadata()
.map_err(|err| {
// Fail if metadata cannot be preserved
PngError::new(&format!(
"Unable to read metadata from input file {:?}: {}",
input_path, err
))
})
.map(Some)?;
trace!("preserving metadata: {:?}", opt_metadata_preserved);
} else {
opt_metadata_preserved = None;
}
PngData::read_file(input_path)?
}
InFile::StdIn => {
opt_metadata_preserved = None;
let mut data = Vec::new();
stdin()
.read_to_end(&mut data)
.map_err(|e| PngError::ReadFailed("stdin".into(), e))?;
.map_err(|e| PngError::new(&format!("Error reading stdin: {}", e)))?;
data
}
};
@ -190,14 +223,14 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> Optimizatio
let in_length = in_data.len();
if is_fully_optimized(in_length, optimized_output.len(), opts) {
if is_fully_optimized(in_data.len(), optimized_output.len(), opts) {
match (output, input) {
// If output path is None, it also means same as the input path
(OutFile::Path { path, .. }, InFile::Path(input_path))
if path.as_ref().is_none_or(|p| p == input_path) =>
// if p is None, it also means same as the input path
(OutFile::Path { path, .. }, InFile::Path(ref input_path))
if path.as_ref().map_or(true, |p| p == input_path) =>
{
info!("Could not optimize further, no change written: {input}");
return Ok((in_length, in_length));
info!("{}: Could not optimize further, no change written", input);
return Ok(());
}
_ => {
optimized_output = in_data;
@ -221,65 +254,51 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> Optimizatio
match (output, input) {
(OutFile::None, _) => {
info!("{savings}: Dry run, no output");
info!("{}: Running in pretend mode, no output", savings);
}
(&OutFile::StdOut, _) | (&OutFile::Path { path: None, .. }, &InFile::StdIn) => {
let mut buffer = BufWriter::new(stdout());
buffer
.write_all(&optimized_output)
.map_err(|e| PngError::WriteFailed("stdout".into(), e))?;
info!("{savings}: stdout");
.map_err(|e| PngError::new(&format!("Unable to write to stdout: {}", e)))?;
}
(
OutFile::Path {
path,
preserve_attrs,
},
_,
) => {
let input_metadata = if *preserve_attrs {
input.path().and_then(|in_path| {
let meta = in_path.metadata();
if let Err(e) = &meta {
warn!("Unable to read metadata from {in_path:?}: {e}");
}
meta.ok()
})
} else {
None
};
(OutFile::Path { path, .. }, _) => {
let output_path = path
.as_ref()
.map_or_else(|| input.path().unwrap(), PathBuf::as_path);
let out_file = File::create(output_path)
.map_err(|err| PngError::WriteFailed(output_path.display().to_string(), err))?;
.map(|p| p.as_path())
.unwrap_or_else(|| input.path().unwrap());
let out_file = File::create(output_path).map_err(|err| {
PngError::new(&format!(
"Unable to write to file {}: {}",
output_path.display(),
err
))
})?;
if let Some(metadata_input) = &opt_metadata_preserved {
copy_permissions(metadata_input, &out_file)?;
}
let mut buffer = BufWriter::new(&out_file);
let mut buffer = BufWriter::new(out_file);
buffer
.write_all(&optimized_output)
// flush BufWriter so IO errors don't get swallowed silently on close() by drop!
.and_then(|()| buffer.flush())
.map_err(|e| PngError::WriteFailed(output_path.display().to_string(), e))?;
.map_err(|e| {
PngError::new(&format!(
"Unable to write to {}: {}",
output_path.display(),
e
))
})?;
// force drop and thereby closing of file handle before modifying any timestamp
std::mem::drop(buffer);
if let Some(metadata_input) = &input_metadata {
let set_time = metadata_input
.modified()
.and_then(|m| out_file.set_modified(m));
if let Err(e) = set_time {
warn!("Unable to set modification time on {output_path:?}: {e}");
}
let set_perm = out_file.set_permissions(metadata_input.permissions());
if let Err(e) = set_perm {
warn!("Unable to set permissions on {output_path:?}: {e}");
}
if let Some(metadata_input) = &opt_metadata_preserved {
copy_times(metadata_input, output_path)?;
}
info!("{}: {}", savings, output_path.display());
}
}
Ok((in_length, optimized_output.len()))
Ok(())
}
/// Perform optimization on the input file using the options provided, where the file is already
@ -304,6 +323,8 @@ pub fn optimize_from_memory(data: &[u8], opts: &Options) -> PngResult<Vec<u8>> {
}
}
type TrialResult = (RowFilter, Vec<u8>);
/// Perform optimization on the input PNG object using the options provided
fn optimize_png(
png: &mut PngData,
@ -320,24 +341,34 @@ fn optimize_png(
raw.ihdr.width, raw.ihdr.height
);
report_format(" ", &raw);
debug!(" IDAT size = {idat_original_size} bytes");
debug!(" File size = {file_original_size} bytes");
let mut opts = opts.to_owned();
preprocess_chunks(&mut png.aux_chunks, &mut opts);
debug!(" IDAT size = {} bytes", idat_original_size);
debug!(" File size = {} bytes", file_original_size);
// Check for APNG by presence of acTL chunk
let opts = if png.aux_chunks.iter().any(|c| &c.name == b"acTL") {
warn!("APNG detected, disabling all reductions");
let mut opts = opts.to_owned();
opts.interlace = None;
opts.bit_depth_reduction = false;
opts.color_type_reduction = false;
opts.palette_reduction = false;
opts.grayscale_reduction = false;
Cow::Owned(opts)
} else {
Cow::Borrowed(opts)
};
let max_size = if opts.force {
None
} else {
Some(png.raw.estimated_output_size(&png.idat_data))
Some(png.estimated_output_size())
};
if let Some(result) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_size) {
png.raw = result.image;
png.idat_data = result.idat_data.unwrap();
recompress_frames(png, &opts, deadline, result.filter)?;
postprocess_chunks(&mut png.aux_chunks, &png.raw.ihdr, &raw.ihdr);
if let Some(new_png) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_size) {
png.raw = new_png.raw;
png.idat_data = new_png.idat_data;
}
postprocess_chunks(png, &opts, deadline, &raw.ihdr);
let output = png.output();
if idat_original_size >= png.idat_data.len() {
@ -369,12 +400,6 @@ fn optimize_png(
);
}
if opts.interlace == Some(true) && !png.raw.ihdr.interlaced {
warn!(
"Interlacing was not enabled as it would result in a larger file. To override this, use `--force`."
);
}
#[cfg(feature = "sanity-checks")]
assert!(sanity_checks::validate_output(&output, original_data));
@ -387,155 +412,186 @@ fn optimize_raw(
opts: &Options,
deadline: Arc<Deadline>,
max_size: Option<usize>,
) -> Option<Candidate> {
// Libdeflate has four algorithms: 0 = 'uncompressed', 1-4 = 'greedy', 5-7 = 'lazy', 8-9 = 'lazy2', 10-12 = 'near-optimal'
) -> Option<PngData> {
// Libdeflate has four algorithms: 1-4 = 'greedy', 5-7 = 'lazy', 8-9 = 'lazy2', 10-12 = 'near-optimal'
// 5 is the minimumm required for a decent evaluation result
// 7 is not noticeably slower than 5 and improves evaluation of filters in 'fast' mode (o2 and lower)
// 8 is a little slower but not noticeably when used only for reductions (o3 and higher)
// 9 is not appreciably better than 8
// 10 and higher are quite slow - good for filters but only good for reductions if matching the main zc level
let compression = match opts.deflater {
Deflater::Libdeflater { compression } => {
let eval_compression = match opts.deflate {
Deflaters::Libdeflater { compression } => {
if opts.fast_evaluation { 7 } else { 8 }.min(compression)
}
_ => 8,
};
let eval_deflater = Deflater::Libdeflater { compression };
// If only one filter is selected, use this for evaluations
let eval_filters = if opts.filters.len() == 1 {
opts.filters.clone()
let eval_filters = if opts.filter.len() == 1 {
opts.filter.clone()
} else {
// None and Bigrams work well together, especially for alpha reductions
indexset! {FilterStrategy::NONE, FilterStrategy::Bigrams}
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_deflater,
eval_compression,
false,
opts.deflater == eval_deflater,
);
let mut new_image = perform_reductions(image.clone(), opts, &deadline, &eval);
let eval_result = eval.get_best_candidate();
let mut png = perform_reductions(image.clone(), opts, &deadline, &eval);
let mut eval_result = eval.get_best_candidate();
if let Some(ref result) = eval_result {
new_image = result.image.clone();
png = result.image.clone();
}
let reduction_occurred = new_image.ihdr.color_type != image.ihdr.color_type
|| new_image.ihdr.bit_depth != image.ihdr.bit_depth
|| new_image.ihdr.interlaced != image.ihdr.interlaced;
let reduction_occurred = png.ihdr.color_type != image.ihdr.color_type
|| png.ihdr.bit_depth != image.ihdr.bit_depth
|| png.ihdr.interlaced != image.ihdr.interlaced;
if reduction_occurred {
report_format("Transformed image to ", &new_image);
report_format("Transformed image to ", &png);
}
let (result, deflater) = if opts.idat_recoding || reduction_occurred {
let result = perform_trials(
new_image,
opts,
deadline,
max_size,
eval_result,
eval_filters,
eval_deflater,
);
(result?, opts.deflater)
} else {
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<TrialResult> = if fast_eval {
// Perform a fast evaluation of selected filters followed by a single main compression trial
if eval_result.is_some() {
// Some filters have already been evaluated, we don't need to try them again
filters = filters.difference(&eval_filters).cloned().collect();
}
if !filters.is_empty() {
trace!("Evaluating: {} filters", filters.len());
let eval = Evaluator::new(deadline, filters, eval_compression, opts.optimize_alpha);
if let Some(ref result) = eval_result {
eval.set_best_size(result.idat_data.len());
}
eval.try_image(png.clone());
if let Some(result) = eval.get_best_candidate() {
eval_result = Some(result);
}
}
// We should have a result here - fail if not (e.g. deadline passed)
let result = eval_result?;
match opts.deflate {
Deflaters::Libdeflater { compression } if compression <= eval_compression => {
// No further compression required
Some((result.filter, result.idat_data))
}
_ => {
debug!("Trying: {}", result.filter);
let best_size = AtomicMin::new(max_size);
perform_trial(&result.filtered, opts, result.filter, &best_size)
}
}
} else {
// Perform full compression trials of selected filters and determine the best
if filters.is_empty() {
// Pick a filter automatically
if png.ihdr.bit_depth as u8 >= 8 {
// Bigrams is the best all-rounder when there's at least one byte per pixel
filters.insert(RowFilter::Bigrams);
} else {
// Otherwise delta filters generally don't work well, so just stick with None
filters.insert(RowFilter::None);
}
}
debug!("Trying: {} filters", filters.len());
let best_size = AtomicMin::new(max_size);
let results_iter = filters.into_par_iter().with_max_len(1);
let best = results_iter.filter_map(|filter| {
if deadline.passed() {
return None;
}
let filtered = &png.filter_image(filter, opts.optimize_alpha);
perform_trial(filtered, opts, filter, &best_size)
});
best.reduce_with(|i, j| {
if i.1.len() < j.1.len() || (i.1.len() == j.1.len() && i.0 < j.0) {
i
} else {
j
}
})
};
if let Some((filter, idat_data)) = best {
let image = PngData {
raw: png,
idat_data,
aux_chunks: Vec::new(),
};
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
debug!("Found better combination:");
debug!(
" zc = {} f = {:8} {} bytes",
opts.deflate,
filter,
image.idat_data.len()
);
return Some(image);
}
}
} else if let Some(result) = eval_result {
// If idat_recoding is off and reductions were attempted but ended up choosing the baseline,
// we should still check if the evaluator compressed the baseline smaller than the original.
(eval_result?, eval_deflater)
};
if result.idat_data.is_some()
&& max_size.is_none_or(|max_size| result.estimated_output_size < max_size)
{
debug!("Found better result:");
debug!(" {}, f = {}", deflater, result.filter);
return Some(result);
let image = PngData {
raw: result.image,
idat_data: result.idat_data,
aux_chunks: Vec::new(),
};
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
debug!("Found better combination:");
debug!(
" zc = {} f = {:8} {} bytes",
eval_compression,
result.filter,
image.idat_data.len()
);
return Some(image);
}
}
None
}
/// Perform compression trials
fn perform_trials(
image: Arc<PngImage>,
/// Execute a compression trial
fn perform_trial(
filtered: &[u8],
opts: &Options,
deadline: Arc<Deadline>,
max_size: Option<usize>,
mut eval_result: Option<Candidate>,
eval_filters: IndexSet<FilterStrategy>,
eval_deflater: Deflater,
) -> Option<Candidate> {
let mut filters = opts.filters.clone();
let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_result.is_some());
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_deflater,
opts.optimize_alpha,
opts.deflater == eval_deflater,
filter: RowFilter,
best_size: &AtomicMin,
) -> Option<TrialResult> {
match opts.deflate.deflate(filtered, best_size) {
Ok(new_idat) => {
let bytes = new_idat.len();
best_size.set_min(bytes);
trace!(
" zc = {} f = {:8} {} bytes",
opts.deflate,
filter,
bytes
);
if let Some(result) = &eval_result {
eval.set_best_size(result.estimated_output_size);
}
eval.try_image(image.clone());
if let Some(result) = eval.get_best_candidate() {
eval_result = Some(result);
}
Some((filter, new_idat))
}
// We should have a result here - fail if not (e.g. deadline passed)
let mut result = eval_result?;
if result.idat_data.is_none() {
// Compress with the main deflater
debug!("Trying filter {} with {}", result.filter, opts.deflater);
let (data, _) = image.filter_image(result.filter_used.clone(), opts.optimize_alpha);
match opts.deflater.deflate(&data, max_size) {
Ok(idat_data) => {
result.estimated_output_size = result.image.estimated_output_size(&idat_data);
result.idat_data = Some(idat_data);
trace!("{} bytes", result.estimated_output_size);
}
Err(PngError::DeflatedDataTooLong(bytes)) => {
trace!(">{bytes} bytes");
}
Err(_) => (),
}
Err(PngError::DeflatedDataTooLong(bytes)) => {
trace!(
" zc = {} f = {:8} >{} bytes",
opts.deflate,
filter,
bytes,
);
None
}
return Some(result);
Err(_) => None,
}
// Perform full compression trials of selected filters and determine the best
if filters.is_empty() {
// Pick a filter automatically
if image.ihdr.bit_depth as u8 >= 8 {
// Bigrams is the best all-rounder when there's at least one byte per pixel
filters.insert(FilterStrategy::Bigrams);
} else {
// Otherwise delta filters generally don't work well, so just stick with None
filters.insert(FilterStrategy::NONE);
}
}
debug!("Trying {} filters with {}", filters.len(), opts.deflater);
let eval = Evaluator::new(deadline, filters, opts.deflater, opts.optimize_alpha, true);
if let Some(max_size) = max_size {
eval.set_best_size(max_size);
}
eval.try_image(image);
eval.get_best_candidate()
}
#[derive(Debug)]
@ -553,7 +609,6 @@ pub struct Deadline {
}
impl Deadline {
#[must_use]
pub fn new(timeout: Option<Duration>) -> Self {
Self {
imp: timeout.map(|timeout| DeadlineImp {
@ -590,57 +645,141 @@ impl Deadline {
/// Display the format of the image data
fn report_format(prefix: &str, png: &PngImage) {
let interlaced = if png.ihdr.interlaced {
"interlaced"
} else {
"non-interlaced"
};
debug!(
"{}{}-bit {}, {}",
prefix, png.ihdr.bit_depth, png.ihdr.color_type, interlaced
prefix, png.ihdr.bit_depth, png.ihdr.color_type, png.ihdr.interlaced
);
}
/// Recompress the additional frames of an APNG
fn recompress_frames(
/// Perform cleanup of certain chunks from the `PngData` object, after optimization has been completed
fn postprocess_chunks(
png: &mut PngData,
opts: &Options,
deadline: Arc<Deadline>,
filter: FilterStrategy,
) -> PngResult<()> {
if !opts.idat_recoding || png.frames.is_empty() {
return Ok(());
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
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
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 opts.idat_recoding {
// Try recompressing the profile
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;
}
}
}
}
}
// Ensure we don't try to recompress frames with a predefined filter
debug_assert!(!matches!(filter, FilterStrategy::Predefined { .. }));
png.frames
.par_iter_mut()
.with_max_len(1)
.enumerate()
.try_for_each(|(i, frame)| {
if deadline.passed() {
return Ok(());
}
let mut ihdr = png.raw.ihdr.clone();
ihdr.width = frame.width;
ihdr.height = frame.height;
let image = PngImage::new(ihdr, &frame.data)?;
let (filtered, _) = image.filter_image(filter.clone(), opts.optimize_alpha);
let max_size = Some(frame.data.len() - 1);
if let Ok(data) = opts.deflater.deflate(&filtered, max_size) {
debug!(
"Recompressed fdAT #{:<2}: {} ({} bytes decrease)",
i,
data.len(),
frame.data.len() - data.len()
// 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()
);
frame.data = data;
}
Ok(())
})
!invalid
});
}
// Find fdAT chunks and attempt to recompress them
// Note if there are multiple fdATs per frame then decompression will fail and nothing will change
let mut fdat: Vec<_> = png
.aux_chunks
.iter_mut()
.filter(|c| &c.name == b"fdAT")
.collect();
if opts.idat_recoding && !fdat.is_empty() {
let buffer_size = orig_ihdr.raw_data_size();
fdat.par_iter_mut()
.with_max_len(1)
.enumerate()
.for_each(|(i, c)| {
if deadline.passed() || c.data.len() <= 4 {
return;
}
if let Ok(mut data) = deflate::inflate(&c.data[4..], buffer_size).and_then(|data| {
let max_size = AtomicMin::new(Some(c.data.len() - 5));
opts.deflate.deflate(&data, &max_size)
}) {
debug!(
"Recompressed fdAT #{:<2}: {} ({} bytes decrease)",
i,
c.data.len(),
c.data.len() - 4 - data.len()
);
c.data.truncate(4);
c.data.append(&mut data);
}
})
}
}
/// Check if an image was already optimized prior to oxipng's operations
const fn is_fully_optimized(original_size: usize, optimized_size: usize, opts: &Options) -> bool {
fn is_fully_optimized(original_size: usize, optimized_size: usize, opts: &Options) -> bool {
original_size <= optimized_size && !opts.force
}
fn copy_permissions(metadata_input: &Metadata, out_file: &File) -> PngResult<()> {
out_file
.set_permissions(metadata_input.permissions())
.map_err(|err_io| {
PngError::new(&format!(
"unable to set permissions for output file: {}",
err_io
))
})
}
#[cfg(not(feature = "filetime"))]
fn copy_times(_: &Metadata, _: &Path) -> PngResult<()> {
Ok(())
}
#[cfg(feature = "filetime")]
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);
trace!(
"attempting to set file times: atime: {:?}, mtime: {:?}",
atime,
mtime
);
filetime::set_file_times(out_path, atime, mtime).map_err(|err_io| {
PngError::new(&format!(
"unable to set file times on {:?}: {}",
out_path, err_io
))
})
}

View file

@ -1,172 +1,99 @@
#![warn(trivial_casts, trivial_numeric_casts, unused_import_braces)]
#![deny(missing_debug_implementations, missing_copy_implementations)]
#![warn(clippy::expl_impl_clone_on_copy)]
#![warn(clippy::float_cmp_const)]
#![warn(clippy::linkedlist)]
#![warn(clippy::map_flatten)]
#![warn(clippy::match_same_arms)]
#![warn(clippy::mem_forget)]
#![warn(clippy::mut_mut)]
#![warn(clippy::mutex_integer)]
#![warn(clippy::needless_continue)]
#![warn(clippy::path_buf_push_overwrite)]
#![warn(clippy::range_plus_one)]
#![allow(clippy::cognitive_complexity)]
#[cfg(not(feature = "parallel"))]
mod rayon;
#[cfg(feature = "zopfli")]
use std::num::NonZeroU64;
use std::{
ffi::{OsStr, OsString},
fs::DirBuilder,
io::{IsTerminal, Write, stdout},
path::PathBuf,
process::ExitCode,
sync::atomic::{AtomicUsize, Ordering::AcqRel},
time::Duration,
};
use std::num::NonZeroU8;
use std::{ffi::OsString, fs::DirBuilder, io::Write, path::PathBuf, process::exit, time::Duration};
use clap::ArgMatches;
mod cli;
use indexmap::IndexSet;
use log::{Level, LevelFilter, error, warn};
#[cfg(feature = "zopfli")]
use oxipng::ZopfliOptions;
use oxipng::{
Deflater, FilterStrategy, InFile, OptimizationResult, Options, OutFile, PngError, StripChunks,
};
use log::{error, warn, Level, LevelFilter};
use oxipng::{Deflaters, InFile, Options, OutFile, RowFilter, StripChunks};
use rayon::prelude::*;
use crate::cli::DISPLAY_CHUNKS;
fn main() -> ExitCode {
fn main() {
let matches = cli::build_command()
// Set the value parser for filters which isn't appropriate to do in the build_command function
.mut_arg("filters", |arg| {
arg.value_parser(|x: &str| {
parse_numeric_range_opts(x, 0, 9).map_err(|_| "Invalid option for filters")
parse_numeric_range_opts(x, 0, RowFilter::LAST)
.map_err(|_| "Invalid option for filters")
})
})
.after_help("Run `oxipng --help` to see full details of all options")
.after_long_help("")
.get_matches_from(std::env::args());
let (mut out_file, out_dir, opts) = match parse_opts_into_struct(&matches) {
if matches.get_flag("backup") {
eprintln!("The --backup flag is no longer supported. Please use --out or --dir to preserve your existing files.");
exit(1)
}
let (out_file, out_dir, opts) = match parse_opts_into_struct(&matches) {
Ok(x) => x,
Err(x) => {
error!("{x}");
return ExitCode::FAILURE;
error!("{}", x);
exit(1)
}
};
// Determine input and output
let file_args = matches.get_many::<PathBuf>("files").unwrap().cloned();
#[cfg(windows)]
let inputs: Vec<_> = file_args.flat_map(apply_glob_pattern).collect();
#[cfg(not(windows))]
let inputs: Vec<_> = file_args.collect();
let using_stdin = inputs.len() == 1 && inputs[0].to_str() == Some("-");
if using_stdin && out_dir.is_some() {
error!("Cannot use --dir when reading from stdin.");
return ExitCode::FAILURE;
}
if using_stdin && matches!(out_file, OutFile::Path { path: None, .. }) {
out_file = OutFile::StdOut;
}
let using_stdout = matches!(out_file, OutFile::StdOut);
let json = matches.get_flag("json");
if using_stdout && json {
error!("Cannot use --json when writing to stdout.");
return ExitCode::FAILURE;
}
let files = collect_files(
#[cfg(windows)]
matches
.get_many::<PathBuf>("files")
.unwrap()
.cloned()
.flat_map(apply_glob_pattern)
.collect(),
#[cfg(not(windows))]
matches
.get_many::<PathBuf>("files")
.unwrap()
.cloned()
.collect(),
&out_dir,
&out_file,
matches.get_flag("recursive"),
true,
);
let files = if using_stdin {
vec![(InFile::StdIn, out_file)]
} else {
collect_files(
inputs,
&out_dir,
&out_file,
matches.get_flag("recursive"),
true,
)
};
let is_verbose = matches.get_count("verbose") > 0;
let print_summary = !matches.get_flag("quiet") && !using_stdout;
let print_progress = print_summary && !is_verbose && stdout().is_terminal();
let total_files = files.len();
let num_processed = AtomicUsize::new(0);
if print_progress {
print!("Files processed: 0/{}...", total_files);
stdout().flush().ok();
}
let process = |(input, output): &(InFile, OutFile)| {
let result = process_file(input, output, &opts);
if print_progress && matches!(result, OptimizationResult::Ok(_)) {
let value = num_processed.fetch_add(1, AcqRel) + 1;
print!("\rFiles processed: {}/{}...", value, total_files);
stdout().flush().ok();
}
result
};
let results: Vec<OptimizationResult> = if matches.get_flag("parallel-files") {
files.par_iter().map(process).collect()
} else {
files.iter().map(process).collect()
};
// Collect stats
let mut num_succeeded = 0;
let mut num_not_optimized = 0;
let mut num_failed = 0;
let mut total_in: i64 = 0;
let mut total_out: i64 = 0;
for result in &results {
match result {
Ok((insize, outsize)) => {
num_succeeded += 1;
total_in += *insize as i64;
total_out += *outsize as i64;
if !opts.force && insize == outsize {
num_not_optimized += 1;
}
let success = files.into_par_iter().filter(|(input, output)| {
match oxipng::optimize(input, output, &opts) {
// For optimizing single files, this will return the correct exit code always.
// For recursive optimization, the correct choice is a bit subjective.
// We're choosing to return a 0 exit code if ANY file in the set
// runs correctly.
// The reason for this is that recursion may pick up files that are not
// PNG files, and return an error for them.
// We don't really want to return an error code for those files.
Ok(_) => true,
Err(e) => {
error!("{}: {}", input, e);
false
}
Err(PngError::C2PAMetadataPreventsChanges | PngError::InflatedDataTooLong(_)) => {}
Err(_) => num_failed += 1,
}
}
});
// Print results
if json {
json_output(&files, &results);
} else if print_summary {
let in_bytes = format_bytes(total_in, true);
let out_bytes = format_bytes(total_out, true);
let saved = total_in - total_out;
let saved_bytes = format_bytes(saved, false);
let percent = if total_in > 0 {
saved as f64 / total_in as f64 * 100_f64
} else {
0_f64
};
if is_verbose {
println!("--------------------");
}
println!("\rFiles processed: {num_succeeded}/{total_files} ");
println!("Input size: {}", in_bytes);
println!("Output size: {}", out_bytes);
println!("Total saved: {} ({:.2}%)", saved_bytes, percent);
if num_not_optimized == 1 {
println!("({num_not_optimized} file could not be optimized further)");
} else if num_not_optimized > 0 {
println!("({num_not_optimized} files could not be optimized further)");
}
if matches.get_flag("dry-run") {
println!("Dry run, no changes saved");
}
}
// For optimizing single files, this will return the correct exit code always.
// For recursive optimization, the correct choice is a bit subjective.
// We're choosing to return a 0 exit code if ANY file in the set
// runs correctly.
// The reason for this is that recursion may pick up files that are not
// PNG files, and return an error for them.
// We don't really want to return an error code for those files.
if num_succeeded > 0 {
ExitCode::SUCCESS
} else if num_failed > 0 {
ExitCode::FAILURE
} else {
ExitCode::from(3)
if success.count() == 0 {
exit(1);
}
}
@ -178,8 +105,10 @@ fn collect_files(
top_level: bool, //explicitly specify files
) -> Vec<(InFile, OutFile)> {
let mut in_out_pairs = Vec::new();
let allow_stdin = top_level && files.len() == 1;
for input in files {
if input.is_dir() {
let using_stdin = allow_stdin && input.to_str().map_or(false, |p| p == "-");
if !using_stdin && input.is_dir() {
if recursive {
match input.read_dir() {
Ok(dir) => {
@ -195,16 +124,7 @@ fn collect_files(
warn!("{} is a directory, skipping", input.display());
}
continue;
}
// Skip non png files if not given on top level
if !top_level && {
let extension = input.extension().map(OsStr::to_ascii_lowercase);
extension != Some(OsString::from("png")) && extension != Some(OsString::from("apng"))
} {
continue;
}
};
let out_file =
if let (Some(out_dir), &OutFile::Path { preserve_attrs, .. }) = (out_dir, out_file) {
let path = Some(out_dir.join(input.file_name().unwrap()));
@ -215,7 +135,19 @@ fn collect_files(
} else {
(*out_file).clone()
};
let in_file = InFile::Path(input);
let in_file = if using_stdin {
InFile::StdIn
} else {
// Skip non png files if not given on top level
if !top_level && {
let extension = input.extension().map(|f| f.to_ascii_lowercase());
extension != Some(OsString::from("png"))
&& extension != Some(OsString::from("apng"))
} {
continue;
}
InFile::Path(input)
};
in_out_pairs.push((in_file, out_file));
}
in_out_pairs
@ -225,8 +157,7 @@ fn collect_files(
fn apply_glob_pattern(path: PathBuf) -> Vec<PathBuf> {
let matches = path
.to_str()
// Use MatchOptions::default() to disable case-sensitivity
.and_then(|pattern| glob::glob_with(pattern, glob::MatchOptions::default()).ok())
.and_then(|pattern| glob::glob(pattern).ok())
.map(|paths| paths.flatten().collect::<Vec<_>>());
match matches {
@ -240,9 +171,8 @@ fn parse_opts_into_struct(
) -> Result<(OutFile, Option<PathBuf>, Options), String> {
let log_level = match matches.get_count("verbose") {
_ if matches.get_flag("quiet") => LevelFilter::Off,
0 => LevelFilter::Warn,
1 => LevelFilter::Info,
2 => LevelFilter::Debug,
0 => LevelFilter::Info,
1 => LevelFilter::Debug,
_ => LevelFilter::Trace,
};
env_logger::builder()
@ -251,8 +181,7 @@ fn parse_opts_into_struct(
match record.level() {
Level::Error | Level::Warn => {
let style = buf.default_level_style(record.level());
// Prepend carriage return to clear progress line
writeln!(buf, "\r{style}{}{style:#}", record.args())
writeln!(buf, "{style}{}{style:#}", record.args())
}
// Leave info, debug and trace unstyled
_ => writeln!(buf, "{}", record.args()),
@ -266,39 +195,11 @@ fn parse_opts_into_struct(
Some(level) => Options::from_preset(level.parse::<u8>().unwrap()),
};
// Get custom brute settings and rebuild the filter set to apply them
let mut brute_lines = matches.get_one::<usize>("brute-lines").copied();
let mut brute_level = matches.get_one::<i64>("brute-level").map(|x| *x as u8);
let mut new_filters = IndexSet::new();
for mut f in opts.filters.drain(..) {
if let FilterStrategy::Brute { num_lines, level } = &mut f {
*num_lines = brute_lines.unwrap_or(*num_lines);
*level = brute_level.unwrap_or(*level);
// If custom settings were not given, we still need to retain the default values
// from the preset so we can re-apply them if the filters are overridden below
brute_lines = Some(*num_lines);
brute_level = Some(*level);
}
new_filters.insert(f);
}
opts.filters = new_filters;
if let Some(x) = matches.get_one::<IndexSet<u8>>("filters") {
opts.filters = x
.iter()
.map(|&f| match f {
0..=4 => FilterStrategy::Basic(f.try_into().unwrap()),
5 => FilterStrategy::MinSum,
6 => FilterStrategy::Entropy,
7 => FilterStrategy::Bigrams,
8 => FilterStrategy::BigEnt,
9 => FilterStrategy::Brute {
num_lines: brute_lines.unwrap_or(3),
level: brute_level.unwrap_or(1),
},
_ => unreachable!(),
})
.collect();
opts.filter.clear();
for &f in x {
opts.filter.insert(f.try_into().unwrap());
}
}
if let Some(&num) = matches.get_one::<u64>("timeout") {
@ -308,9 +209,9 @@ fn parse_opts_into_struct(
let out_dir = if let Some(path) = matches.get_one::<PathBuf>("output_dir") {
if !path.exists() {
match DirBuilder::new().recursive(true).create(path) {
Ok(()) => (),
Err(x) => return Err(format!("Could not create output directory {x}")),
}
Ok(_) => (),
Err(x) => return Err(format!("Could not create output directory {}", x)),
};
} else if !path.is_dir() {
return Err(format!(
"{} is an existing file (not a directory), cannot create directory",
@ -322,7 +223,7 @@ fn parse_opts_into_struct(
None
};
let out_file = if matches.get_flag("dry-run") {
let out_file = if matches.get_flag("pretend") {
OutFile::None
} else if matches.get_flag("stdout") {
OutFile::StdOut
@ -346,8 +247,6 @@ fn parse_opts_into_struct(
opts.fix_errors = matches.get_flag("fix");
opts.max_decompressed_size = matches.get_one::<u64>("max-size").map(|&x| x as usize);
opts.bit_depth_reduction = !matches.get_flag("no-bit-reduction");
opts.color_type_reduction = !matches.get_flag("no-color-reduction");
@ -367,10 +266,10 @@ fn parse_opts_into_struct(
opts.idat_recoding = !matches.get_flag("no-recoding");
if let Some(x) = matches.get_one::<String>("interlace") {
opts.interlace = match x.as_str() {
"off" | "0" => Some(false),
"on" | "1" => Some(true),
_ => None, // keep
opts.interlace = if x == "keep" {
None
} else {
x.parse::<u8>().unwrap().try_into().ok()
};
}
@ -387,9 +286,9 @@ fn parse_opts_into_struct(
})
.collect::<Result<IndexSet<_>, _>>()?;
if keep_display {
names.extend(DISPLAY_CHUNKS.iter().copied());
names.extend(DISPLAY_CHUNKS.iter().cloned());
}
opts.strip = StripChunks::Keep(names);
opts.strip = StripChunks::Keep(names)
}
if let Some(strip) = matches.get_one::<String>("strip") {
@ -411,7 +310,7 @@ fn parse_opts_into_struct(
}
let name = parse_chunk_name(x)?;
if FORBIDDEN_CHUNKS.contains(&name) {
return Err(format!("{x} chunk is not allowed to be stripped"));
return Err(format!("{} chunk is not allowed to be stripped", x));
}
Ok(name)
})
@ -424,32 +323,15 @@ fn parse_opts_into_struct(
opts.strip = StripChunks::Safe;
}
#[cfg(feature = "zopfli")]
if matches.get_flag("zopfli") {
let iteration_count = *matches.get_one::<NonZeroU64>("iterations").unwrap();
let iterations_without_improvement = *matches
.get_one::<NonZeroU64>("iterations-without-improvement")
.unwrap_or(&NonZeroU64::MAX);
if iterations_without_improvement > iteration_count
&& iterations_without_improvement != NonZeroU64::MAX
{
warn!(
"--ziwi ({}) is higher than --zi ({}) and will never be reached.",
iterations_without_improvement, iteration_count
);
#[cfg(feature = "zopfli")]
if let Some(iterations) = NonZeroU8::new(15) {
opts.deflate = Deflaters::Zopfli { iterations };
}
} else if let Deflaters::Libdeflater { compression } = &mut opts.deflate {
if let Some(x) = matches.get_one::<i64>("compression") {
*compression = *x as u8;
}
opts.deflater = Deflater::Zopfli(ZopfliOptions {
iteration_count,
iterations_without_improvement,
..Default::default()
});
}
if let (Deflater::Libdeflater { compression }, Some(x)) =
(&mut opts.deflater, matches.get_one::<i64>("compression"))
{
*compression = *x as u8;
}
#[cfg(feature = "parallel")]
@ -467,7 +349,7 @@ fn parse_chunk_name(name: &str) -> Result<[u8; 4], String> {
name.trim()
.as_bytes()
.try_into()
.map_err(|_| format!("Invalid chunk name {name}"))
.map_err(|_| format!("Invalid chunk name {}", name))
}
fn parse_numeric_range_opts(
@ -522,133 +404,3 @@ fn parse_numeric_range_opts(
Err(ERROR_MESSAGE.to_owned())
}
fn process_file(input: &InFile, output: &OutFile, opts: &Options) -> OptimizationResult {
if let (Some(max_size), InFile::Path(path)) = (opts.max_decompressed_size, input) {
if path.metadata().is_ok_and(|m| m.len() > max_size as u64) {
warn!("{input}: Skipped: File exceeds the maximum size ({max_size} bytes)");
return Err(PngError::InflatedDataTooLong(max_size));
}
}
let result = oxipng::optimize(input, output, opts);
match &result {
Ok(_) => {}
Err(e @ PngError::C2PAMetadataPreventsChanges | e @ PngError::InflatedDataTooLong(_)) => {
warn!("{input}: Skipped: {e}");
}
Err(e) => {
error!("{input}: {e}");
}
}
result
}
/// Write optimization results as json.
/// ```
/// {
/// "results": [
/// {
/// "input": string,
/// "status": "success",
/// "output": string|null,
/// "insize": number,
/// "outsize": number
/// },
/// {
/// "input": string,
/// "status": "error",
/// "error": string
/// }
/// ]
/// }
/// ```
fn json_output(files: &[(InFile, OutFile)], results: &[OptimizationResult]) {
print!(r#"{{"results":["#);
let mut first = true;
results
.iter()
.zip(files)
.for_each(|(result, (input, output))| {
if !first {
print!(",");
}
print!(r#"{{"input":"{}","#, json_escape(&input.to_string()));
match result {
Ok((insize, outsize)) => {
let outpath = match output {
OutFile::None => "null".to_owned(),
OutFile::Path { path: None, .. } => {
format!(r#""{}""#, json_escape(&input.to_string()))
}
OutFile::Path { path: Some(p), .. } => {
format!(r#""{}""#, json_escape(&p.display().to_string()))
}
OutFile::StdOut => unreachable!(),
};
print!(
r#""status":"success","output":{},"insize":{},"outsize":{}}}"#,
outpath, insize, outsize
);
}
Err(e) => {
print!(
r#""status":"error","error":"{}"}}"#,
json_escape(&e.to_string())
);
}
}
first = false;
});
print!("]}}");
}
fn json_escape(string: &str) -> String {
string
.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\t", "\\t")
.replace("\r", "\\r")
.replace("\x08", "\\b")
.replace("\x0c", "\\f")
}
/// Format byte counts as IEC units to 3 significant figures.
fn format_bytes(count: i64, include_raw: bool) -> String {
const K: i64 = 1 << 10;
const M: i64 = 1 << 20;
const G: i64 = 1 << 30;
fn format_3sf(value: f64) -> String {
match value.abs() {
..9.995 => format!("{:.2}", value),
9.995..99.95 => format!("{:.1}", value),
_ => format!("{:.0}", value),
}
}
let formatted = match count.abs() {
..K => format!("{} bytes", count),
K..M => format!("{} KiB", format_3sf(count as f64 / K as f64)),
M..G => format!("{} MiB", format_3sf(count as f64 / M as f64)),
_ => format!("{} GiB", format_3sf(count as f64 / G as f64)),
};
if include_raw && count.abs() >= K {
format!("{} ({} bytes)", formatted, count)
} else {
formatted
}
}
#[cfg(test)]
mod tests {
use super::format_bytes;
#[test]
fn test_format_bytes() {
assert_eq!(format_bytes(1023, false), "1023 bytes");
assert_eq!(format_bytes(800_000, false), "781 KiB");
assert_eq!(format_bytes(12_500_000, false), "11.9 MiB");
assert_eq!(format_bytes(2_000_000_000, false), "1.86 GiB");
assert_eq!(format_bytes(-1024, false), "-1.00 KiB");
}
}

View file

@ -4,13 +4,11 @@ use std::{
time::Duration,
};
use indexmap::{IndexSet, indexset};
use indexmap::{indexset, IndexSet};
use log::warn;
use crate::{deflate::Deflater, filters::FilterStrategy, headers::StripChunks};
use crate::{deflate::Deflaters, filters::RowFilter, headers::StripChunks, interlace::Interlacing};
/// Write destination for [`optimize`][crate::optimize].
/// You can use [`optimize_from_memory`](crate::optimize_from_memory) to avoid external I/O.
#[derive(Clone, Debug)]
pub enum OutFile {
/// Don't actually write any output, just calculate the best results.
@ -31,18 +29,16 @@ impl OutFile {
/// Construct a new `OutFile` with the given path.
///
/// This is a convenience method for `OutFile::Path { path: Some(path), preserve_attrs: false }`.
#[must_use]
pub const fn from_path(path: PathBuf) -> Self {
Self::Path {
pub fn from_path(path: PathBuf) -> Self {
OutFile::Path {
path: Some(path),
preserve_attrs: false,
}
}
#[must_use]
pub fn path(&self) -> Option<&Path> {
match *self {
Self::Path {
OutFile::Path {
path: Some(ref p), ..
} => Some(p.as_path()),
_ => None,
@ -50,8 +46,7 @@ impl OutFile {
}
}
/// Where to read images from in [`optimize`][crate::optimize].
/// You can use [`optimize_from_memory`](crate::optimize_from_memory) to avoid external I/O.
/// Where to read images from
#[derive(Clone, Debug)]
pub enum InFile {
Path(PathBuf),
@ -59,11 +54,10 @@ pub enum InFile {
}
impl InFile {
#[must_use]
pub fn path(&self) -> Option<&Path> {
match *self {
Self::Path(ref p) => Some(p.as_path()),
Self::StdIn => None,
InFile::Path(ref p) => Some(p.as_path()),
InFile::StdIn => None,
}
}
}
@ -71,15 +65,15 @@ impl InFile {
impl fmt::Display for InFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::Path(ref p) => write!(f, "{}", p.display()),
Self::StdIn => f.write_str("stdin"),
InFile::Path(ref p) => write!(f, "{}", p.display()),
InFile::StdIn => f.write_str("stdin"),
}
}
}
impl<T: Into<PathBuf>> From<T> for InFile {
fn from(s: T) -> Self {
Self::Path(s.into())
InFile::Path(s.into())
}
}
@ -94,20 +88,19 @@ pub struct Options {
///
/// Default: `false`
pub force: bool,
/// Which `FilterStrategy` to try on the file
/// Which RowFilters to try on the file
///
/// Default: `None,Sub,Entropy,Bigrams`
pub filters: IndexSet<FilterStrategy>,
/// Whether to change the interlacing of the file.
pub filter: IndexSet<RowFilter>,
/// Whether to change the interlacing type of the file.
///
/// - `None` will not change the current interlacing.
/// - `Some(x)` will turn interlacing on or off.
/// `None` will not change the current interlacing type.
///
/// Default: `Some(false)`
pub interlace: Option<bool>,
/// `Some(x)` will change the file to interlacing mode `x`.
///
/// Default: `Some(Interlacing::None)`
pub interlace: Option<Interlacing>,
/// Whether to allow transparent pixels to be altered to improve compression.
///
/// Default: `false`
pub optimize_alpha: bool,
/// Whether to attempt bit depth reduction
///
@ -140,31 +133,23 @@ pub struct Options {
///
/// Default: `None`
pub strip: StripChunks,
/// Which DEFLATE (zlib) algorithm to use
#[cfg_attr(feature = "zopfli", doc = "(e.g. Zopfli)")]
/// Which DEFLATE algorithm to use
///
/// Default: `Libdeflater`
pub deflater: Deflater,
pub deflate: Deflaters,
/// Whether to use fast evaluation to pick the best filter
///
/// Default: `true`
pub fast_evaluation: bool,
/// Maximum amount of time to spend on optimizations.
/// Further potential optimizations are skipped if the timeout is exceeded.
///
/// Default: `None`
pub timeout: Option<Duration>,
/// Maximum decompressed size of the input IDAT.
/// If decompression would exceed this size, it will be rejected.
///
/// Default: `None`
pub max_decompressed_size: Option<usize>,
}
impl Options {
#[must_use]
pub fn from_preset(level: u8) -> Self {
let opts = Self::default();
pub fn from_preset(level: u8) -> Options {
let opts = Options::default();
match level {
0 => opts.apply_preset_0(),
1 => opts.apply_preset_1(),
@ -180,112 +165,77 @@ impl Options {
}
}
#[must_use]
pub fn max_compression() -> Self {
Self::from_preset(6)
pub fn max_compression() -> Options {
Options::from_preset(6)
}
// The following methods make assumptions that they are operating
// on an `Options` struct generated by the `default` method.
fn apply_preset_0(mut self) -> Self {
self.filters.clear();
self.deflater = Deflater::Libdeflater { compression: 5 };
self.filter.clear();
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
*compression = 5;
}
self
}
fn apply_preset_1(mut self) -> Self {
self.filters.clear();
self.deflater = Deflater::Libdeflater { compression: 10 };
self.filter.clear();
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
*compression = 10;
}
self
}
const fn apply_preset_2(self) -> Self {
fn apply_preset_2(self) -> Self {
self
}
fn apply_preset_3(mut self) -> Self {
self.fast_evaluation = false;
self.filters = indexset! {
FilterStrategy::NONE,
FilterStrategy::Bigrams,
FilterStrategy::BigEnt,
FilterStrategy::Brute {
num_lines: 3,
level: 1,
},
self.filter = indexset! {
RowFilter::None,
RowFilter::Bigrams,
RowFilter::BigEnt,
RowFilter::Brute
};
self
}
fn apply_preset_4(mut self) -> Self {
self.fast_evaluation = false;
self.filters = indexset! {
FilterStrategy::NONE,
FilterStrategy::Bigrams,
FilterStrategy::BigEnt,
FilterStrategy::Brute {
num_lines: 4,
level: 1,
},
};
self.deflater = Deflater::Libdeflater { compression: 12 };
self
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
*compression = 12;
}
self.apply_preset_3()
}
fn apply_preset_5(mut self) -> Self {
self.fast_evaluation = false;
self.filters = indexset! {
FilterStrategy::NONE,
FilterStrategy::SUB,
FilterStrategy::UP,
FilterStrategy::MinSum,
FilterStrategy::Entropy,
FilterStrategy::Bigrams,
FilterStrategy::BigEnt,
FilterStrategy::Brute {
num_lines: 4,
level: 4,
},
};
self.deflater = Deflater::Libdeflater { compression: 12 };
self.filter.insert(RowFilter::Up);
self.filter.insert(RowFilter::MinSum);
self.filter.insert(RowFilter::BigEnt);
self.filter.insert(RowFilter::Brute);
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
*compression = 12;
}
self
}
fn apply_preset_6(mut self) -> Self {
self.fast_evaluation = false;
self.filters = indexset! {
FilterStrategy::NONE,
FilterStrategy::SUB,
FilterStrategy::UP,
FilterStrategy::AVERAGE,
FilterStrategy::PAETH,
FilterStrategy::MinSum,
FilterStrategy::Entropy,
FilterStrategy::Bigrams,
FilterStrategy::BigEnt,
FilterStrategy::Brute {
num_lines: 8,
level: 5,
},
};
self.deflater = Deflater::Libdeflater { compression: 12 };
self
self.filter.insert(RowFilter::Average);
self.filter.insert(RowFilter::Paeth);
self.apply_preset_5()
}
}
impl Default for Options {
fn default() -> Self {
fn default() -> Options {
// Default settings based on -o 2 from the CLI interface
Self {
Options {
fix_errors: false,
force: false,
filters: indexset! {
FilterStrategy::NONE,
FilterStrategy::SUB,
FilterStrategy::Entropy,
FilterStrategy::Bigrams
},
interlace: Some(false),
filter: indexset! {RowFilter::None, RowFilter::Sub, RowFilter::Entropy, RowFilter::Bigrams},
interlace: Some(Interlacing::None),
optimize_alpha: false,
bit_depth_reduction: true,
color_type_reduction: true,
@ -294,10 +244,9 @@ impl Default for Options {
idat_recoding: true,
scale_16: false,
strip: StripChunks::None,
deflater: Deflater::Libdeflater { compression: 11 },
deflate: Deflaters::Libdeflater { compression: 11 },
fast_evaluation: true,
timeout: None,
max_decompressed_size: None,
}
}
}

View file

@ -1,24 +1,35 @@
use std::{fs, path::Path, sync::Arc};
use std::{
fs::File,
io::{BufReader, Read, Write},
path::Path,
sync::Arc,
};
use bitvec::bitarr;
use libdeflater::{CompressionLvl, Compressor};
use log::warn;
use rgb::ComponentSlice;
use rustc_hash::FxHashMap;
use crate::{
Options, PngResult,
apng::*,
colors::{BitDepth, ColorType},
deflate,
error::PngError,
filters::*,
headers::*,
interlace::{deinterlace_image, interlace_image},
interlace::{deinterlace_image, interlace_image, Interlacing},
Options,
};
pub(crate) mod scan_lines;
use self::scan_lines::ScanLines;
/// Compression level to use for the Brute filter strategy
const BRUTE_LEVEL: i32 = 1; // 1 is fastest, 2-4 are not useful, 5 is slower but more effective
/// Number of lines to compress with the Brute filter strategy
const BRUTE_LINES: usize = 4; // Values over 8 are generally not useful
#[derive(Debug, Clone)]
pub struct PngImage {
/// The headers stored in the IHDR chunk
@ -36,25 +47,44 @@ pub struct PngData {
pub idat_data: Vec<u8>,
/// All non-critical chunks from the PNG are stored here
pub aux_chunks: Vec<Chunk>,
/// APNG frames
pub frames: Vec<Frame>,
}
impl PngData {
/// Create a new `PngData` struct by opening a file
#[inline]
pub fn new(filepath: &Path, opts: &Options) -> PngResult<Self> {
pub fn new(filepath: &Path, opts: &Options) -> Result<Self, PngError> {
let byte_data = Self::read_file(filepath)?;
Self::from_slice(&byte_data, opts)
}
pub fn read_file(filepath: &Path) -> PngResult<Vec<u8>> {
fs::read(filepath).map_err(|e| PngError::ReadFailed(filepath.display().to_string(), e))
pub fn read_file(filepath: &Path) -> Result<Vec<u8>, PngError> {
let file = match File::open(filepath) {
Ok(f) => f,
Err(_) => return Err(PngError::new("Failed to open file for reading")),
};
let file_len = file.metadata().map(|m| m.len() as usize).unwrap_or(0);
let mut reader = BufReader::new(file);
// Check file for PNG header
let mut header = [0; 8];
if reader.read_exact(&mut header).is_err() {
return Err(PngError::new("Not a PNG file: too small"));
}
if !file_header_is_valid(&header) {
return Err(PngError::new("Invalid PNG header detected"));
}
// Read raw png data into memory
let mut byte_data: Vec<u8> = Vec::with_capacity(file_len);
byte_data.extend_from_slice(&header);
match reader.read_to_end(&mut byte_data) {
Ok(_) => (),
Err(_) => return Err(PngError::new("Failed to read from file")),
}
Ok(byte_data)
}
/// Create a new `PngData` struct by reading a slice
pub fn from_slice(byte_data: &[u8], opts: &Options) -> PngResult<Self> {
pub fn from_slice(byte_data: &[u8], opts: &Options) -> Result<Self, PngError> {
let mut byte_offset: usize = 0;
// Test that png header is valid
let header = byte_data.get(0..8).ok_or(PngError::TruncatedData)?;
@ -67,8 +97,6 @@ impl PngData {
let mut idat_data: Vec<u8> = Vec::new();
let mut key_chunks: FxHashMap<[u8; 4], Vec<u8>> = FxHashMap::default();
let mut aux_chunks: Vec<Chunk> = Vec::new();
let mut frames: Vec<Frame> = Vec::new();
let mut sequence_number = 0;
while let Some(chunk) = parse_next_chunk(byte_data, &mut byte_offset, opts.fix_errors)? {
match &chunk.name {
b"IDAT" => {
@ -77,53 +105,25 @@ impl PngData {
aux_chunks.push(Chunk {
name: chunk.name,
data: Vec::new(),
});
})
}
idat_data.extend_from_slice(chunk.data);
}
b"IHDR" | b"PLTE" | b"tRNS" => {
key_chunks.insert(chunk.name, chunk.data.to_owned());
}
_ if opts.strip.keep(&chunk.name) => {
if chunk.is_c2pa() {
// StripChunks::None is the default value, so to keep optimizing by default,
// interpret it as stripping the C2PA metadata.
// The C2PA metadata is invalidated if the file changes, so it shouldn't be kept.
if opts.strip == StripChunks::None {
continue;
}
return Err(PngError::C2PAMetadataPreventsChanges);
_ => {
if opts.strip.keep(&chunk.name) {
aux_chunks.push(Chunk {
name: chunk.name,
data: chunk.data.to_owned(),
})
} else if chunk.name == *b"acTL" {
warn!(
"Stripping animation data from APNG - image will become standard PNG"
);
}
if chunk.name == *b"fcTL" || chunk.name == *b"fdAT" {
// Validate the sequence number
if read_be_u32(&chunk.data[0..4]) != sequence_number {
return Err(PngError::APNGOutOfOrder);
}
sequence_number += 1;
if chunk.name == *b"fcTL" && !idat_data.is_empty() {
// Only create a Frame if it's after the IDAT (else store it as an aux chunk)
frames.push(Frame::from_fctl_data(chunk.data)?);
continue;
} else if chunk.name == *b"fdAT" {
// Append the data to the last frame
frames
.last_mut()
.ok_or(PngError::APNGOutOfOrder)?
.data
.extend_from_slice(&chunk.data[4..]);
continue;
}
}
// Regular ancillary chunk
aux_chunks.push(Chunk {
name: chunk.name,
data: chunk.data.to_owned(),
});
}
b"acTL" => {
warn!("Stripping animation data from APNG - image will become standard PNG");
}
_ => (),
}
}
@ -131,57 +131,64 @@ impl PngData {
if idat_data.is_empty() {
return Err(PngError::ChunkMissing("IDAT"));
}
let Some(ihdr_chunk) = key_chunks.remove(b"IHDR") else {
return Err(PngError::ChunkMissing("IHDR"));
let ihdr_chunk = match key_chunks.remove(b"IHDR") {
Some(ihdr) => ihdr,
None => return Err(PngError::ChunkMissing("IHDR")),
};
let ihdr = parse_ihdr_chunk(
&ihdr_chunk,
key_chunks.remove(b"PLTE"),
key_chunks.remove(b"tRNS"),
)?;
if let Some(max) = opts.max_decompressed_size {
if ihdr.raw_data_size() > max {
return Err(PngError::InflatedDataTooLong(max));
}
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.raw_data_size() {
return Err(PngError::TruncatedData);
}
let raw = PngImage::new(ihdr, &idat_data)?;
let mut raw = PngImage {
ihdr,
data: raw_data,
};
raw.data = raw.unfilter_image()?;
// Return the PngData
Ok(Self {
idat_data,
raw: Arc::new(raw),
aux_chunks,
frames,
})
}
/// Return an estimate of the output size which can help with evaluation of very small data
pub fn estimated_output_size(&self) -> usize {
self.idat_data.len() + self.raw.key_chunks_size()
}
/// Format the `PngData` struct into a valid PNG bytestream
#[must_use]
pub fn output(&self) -> Vec<u8> {
// PNG header
let mut output = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
// IHDR
let mut ihdr_data = Vec::with_capacity(13);
ihdr_data.extend_from_slice(&self.raw.ihdr.width.to_be_bytes());
ihdr_data.extend_from_slice(&self.raw.ihdr.height.to_be_bytes());
ihdr_data.extend_from_slice(&[self.raw.ihdr.bit_depth as u8]);
ihdr_data.extend_from_slice(&[
self.raw.ihdr.color_type.png_header_code(),
0, // Compression -- deflate
0, // Filter method -- 5-way adaptive filtering
self.raw.ihdr.interlaced as u8,
]);
ihdr_data.write_all(&self.raw.ihdr.width.to_be_bytes()).ok();
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.color_type.png_header_code()])
.ok();
ihdr_data.write_all(&[0]).ok(); // Compression -- deflate
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 chunks - split into those that come before IDAT and those that come after
let mut aux_split = self.aux_chunks.split(|c| &c.name == b"IDAT");
let aux_pre = aux_split.next().unwrap();
// Many chunks need to be before PLTE, so write all except those that explicitly need to be after
// Note: the PNG spec does not say that fcTL needs to be after PLTE, but some decoders expect
// that (see issue #625)
for chunk in aux_pre
.iter()
.filter(|c| !matches!(&c.name, b"bKGD" | b"hIST" | b"tRNS" | b"fcTL"))
.filter(|c| !(&c.name == b"bKGD" || &c.name == b"hIST" || &c.name == b"tRNS"))
{
write_png_block(&chunk.name, &chunk.data, &mut output);
}
@ -190,7 +197,7 @@ impl PngData {
ColorType::Indexed { palette } => {
let mut palette_data = Vec::with_capacity(palette.len() * 3);
for px in palette {
palette_data.extend_from_slice(px.rgb().as_ref());
palette_data.extend_from_slice(px.rgb().as_slice());
}
write_png_block(b"PLTE", &palette_data, &mut output);
if let Some(last_trns) = palette.iter().rposition(|px| px.a != 255) {
@ -208,30 +215,20 @@ impl PngData {
transparent_color: Some(trns),
} => {
// Transparency pixel - 6 byte RGB16
let trns_data: Vec<_> = trns.iter().flat_map(u16::to_be_bytes).collect();
let trns_data: Vec<_> = trns.iter().flat_map(|c| c.to_be_bytes()).collect();
write_png_block(b"tRNS", &trns_data, &mut output);
}
_ => {}
}
// Special ancillary chunks that need to come after PLTE but before IDAT
let mut sequence_number = 0;
for chunk in aux_pre
.iter()
.filter(|c| matches!(&c.name, b"bKGD" | b"hIST" | b"tRNS" | b"fcTL"))
.filter(|c| &c.name == b"bKGD" || &c.name == b"hIST" || &c.name == b"tRNS")
{
write_png_block(&chunk.name, &chunk.data, &mut output);
if &chunk.name == b"fcTL" {
sequence_number += 1;
}
}
// IDAT data
write_png_block(b"IDAT", &self.idat_data, &mut output);
// APNG frames
for frame in self.frames.iter() {
write_png_block(b"fcTL", &frame.fctl_data(sequence_number), &mut output);
write_png_block(b"fdAT", &frame.fdat_data(sequence_number + 1), &mut output);
sequence_number += 2;
}
// Ancillary chunks that come after IDAT
for aux_post in aux_split {
for chunk in aux_post {
@ -246,33 +243,18 @@ impl PngData {
}
impl PngImage {
pub fn new(ihdr: IhdrData, compressed_data: &[u8]) -> PngResult<Self> {
let raw_data = deflate::inflate(compressed_data, ihdr.raw_data_size())?;
// Reject files with incorrect width/height or truncated data
if raw_data.len() != ihdr.raw_data_size() {
return Err(PngError::TruncatedData);
}
let mut image = Self {
ihdr,
data: raw_data,
};
image.data = image.unfilter_image()?;
Ok(image)
}
/// Enable or disable interlacing
/// Returns the new image if the interlacing was changed, None otherwise
/// Convert the image to the specified interlacing type
/// Returns true if the interlacing was changed, false otherwise
/// The `interlace` parameter specifies the *new* interlacing mode
/// Assumes that the data has already been de-filtered
#[inline]
#[must_use]
pub fn change_interlacing(&self, interlace: bool) -> Option<Self> {
pub fn change_interlacing(&self, interlace: Interlacing) -> Option<PngImage> {
if interlace == self.ihdr.interlaced {
return None;
}
Some(if interlace {
Some(if interlace == Interlacing::Adam7 {
// Convert progressive to interlaced data
interlace_image(self)
} else {
@ -283,15 +265,13 @@ impl PngImage {
/// Return the number of channels in the image, based on color type
#[inline]
#[must_use]
pub const fn channels_per_pixel(&self) -> usize {
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]
#[must_use]
pub const fn bytes_per_channel(&self) -> usize {
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
@ -300,15 +280,15 @@ impl PngImage {
}
/// Calculate the size of the PLTE and tRNS chunks
#[must_use]
pub fn key_chunks_size(&self) -> usize {
match &self.ihdr.color_type {
ColorType::Indexed { palette } => {
let plte = 12 + palette.len() * 3;
palette
.iter()
.rposition(|p| p.a != 255)
.map_or(plte, |trns| plte + 12 + trns + 1)
if let Some(trns) = palette.iter().rposition(|p| p.a != 255) {
plte + 12 + trns + 1
} else {
plte
}
}
ColorType::Grayscale { transparent_shade } if transparent_shade.is_some() => 12 + 2,
ColorType::RGB { transparent_color } if transparent_color.is_some() => 12 + 6,
@ -316,21 +296,14 @@ impl PngImage {
}
}
/// Return an estimate of the output size which can help with evaluation of very small data
#[must_use]
pub fn estimated_output_size(&self, idat_data: &[u8]) -> usize {
idat_data.len() + self.key_chunks_size()
}
/// Return an iterator over the scanlines of the image
#[inline]
#[must_use]
pub fn scan_lines(&self, has_filter: bool) -> ScanLines<'_> {
ScanLines::new(self, has_filter)
}
/// Reverse all filters applied on the image, returning an unfiltered IDAT bytestream
fn unfilter_image(&self) -> PngResult<Vec<u8>> {
fn unfilter_image(&self) -> Result<Vec<u8>, PngError> {
let mut unfiltered = Vec::with_capacity(self.data.len());
let bpp = self.bytes_per_channel() * self.channels_per_pixel();
let mut last_line: Vec<u8> = Vec::new();
@ -342,8 +315,8 @@ impl PngImage {
last_pass = line.pass;
}
last_line.resize(line.data.len(), 0);
let filter = RowFilter::try_from(line.filter).map_err(|()| PngError::InvalidData)?;
filter.unfilter_line(bpp, line.data, &last_line, &mut unfiltered_buf);
let filter = RowFilter::try_from(line.filter).map_err(|_| PngError::InvalidData)?;
filter.unfilter_line(bpp, line.data, &last_line, &mut unfiltered_buf)?;
unfiltered.extend_from_slice(&unfiltered_buf);
std::mem::swap(&mut last_line, &mut unfiltered_buf);
unfiltered_buf.clear();
@ -352,12 +325,7 @@ impl PngImage {
}
/// Apply the specified filter type to all rows in the image
#[must_use]
pub fn filter_image(
&self,
strategy: FilterStrategy,
optimize_alpha: bool,
) -> (Vec<u8>, FilterStrategy) {
pub fn filter_image(&self, filter: RowFilter, optimize_alpha: bool) -> Vec<u8> {
let mut filtered = Vec::with_capacity(self.data.len());
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
@ -370,58 +338,35 @@ impl PngImage {
let mut prev_line = Vec::new();
let mut prev_pass: Option<u8> = None;
let mut f_buf = Vec::new();
// For heuristic strategies, keep track of the actual filter used for each line
let mut filters_used = Vec::new();
// Pre-allocate buffers for the Bigrams strategy to avoid per-line allocations
let (mut bigram_seen, mut bigram_touched) = if matches!(strategy, FilterStrategy::Bigrams) {
(vec![false; 0x10000], Vec::<u16>::new())
} else {
(Vec::new(), Vec::new())
};
for (i, line) in self.scan_lines(false).enumerate() {
for line in self.scan_lines(false) {
if prev_pass != line.pass || line.data.len() != prev_line.len() {
prev_line = vec![0; line.data.len()];
}
// Alpha optimisation may alter the line data, so we need a mutable copy of it
let mut line_data = line.data.to_vec();
if let FilterStrategy::Basic(mut filter) = strategy {
if filter <= RowFilter::Paeth {
// Standard filters
if prev_pass != line.pass && filter > RowFilter::Sub {
filter = RowFilter::None;
}
filter.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
filtered.extend_from_slice(&f_buf);
prev_line = line_data;
} else if let FilterStrategy::Predefined(lines) = &strategy {
// Predefined filter for each line
let filter = lines.get(i).unwrap_or(&RowFilter::None);
let filter = if prev_pass == line.pass || filter <= RowFilter::Sub {
filter
} else {
RowFilter::None
};
filter.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
filtered.extend_from_slice(&f_buf);
prev_line = line_data;
} else {
// Heuristic filter selection strategies
let mut best_filter = RowFilter::None;
if line_data.iter().all(|&x| x == 0) {
// Assume None if the line is all zeros
filtered.push(best_filter as u8);
filtered.extend_from_slice(&line_data);
prev_line = line_data;
filters_used.push(best_filter);
continue;
}
let mut best_line = Vec::new();
let mut best_line_raw = Vec::new();
// Avoid vertical filtering on first line of each interlacing pass
let try_filters = if prev_pass == line.pass {
RowFilter::ALL.iter()
RowFilter::STANDARD.iter()
} else {
RowFilter::SINGLE_LINE.iter()
};
match strategy {
FilterStrategy::MinSum => {
match filter {
RowFilter::MinSum => {
// MSAD algorithm mentioned in libpng reference docs
// http://www.libpng.org/pub/png/book/chapter09.html
let mut best_size = usize::MAX;
@ -435,18 +380,17 @@ impl PngImage {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data);
best_filter = *f;
}
}
}
FilterStrategy::Entropy => {
RowFilter::Entropy => {
// Shannon entropy algorithm, from LodePNG
// https://github.com/lvandeve/lodepng
let mut best_size = i32::MIN;
for f in try_filters {
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
let mut counts = vec![0; 0x100];
for &i in &f_buf {
for &i in f_buf.iter() {
counts[i as usize] += 1;
}
let size = counts.into_iter().fold(0, |acc, x| {
@ -459,42 +403,29 @@ impl PngImage {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data);
best_filter = *f;
}
}
}
FilterStrategy::Bigrams => {
RowFilter::Bigrams => {
// Count distinct bigrams, from pngwolf
// https://bjoern.hoehrmann.de/pngwolf/
let mut best_size = usize::MAX;
for f in try_filters {
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
let mut count = 0;
let mut set = bitarr![0; 0x10000];
for pair in f_buf.windows(2) {
let bigram = ((pair[0] as usize) << 8) | pair[1] as usize;
if !bigram_seen[bigram] {
count += 1;
if count >= best_size {
break;
}
bigram_seen[bigram] = true;
bigram_touched.push(bigram as u16);
}
let bigram = (pair[0] as usize) << 8 | pair[1] as usize;
set.set(bigram, true);
}
if count < best_size {
best_size = count;
let size = set.count_ones();
if size < best_size {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data);
best_filter = *f;
}
// Clear only the entries that were touched
for &idx in &bigram_touched {
bigram_seen[idx as usize] = false;
}
bigram_touched.clear();
}
}
FilterStrategy::BigEnt => {
RowFilter::BigEnt => {
// Bigram entropy, combined from Entropy and Bigrams filters
let mut best_size = i32::MIN;
// FxHasher is the fastest rust hasher currently available for this purpose
@ -503,7 +434,7 @@ impl PngImage {
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
counts.clear();
for pair in f_buf.windows(2) {
let bigram = (u16::from(pair[0]) << 8) | u16::from(pair[1]);
let bigram = (pair[0] as u16) << 8 | pair[1] as u16;
counts.entry(bigram).and_modify(|e| *e += 1).or_insert(1);
}
let size = counts.values().fold(0, |acc, &x| acc + ilog2i(x)) as i32;
@ -511,33 +442,31 @@ impl PngImage {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data);
best_filter = *f;
}
}
}
FilterStrategy::Brute { num_lines, level } => {
RowFilter::Brute => {
// Brute force by compressing each filter attempt
// Similar to that of LodePNG but includes some previous lines for context
let mut best_size = usize::MAX;
let line_start = filtered.len();
filtered.resize(filtered.len() + line.data.len() + 1, 0);
let mut compressor =
Compressor::new(CompressionLvl::new(level.into()).unwrap());
let limit = filtered.len().min((line.data.len() + 1) * num_lines);
let capacity = compressor.deflate_compress_bound(limit);
Compressor::new(CompressionLvl::new(BRUTE_LEVEL).unwrap());
let limit = filtered.len().min((line.data.len() + 1) * BRUTE_LINES);
let capacity = compressor.zlib_compress_bound(limit);
let mut dest = vec![0; capacity];
for f in try_filters {
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
filtered[line_start..].copy_from_slice(&f_buf);
let size = compressor
.deflate_compress(&filtered[filtered.len() - limit..], &mut dest)
.zlib_compress(&filtered[filtered.len() - limit..], &mut dest)
.unwrap_or(usize::MAX);
if size < best_size {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data);
best_filter = *f;
}
}
filtered.resize(line_start, 0);
@ -546,17 +475,11 @@ impl PngImage {
}
filtered.extend_from_slice(&best_line);
prev_line = best_line_raw;
filters_used.push(best_filter);
}
prev_pass = line.pass;
}
if filters_used.is_empty() {
(filtered, strategy)
} else {
(filtered, FilterStrategy::Predefined(filters_used))
}
filtered
}
}
@ -572,7 +495,7 @@ fn write_png_block(key: &[u8], chunk: &[u8], output: &mut Vec<u8>) {
}
// Integer approximation for i * log2(i) - much faster than float calculations
const fn ilog2i(i: u32) -> u32 {
fn ilog2i(i: u32) -> u32 {
let log = 32 - i.leading_zeros() - 1;
i * log + ((i - (1 << log)) << 1)
}

View file

@ -1,4 +1,4 @@
use crate::png::PngImage;
use crate::{interlace::Interlacing, png::PngImage};
/// An iterator over the scan lines of a PNG image
#[derive(Debug, Clone)]
@ -11,7 +11,6 @@ pub struct ScanLines<'a> {
}
impl<'a> ScanLines<'a> {
#[must_use]
pub fn new(png: &'a PngImage, has_filter: bool) -> Self {
Self {
iter: ScanLineRanges::new(png, has_filter),
@ -23,7 +22,6 @@ impl<'a> ScanLines<'a> {
impl<'a> Iterator for ScanLines<'a> {
type Item = ScanLine<'a>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let (len, pass, num_pixels) = self.iter.next()?;
@ -69,7 +67,7 @@ impl ScanLineRanges {
width: png.ihdr.width,
height: png.ihdr.height,
left: png.data.len(),
pass: if png.ihdr.interlaced {
pass: if png.ihdr.interlaced == Interlacing::Adam7 {
Some((1, 0))
} else {
None
@ -81,7 +79,6 @@ impl ScanLineRanges {
impl Iterator for ScanLineRanges {
type Item = (usize, Option<u8>, usize);
fn next(&mut self) -> Option<Self::Item> {
if self.left == 0 {
return None;
@ -138,7 +135,7 @@ impl Iterator for ScanLineRanges {
pixels_per_line += 1;
}
_ => (),
}
};
let current_pass = Some(pass.0);
if pass.1 + y_steps >= self.height {
pass.0 += 1;
@ -157,7 +154,7 @@ impl Iterator for ScanLineRanges {
(self.width, None)
};
let bits_per_line = pixels_per_line as usize * self.bits_per_pixel;
let mut len = bits_per_line.div_ceil(8);
let mut len = (bits_per_line + 7) / 8;
if self.has_filter {
len += 1;
}

View file

@ -1,5 +1,3 @@
#![allow(dead_code)]
pub mod prelude {
pub use super::*;
}
@ -72,10 +70,12 @@ where
impl<I: Iterator> ParallelIterator for I {}
#[allow(dead_code)]
pub fn join<A, B>(a: impl FnOnce() -> A, b: impl FnOnce() -> B) -> (A, B) {
(a(), b())
}
#[allow(dead_code)]
pub fn spawn<A>(a: impl FnOnce() -> A) -> A {
a()
}

View file

@ -7,7 +7,6 @@ use crate::{
};
/// Clean the alpha channel by setting the color of all fully transparent pixels to black
#[must_use]
pub fn cleaned_alpha_channel(png: &PngImage) -> Option<PngImage> {
if !png.ihdr.color_type.has_alpha() {
return None;
@ -17,7 +16,7 @@ pub fn cleaned_alpha_channel(png: &PngImage) -> Option<PngImage> {
let colored_bytes = bpp - byte_depth;
let mut reduced = Vec::with_capacity(png.data.len());
for pixel in png.data.chunks_exact(bpp) {
for pixel in png.data.chunks(bpp) {
if pixel.iter().skip(colored_bytes).all(|b| *b == 0) {
reduced.resize(reduced.len() + bpp, 0);
} else {
@ -46,7 +45,7 @@ pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option<Png
let mut has_transparency = false;
let mut used_colors = vec![false; 256];
for pixel in png.data.chunks_exact(bpp) {
for pixel in png.data.chunks(bpp) {
if optimize_alpha && pixel.iter().skip(colored_bytes).all(|b| *b == 0) {
// Fully transparent, we may be able to reduce with tRNS
has_transparency = true;
@ -75,19 +74,19 @@ pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option<Png
};
let mut raw_data = Vec::with_capacity(png.data.len());
for pixel in png.data.chunks_exact(bpp) {
for pixel in png.data.chunks(bpp) {
match transparency_pixel {
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 => (u16::from(trns) << 8) | u16::from(trns),
_ => u16::from(trns),
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 {

View file

@ -16,13 +16,13 @@ pub fn reduced_bit_depth_16_to_8(png: &PngImage, force_scale: bool) -> Option<Pn
}
// Reduce from 16 to 8 bits per channel per pixel
if png.data.chunks_exact(2).any(|pair| pair[0] != pair[1]) {
if png.data.chunks(2).any(|pair| pair[0] != pair[1]) {
// Can't reduce
return None;
}
Some(PngImage {
data: png.data.chunks_exact(2).map(|pair| pair[0]).collect(),
data: png.data.iter().step_by(2).cloned().collect(),
ihdr: IhdrData {
color_type: png.ihdr.color_type.clone(),
bit_depth: BitDepth::Eight,
@ -41,15 +41,15 @@ pub fn scaled_bit_depth_16_to_8(png: &PngImage) -> Option<PngImage> {
// Reduce from 16 to 8 bits per channel per pixel by scaling when necessary
let data = png
.data
.chunks_exact(2)
.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 = f32::from(u16::from_be_bytes([pair[0], pair[1]]));
(val * (255.0 / 65535.0)).round() as u8
let val = u16::from_be_bytes([pair[0], pair[1]]) as f64;
(val * 255.0 / 65535.0).round() as u8
})
.collect();
@ -70,17 +70,17 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage) -> Option<PngImage> {
return None;
}
let minimum_bits = if let ColorType::Indexed { palette } = &png.ihdr.color_type {
let mut minimum_bits = 1;
if let ColorType::Indexed { palette } = &png.ihdr.color_type {
// We can easily determine minimum depth by the palette size
match palette.len() {
minimum_bits = match palette.len() {
0..=2 => 1,
3..=4 => 2,
5..=16 => 4,
_ => return None,
}
};
} else {
let mut minimum_bits = 1;
// Finding minimum depth for grayscale is much more complicated
let mut mask = 1;
let mut divisions = 1..8;
@ -110,9 +110,7 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage) -> Option<PngImage> {
break;
}
}
minimum_bits
};
}
let mut reduced = Vec::with_capacity(png.data.len());
let mask = (1 << minimum_bits) - 1;
@ -140,7 +138,7 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage) -> Option<PngImage> {
let mut check = reduced_trans;
let mut bits = minimum_bits;
while bits < 8 {
check = (check << bits) | check;
check = check << bits | check;
bits <<= 1;
}
// If the transparency doesn't fit the new bit depth it is therefore unused - set it to None
@ -190,7 +188,7 @@ pub fn expanded_bit_depth_to_8(png: &PngImage) -> Option<PngImage> {
// Expand gray by repeating the bits
let mut bits = bit_depth;
while bits < 8 {
val = (val << bits) | val;
val = val << bits | val;
bits <<= 1;
}
}
@ -209,7 +207,7 @@ pub fn expanded_bit_depth_to_8(png: &PngImage) -> Option<PngImage> {
{
let mut bits = bit_depth;
while bits < 8 {
trans = (trans << bits) | trans;
trans = trans << bits | trans;
bits <<= 1;
}
ColorType::Grayscale {

View file

@ -1,7 +1,7 @@
use std::hash::{BuildHasherDefault, Hash};
use indexmap::IndexSet;
use rgb::{ComponentMap, FromSlice, RGB, RGBA, alt::Gray};
use rgb::{alt::Gray, ComponentMap, ComponentSlice, FromSlice, RGB, RGBA};
use rustc_hash::FxHasher;
use crate::{
@ -49,39 +49,39 @@ pub fn reduced_to_indexed(png: &PngImage, allow_grayscale: bool) -> Option<PngIm
let mut raw_data = Vec::with_capacity(png.data.len() / png.channels_per_pixel());
let palette: Vec<_> = match png.ihdr.color_type {
ColorType::Grayscale { transparent_shade } => {
let pmap = build_palette(png.data.as_gray().iter().copied(), &mut raw_data)?;
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).with_alpha(if Some(px) == transparency_pixel {
0
} else {
RGB::from(px).alpha(if Some(px) != transparency_pixel {
255
} else {
0
})
})
.collect()
}
ColorType::RGB { transparent_color } => {
let pmap = build_palette(png.data.as_rgb().iter().copied(), &mut raw_data)?;
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.with_alpha(if Some(px) == transparency_pixel {
0
} else {
px.alpha(if Some(px) != transparency_pixel {
255
} else {
0
})
})
.collect()
}
ColorType::GrayscaleAlpha => {
let pmap = build_palette(png.data.as_gray_alpha().iter().copied(), &mut raw_data)?;
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().copied(), &mut raw_data)?;
let pmap = build_palette(png.data.as_rgba().iter().cloned(), &mut raw_data)?;
pmap.into_iter().collect()
}
_ => return None,
@ -106,7 +106,7 @@ pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
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_exact(bpp) {
for pixel in png.data.chunks(bpp) {
if byte_depth == 1 {
if pixel[0] != pixel[1] || pixel[1] != pixel[2] {
return None;
@ -138,30 +138,15 @@ pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
/// Attempt to convert indexed to a different color type, returning the resulting image if successful
#[must_use]
pub fn indexed_to_channels(
png: &PngImage,
allow_grayscale: bool,
optimize_alpha: bool,
) -> Option<PngImage> {
pub fn indexed_to_channels(png: &PngImage, allow_grayscale: bool) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight {
return None;
}
let mut palette = match &png.ihdr.color_type {
ColorType::Indexed { palette } => palette.clone(),
let palette = match &png.ihdr.color_type {
ColorType::Indexed { palette } => palette,
_ => return None,
};
// Ensure fully transparent colors are black, which can help with grayscale conversion
if optimize_alpha {
for color in &mut palette {
if color.a == 0 {
color.r = 0;
color.g = 0;
color.b = 0;
}
}
}
// Determine which channels are required
let is_gray = if allow_grayscale {
palette.iter().all(|c| c.r == c.g && c.g == c.b)
@ -193,7 +178,7 @@ pub fn indexed_to_channels(
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_ref()[ch_start..=ch_end]);
data.extend_from_slice(&color.as_slice()[ch_start..=ch_end]);
}
Some(PngImage {

View file

@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::{ColorType, Deadline, Deflater, Options, evaluate::Evaluator, png::PngImage};
use crate::{evaluate::Evaluator, png::PngImage, ColorType, Deadline, Deflaters, Options};
pub mod alpha;
use crate::alpha::*;
@ -21,8 +21,8 @@ pub(crate) fn perform_reductions(
// At low compression levels, skip some transformations which are less likely to be effective
// This currently affects optimization presets 0-2
let cheap = match opts.deflater {
Deflater::Libdeflater { compression } => compression < 12 && opts.fast_evaluation,
let cheap = match opts.deflate {
Deflaters::Libdeflater { compression } => compression < 12 && opts.fast_evaluation,
_ => false,
};
@ -106,9 +106,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 !cheap && opts.color_type_reduction && !deadline.passed() {
if let Some(reduced) =
indexed_to_channels(&png, opts.grayscale_reduction, opts.optimize_alpha)
{
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;
@ -118,7 +116,7 @@ pub(crate) fn perform_reductions(
// Attempt to reduce to indexed
// Keep the existing `png` var in case it is grayscale - we can test both for depth reduction later
let mut indexed = None;
if opts.color_type_reduction && opts.palette_reduction && !deadline.passed() {
if opts.color_type_reduction && !deadline.passed() {
if let Some(reduced) = reduced_to_indexed(&png, opts.grayscale_reduction) {
// Make sure the palette gets sorted (but don't bother evaluating both results)
let new = Arc::new(sorted_palette(&reduced).unwrap_or(reduced));
@ -183,7 +181,7 @@ pub(crate) fn perform_reductions(
if (!cheap || reduced.is_none()) && !deadline.passed() {
if let Some(indexed) = indexed.and_then(|png| reduced_bit_depth_8_or_less(&png)) {
// Only evaluate this if it's different from the first result (which must be grayscale if it exists)
if reduced.as_ref().is_none_or(|r| r.data != indexed.data) {
if reduced.as_ref().map_or(true, |r| r.data != indexed.data) {
eval.try_image(Arc::new(indexed));
evaluation_added = true;
}

View file

@ -4,7 +4,8 @@ use rgb::RGBA8;
use crate::{
colors::{BitDepth, ColorType},
headers::IhdrData,
png::{PngImage, scan_lines::ScanLine},
png::{scan_lines::ScanLine, PngImage},
Interlacing,
};
/// Attempt to reduce the number of colors in the palette, returning the reduced image if successful
@ -86,7 +87,7 @@ pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
let mut enumerated: Vec<_> = palette.iter().enumerate().collect();
// Put the most popular edge color first, which can help slightly if the filter bytes are 0
let keep_first = most_popular_edge_color(palette.len(), png);
let first = keep_first.map(|f| enumerated.remove(f));
let first = enumerated.remove(keep_first);
// Sort the palette
enumerated.sort_by(|a, b| {
@ -103,9 +104,7 @@ pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
};
color_val(a.1).cmp(&color_val(b.1))
});
if let Some(first) = first {
enumerated.insert(0, first);
}
enumerated.insert(0, first);
// Extract the new palette and determine if anything changed
let (remapping, palette): (Vec<_>, Vec<RGBA8>) = enumerated.into_iter().unzip();
@ -133,7 +132,7 @@ pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
#[must_use]
pub fn sorted_palette_mzeng(png: &PngImage) -> Option<PngImage> {
// Interlacing not currently supported
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced {
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced != Interlacing::None {
return None;
}
let palette = match &png.ihdr.color_type {
@ -155,7 +154,7 @@ pub fn sorted_palette_mzeng(png: &PngImage) -> Option<PngImage> {
#[must_use]
pub fn sorted_palette_battiato(png: &PngImage) -> Option<PngImage> {
// Interlacing not currently supported
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced {
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced != Interlacing::None {
return None;
}
let palette = match &png.ihdr.color_type {
@ -205,31 +204,27 @@ fn apply_palette_reorder(png: &PngImage, remapping: &[usize]) -> Option<PngImage
}
// Find the most popular color on the image edges (the pixels neighboring the filter bytes)
fn most_popular_edge_color(num_colors: usize, png: &PngImage) -> Option<usize> {
let mut counts = [0_u32; 256];
fn most_popular_edge_color(num_colors: usize, png: &PngImage) -> usize {
let mut counts = [0u32; 256];
for line in png.scan_lines(false) {
if let &[first, .., last] = line.data {
counts[first as usize] += 1;
counts[last as usize] += 1;
}
}
let max = counts
counts
.iter()
.copied()
.take(num_colors)
.enumerate()
.max_by_key(|&(_, v)| v)
.unwrap();
// Ensure there's a clear winner - return None if multiple colors are tied
let max_equal = counts.iter().filter(|&v| v == max.1).count();
if max_equal > 1 {
return None;
}
Some(max.0)
.unwrap_or_default()
.0
}
// Find the most popular color in the image, along with its count
fn most_popular_color(num_colors: usize, png: &PngImage) -> (usize, u32) {
let mut counts = [0_u32; 256];
let mut counts = [0u32; 256];
for &val in &png.data {
counts[val as usize] += 1;
}
@ -261,7 +256,7 @@ fn apply_most_popular_color(png: &PngImage, remapping: &mut [usize]) {
// Calculate co-occurences matrix
fn co_occurrence_matrix(num_colors: usize, png: &PngImage) -> Vec<Vec<u32>> {
let mut matrix = vec![vec![0_u32; num_colors]; num_colors];
let mut matrix = vec![vec![0u32; num_colors]; num_colors];
let mut prev: Option<ScanLine> = None;
let mut prev_val = None;
for line in png.scan_lines(false) {
@ -283,7 +278,7 @@ fn co_occurrence_matrix(num_colors: usize, png: &PngImage) -> Vec<Vec<u32>> {
matrix[val][prev_val] += 1;
}
}
prev = Some(line);
prev = Some(line)
}
matrix
}

View file

@ -1,10 +1,6 @@
use std::io::Cursor;
use image::{codecs::png::PngDecoder, *};
use log::{error, warn};
#[cfg(not(feature = "parallel"))]
use crate::rayon;
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 {
@ -44,7 +40,7 @@ fn load_png_image_from_memory(png_data: &[u8]) -> Result<Vec<RgbaImage>, image::
decoder
.apng()?
.into_frames()
.map(|f| f.map(image::Frame::into_buffer))
.map(|f| f.map(|f| f.into_buffer()))
.collect()
} else {
DynamicImage::from_decoder(decoder).map(|i| vec![i.into_rgba8()])

View file

@ -1,346 +0,0 @@
use std::{
fs::remove_file,
path::{Path, PathBuf},
};
use oxipng::{internal_tests::*, *};
const GRAYSCALE_ALPHA: u8 = 4;
const RGBA: u8 = 6;
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
let options = oxipng::Options {
force: true,
optimize_alpha: true,
..Default::default()
};
(OutFile::from_path(input.with_extension("out.png")), options)
}
fn test_it_converts(
input: &str,
filter: FilterStrategy,
color_type_in: u8,
bit_depth_in: BitDepth,
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).unwrap();
opts.filters = indexset! {filter};
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) {
Ok(_) => (),
Err(x) => panic!("{}", x),
}
let output = output.path().unwrap();
assert!(output.exists());
let png = match PngData::new(output, &opts) {
Ok(x) => x,
Err(x) => {
remove_file(output).ok();
panic!("{}", x)
}
};
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 ColorType::Indexed { palette } = &png.raw.ihdr.color_type {
assert!(palette.len() <= 1 << (png.raw.ihdr.bit_depth as u8));
}
remove_file(output).ok();
}
#[test]
fn alpha_filter_0_for_rgba_16() {
test_it_converts(
"tests/files/filter_0_for_rgba_16.png",
FilterStrategy::NONE,
RGBA,
BitDepth::Sixteen,
RGBA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_1_for_rgba_16() {
test_it_converts(
"tests/files/filter_1_for_rgba_16.png",
FilterStrategy::SUB,
RGBA,
BitDepth::Sixteen,
RGBA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_2_for_rgba_16() {
test_it_converts(
"tests/files/filter_2_for_rgba_16.png",
FilterStrategy::UP,
RGBA,
BitDepth::Sixteen,
RGBA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_3_for_rgba_16() {
test_it_converts(
"tests/files/filter_3_for_rgba_16.png",
FilterStrategy::AVERAGE,
RGBA,
BitDepth::Sixteen,
RGBA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_4_for_rgba_16() {
test_it_converts(
"tests/files/filter_4_for_rgba_16.png",
FilterStrategy::PAETH,
RGBA,
BitDepth::Sixteen,
RGBA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_5_for_rgba_16() {
test_it_converts(
"tests/files/filter_5_for_rgba_16.png",
FilterStrategy::MinSum,
RGBA,
BitDepth::Sixteen,
RGBA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_0_for_rgba_8() {
test_it_converts(
"tests/files/filter_0_for_rgba_8.png",
FilterStrategy::NONE,
RGBA,
BitDepth::Eight,
RGBA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_1_for_rgba_8() {
test_it_converts(
"tests/files/filter_1_for_rgba_8.png",
FilterStrategy::SUB,
RGBA,
BitDepth::Eight,
RGBA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_2_for_rgba_8() {
test_it_converts(
"tests/files/filter_2_for_rgba_8.png",
FilterStrategy::UP,
RGBA,
BitDepth::Eight,
RGBA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_3_for_rgba_8() {
test_it_converts(
"tests/files/filter_3_for_rgba_8.png",
FilterStrategy::AVERAGE,
RGBA,
BitDepth::Eight,
RGBA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_4_for_rgba_8() {
test_it_converts(
"tests/files/filter_4_for_rgba_8.png",
FilterStrategy::PAETH,
RGBA,
BitDepth::Eight,
RGBA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_5_for_rgba_8() {
test_it_converts(
"tests/files/filter_5_for_rgba_8.png",
FilterStrategy::MinSum,
RGBA,
BitDepth::Eight,
RGBA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_0_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_0_for_grayscale_alpha_16.png",
FilterStrategy::NONE,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_1_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_1_for_grayscale_alpha_16.png",
FilterStrategy::SUB,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_2_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_2_for_grayscale_alpha_16.png",
FilterStrategy::UP,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_3_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_3_for_grayscale_alpha_16.png",
FilterStrategy::AVERAGE,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_4_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_4_for_grayscale_alpha_16.png",
FilterStrategy::PAETH,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_5_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_5_for_grayscale_alpha_16.png",
FilterStrategy::MinSum,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_0_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_0_for_grayscale_alpha_8.png",
FilterStrategy::NONE,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_1_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_1_for_grayscale_alpha_8.png",
FilterStrategy::SUB,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_2_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_2_for_grayscale_alpha_8.png",
FilterStrategy::UP,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_3_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_3_for_grayscale_alpha_8.png",
FilterStrategy::AVERAGE,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_4_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_4_for_grayscale_alpha_8.png",
FilterStrategy::PAETH,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_5_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_5_for_grayscale_alpha_8.png",
FilterStrategy::MinSum,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
BitDepth::Eight,
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 B

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 B

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 B

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 B

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 B

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Some files were not shown because too many files have changed in this diff Show more