Compare commits

..

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

78 changed files with 1503 additions and 2225 deletions

View file

@ -1,2 +1,25 @@
[alias]
xtask = "run --manifest-path xtask/Cargo.toml --"
[target.'cfg(all(target_os = "linux", target_arch = "aarch64"))']
runner = "qemu-aarch64" # May need to remove this if targeting AArch64 from an AArch64 Linux box
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-musl-gcc"
# AArch64 Linux musl targets are repeatedly affected under varying
# circumstances by fixedn't linking issues to compiler builtin symbols.
# Linking to libgcc, though arguably an inelegant hack that's only portable
# to our glibc from musl crosscompilation scenario, is effective to reliably
# provide an implementation of those fundamental symbols over time, without
# user-visible impacts on the final executables. See:
# https://github.com/rust-lang/rust/issues/46651
# https://github.com/rust-lang/compiler-builtins/issues/201
# https://github.com/rust-lang/rust/issues/128401 (the issue most relevant to
# our observed failures)
# Note that these flags are not enforced on CI because CI overrides them through
# the RUSTFLAG environment variable. Check out the CI workflow definition file at
# .github/workflows/oxipng.yml for more info
rustflags = ["-Clink-args=-lgcc"]

View file

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

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@v9
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,19 @@ 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"
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

@ -2,9 +2,9 @@ name: docker
on:
push:
branches:
- master
- 'master'
tags:
- v*.*.*
- 'v*.*.*'
pull_request:
types:
- opened
@ -17,44 +17,76 @@ on:
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
env:
REGISTRY: ghcr.io
# ghcr.io/OWNER/REPO
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
uses: docker/github-builder/.github/workflows/build.yml@v1
runs-on: ubuntu-latest
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
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# For some reason the title have to be set manually
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
labels: |
org.opencontainers.image.title=Oxipng
annotations: |
org.opencontainers.image.title=Oxipng
# Build and push Docker image with Buildx (don't push on PR)
# Cache isn't used for tags and on workflow_dispatch if specified
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
no-cache: ${{ (github.event_name == 'workflow_dispatch' && !inputs.use_cache) || startsWith(github.ref, 'refs/tags/') }}
# Attest the build provenance
# TODO: enable push to registry when referrers API will be supported by ghcr.io
- name: Attest Build Provenance
if: github.event_name != 'pull_request'
uses: actions/attest-build-provenance@v2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build-and-push.outputs.digest }}
push-to-registry: false

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,49 +33,73 @@ 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-13 # x86_64 runner
- target: aarch64-apple-darwin
os: macos-15
os: macos-14 # ARM64 runner
env:
CARGO_BUILD_TARGET: ${{ matrix.target }}
# Hopefully temporary workaround for https://github.com/rust-lang/rust/issues/128401
# See the comments for similar rustflag settings at .cargo/config.toml for more context
RUSTFLAGS: ${{ matrix.target == 'aarch64-unknown-linux-musl' && '-Zlocation-detail=none -Clink-args=-lgcc' || '-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
@ -129,12 +148,10 @@ 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: |
@ -142,7 +159,7 @@ jobs:
target/${{ env.CARGO_BUILD_TARGET }}/release/oxipng.exe
- name: Upload analysis results to GitHub
uses: github/codeql-action/upload-sarif@v4
uses: github/codeql-action/upload-sarif@v3
if: always() && matrix.target == 'x86_64-unknown-linux-gnu'
continue-on-error: true
with:
@ -160,16 +177,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,36 +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.
@ -199,51 +166,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 +219,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 +249,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 +301,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 +333,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 +372,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 +397,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 +442,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 +474,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 +490,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 +501,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

440
Cargo.lock generated
View file

@ -1,18 +1,18 @@
# 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"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "anstream"
version = "1.0.0"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
@ -25,50 +25,56 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.14"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell_polyfill",
"once_cell",
"windows-sys",
]
[[package]]
name = "autocfg"
version = "1.5.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[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.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "bitvec"
@ -84,15 +90,15 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.20.2"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "bytemuck"
version = "1.25.0"
version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
[[package]]
name = "byteorder-lite"
@ -102,34 +108,33 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "cc"
version = "1.2.60"
version = "1.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[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.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
version = "4.5.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5"
dependencies = [
"anstream",
"anstyle",
@ -140,25 +145,34 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "1.1.0"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "colorchoice"
version = "1.0.5"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "crc32fast"
version = "1.5.0"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
@ -192,18 +206,18 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "env_filter"
version = "1.0.1"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
]
[[package]]
name = "env_logger"
version = "0.11.10"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
@ -219,9 +233,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
dependencies = [
"libc",
"windows-sys",
@ -237,16 +251,22 @@ dependencies = [
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
name = "filetime"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys",
]
[[package]]
name = "flate2"
version = "1.1.9"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
dependencies = [
"crc32fast",
"miniz_oxide",
@ -260,34 +280,33 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "glob"
version = "0.3.3"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "hashbrown"
version = "0.17.0"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "image"
version = "0.25.9"
version = "0.25.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png",
]
[[package]]
name = "indexmap"
version = "2.14.0"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
@ -296,27 +315,21 @@ dependencies = [
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
version = "1.70.1"
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"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "libc"
version = "0.2.185"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libdeflate-sys"
version = "1.25.2"
version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72753e0008ea87963d2f0770042d0df7abe51fafbb8dcaf618ac440f2f1fec0a"
checksum = "38b72ad3fbf5ac78f2df7b36075e48adf2459b57c150b9e63937d0204d0f9cd7"
dependencies = [
"cc",
"pkg-config",
@ -324,49 +337,44 @@ dependencies = [
[[package]]
name = "libdeflater"
version = "1.25.2"
version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1ee41cf6fb1bb6030dfb59ffb7bc01ab26aade44142084c87f0fc7a1658fe71"
checksum = "013344b17f9dceddff4872559ae19378bd8ee0479eccdd266d2dd2e894b4792f"
dependencies = [
"libdeflate-sys",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "log"
version = "0.4.30"
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"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"adler2",
"simd-adler32",
"bitflags 2.9.0",
"libc",
"redox_syscall",
]
[[package]]
name = "moxcms"
version = "0.7.11"
name = "linux-raw-sys"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "miniz_oxide"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
dependencies = [
"num-traits",
"pxfm",
"adler2",
"simd-adler32",
]
[[package]]
@ -379,80 +387,50 @@ dependencies = [
]
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "oxipng"
version = "10.1.1"
version = "9.1.5"
dependencies = [
"bitvec",
"clap",
"crossbeam-channel",
"env_logger",
"filetime",
"glob",
"image",
"indexmap",
"libdeflater",
"log",
"parse-size",
"rayon",
"rgb",
"rustc-hash",
"serde_json",
"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"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "png"
version = "0.18.1"
version = "0.17.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
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 +439,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,84 +449,51 @@ 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.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3"
dependencies = [
"bitflags 2.9.0",
]
[[package]]
name = "rgb"
version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
dependencies = [
"bytemuck",
]
[[package]]
name = "rustc-hash"
version = "2.1.2"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustix"
version = "1.1.4"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
dependencies = [
"bitflags",
"bitflags 2.9.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "serde"
version = "1.0.228"
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"
@ -557,9 +502,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[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 +512,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,20 +520,14 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "terminal_size"
version = "0.4.4"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
dependencies = [
"rustix",
"windows-sys",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf8parse"
version = "0.2.2"
@ -607,20 +535,78 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-link"
version = "0.2.1"
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wyz"
version = "0.5.1"
@ -630,17 +616,11 @@ 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.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7"
dependencies = [
"bumpalo",
"crc32fast",

View file

@ -4,7 +4,7 @@ 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",
@ -16,15 +16,15 @@ exclude = [
"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.5"
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,32 +38,55 @@ name = "zopfli"
required-features = ["zopfli"]
[dependencies]
zopfli = { version = "0.8.2", optional = true, default-features = false, features = ["std", "zlib"] }
rgb = "0.8.50"
indexmap = "2.9.0"
libdeflater = "1.23.1"
log = "0.4.27"
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 = "2.1.1"
[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.8"
[dev-dependencies]
serde_json = "1.0.150"
[dependencies.crossbeam-channel]
optional = true
version = "0.5.15"
[dependencies.filetime]
optional = true
version = "0.2.25"
[dependencies.rayon]
optional = true
version = "1.10.0"
[dependencies.clap]
optional = true
version = "4.5.36"
features = ["wrap_help"]
[target.'cfg(windows)'.dependencies.glob]
optional = true
version = "0.3.2"
[dependencies.image]
optional = true
default-features = false
features = ["png"]
version = "0.25.6"
[features]
binary = ["dep:clap", "dep:glob", "dep:env_logger", "dep:parse-size"]
default = ["binary", "parallel", "zopfli"]
parallel = ["dep:rayon", "indexmap/rayon"]
binary = ["dep:clap", "dep:glob", "dep:env_logger"]
default = ["binary", "parallel", "zopfli", "filetime"]
parallel = ["dep:rayon", "indexmap/rayon", "dep:crossbeam-channel"]
freestanding = ["libdeflater/freestanding"]
sanity-checks = ["dep:image"]
zopfli = ["dep:zopfli"]
filetime = ["dep:filetime"]
system-libdeflate = ["libdeflater/dynamic"]
[lib]
@ -85,37 +108,3 @@ assets = [
["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

@ -2,7 +2,7 @@
# check=error=true
FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
FROM --platform=$BUILDPLATFORM rust:1.85.1-alpine AS base
FROM --platform=$BUILDPLATFORM rust:1.74-alpine AS base
RUN apk update && \
apk add \
@ -37,7 +37,7 @@ 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"
LABEL org.opencontainers.image.source="https://github.com/shssoichiro/oxipng"
COPY --from=base /src/target/oxipng /usr/local/bin/oxipng

View file

@ -1,4 +1,4 @@
oxipng 10.1.0
oxipng 9.1.5
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.
@ -166,12 +159,12 @@ 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'.
@ -181,18 +174,6 @@ Options:
[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,15 +181,6 @@ 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.

View file

@ -1,19 +1,19 @@
# 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,
[Releases](https://github.com/shssoichiro/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.
@ -34,13 +34,13 @@ Oxipng can also 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.
@ -76,14 +76,7 @@ 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.
compression (`-Z`) is not guaranteed to always be better than without.
## Git integration via [pre-commit]
@ -92,8 +85,8 @@ 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.1.4
hooks:
- id: oxipng
args: ["-o", "4", "--strip", "safe", "--alpha"]
@ -102,16 +95,14 @@ repos:
## Docker
A Docker image is availlable at [`ghcr.io/oxipng/oxipng`](https://github.com/oxipng/oxipng/pkgs/container/oxipng) for `linux/amd64` and `linux/arm64`.
A Docker image is availlable at `ghcr.io/shssoichiro/oxipng` for `linux/amd64` and `linux/arm64`.
You can use it the following way:
```bash
docker run --rm -v $(pwd):/work ghcr.io/oxipng/oxipng -o 4 /work/file.png
docker run --rm -v $(pwd):/work ghcr.io/shssoichiro/oxipng -o 4 /work/file.png
```
Some older images are also available at [`ghcr.io/shssoichiro/oxipng`](https://github.com/users/shssoichiro/packages/container/package/oxipng).
## Library Usage
Although originally intended to be used as an executable, Oxipng can also be used as a library in
@ -124,27 +115,31 @@ input filename, into the [optimize function](https://docs.rs/oxipng/latest/oxipn
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 }`
`oxipng = { version = "9.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
- [Curtial](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
## 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 +149,42 @@ 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
```

View file

@ -7,7 +7,7 @@ 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.
To privately report a vulnerability in oxipng, please visit [our advisories page](https://github.com/shssoichiro/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,7 @@ 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 +21,7 @@ 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 +31,7 @@ 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 +41,7 @@ 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 +51,7 @@ 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 +59,7 @@ 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 +67,7 @@ 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 +77,7 @@ 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 +87,7 @@ 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 +97,7 @@ 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 +105,7 @@ 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 +113,7 @@ 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 +123,7 @@ 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 +133,7 @@ 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 +143,7 @@ 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 +151,7 @@ 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 +159,7 @@ 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 +169,7 @@ 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 +179,7 @@ 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 +189,7 @@ 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 +197,7 @@ 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 +205,7 @@ 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 +215,7 @@ 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 +225,7 @@ 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 +235,7 @@ 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 +243,7 @@ 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 +251,7 @@ 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 +261,7 @@ 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 +271,7 @@ 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 +281,5 @@ 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

@ -13,7 +13,7 @@ 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 +21,7 @@ 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 +29,7 @@ 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 +37,7 @@ 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]
@ -45,13 +45,5 @@ fn filters_brute(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(
FilterStrategy::Brute {
num_lines: 4,
level: 1,
},
false,
)
});
b.iter(|| 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();
});
}

View file

@ -1,7 +1,9 @@
use std::io::Write;
use crate::{
PngResult,
error::PngError,
headers::{read_be_u16, read_be_u32},
PngResult,
};
#[derive(Debug, Clone)]
@ -29,11 +31,11 @@ pub struct Frame {
impl Frame {
/// Construct a new Frame from the data in a fcTL chunk
pub fn from_fctl_data(byte_data: &[u8]) -> PngResult<Self> {
pub fn from_fctl_data(byte_data: &[u8]) -> PngResult<Frame> {
if byte_data.len() < 26 {
return Err(PngError::TruncatedData);
}
Ok(Self {
Ok(Frame {
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]),
@ -50,14 +52,15 @@ impl Frame {
#[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.write_all(&sequence_number.to_be_bytes()).unwrap();
byte_data.write_all(&self.width.to_be_bytes()).unwrap();
byte_data.write_all(&self.height.to_be_bytes()).unwrap();
byte_data.write_all(&self.x_offset.to_be_bytes()).unwrap();
byte_data.write_all(&self.y_offset.to_be_bytes()).unwrap();
byte_data.write_all(&self.delay_num.to_be_bytes()).unwrap();
byte_data.write_all(&self.delay_den.to_be_bytes()).unwrap();
byte_data.push(self.dispose_op);
byte_data.push(self.blend_op);
byte_data
}
@ -65,8 +68,8 @@ impl Frame {
#[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.write_all(&sequence_number.to_be_bytes()).unwrap();
byte_data.write_all(&self.data).unwrap();
byte_data
}
}

View file

@ -15,7 +15,11 @@ 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)
}
}
/// 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.")
@ -107,10 +97,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 +123,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))
@ -177,23 +163,24 @@ transformation and may be unsuitable for some applications.")
.action(ArgAction::SetTrue),
)
.arg(
// Note: The default value is not explicitly set here, as it is dependant on the `--nx` flag.
Arg::new("interlace")
.help("Set PNG interlacing (off, on, keep)")
.help("Set PNG interlacing type (0, 1, keep) [default: 0]")
.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.")
believe the benefits outweigh the costs for your use case.
[default: 0]")
.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")
.value_parser(["0", "1", "keep"])
.hide_possible_values(true),
)
.arg(
@ -212,7 +199,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 +207,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 +229,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.")
@ -298,8 +277,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 +309,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,8 +325,7 @@ 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),
)
@ -362,40 +338,9 @@ 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))
.value_parser(1..=255)
.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")
@ -409,21 +354,6 @@ high compression levels.")
.value_name("secs")
.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]")

View file

@ -116,7 +116,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

@ -22,7 +22,7 @@ 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)

View file

@ -1,20 +1,19 @@
mod deflater;
#[cfg(feature = "zopfli")]
use std::num::NonZeroU8;
use std::{fmt, fmt::Display};
pub use deflater::{crc32, deflate, inflate};
use crate::{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 {
pub enum Deflaters {
/// Use libdeflater.
Libdeflater {
/// Which compression level to use on the file (0-12)
@ -22,15 +21,20 @@ pub enum Deflater {
},
#[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 {
impl Deflaters {
pub(crate) fn deflate(self, data: &[u8], max_size: Option<usize>) -> 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 compressed.len() > 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}"),
#[cfg(feature = "zopfli")]
Self::Zopfli(options) => write!(f, "zopfli, zi = {}", options.iteration_count),
Self::Zopfli { iterations } => write!(f, "zopfli, zi = {iterations}"),
}
}
}

View file

@ -1,13 +1,19 @@
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());
let options = zopfli::Options {
iteration_count: iterations.into(),
..Default::default()
};
// 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(()) => (),
Ok(_) => (),
Err(_) => return Err(PngError::new("Failed to compress in zopfli")),
}
};
output.shrink_to_fit();
Ok(output)
}

View file

@ -2,22 +2,20 @@ 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,
APNGOutOfOrder,
InvalidData,
TruncatedData,
WriteFailed(String, std::io::Error),
ChunkMissing(&'static str),
InvalidDepthForType(BitDepth, ColorType),
IncorrectDataLength(usize, usize),
C2PAMetadataPreventsChanges,
Other(Box<str>),
}
@ -28,34 +26,27 @@ 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!(
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")
}
PngError::APNGNotSupported => f.write_str("APNG files are not (yet) supported"),
PngError::APNGOutOfOrder => f.write_str("APNG chunks are out of order"),
PngError::ChunkMissing(s) => write!(f, "Chunk {s} missing or empty"),
PngError::InvalidDepthForType(d, ref c) => {
write!(f, "Invalid bit depth {d} for color type {c}")
}
PngError::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)"
PngError::C2PAMetadataPreventsChanges => f.write_str(
"The image contains C2PA manifest that would be invalidated by any file changes",
),
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}")
}
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::Other(ref s) => f.write_str(s),
}
}
}
@ -63,7 +54,7 @@ impl fmt::Display for PngError {
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,42 +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 deflate::Deflaters;
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 image: Arc<PngImage>,
pub idat_data: Option<Vec<u8>>,
pub data: Vec<u8>,
pub data_is_compressed: bool,
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 filter: RowFilter,
// For determining tie-breaker
nth: usize,
}
impl Candidate {
fn cmp_key(&self) -> impl Ord + use<> {
fn cmp_key(&self) -> impl Ord {
(
self.estimated_output_size,
self.image.data.len(),
self.filter.clone(),
self.filter,
// Prefer the later image added (e.g. baseline, which is always added last)
usize::MAX - self.nth,
)
@ -49,8 +44,8 @@ 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>,
deflater: Deflaters,
optimize_alpha: bool,
final_round: bool,
nth: AtomicUsize,
@ -67,13 +62,13 @@ pub(crate) struct Evaluator {
impl Evaluator {
pub fn new(
deadline: Arc<Deadline>,
filters: IndexSet<FilterStrategy>,
deflater: Deflater,
filters: IndexSet<RowFilter>,
deflater: Deflaters,
optimize_alpha: bool,
final_round: bool,
) -> Self {
#[cfg(feature = "parallel")]
let eval_channel = channel();
let eval_channel = unbounded();
Self {
deadline,
filters,
@ -118,7 +113,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);
}
@ -146,37 +141,31 @@ 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 filtered = image.filter_image(filter, optimize_alpha);
let idat_data = deflater.deflate(&filtered, best_candidate_size.get());
if let Ok(idat_data) = idat_data {
let estimated_output_size = image.estimated_output_size(&idat_data);
trace!(
"Eval: {}-bit {:23} {:8} {} bytes",
image.ihdr.bit_depth, description, filter, estimated_output_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
// For the final round we need the IDAT data, otherwise the filtered data
let new = Candidate {
image: image.clone(),
idat_data: if final_round { Some(idat_data) } else { None },
data: if final_round { idat_data } else { filtered },
data_is_compressed: final_round,
estimated_output_size,
filter: filter.clone(),
filter_used,
filter,
nth,
};
best_candidate_size.set_min(estimated_output_size);
trace!(
"Eval: {}-bit {:23} {:8} {} bytes",
image.ihdr.bit_depth,
description,
filter,
estimated_output_size
);
#[cfg(feature = "parallel")]
{
@ -193,7 +182,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,30 @@
use std::{fmt, fmt::Display, mem::transmute};
use crate::error::PngError;
/// 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>),
}
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 +40,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 +52,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 +97,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!(),
}
}
@ -164,28 +139,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 +171,7 @@ impl RowFilter {
};
}
}
_ => unreachable!(),
}
}
}
@ -225,7 +183,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 +195,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 +211,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 +232,9 @@ impl RowFilter {
);
}
}
_ => return Err(PngError::InvalidData),
}
Ok(())
}
}

View file

@ -3,11 +3,12 @@ use log::{debug, trace, warn};
use rgb::{RGB16, RGBA8};
use crate::{
Deflater, Options, PngResult,
colors::{BitDepth, ColorType},
deflate::{crc32, inflate},
display_chunks::DISPLAY_CHUNKS,
error::PngError,
interlace::Interlacing,
Deflaters, Options, PngResult,
};
#[derive(Debug, Clone)]
@ -21,8 +22,8 @@ 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 {
@ -35,16 +36,18 @@ impl IhdrData {
/// 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 {
fn bitmap_size(bpp: usize, w: usize, h: usize) -> usize {
(w * bpp).div_ceil(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
}
}
}
@ -172,7 +173,10 @@ pub fn parse_next_chunk<'a>(
let chunk_bytes = &byte_data[chunk_start..chunk_start + 4 + length as usize];
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();
@ -205,16 +209,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,7 +223,7 @@ 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)
.map(|color| RGBA8::new(color[0], color[1], color[2], 255))
@ -248,7 +248,7 @@ pub fn read_be_u32(bytes: &[u8]) -> u32 {
}
/// 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,12 +263,9 @@ 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
@ -279,7 +276,7 @@ pub fn extract_icc(iccp: &Chunk, max_size: Option<usize>) -> Option<Vec<u8>> {
}
/// Make an iCCP chunk by compressing the ICC profile
pub fn make_iccp(icc: &[u8], deflater: Deflater, max_size: Option<usize>) -> PngResult<Chunk> {
pub fn make_iccp(icc: &[u8], deflater: Deflaters, max_size: Option<usize>) -> PngResult<Chunk> {
let mut compressed = deflater.deflate(icc, max_size)?;
let mut data = Vec::with_capacity(compressed.len() + 5);
data.extend(b"icc"); // Profile name - generally unused, can be anything
@ -334,7 +331,7 @@ pub fn preprocess_chunks(aux_chunks: &mut Vec<Chunk>, opts: &mut Options) {
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) {
} else if let Some(icc) = extract_icc(&aux_chunks[iccp_idx]) {
let intent = if may_replace_iccp {
srgb_rendering_intent(&icc)
} else {
@ -351,7 +348,7 @@ pub fn preprocess_chunks(aux_chunks: &mut Vec<Chunk>, opts: &mut Options) {
} 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)) {
if let Ok(iccp) = make_iccp(&icc, opts.deflate, Some(cur_len - 1)) {
debug!(
"Recompressed iCCP chunk: {} ({} bytes decrease)",
iccp.data.len(),
@ -410,16 +407,4 @@ pub fn postprocess_chunks(aux_chunks: &mut Vec<Chunk>, ihdr: &IhdrData, orig_ihd
!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,42 @@
use std::{fmt, fmt::Display};
use bitvec::prelude::*;
use crate::{headers::IhdrData, png::PngImage};
use crate::{headers::IhdrData, png::PngImage, PngError};
/// Whether to enable progressive rendering. See [`Options`][crate::Options])
#[repr(u8)]
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Interlacing {
/// Makes images load top to bottom.
None,
/// Makes it possible to render partially-loaded images at lower resolution. Usually increases file sizes.
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 +89,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 +103,7 @@ pub fn deinterlace_image(png: &PngImage) -> PngImage {
},
ihdr: IhdrData {
color_type: png.ihdr.color_type.clone(),
interlaced: false,
interlaced: Interlacing::None,
..png.ihdr
},
}
@ -149,7 +185,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,29 +26,28 @@ extern crate rayon;
mod rayon;
use std::{
fs::File,
io::{BufWriter, Read, Write, stdin, stdout},
path::PathBuf,
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,
deflate::Deflaters,
error::PngError,
filters::{FilterStrategy, RowFilter},
filters::RowFilter,
headers::StripChunks,
interlace::Interlacing,
options::{InFile, Options, OutFile},
};
use crate::{
@ -64,7 +82,6 @@ pub mod internal_tests {
}
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 +104,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,
@ -113,7 +130,7 @@ impl RawImage {
height,
color_type,
bit_depth,
interlaced: false,
interlaced: Interlacing::None,
},
data,
}),
@ -129,7 +146,7 @@ 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 };
let deflater = Deflaters::Libdeflater { compression: 1 };
if let Ok(iccp) = make_iccp(data, deflater, None) {
self.aux_chunks.push(iccp);
}
@ -153,7 +170,7 @@ impl RawImage {
let mut png = PngData {
raw: result.image,
idat_data: result.idat_data.unwrap(),
idat_data: result.data,
aux_chunks,
frames: Vec::new(),
};
@ -164,21 +181,44 @@ impl RawImage {
}
/// 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}");
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 +230,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!("{input}: Could not optimize further, no change written");
return Ok(());
}
_ => {
optimized_output = in_data;
@ -221,65 +261,51 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> Optimizatio
match (output, input) {
(OutFile::None, _) => {
info!("{savings}: Dry run, no output");
info!("{savings}: Running in pretend mode, no output");
}
(&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
@ -333,7 +359,7 @@ fn optimize_png(
};
if let Some(result) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_size) {
png.raw = result.image;
png.idat_data = result.idat_data.unwrap();
png.idat_data = result.data;
recompress_frames(png, &opts, deadline, result.filter)?;
postprocess_chunks(&mut png.aux_chunks, &png.raw.ihdr, &raw.ihdr);
}
@ -369,10 +395,8 @@ 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`."
);
if opts.interlace == Some(Interlacing::Adam7) && png.raw.ihdr.interlaced != Interlacing::Adam7 {
warn!("Interlacing was not enabled as it would result in a larger file. To override this, use `--force`.");
}
#[cfg(feature = "sanity-checks")]
@ -394,19 +418,19 @@ fn optimize_raw(
// 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 compression = match opts.deflate {
Deflaters::Libdeflater { compression } => {
if opts.fast_evaluation { 7 } else { 8 }.min(compression)
}
_ => 8,
};
let eval_deflater = Deflater::Libdeflater { compression };
let eval_deflater = Deflaters::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(
@ -414,7 +438,7 @@ fn optimize_raw(
eval_filters.clone(),
eval_deflater,
false,
opts.deflater == eval_deflater,
opts.deflate == eval_deflater,
);
let mut new_image = perform_reductions(image.clone(), opts, &deadline, &eval);
let eval_result = eval.get_best_candidate();
@ -431,23 +455,23 @@ fn optimize_raw(
let (result, deflater) = if opts.idat_recoding || reduction_occurred {
let result = perform_trials(
new_image,
new_image.clone(),
opts,
deadline,
deadline.clone(),
max_size,
eval_result,
eval_filters,
eval_deflater,
);
(result?, opts.deflater)
(result?, opts.deflate)
} else {
// 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)
if result.data_is_compressed
&& max_size.map_or(true, |max_size| result.estimated_output_size < max_size)
{
debug!("Found better result:");
debug!(" {}, f = {}", deflater, result.filter);
@ -463,27 +487,27 @@ fn perform_trials(
deadline: Arc<Deadline>,
max_size: Option<usize>,
mut eval_result: Option<Candidate>,
eval_filters: IndexSet<FilterStrategy>,
eval_deflater: Deflater,
eval_filters: IndexSet<RowFilter>,
eval_deflater: Deflaters,
) -> Option<Candidate> {
let mut filters = opts.filters.clone();
let mut filters = opts.filter.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();
filters = filters.difference(&eval_filters).copied().collect();
}
if !filters.is_empty() {
trace!("Evaluating {} filters", filters.len());
let eval = Evaluator::new(
deadline,
deadline.clone(),
filters,
eval_deflater,
opts.optimize_alpha,
opts.deflater == eval_deflater,
opts.deflate == eval_deflater,
);
if let Some(result) = &eval_result {
eval.set_best_size(result.estimated_output_size);
@ -497,21 +521,21 @@ fn perform_trials(
// We should have a result here - fail if not (e.g. deadline passed)
let mut result = eval_result?;
if result.idat_data.is_none() {
if !result.data_is_compressed {
// 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) {
debug!("Trying filter {} with {}", result.filter, opts.deflate);
match opts.deflate.deflate(&result.data, max_size) {
Ok(idat_data) => {
result.estimated_output_size = result.image.estimated_output_size(&idat_data);
result.idat_data = Some(idat_data);
result.data = idat_data;
result.data_is_compressed = true;
trace!("{} bytes", result.estimated_output_size);
}
Err(PngError::DeflatedDataTooLong(bytes)) => {
trace!(">{bytes} bytes");
}
Err(_) => (),
}
};
}
return Some(result);
}
@ -522,15 +546,15 @@ fn perform_trials(
// 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);
filters.insert(RowFilter::Bigrams);
} else {
// Otherwise delta filters generally don't work well, so just stick with None
filters.insert(FilterStrategy::NONE);
filters.insert(RowFilter::None);
}
}
debug!("Trying {} filters with {}", filters.len(), opts.deflater);
let eval = Evaluator::new(deadline, filters, opts.deflater, opts.optimize_alpha, true);
debug!("Trying {} filters with {}", filters.len(), opts.deflate);
let eval = Evaluator::new(deadline, filters, opts.deflate, opts.optimize_alpha, true);
if let Some(max_size) = max_size {
eval.set_best_size(max_size);
}
@ -590,14 +614,9 @@ 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
);
}
@ -606,13 +625,11 @@ fn recompress_frames(
png: &mut PngData,
opts: &Options,
deadline: Arc<Deadline>,
filter: FilterStrategy,
filter: RowFilter,
) -> PngResult<()> {
if !opts.idat_recoding || png.frames.is_empty() {
return Ok(());
}
// 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)
@ -625,9 +642,9 @@ fn recompress_frames(
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 filtered = image.filter_image(filter, opts.optimize_alpha);
let max_size = Some(frame.data.len() - 1);
if let Ok(data) = opts.deflater.deflate(&filtered, max_size) {
if let Ok(data) = opts.deflate.deflate(&filtered, max_size) {
debug!(
"Recompressed fdAT #{:<2}: {} ({} bytes decrease)",
i,
@ -641,6 +658,33 @@ fn recompress_frames(
}
/// 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: {atime:?}, mtime: {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,27 +1,32 @@
#![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::num::NonZeroU8;
use std::{
ffi::{OsStr, OsString},
fs::DirBuilder,
io::{IsTerminal, Write, stdout},
path::PathBuf,
process::ExitCode,
sync::atomic::{AtomicUsize, Ordering::AcqRel},
time::Duration,
ffi::OsString, fs::DirBuilder, io::Write, path::PathBuf, process::ExitCode, 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, PngError, RowFilter, StripChunks};
use rayon::prelude::*;
use crate::cli::DISPLAY_CHUNKS;
@ -31,14 +36,15 @@ fn main() -> ExitCode {
// 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) {
let (out_file, out_dir, opts) = match parse_opts_into_struct(&matches) {
Ok(x) => x,
Err(x) => {
error!("{x}");
@ -46,128 +52,51 @@ fn main() -> ExitCode {
}
};
// 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)]
let parallel_files = matches.get_flag("parallel-files");
let summary = if parallel_files {
files
.into_par_iter()
.map(|(input, output)| process_file(&input, &output, &opts))
.min()
} else {
collect_files(
inputs,
&out_dir,
&out_file,
matches.get_flag("recursive"),
true,
)
files
.into_iter()
.map(|(input, output)| process_file(&input, &output, &opts))
.min()
};
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();
match summary.unwrap_or(OptimizationResult::Skipped) {
OptimizationResult::Ok => ExitCode::SUCCESS,
OptimizationResult::Failed => ExitCode::FAILURE,
OptimizationResult::Skipped => ExitCode::from(3),
}
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;
}
}
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)
}
#[derive(Eq, PartialEq, Ord, PartialOrd)]
enum OptimizationResult {
Ok,
Failed,
Skipped,
}
fn collect_files(
@ -178,8 +107,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() == Some("-");
if !using_stdin && input.is_dir() {
if recursive {
match input.read_dir() {
Ok(dir) => {
@ -195,16 +126,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 +137,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
@ -240,9 +174,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 +184,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 +198,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") {
@ -310,7 +214,7 @@ fn parse_opts_into_struct(
match DirBuilder::new().recursive(true).create(path) {
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 +226,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 +250,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 +269,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()
};
}
@ -426,28 +328,13 @@ fn parse_opts_into_struct(
#[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
);
}
opts.deflater = Deflater::Zopfli(ZopfliOptions {
iteration_count,
iterations_without_improvement,
..Default::default()
});
let iterations = *matches.get_one::<i64>("iterations").unwrap();
opts.deflate = Deflaters::Zopfli {
iterations: NonZeroU8::new(iterations as u8).unwrap(),
};
}
if let (Deflater::Libdeflater { compression }, Some(x)) =
(&mut opts.deflater, matches.get_one::<i64>("compression"))
if let (Deflaters::Libdeflater { compression }, Some(x)) =
(&mut opts.deflate, matches.get_one::<i64>("compression"))
{
*compression = *x as u8;
}
@ -524,131 +411,22 @@ fn parse_numeric_range_opts(
}
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}");
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(_) => OptimizationResult::Ok,
Err(e @ PngError::C2PAMetadataPreventsChanges) => {
warn!("{input}: {e}");
OptimizationResult::Skipped
}
Err(e) => {
error!("{input}: {e}");
OptimizationResult::Failed
}
}
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,10 +4,10 @@ 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.
@ -32,8 +32,8 @@ impl OutFile {
///
/// 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,
}
@ -94,17 +94,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.
/// These are the interlacing types avaliable:
/// - `None` will not change the current interlacing type.
/// - `Some(x)` will change the file to interlacing mode `x`.
/// See [`Interlacing`] for the possible interlacing types.
///
/// Default: `Some(false)`
pub interlace: Option<bool>,
/// Default: `Some(Interlacing::None)`
pub interlace: Option<Interlacing>,
/// Whether to allow transparent pixels to be altered to improve compression.
///
/// Default: `false`
@ -144,7 +146,7 @@ pub struct Options {
#[cfg_attr(feature = "zopfli", doc = "(e.g. Zopfli)")]
///
/// Default: `Libdeflater`
pub deflater: Deflater,
pub deflate: Deflaters,
/// Whether to use fast evaluation to pick the best filter
///
/// Default: `true`
@ -154,11 +156,6 @@ pub struct Options {
///
/// 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 {
@ -188,88 +185,59 @@ impl Options {
// 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()
}
}
@ -279,13 +247,8 @@ impl Default for Options {
Self {
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 +257,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,36 @@
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
@ -43,18 +55,39 @@ pub struct PngData {
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)?;
@ -121,7 +154,7 @@ impl PngData {
});
}
b"acTL" => {
warn!("Stripping animation data from APNG - image will become standard PNG");
warn!("Stripping animation data from APNG - image will become standard PNG")
}
_ => (),
}
@ -131,19 +164,15 @@ 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 = PngImage::new(ihdr, &idat_data)?;
@ -163,15 +192,17 @@ impl PngData {
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");
@ -190,7 +221,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) {
@ -246,7 +277,7 @@ impl PngData {
}
impl PngImage {
pub fn new(ihdr: IhdrData, compressed_data: &[u8]) -> PngResult<Self> {
pub fn new(ihdr: IhdrData, compressed_data: &[u8]) -> Result<Self, PngError> {
let raw_data = deflate::inflate(compressed_data, ihdr.raw_data_size())?;
// Reject files with incorrect width/height or truncated data
@ -262,17 +293,18 @@ impl PngImage {
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<Self> {
if interlace == self.ihdr.interlaced {
return None;
}
Some(if interlace {
Some(if interlace == Interlacing::Adam7 {
// Convert progressive to interlaced data
interlace_image(self)
} else {
@ -305,10 +337,11 @@ impl PngImage {
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,
@ -330,7 +363,7 @@ impl PngImage {
}
/// 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 +375,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();
@ -353,11 +386,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,45 +399,31 @@ 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.push(RowFilter::None as u8);
filtered.extend_from_slice(&line_data);
prev_line = line_data;
filters_used.push(best_filter);
continue;
}
@ -416,12 +431,12 @@ impl PngImage {
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,11 +450,10 @@ 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;
@ -459,42 +473,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);
}
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
@ -511,33 +512,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 +545,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
}
}

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)]
@ -69,7 +69,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
@ -138,7 +138,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;

View file

@ -81,7 +81,7 @@ pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option<Png
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

View file

@ -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;

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::{
@ -54,10 +54,10 @@ pub fn reduced_to_indexed(png: &PngImage, allow_grayscale: bool) -> Option<PngIm
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).with_alpha(if Some(px) != transparency_pixel {
255
} else {
0
})
})
.collect()
@ -68,10 +68,10 @@ pub fn reduced_to_indexed(png: &PngImage, allow_grayscale: bool) -> Option<PngIm
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.with_alpha(if Some(px) != transparency_pixel {
255
} else {
0
})
})
.collect()
@ -193,7 +193,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,
};
@ -118,7 +118,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 +183,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
@ -133,7 +134,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 +156,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 {
@ -206,7 +207,7 @@ 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];
let mut counts = [0u32; 256];
for line in png.scan_lines(false) {
if let &[first, .., last] = line.data {
counts[first as usize] += 1;
@ -229,7 +230,7 @@ fn most_popular_edge_color(num_colors: usize, png: &PngImage) -> Option<usize> {
// 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 +262,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) {

View file

@ -44,7 +44,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: 296 KiB

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 87 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: 296 KiB

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 87 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: 296 KiB

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 87 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: 296 KiB

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 87 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: 296 KiB

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 87 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: 296 KiB

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 87 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: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View file

@ -12,16 +12,20 @@ const GRAYSCALE_ALPHA: u8 = 4;
const RGBA: u8 = 6;
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
let options = oxipng::Options {
let mut options = oxipng::Options {
force: true,
..Default::default()
};
let mut filter = IndexSet::new();
filter.insert(RowFilter::None);
options.filter = filter;
(OutFile::from_path(input.with_extension("out.png")), options)
}
fn test_it_converts(
input: &str,
filter: FilterStrategy,
filter: RowFilter,
color_type_in: u8,
bit_depth_in: BitDepth,
color_type_out: u8,
@ -31,14 +35,15 @@ fn test_it_converts(
let (output, mut opts) = get_opts(&input);
let png = PngData::new(&input, &opts).unwrap();
opts.filters = indexset! {filter};
opts.filter = IndexSet::new();
opts.filter.insert(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());
@ -63,7 +68,7 @@ fn test_it_converts(
fn filter_0_for_rgba_16() {
test_it_converts(
"tests/files/filter_0_for_rgba_16.png",
FilterStrategy::NONE,
RowFilter::None,
RGBA,
BitDepth::Sixteen,
RGBA,
@ -75,7 +80,7 @@ fn filter_0_for_rgba_16() {
fn filter_1_for_rgba_16() {
test_it_converts(
"tests/files/filter_1_for_rgba_16.png",
FilterStrategy::SUB,
RowFilter::Sub,
RGBA,
BitDepth::Sixteen,
RGBA,
@ -87,7 +92,7 @@ fn filter_1_for_rgba_16() {
fn filter_2_for_rgba_16() {
test_it_converts(
"tests/files/filter_2_for_rgba_16.png",
FilterStrategy::UP,
RowFilter::Up,
RGBA,
BitDepth::Sixteen,
RGBA,
@ -99,7 +104,7 @@ fn filter_2_for_rgba_16() {
fn filter_3_for_rgba_16() {
test_it_converts(
"tests/files/filter_3_for_rgba_16.png",
FilterStrategy::AVERAGE,
RowFilter::Average,
RGBA,
BitDepth::Sixteen,
RGBA,
@ -111,7 +116,7 @@ fn filter_3_for_rgba_16() {
fn filter_4_for_rgba_16() {
test_it_converts(
"tests/files/filter_4_for_rgba_16.png",
FilterStrategy::PAETH,
RowFilter::Paeth,
RGBA,
BitDepth::Sixteen,
RGBA,
@ -123,7 +128,7 @@ fn filter_4_for_rgba_16() {
fn filter_5_for_rgba_16() {
test_it_converts(
"tests/files/filter_5_for_rgba_16.png",
FilterStrategy::MinSum,
RowFilter::MinSum,
RGBA,
BitDepth::Sixteen,
RGBA,
@ -135,7 +140,7 @@ fn filter_5_for_rgba_16() {
fn filter_0_for_rgba_8() {
test_it_converts(
"tests/files/filter_0_for_rgba_8.png",
FilterStrategy::NONE,
RowFilter::None,
RGBA,
BitDepth::Eight,
RGBA,
@ -147,7 +152,7 @@ fn filter_0_for_rgba_8() {
fn filter_1_for_rgba_8() {
test_it_converts(
"tests/files/filter_1_for_rgba_8.png",
FilterStrategy::SUB,
RowFilter::Sub,
RGBA,
BitDepth::Eight,
RGBA,
@ -159,7 +164,7 @@ fn filter_1_for_rgba_8() {
fn filter_2_for_rgba_8() {
test_it_converts(
"tests/files/filter_2_for_rgba_8.png",
FilterStrategy::UP,
RowFilter::Up,
RGBA,
BitDepth::Eight,
RGBA,
@ -171,7 +176,7 @@ fn filter_2_for_rgba_8() {
fn filter_3_for_rgba_8() {
test_it_converts(
"tests/files/filter_3_for_rgba_8.png",
FilterStrategy::AVERAGE,
RowFilter::Average,
RGBA,
BitDepth::Eight,
RGBA,
@ -183,7 +188,7 @@ fn filter_3_for_rgba_8() {
fn filter_4_for_rgba_8() {
test_it_converts(
"tests/files/filter_4_for_rgba_8.png",
FilterStrategy::PAETH,
RowFilter::Paeth,
RGBA,
BitDepth::Eight,
RGBA,
@ -195,7 +200,7 @@ fn filter_4_for_rgba_8() {
fn filter_5_for_rgba_8() {
test_it_converts(
"tests/files/filter_5_for_rgba_8.png",
FilterStrategy::MinSum,
RowFilter::MinSum,
RGBA,
BitDepth::Eight,
RGBA,
@ -207,7 +212,7 @@ fn filter_5_for_rgba_8() {
fn filter_0_for_rgb_16() {
test_it_converts(
"tests/files/filter_0_for_rgb_16.png",
FilterStrategy::NONE,
RowFilter::None,
RGB,
BitDepth::Sixteen,
RGB,
@ -219,7 +224,7 @@ fn filter_0_for_rgb_16() {
fn filter_1_for_rgb_16() {
test_it_converts(
"tests/files/filter_1_for_rgb_16.png",
FilterStrategy::SUB,
RowFilter::Sub,
RGB,
BitDepth::Sixteen,
RGB,
@ -231,7 +236,7 @@ fn filter_1_for_rgb_16() {
fn filter_2_for_rgb_16() {
test_it_converts(
"tests/files/filter_2_for_rgb_16.png",
FilterStrategy::UP,
RowFilter::Up,
RGB,
BitDepth::Sixteen,
RGB,
@ -243,7 +248,7 @@ fn filter_2_for_rgb_16() {
fn filter_3_for_rgb_16() {
test_it_converts(
"tests/files/filter_3_for_rgb_16.png",
FilterStrategy::AVERAGE,
RowFilter::Average,
RGB,
BitDepth::Sixteen,
RGB,
@ -255,7 +260,7 @@ fn filter_3_for_rgb_16() {
fn filter_4_for_rgb_16() {
test_it_converts(
"tests/files/filter_4_for_rgb_16.png",
FilterStrategy::PAETH,
RowFilter::Paeth,
RGB,
BitDepth::Sixteen,
RGB,
@ -267,7 +272,7 @@ fn filter_4_for_rgb_16() {
fn filter_5_for_rgb_16() {
test_it_converts(
"tests/files/filter_5_for_rgb_16.png",
FilterStrategy::MinSum,
RowFilter::MinSum,
RGB,
BitDepth::Sixteen,
RGB,
@ -279,7 +284,7 @@ fn filter_5_for_rgb_16() {
fn filter_0_for_rgb_8() {
test_it_converts(
"tests/files/filter_0_for_rgb_8.png",
FilterStrategy::NONE,
RowFilter::None,
RGB,
BitDepth::Eight,
RGB,
@ -291,7 +296,7 @@ fn filter_0_for_rgb_8() {
fn filter_1_for_rgb_8() {
test_it_converts(
"tests/files/filter_1_for_rgb_8.png",
FilterStrategy::SUB,
RowFilter::Sub,
RGB,
BitDepth::Eight,
RGB,
@ -303,7 +308,7 @@ fn filter_1_for_rgb_8() {
fn filter_2_for_rgb_8() {
test_it_converts(
"tests/files/filter_2_for_rgb_8.png",
FilterStrategy::UP,
RowFilter::Up,
RGB,
BitDepth::Eight,
RGB,
@ -315,7 +320,7 @@ fn filter_2_for_rgb_8() {
fn filter_3_for_rgb_8() {
test_it_converts(
"tests/files/filter_3_for_rgb_8.png",
FilterStrategy::AVERAGE,
RowFilter::Average,
RGB,
BitDepth::Eight,
RGB,
@ -327,7 +332,7 @@ fn filter_3_for_rgb_8() {
fn filter_4_for_rgb_8() {
test_it_converts(
"tests/files/filter_4_for_rgb_8.png",
FilterStrategy::PAETH,
RowFilter::Paeth,
RGB,
BitDepth::Eight,
RGB,
@ -339,7 +344,7 @@ fn filter_4_for_rgb_8() {
fn filter_5_for_rgb_8() {
test_it_converts(
"tests/files/filter_5_for_rgb_8.png",
FilterStrategy::MinSum,
RowFilter::MinSum,
RGB,
BitDepth::Eight,
RGB,
@ -351,7 +356,7 @@ fn filter_5_for_rgb_8() {
fn filter_0_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_0_for_grayscale_alpha_16.png",
FilterStrategy::NONE,
RowFilter::None,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
@ -363,7 +368,7 @@ fn filter_0_for_grayscale_alpha_16() {
fn filter_1_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_1_for_grayscale_alpha_16.png",
FilterStrategy::SUB,
RowFilter::Sub,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
@ -375,7 +380,7 @@ fn filter_1_for_grayscale_alpha_16() {
fn filter_2_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_2_for_grayscale_alpha_16.png",
FilterStrategy::UP,
RowFilter::Up,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
@ -387,7 +392,7 @@ fn filter_2_for_grayscale_alpha_16() {
fn filter_3_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_3_for_grayscale_alpha_16.png",
FilterStrategy::AVERAGE,
RowFilter::Average,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
@ -399,7 +404,7 @@ fn filter_3_for_grayscale_alpha_16() {
fn filter_4_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_4_for_grayscale_alpha_16.png",
FilterStrategy::PAETH,
RowFilter::Paeth,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
@ -411,7 +416,7 @@ fn filter_4_for_grayscale_alpha_16() {
fn filter_5_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_5_for_grayscale_alpha_16.png",
FilterStrategy::MinSum,
RowFilter::MinSum,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
@ -423,7 +428,7 @@ fn filter_5_for_grayscale_alpha_16() {
fn filter_0_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_0_for_grayscale_alpha_8.png",
FilterStrategy::NONE,
RowFilter::None,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
@ -435,7 +440,7 @@ fn filter_0_for_grayscale_alpha_8() {
fn filter_1_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_1_for_grayscale_alpha_8.png",
FilterStrategy::SUB,
RowFilter::Sub,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
@ -447,7 +452,7 @@ fn filter_1_for_grayscale_alpha_8() {
fn filter_2_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_2_for_grayscale_alpha_8.png",
FilterStrategy::UP,
RowFilter::Up,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
@ -459,7 +464,7 @@ fn filter_2_for_grayscale_alpha_8() {
fn filter_3_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_3_for_grayscale_alpha_8.png",
FilterStrategy::AVERAGE,
RowFilter::Average,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
@ -471,7 +476,7 @@ fn filter_3_for_grayscale_alpha_8() {
fn filter_4_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_4_for_grayscale_alpha_8.png",
FilterStrategy::PAETH,
RowFilter::Paeth,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
@ -483,7 +488,7 @@ fn filter_4_for_grayscale_alpha_8() {
fn filter_5_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_5_for_grayscale_alpha_8.png",
FilterStrategy::MinSum,
RowFilter::MinSum,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
@ -495,7 +500,7 @@ fn filter_5_for_grayscale_alpha_8() {
fn filter_0_for_grayscale_16() {
test_it_converts(
"tests/files/filter_0_for_grayscale_16.png",
FilterStrategy::NONE,
RowFilter::None,
GRAYSCALE,
BitDepth::Sixteen,
GRAYSCALE,
@ -507,7 +512,7 @@ fn filter_0_for_grayscale_16() {
fn filter_1_for_grayscale_16() {
test_it_converts(
"tests/files/filter_1_for_grayscale_16.png",
FilterStrategy::SUB,
RowFilter::Sub,
GRAYSCALE,
BitDepth::Sixteen,
GRAYSCALE,
@ -519,7 +524,7 @@ fn filter_1_for_grayscale_16() {
fn filter_2_for_grayscale_16() {
test_it_converts(
"tests/files/filter_2_for_grayscale_16.png",
FilterStrategy::UP,
RowFilter::Up,
GRAYSCALE,
BitDepth::Sixteen,
GRAYSCALE,
@ -531,7 +536,7 @@ fn filter_2_for_grayscale_16() {
fn filter_3_for_grayscale_16() {
test_it_converts(
"tests/files/filter_3_for_grayscale_16.png",
FilterStrategy::AVERAGE,
RowFilter::Average,
GRAYSCALE,
BitDepth::Sixteen,
GRAYSCALE,
@ -543,7 +548,7 @@ fn filter_3_for_grayscale_16() {
fn filter_4_for_grayscale_16() {
test_it_converts(
"tests/files/filter_4_for_grayscale_16.png",
FilterStrategy::PAETH,
RowFilter::Paeth,
GRAYSCALE,
BitDepth::Sixteen,
GRAYSCALE,
@ -555,7 +560,7 @@ fn filter_4_for_grayscale_16() {
fn filter_5_for_grayscale_16() {
test_it_converts(
"tests/files/filter_5_for_grayscale_16.png",
FilterStrategy::MinSum,
RowFilter::MinSum,
GRAYSCALE,
BitDepth::Sixteen,
GRAYSCALE,
@ -567,7 +572,7 @@ fn filter_5_for_grayscale_16() {
fn filter_0_for_grayscale_8() {
test_it_converts(
"tests/files/filter_0_for_grayscale_8.png",
FilterStrategy::NONE,
RowFilter::None,
GRAYSCALE,
BitDepth::Eight,
GRAYSCALE,
@ -579,7 +584,7 @@ fn filter_0_for_grayscale_8() {
fn filter_1_for_grayscale_8() {
test_it_converts(
"tests/files/filter_1_for_grayscale_8.png",
FilterStrategy::SUB,
RowFilter::Sub,
GRAYSCALE,
BitDepth::Eight,
GRAYSCALE,
@ -591,7 +596,7 @@ fn filter_1_for_grayscale_8() {
fn filter_2_for_grayscale_8() {
test_it_converts(
"tests/files/filter_2_for_grayscale_8.png",
FilterStrategy::UP,
RowFilter::Up,
GRAYSCALE,
BitDepth::Eight,
GRAYSCALE,
@ -603,7 +608,7 @@ fn filter_2_for_grayscale_8() {
fn filter_3_for_grayscale_8() {
test_it_converts(
"tests/files/filter_3_for_grayscale_8.png",
FilterStrategy::AVERAGE,
RowFilter::Average,
GRAYSCALE,
BitDepth::Eight,
GRAYSCALE,
@ -615,7 +620,7 @@ fn filter_3_for_grayscale_8() {
fn filter_4_for_grayscale_8() {
test_it_converts(
"tests/files/filter_4_for_grayscale_8.png",
FilterStrategy::PAETH,
RowFilter::Paeth,
GRAYSCALE,
BitDepth::Eight,
GRAYSCALE,
@ -627,7 +632,7 @@ fn filter_4_for_grayscale_8() {
fn filter_5_for_grayscale_8() {
test_it_converts(
"tests/files/filter_5_for_grayscale_8.png",
FilterStrategy::MinSum,
RowFilter::MinSum,
GRAYSCALE,
BitDepth::Eight,
GRAYSCALE,
@ -639,7 +644,7 @@ fn filter_5_for_grayscale_8() {
fn filter_0_for_palette_4() {
test_it_converts(
"tests/files/filter_0_for_palette_4.png",
FilterStrategy::NONE,
RowFilter::None,
INDEXED,
BitDepth::Four,
INDEXED,
@ -651,7 +656,7 @@ fn filter_0_for_palette_4() {
fn filter_1_for_palette_4() {
test_it_converts(
"tests/files/filter_1_for_palette_4.png",
FilterStrategy::SUB,
RowFilter::Sub,
INDEXED,
BitDepth::Four,
INDEXED,
@ -663,7 +668,7 @@ fn filter_1_for_palette_4() {
fn filter_2_for_palette_4() {
test_it_converts(
"tests/files/filter_2_for_palette_4.png",
FilterStrategy::UP,
RowFilter::Up,
INDEXED,
BitDepth::Four,
INDEXED,
@ -675,7 +680,7 @@ fn filter_2_for_palette_4() {
fn filter_3_for_palette_4() {
test_it_converts(
"tests/files/filter_3_for_palette_4.png",
FilterStrategy::AVERAGE,
RowFilter::Average,
INDEXED,
BitDepth::Four,
INDEXED,
@ -687,7 +692,7 @@ fn filter_3_for_palette_4() {
fn filter_4_for_palette_4() {
test_it_converts(
"tests/files/filter_4_for_palette_4.png",
FilterStrategy::PAETH,
RowFilter::Paeth,
INDEXED,
BitDepth::Four,
INDEXED,
@ -699,7 +704,7 @@ fn filter_4_for_palette_4() {
fn filter_5_for_palette_4() {
test_it_converts(
"tests/files/filter_5_for_palette_4.png",
FilterStrategy::MinSum,
RowFilter::MinSum,
INDEXED,
BitDepth::Four,
INDEXED,
@ -711,7 +716,7 @@ fn filter_5_for_palette_4() {
fn filter_0_for_palette_2() {
test_it_converts(
"tests/files/filter_0_for_palette_2.png",
FilterStrategy::NONE,
RowFilter::None,
INDEXED,
BitDepth::Two,
INDEXED,
@ -723,7 +728,7 @@ fn filter_0_for_palette_2() {
fn filter_1_for_palette_2() {
test_it_converts(
"tests/files/filter_1_for_palette_2.png",
FilterStrategy::SUB,
RowFilter::Sub,
INDEXED,
BitDepth::Two,
INDEXED,
@ -735,7 +740,7 @@ fn filter_1_for_palette_2() {
fn filter_2_for_palette_2() {
test_it_converts(
"tests/files/filter_2_for_palette_2.png",
FilterStrategy::UP,
RowFilter::Up,
INDEXED,
BitDepth::Two,
INDEXED,
@ -747,7 +752,7 @@ fn filter_2_for_palette_2() {
fn filter_3_for_palette_2() {
test_it_converts(
"tests/files/filter_3_for_palette_2.png",
FilterStrategy::AVERAGE,
RowFilter::Average,
INDEXED,
BitDepth::Two,
INDEXED,
@ -759,7 +764,7 @@ fn filter_3_for_palette_2() {
fn filter_4_for_palette_2() {
test_it_converts(
"tests/files/filter_4_for_palette_2.png",
FilterStrategy::PAETH,
RowFilter::Paeth,
INDEXED,
BitDepth::Two,
INDEXED,
@ -771,7 +776,7 @@ fn filter_4_for_palette_2() {
fn filter_5_for_palette_2() {
test_it_converts(
"tests/files/filter_5_for_palette_2.png",
FilterStrategy::MinSum,
RowFilter::MinSum,
INDEXED,
BitDepth::Two,
INDEXED,
@ -783,7 +788,7 @@ fn filter_5_for_palette_2() {
fn filter_0_for_palette_1() {
test_it_converts(
"tests/files/filter_0_for_palette_1.png",
FilterStrategy::NONE,
RowFilter::None,
INDEXED,
BitDepth::One,
INDEXED,
@ -795,7 +800,7 @@ fn filter_0_for_palette_1() {
fn filter_1_for_palette_1() {
test_it_converts(
"tests/files/filter_1_for_palette_1.png",
FilterStrategy::SUB,
RowFilter::Sub,
INDEXED,
BitDepth::One,
INDEXED,
@ -807,7 +812,7 @@ fn filter_1_for_palette_1() {
fn filter_2_for_palette_1() {
test_it_converts(
"tests/files/filter_2_for_palette_1.png",
FilterStrategy::UP,
RowFilter::Up,
INDEXED,
BitDepth::One,
INDEXED,
@ -819,7 +824,7 @@ fn filter_2_for_palette_1() {
fn filter_3_for_palette_1() {
test_it_converts(
"tests/files/filter_3_for_palette_1.png",
FilterStrategy::AVERAGE,
RowFilter::Average,
INDEXED,
BitDepth::One,
INDEXED,
@ -831,7 +836,7 @@ fn filter_3_for_palette_1() {
fn filter_4_for_palette_1() {
test_it_converts(
"tests/files/filter_4_for_palette_1.png",
FilterStrategy::PAETH,
RowFilter::Paeth,
INDEXED,
BitDepth::One,
INDEXED,
@ -843,7 +848,7 @@ fn filter_4_for_palette_1() {
fn filter_5_for_palette_1() {
test_it_converts(
"tests/files/filter_5_for_palette_1.png",
FilterStrategy::MinSum,
RowFilter::MinSum,
INDEXED,
BitDepth::One,
INDEXED,

View file

@ -1,12 +1,13 @@
use indexmap::indexset;
use oxipng::{internal_tests::*, *};
use serde_json::Value;
#[cfg(feature = "zopfli")]
use std::num::NonZeroU8;
use std::{
fs::remove_file,
path::{Path, PathBuf},
process::Command,
};
use indexmap::indexset;
use oxipng::{internal_tests::*, *};
const GRAY: u8 = 0;
const RGB: u8 = 2;
const INDEXED: u8 = 3;
@ -15,7 +16,7 @@ fn get_opts(input: &Path) -> (OutFile, Options) {
let options = Options {
force: true,
fast_evaluation: false,
filters: indexset! {FilterStrategy::NONE},
filter: indexset! {RowFilter::None},
..Default::default()
};
(OutFile::from_path(input.with_extension("out.png")), options)
@ -51,7 +52,7 @@ fn test_it_converts_callbacks<CBPRE, CBPOST>(
match oxipng::optimize(&InFile::Path(input), output, opts) {
Ok(_) => (),
Err(x) => panic!("{}", x),
}
};
let output = output.path().unwrap();
assert!(output.exists());
@ -96,9 +97,13 @@ fn test_it_converts(
#[test]
fn verbose_mode() {
use log::{Level, LevelFilter, Log, Metadata, Record, set_logger, set_max_level};
use std::cell::RefCell;
use std::sync::mpsc::{Sender, channel};
#[cfg(not(feature = "parallel"))]
use std::sync::mpsc::{channel as unbounded, Sender};
#[cfg(feature = "parallel")]
use crossbeam_channel::{unbounded, Sender};
use log::{set_logger, set_max_level, Level, LevelFilter, Log, Metadata, Record};
// Rust runs tests in parallel by default.
// We want to make sure that we verify only logs from our test.
@ -138,7 +143,7 @@ fn verbose_mode() {
let input = PathBuf::from("tests/files/verbose_mode.png");
let (output, opts) = get_opts(&input);
let (sender, receiver) = channel();
let (sender, receiver) = unbounded();
let thread_init = move || {
// Initialise logs storage for all threads within our test.
@ -311,7 +316,7 @@ fn strip_chunks_none() {
fn interlacing_0_to_1() {
let input = PathBuf::from("tests/files/interlacing_0_to_1.png");
let (output, mut opts) = get_opts(&input);
opts.interlace = Some(true);
opts.interlace = Some(Interlacing::Adam7);
test_it_converts_callbacks(
input,
@ -322,10 +327,10 @@ fn interlacing_0_to_1() {
RGB,
BitDepth::Eight,
|png| {
assert!(!png.raw.ihdr.interlaced);
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
},
|png| {
assert!(png.raw.ihdr.interlaced);
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
},
);
}
@ -334,7 +339,7 @@ fn interlacing_0_to_1() {
fn interlacing_1_to_0() {
let input = PathBuf::from("tests/files/interlacing_1_to_0.png");
let (output, mut opts) = get_opts(&input);
opts.interlace = Some(false);
opts.interlace = Some(Interlacing::None);
test_it_converts_callbacks(
input,
@ -345,10 +350,10 @@ fn interlacing_1_to_0() {
RGB,
BitDepth::Eight,
|png| {
assert!(png.raw.ihdr.interlaced);
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
},
|png| {
assert!(!png.raw.ihdr.interlaced);
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
},
);
}
@ -357,7 +362,7 @@ fn interlacing_1_to_0() {
fn interlacing_0_to_1_small_files() {
let input = PathBuf::from("tests/files/interlacing_0_to_1_small_files.png");
let (output, mut opts) = get_opts(&input);
opts.interlace = Some(true);
opts.interlace = Some(Interlacing::Adam7);
test_it_converts_callbacks(
input,
@ -368,10 +373,10 @@ fn interlacing_0_to_1_small_files() {
RGB,
BitDepth::Eight,
|png| {
assert!(!png.raw.ihdr.interlaced);
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
},
|png| {
assert!(png.raw.ihdr.interlaced);
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
},
);
}
@ -380,7 +385,7 @@ fn interlacing_0_to_1_small_files() {
fn interlacing_1_to_0_small_files() {
let input = PathBuf::from("tests/files/interlacing_1_to_0_small_files.png");
let (output, mut opts) = get_opts(&input);
opts.interlace = Some(false);
opts.interlace = Some(Interlacing::None);
test_it_converts_callbacks(
input,
@ -391,10 +396,10 @@ fn interlacing_1_to_0_small_files() {
RGB,
BitDepth::Eight,
|png| {
assert!(png.raw.ihdr.interlaced);
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
},
|png| {
assert!(!png.raw.ihdr.interlaced);
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
},
);
}
@ -403,8 +408,8 @@ fn interlacing_1_to_0_small_files() {
fn interlaced_0_to_1_other_filter_mode() {
let input = PathBuf::from("tests/files/interlaced_0_to_1_other_filter_mode.png");
let (output, mut opts) = get_opts(&input);
opts.interlace = Some(true);
opts.filters = indexset! {FilterStrategy::PAETH};
opts.interlace = Some(Interlacing::Adam7);
opts.filter = indexset! {RowFilter::Paeth};
test_it_converts_callbacks(
input,
@ -415,10 +420,10 @@ fn interlaced_0_to_1_other_filter_mode() {
GRAY,
BitDepth::Sixteen,
|png| {
assert!(!png.raw.ihdr.interlaced);
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
},
|png| {
assert!(png.raw.ihdr.interlaced);
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
},
);
}
@ -427,9 +432,14 @@ fn interlaced_0_to_1_other_filter_mode() {
fn preserve_attrs() {
let input = PathBuf::from("tests/files/preserve_attrs.png");
#[cfg(feature = "filetime")]
let meta_input = input
.metadata()
.expect("unable to get metadata for input file");
.expect("unable to get file metadata for output file");
#[cfg(feature = "filetime")]
let atime_canon = filetime::FileTime::from_last_access_time(&meta_input);
#[cfg(feature = "filetime")]
let mtime_canon = filetime::FileTime::from_last_modification_time(&meta_input);
let (mut output, opts) = get_opts(&input);
if let OutFile::Path { preserve_attrs, .. } = &mut output {
@ -439,10 +449,27 @@ fn preserve_attrs() {
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
Err(x) => panic!("{}", x),
}
};
let output = output.path().unwrap();
assert!(output.exists());
#[cfg(feature = "filetime")]
let meta_output = output
.metadata()
.expect("unable to get file metadata for output file");
#[cfg(feature = "filetime")]
assert_eq!(
&atime_canon,
&filetime::FileTime::from_last_access_time(&meta_output),
"expected access time to be identical to that of input",
);
#[cfg(feature = "filetime")]
assert_eq!(
&mtime_canon,
&filetime::FileTime::from_last_modification_time(&meta_output),
"expected modification time to be identical to that of input",
);
match PngData::new(output, &opts) {
Ok(x) => x,
Err(x) => {
@ -451,21 +478,9 @@ fn preserve_attrs() {
}
};
let meta_output = output
.metadata()
.expect("unable to get metadata for output file");
assert_eq!(
meta_input.modified().ok(),
meta_output.modified().ok(),
"expected modification time to be identical to that of input",
);
assert_eq!(
meta_input.permissions(),
meta_output.permissions(),
"expected permissions to be identical to that of input",
);
remove_file(output).ok();
// TODO: Actually check permissions
}
#[test]
@ -629,25 +644,14 @@ fn scale_16() {
);
}
#[test]
fn max_size() {
let result = oxipng::optimize(
&"tests/files/max_size.png".into(),
&OutFile::None,
&Options {
max_decompressed_size: Some(150_000),
..Options::default()
},
);
assert!(matches!(result, Err(PngError::InflatedDataTooLong(_))));
}
#[test]
#[cfg(feature = "zopfli")]
fn zopfli_mode() {
let input = PathBuf::from("tests/files/zopfli_mode.png");
let (output, mut opts) = get_opts(&input);
opts.deflater = Deflater::Zopfli(ZopfliOptions::default());
opts.deflate = Deflaters::Zopfli {
iterations: NonZeroU8::new(15).unwrap(),
};
test_it_converts(
input,
@ -659,39 +663,3 @@ fn zopfli_mode() {
BitDepth::Eight,
);
}
#[test]
fn json_success() {
let path = "tests/files/json.png";
let output = Command::new(env!("CARGO_BIN_EXE_oxipng"))
.args(["--json", "--nx", "--nz", path])
.output()
.expect("Failed to run executable");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let data: Value = serde_json::from_str(&stdout).expect("Failed to parse JSON");
let result = &data["results"][0];
assert_eq!(result["input"], path);
assert_eq!(result["status"], "success");
assert_eq!(result["output"], path);
assert_eq!(result["insize"], result["outsize"]);
}
#[test]
fn json_dry_run() {
let path = "tests/files/json.png";
let path2 = "tests/files/escape chars \\ \" \n \t \r \x08 \x0c.png";
let output = Command::new(env!("CARGO_BIN_EXE_oxipng"))
.args(["--json", "--dry-run", path, path2])
.output()
.expect("Failed to run executable");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let data: Value = serde_json::from_str(&stdout).expect("Failed to parse JSON");
let result = &data["results"][0];
assert_eq!(result["input"], path);
assert_eq!(result["status"], "success");
assert_eq!(result["output"], Value::Null);
let result2 = &data["results"][1];
assert_eq!(result2["input"], path2);
assert_eq!(result2["status"], "error");
}

View file

@ -12,13 +12,16 @@ const GRAYSCALE_ALPHA: u8 = 4;
const RGBA: u8 = 6;
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
let options = oxipng::Options {
let mut options = oxipng::Options {
force: true,
fast_evaluation: false,
filters: indexset! {FilterStrategy::NONE},
interlace: None,
..Default::default()
};
let mut filter = IndexSet::new();
filter.insert(RowFilter::None);
options.filter = filter;
(OutFile::from_path(input.with_extension("out.png")), options)
}
@ -35,12 +38,12 @@ fn test_it_converts(
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);
assert!(png.raw.ihdr.interlaced);
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
Err(x) => panic!("{}", x),
}
};
let output = output.path().unwrap();
assert!(output.exists());

View file

@ -9,17 +9,20 @@ const RGB: u8 = 2;
const INDEXED: u8 = 3;
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
let options = oxipng::Options {
let mut options = oxipng::Options {
force: true,
filters: indexset! {FilterStrategy::NONE},
..Default::default()
};
let mut filter = IndexSet::new();
filter.insert(RowFilter::None);
options.filter = filter;
(OutFile::from_path(input.with_extension("out.png")), options)
}
fn test_it_converts(
input: &str,
interlace: bool,
interlace: Interlacing,
color_type_in: u8,
bit_depth_in: BitDepth,
color_type_out: u8,
@ -31,12 +34,19 @@ fn test_it_converts(
opts.interlace = Some(interlace);
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);
assert_eq!(png.raw.ihdr.interlaced, !interlace);
assert_eq!(
png.raw.ihdr.interlaced,
if interlace == Interlacing::Adam7 {
Interlacing::None
} else {
Interlacing::Adam7
}
);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
Err(x) => panic!("{}", x),
}
};
let output = output.path().unwrap();
assert!(output.exists());
@ -58,7 +68,7 @@ fn test_it_converts(
fn deinterlace_rgb_16() {
test_it_converts(
"tests/files/interlaced_rgb_16_should_be_rgb_16.png",
false,
Interlacing::None,
RGB,
BitDepth::Sixteen,
RGB,
@ -70,7 +80,7 @@ fn deinterlace_rgb_16() {
fn deinterlace_rgb_8() {
test_it_converts(
"tests/files/interlaced_rgb_8_should_be_rgb_8.png",
false,
Interlacing::None,
RGB,
BitDepth::Eight,
RGB,
@ -82,7 +92,7 @@ fn deinterlace_rgb_8() {
fn deinterlace_palette_8() {
test_it_converts(
"tests/files/interlaced_palette_8_should_be_palette_8.png",
false,
Interlacing::None,
INDEXED,
BitDepth::Eight,
INDEXED,
@ -94,7 +104,7 @@ fn deinterlace_palette_8() {
fn deinterlace_palette_4() {
test_it_converts(
"tests/files/interlaced_palette_4_should_be_palette_4.png",
false,
Interlacing::None,
INDEXED,
BitDepth::Four,
INDEXED,
@ -106,7 +116,7 @@ fn deinterlace_palette_4() {
fn deinterlace_palette_2() {
test_it_converts(
"tests/files/interlaced_palette_2_should_be_palette_2.png",
false,
Interlacing::None,
INDEXED,
BitDepth::Two,
INDEXED,
@ -118,7 +128,7 @@ fn deinterlace_palette_2() {
fn deinterlace_palette_1() {
test_it_converts(
"tests/files/interlaced_palette_1_should_be_palette_1.png",
false,
Interlacing::None,
INDEXED,
BitDepth::One,
INDEXED,
@ -130,7 +140,7 @@ fn deinterlace_palette_1() {
fn interlace_rgb_16() {
test_it_converts(
"tests/files/rgb_16_should_be_rgb_16.png",
true,
Interlacing::Adam7,
RGB,
BitDepth::Sixteen,
RGB,
@ -142,7 +152,7 @@ fn interlace_rgb_16() {
fn interlace_rgb_8() {
test_it_converts(
"tests/files/rgb_8_should_be_rgb_8.png",
true,
Interlacing::Adam7,
RGB,
BitDepth::Eight,
RGB,
@ -154,7 +164,7 @@ fn interlace_rgb_8() {
fn interlace_palette_8() {
test_it_converts(
"tests/files/palette_8_should_be_palette_8.png",
true,
Interlacing::Adam7,
INDEXED,
BitDepth::Eight,
INDEXED,
@ -166,7 +176,7 @@ fn interlace_palette_8() {
fn interlace_palette_4() {
test_it_converts(
"tests/files/palette_4_should_be_palette_4.png",
true,
Interlacing::Adam7,
INDEXED,
BitDepth::Four,
INDEXED,
@ -178,7 +188,7 @@ fn interlace_palette_4() {
fn interlace_palette_2() {
test_it_converts(
"tests/files/palette_2_should_be_palette_2.png",
true,
Interlacing::Adam7,
INDEXED,
BitDepth::Two,
INDEXED,
@ -190,7 +200,7 @@ fn interlace_palette_2() {
fn interlace_palette_1() {
test_it_converts(
"tests/files/palette_1_should_be_palette_1.png",
true,
Interlacing::Adam7,
INDEXED,
BitDepth::One,
INDEXED,

View file

@ -5,7 +5,7 @@ use oxipng::{internal_tests::*, *};
fn get_opts() -> Options {
Options {
force: true,
filters: indexset! {FilterStrategy::NONE},
filter: indexset! { RowFilter::None },
..Default::default()
}
}

View file

@ -12,12 +12,15 @@ const GRAYSCALE_ALPHA: u8 = 4;
const RGBA: u8 = 6;
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
let options = oxipng::Options {
let mut options = oxipng::Options {
force: true,
fast_evaluation: false,
filters: indexset! {FilterStrategy::NONE},
..Default::default()
};
let mut filter = IndexSet::new();
filter.insert(RowFilter::None);
options.filter = filter;
(OutFile::from_path(input.with_extension("out.png")), options)
}
@ -36,12 +39,12 @@ fn test_it_converts(
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in, "test file is broken");
assert!(!png.raw.ihdr.interlaced);
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
Err(x) => panic!("{}", x),
}
};
let output = output.path().unwrap();
assert!(output.exists());
@ -999,7 +1002,7 @@ fn palette_should_be_reduced_with_dupes() {
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
Err(x) => panic!("{}", x),
}
};
let output = output.path().unwrap();
assert!(output.exists());
@ -1036,7 +1039,7 @@ fn palette_should_be_reduced_with_unused() {
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
Err(x) => panic!("{}", x),
}
};
let output = output.path().unwrap();
assert!(output.exists());
@ -1074,7 +1077,7 @@ fn palette_should_be_reduced_with_bkgd() {
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
Err(x) => panic!("{}", x),
}
};
let output = output.path().unwrap();
assert!(output.exists());
@ -1112,7 +1115,7 @@ fn palette_should_be_reduced_with_both() {
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
Err(x) => panic!("{}", x),
}
};
let output = output.path().unwrap();
assert!(output.exists());
@ -1149,7 +1152,7 @@ fn palette_should_be_reduced_with_missing() {
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
Err(x) => panic!("{}", x),
}
};
let output = output.path().unwrap();
assert!(output.exists());

View file

@ -12,11 +12,14 @@ const GRAYSCALE_ALPHA: u8 = 4;
const RGBA: u8 = 6;
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
let options = oxipng::Options {
let mut options = oxipng::Options {
force: true,
filters: indexset! {FilterStrategy::NONE},
..Default::default()
};
let mut filter = IndexSet::new();
filter.insert(RowFilter::None);
options.filter = filter;
(OutFile::from_path(input.with_extension("out.png")), options)
}
@ -42,7 +45,7 @@ fn test_it_converts(
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
Err(x) => panic!("{}", x),
}
};
let output = output.path().unwrap();
assert!(output.exists());
@ -75,7 +78,7 @@ fn test_it_converts(
fn issue_42() {
let input = "tests/files/issue-42.png";
let (output, mut opts) = get_opts(Path::new(input));
opts.interlace = Some(true);
opts.interlace = Some(Interlacing::Adam7);
test_it_converts(
input,
Some((output, opts)),
@ -186,7 +189,7 @@ fn issue_175() {
fn issue_182() {
let input = "tests/files/issue-182.png";
let (output, mut opts) = get_opts(Path::new(input));
opts.interlace = Some(true);
opts.interlace = Some(Interlacing::Adam7);
test_it_converts(
input,

View file

@ -11,16 +11,20 @@ const INDEXED: u8 = 3;
const RGBA: u8 = 6;
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
let options = oxipng::Options {
let mut options = oxipng::Options {
force: true,
..Default::default()
};
let mut filter = IndexSet::new();
filter.insert(RowFilter::None);
options.filter = filter;
(OutFile::from_path(input.with_extension("out.png")), options)
}
fn test_it_converts(
input: &str,
filter: FilterStrategy,
filter: RowFilter,
color_type_in: u8,
bit_depth_in: BitDepth,
color_type_out: u8,
@ -30,14 +34,15 @@ fn test_it_converts(
let (output, mut opts) = get_opts(&input);
let png = PngData::new(&input, &opts).unwrap();
opts.filters = indexset! {filter};
opts.filter = IndexSet::new();
opts.filter.insert(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());
@ -62,7 +67,7 @@ fn test_it_converts(
fn filter_minsum() {
test_it_converts(
"tests/files/rgb_16_should_be_rgb_16.png",
FilterStrategy::MinSum,
RowFilter::MinSum,
RGB,
BitDepth::Sixteen,
RGB,
@ -74,7 +79,7 @@ fn filter_minsum() {
fn filter_entropy() {
test_it_converts(
"tests/files/rgb_8_should_be_rgb_8.png",
FilterStrategy::Entropy,
RowFilter::Entropy,
RGB,
BitDepth::Eight,
RGB,
@ -86,7 +91,7 @@ fn filter_entropy() {
fn filter_bigrams() {
test_it_converts(
"tests/files/rgba_8_should_be_rgba_8.png",
FilterStrategy::Bigrams,
RowFilter::Bigrams,
RGBA,
BitDepth::Eight,
RGBA,
@ -98,7 +103,7 @@ fn filter_bigrams() {
fn filter_bigent() {
test_it_converts(
"tests/files/grayscale_8_should_be_grayscale_8.png",
FilterStrategy::BigEnt,
RowFilter::BigEnt,
GRAYSCALE,
BitDepth::Eight,
GRAYSCALE,
@ -110,10 +115,7 @@ fn filter_bigent() {
fn filter_brute() {
test_it_converts(
"tests/files/palette_8_should_be_palette_8.png",
FilterStrategy::Brute {
num_lines: 4,
level: 1,
},
RowFilter::Brute,
INDEXED,
BitDepth::Eight,
INDEXED,

9
xtask/Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
version = 3
[[package]]
name = "anstream"
@ -100,12 +100,6 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "parse-size"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b"
[[package]]
name = "roff"
version = "0.2.2"
@ -203,5 +197,4 @@ version = "0.1.0"
dependencies = [
"clap",
"clap_mangen",
"parse-size",
]

View file

@ -8,4 +8,3 @@ publish = false
[dependencies]
clap = "4.5.21"
clap_mangen = "0.2.24"
parse-size = "1.1.0"