Compare commits

..

1 commit

Author SHA1 Message Date
Josh Holmer
85b0e8ec0b Initial work on APNG support 2018-10-12 13:44:17 -04:00
335 changed files with 6612 additions and 9882 deletions

View file

@ -1,2 +0,0 @@
[alias]
xtask = "run --manifest-path xtask/Cargo.toml --"

View file

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

View file

@ -1,11 +1,10 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.rs]
charset = utf-8
indent_style = space
indent_size = 4

2
.gitattributes vendored
View file

@ -1,2 +0,0 @@
# Enforce Unix newlines
* text=auto eol=lf

View file

@ -1,24 +0,0 @@
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"

View file

@ -1,138 +0,0 @@
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
permissions:
actions: read
contents: write
jobs:
deploy:
name: Deploy release
runs-on: ubuntu-latest
timeout-minutes: 30
# Prevent job from running on forks
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
- x86_64-unknown-linux-musl
- aarch64-unknown-linux-gnu
- aarch64-unknown-linux-musl
- x86_64-pc-windows-msvc
- i686-pc-windows-msvc
- x86_64-apple-darwin
- aarch64-apple-darwin
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
- 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"
- name: Retrieve ${{ matrix.target }} binary
uses: dawidd6/action-download-artifact@v21
with:
workflow: oxipng.yml
commit: ${{ github.sha }}
name: Oxipng binary (${{ matrix.target }})
path: target
- name: Generate up to date manual
run: scripts/manual.sh
- name: Build archives
working-directory: target
run: |
ARCHIVE_NAME="oxipng-${{ steps.oxipngMeta.outputs.version }}-${{ matrix.target }}"
mkdir "$ARCHIVE_NAME"
cp ../CHANGELOG.md ../README.md ../MANUAL.txt "$ARCHIVE_NAME"
case '${{ matrix.target }}' in
*-windows-*)
cp ../LICENSE "$ARCHIVE_NAME/LICENSE.txt"
cp oxipng.exe "$ARCHIVE_NAME"
zip "${ARCHIVE_NAME}.zip" "$ARCHIVE_NAME"/*;;
*)
cp ../LICENSE "$ARCHIVE_NAME"
cp oxipng "$ARCHIVE_NAME"
# Execute permissions are not stored in artifact files,
# so make the binary world-executable to meet user
# expectations set by preceding releases.
# Related issue:
# https://github.com/oxipng/oxipng/issues/575
chmod ugo+x "$ARCHIVE_NAME"/oxipng
tar -vczf "${ARCHIVE_NAME}.tar.gz" "$ARCHIVE_NAME"/*;;
esac
- name: Install AArch64 libc components
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get -yq update
# The shared libc AArch64 libraries are needed for cargo deb below
# to be able to infer package requirements with dpkg-shlibdeps
# properly
sudo apt-get -yq install libc6-arm64-cross libgcc-s1-arm64-cross
- name: Build Debian packages
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 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
with:
name: v${{ steps.oxipngMeta.outputs.version }}
body_path: RELEASE_NOTES.txt
files: |
target/*.zip
target/*.tar.gz
target/${{ matrix.target }}/debian/*.deb

View file

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

View file

@ -1,180 +0,0 @@
name: oxipng
on:
push:
pull_request:
types:
- opened
- 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
runs-on: ${{ matrix.os }}
timeout-minutes: 60
# Prevent tags and in-repo PRs from triggering this workflow more than once for a commit
if: github.ref_type != 'tag' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork)
strategy:
fail-fast: false
matrix:
target:
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
- aarch64-unknown-linux-gnu
- aarch64-unknown-linux-musl
- x86_64-pc-windows-msvc
- i686-pc-windows-msvc
- x86_64-apple-darwin
- aarch64-apple-darwin
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-24.04
target-apt-arch: amd64
- target: x86_64-unknown-linux-musl
os: ubuntu-24.04
target-apt-arch: amd64
- target: aarch64-unknown-linux-gnu
os: ubuntu-24.04-arm
target-apt-arch: arm64
- target: aarch64-unknown-linux-musl
os: ubuntu-24.04-arm
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
- target: aarch64-apple-darwin
os: macos-15
env:
CARGO_BUILD_TARGET: ${{ matrix.target }}
steps:
- name: Checkout source
uses: actions/checkout@v6
with:
persist-credentials: false
- 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 Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: nightly
target: ${{ env.CARGO_BUILD_TARGET }}
components: clippy, rustfmt, rust-src
cache-bin: false
cache-shared-key: cache
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Install cargo-hack
if: matrix.target == 'x86_64-unknown-linux-gnu'
uses: taiki-e/install-action@cargo-hack
- name: Install clippy-sarif
if: matrix.target == 'x86_64-unknown-linux-gnu'
uses: taiki-e/install-action@v2
with:
tool: clippy-sarif
- name: Install sarif-fmt
if: matrix.target == 'x86_64-unknown-linux-gnu'
uses: taiki-e/install-action@v2
with:
tool: sarif-fmt
- name: Run rustfmt
if: matrix.target == 'x86_64-unknown-linux-gnu'
run: cargo fmt --check
- name: Run Clippy for all feature combinations
if: matrix.target == 'x86_64-unknown-linux-gnu'
run: >
set -o pipefail;
cargo hack clippy --no-deps --all-targets --feature-powerset \
--exclude-features sanity-checks,system-libdeflate \
--message-format=json -- -D warnings \
| clippy-sarif
| tee clippy-results.sarif
| sarif-fmt
- name: Run tests
run: |
cargo nextest run --release --features sanity-checks
cargo test --doc --release --features sanity-checks
- name: Build benchmarks
run: cargo bench --no-run
- name: Build docs
if: matrix.target == 'x86_64-unknown-linux-gnu'
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"
- name: Upload CLI binary as artifact
uses: actions/upload-artifact@v7
with:
name: Oxipng binary (${{ matrix.target }})
path: |
target/${{ env.CARGO_BUILD_TARGET }}/release/oxipng
target/${{ env.CARGO_BUILD_TARGET }}/release/oxipng.exe
- name: Upload analysis results to GitHub
uses: github/codeql-action/upload-sarif@v4
if: always() && matrix.target == 'x86_64-unknown-linux-gnu'
continue-on-error: true
with:
sarif_file: clippy-results.sarif
category: clippy
msrv-check:
name: MSRV check
runs-on: ubuntu-latest
timeout-minutes: 30
# Prevent tags and in-repo PRs from triggering this workflow more than once for a commit
if: github.ref_type != 'tag' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork)
steps:
- name: Checkout source
uses: actions/checkout@v6
with:
persist-credentials: false
- 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
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Run tests
run: |
cargo nextest run --release --features sanity-checks
cargo test --doc --release --features sanity-checks

9
.gitignore vendored
View file

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

View file

@ -1,7 +0,0 @@
- id: oxipng
name: oxipng
description: 'Multithreaded PNG optimizer written in Rust.'
entry: oxipng
language: rust
types: [png]
require_serial: true

49
.travis.yml Normal file
View file

@ -0,0 +1,49 @@
language: rust
env:
global:
- PROJECT_NAME=oxipng
matrix:
include:
- os: osx
rust: stable
env: TARGET=x86_64-apple-darwin
cache: cargo
- os: linux
rust: 1.27.0
env: TARGET=x86_64-unknown-linux-gnu
cache: cargo
- os: linux
rust: stable
env: TARGET=x86_64-unknown-linux-gnu
cache: cargo
- os: linux
rust: beta
env: TARGET=x86_64-unknown-linux-gnu
cache: cargo
- os: linux
rust: nightly
env: TARGET=x86_64-unknown-linux-gnu
cache: cargo
allow_failures:
- rust: beta
- rust: nightly
deploy:
provider: releases
api_key:
secure: FarU1VnPW15WXkNP76rQH0VdB+dnp6Op7t2/bl5oCjn7OQ8R3vB32vxCP7DXtBvtE2U9z3CeRmE4pKIRN1rObb/GUyNKxPH0UJiTtDgGtxtTYZHdBjED2nEqdcA2+G1ElCub5O3NhtzsRLM1KPPukJcts6gCFgK/tgcDyKLMX/iGsXOuxOwqu6algZOTcHIaT4M8/TjH7g/hzOz8UKoaqZgSLuKLJ+J30LCJKK4umW57Nev66ATavBWQ3MIFCTyBc9YSsMSQcsjtlA7jcHNhelsCxQHzbxXWQK1jF439QXyNHog3tS41qmfMal+TUhW/NiSkqwJ1//PowD+oYI7si/biBawf6PU2oz5AfWknzPr4k6lAX2WYDcRLIkwEwKi5fHrvMYL0TuNWGb8gF+7DD+7DRaZrRLAbrA6SOFGydSPNzO7HvEYJjs39/2LfqXOQ0aie4ez5fGzsE655D+U1ulmVOx3Y6C1rxKi/oBVJZNuvdMPqtRpgbFYtAsk//wM+nt+YH8TndFR+x6AjZ9OTSDbbJwMLm1SckDLAE/sAC/vU+A6io82uMuDvKmPrtiOKsZp4kmwo5N1CWvIBtvd8evIMPAOFW51AncxnCvCiqYGtfB/BkKAr7mcxiwuWVwXMkOpSr7JzfQd8C5gOEMVczA9qnB6fRCY+dpxGjY0LSY0=
file_glob: true
file: ${PROJECT_NAME}-${TRAVIS_TAG}-${TARGET}.*
# don't delete the artifacts from previous phases
skip_cleanup: true
# deploy when a new tag is pushed
on:
# channel to use to produce the release artifacts
condition: $TRAVIS_RUST_VERSION = stable
tags: true
notifications:
email:
on_success: never

View file

@ -1,587 +1,265 @@
## Version 10.1.1
- [Performance] Improve Bigrams performance, giving notably faster results at lower levels.
- [Improvement] Change `--np` flag to also prevent conversion to indexed from other color types.
- [Improvement] Improve support for recompressing ICC profiles with high compression ratios.
- [Misc] Add warning when `--ziwi` exceeds `--zi`.
- [Build] Further reduce size of binaries.
## Version 10.1.0
- [Feature] Add `--json` option for machine-readable output.
- [Improvement] New default output with file counter and summary. (Use an extra `-v` flag to get the same output as before.)
- [Bugfix] Fix fast mode sometimes giving suboptimal results for small, indexed images.
## Version 10.0.0
- [Breaking] CLI: Change short zopfli flag from `-Z` to `-z`.
- [Breaking] CLI: Change `--pretend`/`-P` to `--dry-run`/`-d`.
- [Breaking] CLI: Change `--interlace` options from `1`/`0` to `on`/`off`.
- [Breaking] API: Change `Options.interlace: Option<Interlacing>` to `Options.interlace: Option<bool>`.
- [Breaking] API: Change `Options.filter: IndexSet<RowFilter>` to `Options.filters: IndexSet<FilterStrategy>`.
- [Breaking] API: Change `Options.deflate: Deflaters` to `Options.deflater: Deflater`.
- [Breaking] API: Change `Deflaters::Zopfli { .. }` to `Deflater::Zopfli(ZopfliOptions { .. })`.
- [Breaking] API: Change `optimize()` to return `(usize, usize)`, with original and optimized sizes.
- [Feature] Allow zopfli iterations higher than 255.
- [Feature] Add `--ziwi` option for zopfli iterations without improvement.
- [Feature] Add `--max-raw-size` option to skip images that are too large.
- [Feature] Add `--brute-level` and `--brute-lines` options for advanced control of Brute filter.
- [Improvement] Reduce memory usage for `fast` mode.
- [Improvement] Slightly improve compression with Brute filter at levels 5 and 6.
- [Misc] Change `--preserve` option to no longer preserve last access time.
- [Misc] Make help output colored.
## Version 9.1.5
- [Feature] Add `--sequential` option to process files sequentially rather than in parallel.
- [Performance] Update to latest Zopfli with greatly improved performance.
- [Improvement] Reduce memory usage.
- [Bugfix] Correct handling of grayscale conversion when ICC profile is present.
## Version 9.1.4
- [Improvement] Improve optimization of APNG files (reductions still not supported yet).
- [Improvement] Improve reductions for small images and ensure consistent results for repeat runs.
- [Build] Add feature `system-libdeflate` to use the system-installed version of libdeflate.
- [Misc] Strip C2PA metadata by default.
## Version 9.1.3
- [Feature] Add `--zi` option to control the number of Zopfli iterations.
- [Improvement] Allow setting compression level to 0.
- [Performance] Improve filtering performance for some images.
- [Build] Move man page generation to an xtask.
## Version 9.1.2
- [Bugfix] Fix `--nx` still applying deinterlacing by default.
- [Bugfix] Fix wildcard matching being case-sensitive on Windows.
- [Bugfix] Fix optimized APNGs not being compatible with some programs.
- [Build] Fix feature `sanity-checks` not working without `parallel`.
- [Misc] Resolve ambiguity between optional dependencies and crate features.
## Version 9.1.1
- [Build] Change man page generation path to resolve issue with cargo publish.
## Version 9.1.0
- [Improvement] Add `--keep display` equivalent to `--strip safe`.
- [Improvement] Add modified zeng palette sorting method, improving optimization of indexed images.
- [Improvement] If only one filter is specified, guarantee to only use this one.
- [Improvement] Evaluate low-depth indexed even if low-depth grayscale was already achieved.
- [Bugfix] Fix battiato palette sorting method not being used if the input was not already indexed.
- [Bugfix] Fix rare crash caused by a truncated palette.
- [Build] Reduce size of binaries.
- [Build] Add man page generation.
- [Build] Publish deb archives for Linux.
- [Misc] Bump minimum Rust version to 1.74.0.
## Version 9.0.0
- [Breaking] Remove `--backup` option. Use `--out` or `--dir` to preserve existing files.
- [Breaking] Remove `--check` option. Use `--nx --nz` to perform a non-optimizing run.
- [Breaking] API: Replace `pretend` option with `OutFile::None`.
- [Breaking] API: Move `preserve_attrs` into `OutFile::Path`.
- [Breaking] Default to removing interlacing. Use `-i keep` to retain existing interlacing.
- [Feature] Add Raw API for creating optimised PNGs from raw image data.
- [Feature] Add basic support for APNG files.
- [Feature] Add `--scale16` option to forcibly reduce 16-bit images to 8-bit.
- [Improvement] Process multiple files in parallel.
- [Improvement] Improve reductions, particularly for indexed or very small images.
- [Improvement] Improve compression with latest version of libdeflate.
- [Improvement] Recompress iCCP chunks.
- [Improvement] Change recursive mode to only process .png/.apng files.
- [Improvement] Add support for glob patterns in quotes on Windows.
- [Improvement] Quieter default output logging, with multiple levels of verbosity.
- [Bugfix] Fix deadlock when using oxipng within an existing Rayon thread pool.
- [Bugfix] Fix early abort in recursive mode when a read error occurred.
- [Bugfix] Fix losing aux chunks when there's more than one of the same type.
- [Bugfix] Fix sometimes writing output even when it was larger.
- [Misc] Revamp CI workflow to upload artifacts and generate binaries for additional architectures.
- [Misc] Bump minimum Rust version to 1.66.0.
## Version 8.0.0
- [Breaking] Revamp alpha optimization
- [Bugfix] Fix grayscale depth reduction with tRNS pixel
- [Bugfix] Fix fast mode with zopfli
- [Improvement] Tweaks to interlacing and format display
- [Improvement] Ability to reduce alpha channel to tRNS pixel
- [Improvement] Performance improvements to colorspace reduction
- [Misc] Disable image validation in release mode, as it is deemed no longer necessary
## Version 7.0.0
- [Breaking] Switch to libdeflater as the default
- [Breaking] Rebalance presets
- [Breaking] Update safe headers to remove
- [Breaking] Bump minimum Rust version to 1.61.0
- [Feature] Introduce new filter strategies
- [Feature] Allow setting libdeflate compression level
- [Feature] Add optional dockerization
- [Improvement] Improve alpha reductions
- [Improvement] Fast deinterlace for 8-bit
- [Improvement] Refactor heuristics for fast filter evaluation
- [Improvement] Improve file size reduction further
- [Bugfix] Avoid printing output bytes on decompress error
- [Bugfix] Fix compression level 0
- [Bugfix] Fix directional alpha reductions
- [Bugfix] Fix edge cases in palette reduction
### Version 6.0.1
- [Bugfix] Fix an issue where Zopfli mode could generate corrupt images
### Version 6.0.0
- [Breaking] Bump minimum Rust version to 1.57.0
- [Feature] Add `--check`/`-c` CLI option
- [Security] Update stderrlog to 0.5.2 (Fixes RUSTSEC-2022-0006)
- [Security] Remove chrono as a transitive dependency (Fixes RUSTSEC-2020-0159)
- [Misc] Bump `clap` to 3.2
- [Misc] Bump `zopfli` to 0.7
- [Misc] Bump `libdeflater` to 0.10
- [Misc] Remove `byteorder` dependency in favor of stdlib functions
- [Misc] Bump `image` to 0.24
- [Misc] Bump `crc` to 3.0
- [Misc] Bump `miniz_oxide` to 0.6
- [Misc] Update to Rust edition 2021
- [Misc] Various internal improvements
### Version 5.0.1
- [Bugfix] Fix an issue where -o5 and -o6 were not testing all options
- [Misc] Bump miniz_oxide to 0.5
### Version 5.0.0
- [Breaking] Bump minimum Rust version to 1.46.0
- [Bugfix] Avoid many panics that may occur on broken files
- [Bugfix] Fix race condition that may cause tests to fail
- [Performance] Optimizations to unfiltering and copying
- [Feature] Add `--ng` option to skip grayscale reduction
- [Feature] Also preserve timestamps when using `--preserve`
- [Misc] Bump `crc` to 2.x
### Version 4.0.3
- Bump itertools to 0.10.x
- Temporarily disable i686 releases, which were failing due to an odd linker issue,
so that at least amd64 builds will publish successfully
- This only relates to the releases published on Github. You can still manually compile oxipng for any platform.
### Version 4.0.2
- Includes another update to libdeflater that improves support for targets
without a C stdlib, like wasm32.
### Version 4.0.1
- Includes an update to libdeflater that improves support for targets
without a C stdlib, like wasm32.
### Version 4.0.0
- [Breaking] Bump minimum Rust version to 1.45.0
- [Feature] Make `libdeflater` and `zopfli` optional for API users
- [Bugfix] Fix cloudflare-zlib on aarch64 CPUs
- [Bugfix] Don't exit on finding a non-PNG file when crawling recursively
- [Bugfix] Make `rayon` truly optional
- Various internal improvements
### Version 3.0.1
- [Bugfix] Re-add `--force` flag to CLI
- This was accidentally removed somehow
- Many non-breaking dependency version bumps
### 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))
- 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))
- [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))
- 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))
### 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))
- 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))
### 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))
- Update `image` crate to 0.21.0
- Update `itertools` crate to 0.8.0
- Update `zopfli` crate to 0.4.0
- Use Rust edition 2018
- Bump minimum required Rust version to 1.31.0
### Version 2.1.8
- Fix non-standard sBIT headers in other code locations ([#153](https://github.com/oxipng/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))
### 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))
- 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
- Bump `image` crate to 0.20.0
- Bump `image` crate to 0.20.0
### Version 2.1.3
- Fix i686 builds
- Performance improvements
- Fix i686 builds
- Performance improvements
### 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))
- Minor fixes and cleanup
- [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/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
- Revert making Cloudflare zlib the default, as it introduced a major memory leak. It will be put back behind a feature flag, and reenabled when the issue is fixed.
- Revert minimum Rust version to 1.24.0
- Revert making Cloudflare zlib the default, as it introduced a major memory leak. It will be put back behind a feature flag, and reenabled when the issue is fixed.
- Revert minimum Rust version to 1.24.0
### Version 2.0.0
- [SEMVER_MAJOR] Make PngError a proper Error enum
- [SEMVER_MINOR] Bump minimum Rust version to 1.27.0
- [SEMVER_MINOR] Make Rayon an optional dependency (enabled by default)
- [SEMVER_MINOR] Option to limit wall clock time spent in optimization trials
- [SEMVER_MINOR] `--keep` option (works similar to `--strip`, but takes a comma-separated list of headers to keep, and removes all other non-critical headers)
- Use faster Cloudflare zlib compression on platforms that support it
- Allow specifying more than 2 filter types via the CLI
- Avoid double glob processing on unix
- Fix reading from stdin
- Cleanup help text
- Various performance improvements
- [SEMVER_MAJOR] Make PngError a proper Error enum
- [SEMVER_MINOR] Bump minimum Rust version to 1.27.0
- [SEMVER_MINOR] Make Rayon an optional dependency (enabled by default)
- [SEMVER_MINOR] Option to limit wall clock time spent in optimization trials
- [SEMVER_MINOR] `--keep` option (works similar to `--strip`, but takes a comma-separated list of headers to keep, and removes all other non-critical headers)
- Use faster Cloudflare zlib compression on platforms that support it
- Allow specifying more than 2 filter types via the CLI
- Avoid double glob processing on unix
- Fix reading from stdin
- Cleanup help text
- Various performance improvements
### Version 1.0.4
- Bump `image` to 0.19.0
- Bump `bit-vec` to 0.5.0
- Bump `regex` to 1.0.0
- Bump `image` to 0.19.0
- Bump `bit-vec` to 0.5.0
- Bump `regex` to 1.0.0
### Version 1.0.3
- Bump dependencies
- Bump dependencies
### Version 1.0.2
- Fix a change that breaks Itertools::flatten with recent Rust nightlies
- Fix a change that breaks Itertools::flatten with recent Rust nightlies
### Version 1.0.1
- Bump rayon to 1.0 ([#99](https://github.com/oxipng/oxipng/pull/99) @cuviper)
- Bump minor versions of other dependencies for binary distribution
- 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))
- 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.
- [SEMVER_MAJOR] Adjust the presets, now that `-zm` is no longer an option.
- `-o3` now tests all filter types. This will result in 50% more trials than before, but may give up to 10% more compression gain.
- `-o4` and higher now test all alpha optimization types. This adds 5 trials specific to the alpha channel. Only transparent images are affected.
- Implement unix-specific permissions copying for `-p` option
- Performance optimizations
- Refactor of internal code
- 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.
- [SEMVER_MAJOR] Adjust the presets, now that `-zm` is no longer an option.
- `-o3` now tests all filter types. This will result in 50% more trials than before, but may give up to 10% more compression gain.
- `-o4` and higher now test all alpha optimization types. This adds 5 trials specific to the alpha channel. Only transparent images are affected.
- Implement unix-specific permissions copying for `-p` option
- Performance optimizations
- Refactor of internal code
### Version 0.19.0
- [SEMVER_MAJOR] Default to overwriting the input file if `out_file` is not set.
This does not affect the CLI, but with the library, it was easy to forget to set the `out_file`,
and there was no warning that no output file would be written.
- Bump dependencies, reduces binary size by a considerable amount
- Hide all modules from documentation, and only export the specific structures that should be public.
Previously there were too many implementation details made public. The modules are still public for the purposes of our integration tests,
but we strongly advise against using undocumented modules. These may become private in the future.
- Internal refactoring and code cleanup
- Fix an error message that was displaying the wrong file path
- Fix an issue where the output file would not be written if the input was already optimized,
even if the output path was different from the input path
- [SEMVER_MAJOR] Default to overwriting the input file if `out_file` is not set.
This does not affect the CLI, but with the library, it was easy to forget to set the `out_file`,
and there was no warning that no output file would be written.
- Bump dependencies, reduces binary size by a considerable amount
- Hide all modules from documentation, and only export the specific structures that should be public.
Previously there were too many implementation details made public. The modules are still public for the purposes of our integration tests,
but we strongly advise against using undocumented modules. These may become private in the future.
- Internal refactoring and code cleanup
- Fix an error message that was displaying the wrong file path
- Fix an issue where the output file would not be written if the input was already optimized,
even if the output path was different from the input path
### 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))
- Bump `image` to 0.18
- 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))
- Bump `rayon` to 0.9
- 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))
- 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/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)
- Bump `image` to 0.16
- 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
- Remove VC++ download requirement for Windows users
- Remove VC++ download requirement for Windows users
### Version 0.17.0
- [SEMVER_MAJOR] Bump minimum required rustc version to 1.19.0
- [SEMVER_MINOR] Oxipng will now, by default, attempt to change all transparent pixels to `rgba(0, 0, 0, 0)` to improve compression.
It does fast trials with filters 0 and 5 to see if this is an improvement over
the existing alpha channel.
- [SEMVER_MINOR] Add a `-a` option to the command line (`alphas` in the struct) which enables 6 different
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))
- Code cleanup
- Bump `image` to 0.15
- [SEMVER_MAJOR] Bump minimum required rustc version to 1.19.0
- [SEMVER_MINOR] Oxipng will now, by default, attempt to change all transparent pixels to `rgba(0, 0, 0, 0)` to improve compression.
It does fast trials with filters 0 and 5 to see if this is an improvement over
the existing alpha channel.
- [SEMVER_MINOR] Add a `-a` option to the command line (`alphas` in the struct) which enables 6 different
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/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
- Publicly export `error` module
- Publicly export `error` module
### Version 0.16.1
- Fix rayon's breaking changes that they made in a point release
- Fix rayon's breaking changes that they made in a point release
### Version 0.16.0
- [SEMVER_MAJOR] Bump minimum rustc version to 1.17.0
- Bump `image` to 0.14
- Bump `rayon` to 0.8
- [SEMVER_MAJOR] Bump minimum rustc version to 1.17.0
- Bump `image` to 0.14
- Bump `rayon` to 0.8
### Version 0.15.2
- Bump `image` to 0.13 ([#65](https://github.com/oxipng/oxipng/pull/65))
- Bump `rayon` to 0.7
- Bump `itertools` to 0.6
- 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))
- Don't write new file if moving from interlaced to non-interlaced if new file would be larger
- [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
- Fix bug when reducing RGBA to Indexed if image has 256 colors plus a background color
- Fix bug when reducing RGBA to Indexed if image has 256 colors plus a background color
### Version 0.14.3
- Fix multiple bugs when reducing transparency palettes
- Fix multiple bugs when reducing transparency palettes
### Version 0.14.2
- Fix a bug when reducing palette in images with bit depth less than 8
- Fix a bug when reducing palette in images with transparency
- Fix a bug when reducing palette in images with bit depth less than 8
- Fix a bug when reducing palette in images with transparency
### Version 0.14.1
- Remove zlib dependency and switch entirely to miniz, since zlib 1.2.11 was not working with oxipng. This costs some performance, but is better than having a broken application.
- Remove zlib dependency and switch entirely to miniz, since zlib 1.2.11 was not working with oxipng. This costs some performance, but is better than having a broken application.
### Version 0.14.0
- Performance optimizations
- [SEMVER_MAJOR] Bump minimum rustc version to 1.13.0
- Add categories on crates.io
- Performance optimizations
- [SEMVER_MAJOR] Bump minimum rustc version to 1.13.0
- Add categories on crates.io
### Version 0.13.1
- Bump regex dependency to 0.2
- Bump byteorder dependency to 1.0
- Bump rayon dependency to 0.6
- Bump regex dependency to 0.2
- Bump byteorder dependency to 1.0
- Bump rayon dependency to 0.6
### Version 0.13.0
- Fix bug in certain PNG headers when reducing color type ([#52](https://github.com/oxipng/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
- 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))
- [SEMVER_MINOR] Add zopfli option (-Z), disabled by default. Gives about 10% better compression, but is currently 50-100x slower.
- Performance optimizations
- 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
- [SEMVER_MAJOR] Bump minimum rustc version to 1.9.0, required by dependencies
- [SEMVER_MINOR] Allow calling optimization presets via crate using `Options::from_preset`
- [SEMVER_MAJOR] Return proper `PngError` type which implements `std::error::Error` from `Result`s
- [SEMVER_MAJOR] Rename module `deflate::deflate` to `deflate`
- Performance optimizations
- [SEMVER_MAJOR] Bump minimum rustc version to 1.9.0, required by dependencies
- [SEMVER_MINOR] Allow calling optimization presets via crate using `Options::from_preset`
- [SEMVER_MAJOR] Return proper `PngError` type which implements `std::error::Error` from `Result`s
- [SEMVER_MAJOR] Rename module `deflate::deflate` to `deflate`
- Performance optimizations
### Version 0.10.0
- [SEMVER_MINOR] Make clap and regex dependencies optional
- Enabled by default, needed for executable build; can be disabled for use in crates
- Remove reduction from palette to grayscale, which was not working and provided minimal benefit
- [SEMVER_MINOR] Make clap and regex dependencies optional
- Enabled by default, needed for executable build; can be disabled for use in crates
- Remove reduction from palette to grayscale, which was not working and provided minimal benefit
### Version 0.9.0
- [SEMVER_MAJOR] Significant refactoring of modules
- Use `itertools` to cleanup areas of code
- Use multiple threads for filtering trials
- [SEMVER_MAJOR] Significant refactoring of modules
- Use `itertools` to cleanup areas of code
- Use multiple threads for filtering trials
### 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
- Minor optimizations
- Fix issue where interlaced images with certain widths would fail to optimize
- Minor optimizations
- Fix issue where interlaced images with certain widths would fail to optimize
### Version 0.8.0
- [SEMVER_MINOR] Add support for optimizing PNGs already loaded into memory via library function
- [SEMVER_MINOR] Add support for optimizing PNGs already loaded into memory via library function
### Version 0.7.0
- 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))
- 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/shssoichiro/oxipng/issues/39))
### Version 0.6.0
- Fix issue where output directory would not be created if it did not exist
- Use miniz for compression strategies where it outperforms zlib
- [SEMVER_MINOR] Partially implement -p / --preserve, as far as stable Rust will allow for now
- [SEMVER_MINOR] Implement --fix to ignore CRC errors and recalculate correct CRC in output
- Fix issue where output directory would not be created if it did not exist
- Use miniz for compression strategies where it outperforms zlib
- [SEMVER_MINOR] Partially implement -p / --preserve, as far as stable Rust will allow for now
- [SEMVER_MINOR] Implement --fix to ignore CRC errors and recalculate correct CRC in output
### Version 0.5.0
- [SEMVER_MINOR] Palette entries can now reduced, on by default ([#11](https://github.com/oxipng/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))
- [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/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))
- Update byteorder and clap crates
- Fix issue where interlaced images incorrectly applied filters on the first line of a pass
- Performance optimizations
- [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))
- Fix a bug that would cause oxipng to crash on very small images
- Properly decode interlaced images
- [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))
- Limit number of threads to 1.5x number of cores
- 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))
- Add rustdoc for public methods and structs
- 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))
- 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))
- [SEMVER_MAJOR] Allow 'safe', 'all', or comma-separated list as options for `--strip`
- [SEMVER_MINOR] Add `-s` alias for `--strip`
- Fix program version that is displayed when running `oxipng -V`
- 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/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
- Initial beta release
- Reduce color type and bit depth
- Recompress with zlib
- Multithreading
- Strip headers option
- Backup file before writing option
- Write to stdout option
- Initial beta release
- Reduce color type and bit depth
- Recompress with zlib
- Multithreading
- Strip headers option
- Backup file before writing option
- Write to stdout option

708
Cargo.lock generated
View file

@ -1,649 +1,459 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "adler32"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "adler2"
version = "2.0.1"
name = "adler32"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "anstream"
version = "1.0.0"
name = "ansi_term"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
"winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "anstyle"
version = "1.0.14"
name = "arrayvec"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
"nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
name = "atty"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
"libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
"termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
name = "bit-vec"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
version = "2.11.1"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
name = "bitvec"
version = "1.0.1"
name = "build_const"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "bumpalo"
version = "3.20.2"
name = "byteorder"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "bytemuck"
version = "1.25.0"
name = "byteorder"
version = "1.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "cc"
version = "1.2.60"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clap"
version = "4.6.1"
version = "2.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
"ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
"strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
name = "cloudflare-zlib-sys"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
"terminal_size",
"cc 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
name = "crc"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
"build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
"crossbeam-epoch 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
"arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
"cfg-if 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
"scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
dependencies = [
"cfg-if 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "deflate"
version = "0.7.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "either"
version = "1.15.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "env_filter"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
dependencies = [
"log",
]
[[package]]
name = "env_logger"
version = "0.11.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"log",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "flate2"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "glob"
version = "0.3.3"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "hashbrown"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]]
name = "image"
version = "0.25.9"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png",
"byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
"lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
"num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
"num-rational 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
"png 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "indexmap"
version = "2.14.0"
name = "inflate"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown",
"rayon",
"adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
name = "itertools"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
dependencies = [
"either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "itoa"
version = "1.0.18"
name = "lazy_static"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
dependencies = [
"version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "libc"
version = "0.2.185"
version = "0.2.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
[[package]]
name = "libdeflate-sys"
version = "1.25.2"
name = "lzw"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72753e0008ea87963d2f0770042d0df7abe51fafbb8dcaf618ac440f2f1fec0a"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "libdeflater"
version = "1.25.2"
name = "memoffset"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1ee41cf6fb1bb6030dfb59ffb7bc01ab26aade44142084c87f0fc7a1658fe71"
dependencies = [
"libdeflate-sys",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
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"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
"adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "moxcms"
version = "0.7.11"
name = "nodrop"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "num-integer"
version = "0.1.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
dependencies = [
"num-traits",
"pxfm",
"num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num-iter"
version = "0.1.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num-rational"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num-traits"
version = "0.2.19"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
name = "num_cpus"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
dependencies = [
"libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "oxipng"
version = "10.1.1"
version = "2.1.5"
dependencies = [
"bitvec",
"clap",
"env_logger",
"glob",
"image",
"indexmap",
"libdeflater",
"log",
"parse-size",
"rayon",
"rgb",
"rustc-hash",
"serde_json",
"zopfli",
"bit-vec 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)",
"cloudflare-zlib-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
"image 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)",
"itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)",
"miniz_oxide 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rayon 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"wild 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"zopfli 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "parse-size"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b"
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "png"
version = "0.18.1"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [
"bitflags",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
"bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
"deflate 0.7.19 (registry+https://github.com/rust-lang/crates.io-index)",
"inflate 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rayon"
version = "1.12.0"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
"crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rayon-core 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
"crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rgb"
version = "0.8.53"
name = "redox_syscall"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "redox_termios"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
dependencies = [
"bytemuck",
"redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rustc-hash"
version = "2.1.2"
name = "scopeguard"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "simd-adler32"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "strsim"
version = "0.11.1"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.117"
name = "termion"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
"libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "tap"
version = "1.0.1"
name = "textwrap"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "terminal_size"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
dependencies = [
"rustix",
"windows-sys",
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
name = "typed-arena"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf8parse"
version = "0.2.2"
name = "unicode-width"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-link"
version = "0.2.1"
name = "vec_map"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
name = "version_check"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "wild"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
"glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "wyz"
version = "0.5.1"
name = "winapi"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
"winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zmij"
version = "1.0.21"
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zopfli"
version = "0.8.3"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
"adler32 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
"crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
"typed-arena 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[metadata]
"checksum adler32 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e928aa58f6dbd754bda26eca562a242549cb606e27a2240fc305fc75a7f12af9"
"checksum adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7e522997b529f05601e05166c07ed17789691f562762c7f3b987263d2dedee5c"
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
"checksum arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a1e964f9e24d588183fcb43503abda40d288c8657dfc27311516ce2f05675aef"
"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
"checksum bit-vec 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4440d5cb623bb7390ae27fec0bb6c61111969860f8e3ae198bfa0663645e67cf"
"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12"
"checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39"
"checksum byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0fc10e8cc6b2580fda3f36eb6dc5316657f812a3df879a44a66fc9f0fdbc4855"
"checksum byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "90492c5858dd7d2e78691cfb89f90d273a2800fc11d98f60786e5d87e2f83781"
"checksum cc 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "f159dfd43363c4d08055a07703eb7a3406b0dac4d0584d96965a3262db3c9d16"
"checksum cfg-if 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0c4e7bb64a8ebb0d856483e1e682ea3422f883c5f5615a90d51a2c82fe87fdd3"
"checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e"
"checksum cloudflare-zlib-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1a27574ebbf993003163c445f5f70d3fa9017f09013e5007889991f46aa85ea2"
"checksum crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb"
"checksum crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f739f8c5363aca78cfb059edf753d8f0d36908c348f3d8d1503f03d8b75d9cf3"
"checksum crossbeam-epoch 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "927121f5407de9956180ff5e936fe3cf4324279280001cd56b669d28ee7e9150"
"checksum crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2760899e32a1d58d5abb31129f8fae5de75220bc2176e77ff7c627ae45c918d9"
"checksum deflate 0.7.19 (registry+https://github.com/rust-lang/crates.io-index)" = "8a6abb26e16e8d419b5c78662aa9f82857c2386a073da266840e474d5055ec86"
"checksum either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0"
"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
"checksum image 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60710fd3cb40c2434451d8d5147bcf39bbb68aae0741041133e09439cb2401e3"
"checksum inflate 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6f53b811ee8e2057ccf9643ca6b4277de90efaf5e61e55fd5254576926bb4245"
"checksum itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)" = "f58856976b776fedd95533137617a02fb25719f40e7d9b01c7043cd65474f450"
"checksum lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca488b89a5657b0a2ecd45b95609b3e848cf1755da332a0da46e2b2b1cb371a7"
"checksum libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)" = "76e3a3ef172f1a0b9a9ff0dd1491ae5e6c948b94479a3021819ba7d860c8645d"
"checksum lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084"
"checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3"
"checksum miniz_oxide 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9ba430291c9d6cedae28bcd2d49d1c32fc57d60cd49086646c5dd5673a870eb5"
"checksum nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2228dca57108069a5262f2ed8bd2e82496d2e074a06d1ccc7ce1687b6ae0a2"
"checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea"
"checksum num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "af3fdbbc3291a5464dc57b03860ec37ca6bf915ed6ee385e7c6c052c422b2124"
"checksum num-rational 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4e96f040177bb3da242b5b1ecf3f54b5d5af3efbbfb18608977a5d2767b22f10"
"checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1"
"checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30"
"checksum png 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f54b9600d584d3b8a739e1662a595fab051329eff43f20e7d8cc22872962145b"
"checksum rayon 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "df7a791f788cb4c516f0e091301a29c2b71ef680db5e644a7d68835c8ae6dbfa"
"checksum rayon-core 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b055d1e92aba6877574d8fe604a63c8b5df60f60e5982bf7ccbb1338ea527356"
"checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1"
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
"checksum textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "307686869c93e71f94da64286f9a9524c0f308a9e1c87a583de8e9c9039ad3f6"
"checksum typed-arena 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c6c06a92aef38bb4dc5b0df00d68496fc31307c5344c867bb61678c6e1671ec5"
"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526"
"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
"checksum wild 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "690e5dbd46cfaf2f3bd09875ad94e92cc56459fce505807d6ce5332671aa93ae"
"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0"
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
"checksum zopfli 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "9fd2fb70c78a87410032abe2a9eb2bec4984a2d9bc26adb1f8f768e02673e84e"

View file

@ -1,30 +1,23 @@
[package]
authors = ["Joshua Holmer <jholmer.in@gmail.com>"]
categories = ["command-line-utilities", "compression"]
description = "A lossless PNG compression optimizer"
keywords = ["png", "image-compression", "optimization", "multi-threading", "lossless"]
documentation = "https://docs.rs/oxipng"
edition = "2024"
exclude = [
".editorconfig",
".gitattributes",
".github/*",
".gitignore",
".pre-commit-hooks.yaml",
"Dockerfile",
"scripts/*",
"tests/*",
"xtask/*",
categories = [
"command-line-utilities",
"compression",
]
homepage = "https://github.com/oxipng/oxipng"
description = "A lossless PNG compression optimizer"
documentation = "https://docs.rs/oxipng"
exclude = [
"tests/*",
"bench/*",
]
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 = "2.1.5"
[badges]
travis-ci = { repository = "oxipng/oxipng", branch = "master" }
travis-ci = { repository = "shssoichiro/oxipng", branch = "master" }
maintenance = { status = "actively-developed" }
[[bin]]
@ -33,89 +26,53 @@ name = "oxipng"
path = "src/main.rs"
required-features = ["binary"]
[[bench]]
name = "zopfli"
required-features = ["zopfli"]
[dependencies]
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"] }
bit-vec = "^0.5.0"
byteorder = "^1.0.0"
crc = "^1.2.0"
itertools = "^0.7.7"
num_cpus = "^1.0.0"
zopfli = "^0.3.4"
miniz_oxide = "0.1.2"
[target.'cfg(windows)'.dependencies]
glob = { version = "0.3.3", optional = true }
[dependencies.rayon]
optional = true
version = "^1.0.0"
[dev-dependencies]
serde_json = "1.0.150"
[dependencies.clap]
optional = true
version = "^2.10.0"
[dependencies.wild]
optional = true
version = "2.0.0"
[target.'cfg(any(target_arch = "x86_64", target_arch = "aarch64"))'.dependencies]
cloudflare-zlib-sys = "^0.1.2"
[dependencies.image]
default-features = false
features = ["png_codec"]
version = "^0.20.0"
[features]
binary = ["dep:clap", "dep:glob", "dep:env_logger", "dep:parse-size"]
default = ["binary", "parallel", "zopfli"]
parallel = ["dep:rayon", "indexmap/rayon"]
freestanding = ["libdeflater/freestanding"]
sanity-checks = ["dep:image"]
zopfli = ["dep:zopfli"]
system-libdeflate = ["libdeflater/dynamic"]
binary = [
"clap",
"wild",
]
default = ["binary", "parallel"]
parallel = ["rayon"]
dev = [
"nightly-binary",
]
nightly = []
nightly-binary = [
"binary",
"nightly",
]
[lib]
name = "oxipng"
path = "src/lib.rs"
[profile.dev]
opt-level = 2
[profile.release]
lto = "fat"
strip = "symbols"
panic = "abort"
[package.metadata.deb]
assets = [
["target/release/oxipng", "usr/bin/", "755"],
["target/xtask/mangen/manpages/oxipng.1", "usr/share/man/man1/", "644"],
["README.md", "usr/share/doc/oxipng/", "644"],
["CHANGELOG.md", "usr/share/doc/oxipng/", "644"],
]
[lints.rust]
missing_copy_implementations = "deny"
missing_debug_implementations = "deny"
trivial_casts = "warn"
trivial_numeric_casts = "warn"
unused_import_braces = "warn"
[lints.clippy]
cloned_instead_of_copied = "warn"
expl_impl_clone_on_copy = "warn"
float_cmp_const = "warn"
if_not_else = "warn"
ignored_unit_patterns = "warn"
linkedlist = "warn"
manual_let_else = "warn"
map_flatten = "warn"
map_unwrap_or = "warn"
match_same_arms = "warn"
mem_forget = "warn"
missing_const_for_fn = "warn"
mut_mut = "warn"
mutex_integer = "warn"
needless_continue = "warn"
option_if_let_else = "warn"
path_buf_push_overwrite = "warn"
range_plus_one = "warn"
redundant_clone = "warn"
redundant_closure_for_method_calls = "warn"
semicolon_if_nothing_returned = "warn"
unnecessary_semicolon = "warn"
unseparated_literal_suffix = "warn"
use_self = "warn"
useless_let_if_seq = "warn"

View file

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

View file

@ -1,227 +0,0 @@
oxipng 10.1.0
Losslessly improve compression of PNG files
Usage: oxipng [OPTIONS] <files>...
Arguments:
<files>...
File(s) to compress (use '-' for stdin)
Options:
-o, --opt <level>
Set the optimization level preset. The default level 2 is quite fast and provides good
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)
Manually specifying a compression option (zc, f, etc.) will override the optimization
preset, regardless of the order you write the arguments.
[default: 2]
-r, --recursive
When directories are given as input, traverse the directory trees and optimize all PNG
files found (files with “.png” or “.apng” extension).
--dir <directory>
Write output file(s) to <directory>. If the directory does not exist, it will be created.
Note that this will not preserve the directory structure of the input files when used with
'--recursive'.
--out <file>
Write output file to <file>
--stdout
Write output to stdout
-p, --preserve
Preserve file permissions and timestamps if possible
-d, --dry-run
Do not write any files, only show compression results
-s
Strip safely-removable chunks, same as '--strip safe'
--strip <mode>
Strip metadata chunks, where <mode> is one of:
safe => Strip all non-critical chunks, except for the following:
cICP, iCCP, sRGB, pHYs, acTL, fcTL, fdAT
all => Strip all non-critical chunks
<list> => Strip chunks in the comma-separated list, e.g. 'bKGD,cHRM'
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.
The default when --strip is not passed is to keep all chunks that remain valid.
--keep <list>
Strip all metadata chunks except those in the comma-separated list. The special value
'display' includes chunks that affect the image appearance, equivalent to '--strip safe'.
E.g. '--keep eXIf,display' will strip chunks, keeping only eXIf and those that affect the
image appearance.
-a, --alpha
Perform additional optimization on images with an alpha channel, by altering the color
values of fully transparent pixels. This is generally recommended for better compression,
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:
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
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]
--scale16
Forcibly reduce images with 16 bits per channel to 8 bits per channel. This is a lossy
operation but can provide significant savings when you have no need for higher depth.
Reduction is performed by scaling the values such that, e.g. 0x00FF is reduced to 0x01
rather than 0x00.
Without this flag, 16-bit images will only be reduced in depth if it can be done
losslessly.
-v, --verbose...
Show per-file info (use multiple times for more detail)
-q, --quiet
Suppress all output messages
-j, --json
Print results as JSON
-f, --filters <list>
Perform compression trials with each of the given filter types. You can specify a
comma-separated list, or a range of values. E.g. '-f 0-3' is the same as '-f 0,1,2,3'.
PNG delta filters (apply the same filter to every line)
0 => None (recommended to always include this filter)
1 => Sub
2 => Up
3 => Average
4 => Paeth
Heuristic strategies (try to find the best delta filter for each line)
5 => MinSum Minimum sum of absolute differences
6 => Entropy Smallest Shannon entropy
7 => Bigrams Lowest count of distinct bigrams
8 => BigEnt Smallest Shannon entropy of bigrams
9 => Brute Smallest compressed size (slow)
The default value depends on the optimization level preset.
--fast
Perform a fast compression evaluation of each enabled filter, followed by a single main
compression trial of the best result. Recommended if you have more filters enabled than
CPU cores.
--zc <level>
Deflate compression level (0-12) for main compression trials. The levels here are defined
by the libdeflate compression library.
The default value depends on the optimization level preset.
--nb
Do not change bit depth
--nc
Do not change color type
--np
Do not change color palette
--ng
Do not change to or from grayscale
--nx
Do not perform any transformations and do not deinterlace by default.
--nz
Do not recompress IDAT unless required due to transformations. Recompression of other
compressed chunks (such as iCCP) will also be disabled. Note that the combination of
'--nx' and '--nz' will fully disable all optimization.
--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.
--force
Write the output even if it is larger than the input
-z, --zopfli
Use the much slower but stronger Zopfli compressor for main compression trials.
Recommended use is with '-o max' and '--fast'.
--zi <iterations>
Set the number of iterations to use for Zopfli compression. Using fewer iterations may
speed up compression for large files. This option requires '--zopfli' to be set.
[default: 15]
--ziwi <iterations>
Stop Zopfli compression after this number of iterations without improvement. Use this in
conjunction with a high value for '--zi' to achieve better compression in reasonable time.
--brute-level <level>
Set the libdeflate compression level to use with the Brute filter strategy. Sane values
are 1-5. Higher values are not necessarily better.
--brute-lines <lines>
Set the number of lines to compress at once with the Brute filter strategy. Sane values
are 2-16. Higher values are not necessarily better.
--timeout <secs>
Maximum amount of time, in seconds, to spend on optimizations. Oxipng will check the
timeout before each transformation or compression trial, and will stop trying to optimize
the file if the timeout is exceeded. Note that this does not cut short any operations that
are already in progress, so it is currently of limited effectiveness for large files with
high compression levels.
--max-raw-size <bytes>
Maximum size to allow for the input image. If the raw, decompressed image data (or the
file size) of the image exceeds this size, it will be skipped. This is useful for limiting
memory usage or avoiding long processing times on large images. The value may be specified
with a unit suffix such as k, KB, m, MB, etc.
The decompressed size of an image is roughly equal to width * height * bit-depth / 8. E.g.
a 1920x1080 image with 24-bit color depth would be roughly 6MB.
-t, --threads <num>
Set the maximum number of threads to use. Oxipng uses multithreading to evaluate multiple
optimizations on the same file in parallel as well as process multiple files in parallel.
You can set this to a lower value if you need to limit memory or CPU usage.
[default: num logical CPUs]
--sequential
Process multiple files sequentially rather than in parallel. Use this if you need
determinism in the processing order. Note this is not necessary if using '--threads 1'.
-h, --help
Print help (see a summary with '-h')
-V, --version
Print version

196
README.md
View file

@ -1,150 +1,84 @@
# Oxipng
[![Build Status](https://github.com/oxipng/oxipng/workflows/oxipng/badge.svg)](https://github.com/oxipng/oxipng/actions?query=branch%3Amaster)
[![Build Status](https://travis-ci.org/shssoichiro/oxipng.svg?branch=master)](https://travis-ci.org/shssoichiro/oxipng)
[![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)
[![Docs](https://docs.rs/oxipng/badge.svg)](https://docs.rs/oxipng)
[![License](https://img.shields.io/crates/l/oxipng.svg)](https://github.com/shssoichiro/oxipng/blob/master/LICENSE)
## 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.
Oxipng is a multithreaded lossless PNG compression optimizer. It can be used via a command-line
interface or as a library in other Rust programs.
## Installing
Oxipng for Windows can be downloaded via the
[Releases](https://github.com/oxipng/oxipng/releases) section on its GitHub page. Recently,
however, Oxipng has also been made available through package managers. Check the list below for
up-to-date options.
For MacOS or Linux, it is recommended to install from your distro's package repository, provided
Oxipng is available there in a not too outdated version for your use case.
Oxipng is known to be packaged for the environments listed below.
[![Packaging status](https://repology.org/badge/vertical-allrepos/oxipng.svg?exclude_unsupported=1&columns=3&exclude_sources=modules,site)](https://repology.org/project/oxipng/versions)
Alternatively, Oxipng can be installed from Cargo, via the following command:
Oxipng can be downloaded from the [Releases](https://github.com/shssoichiro/oxipng/releases) link on the GitHub page.
Oxipng can also be installed from Cargo, via the following command:
```
cargo install oxipng
```
Oxipng can also be built from source using the latest stable or nightly Rust.
This is primarily useful for developing on Oxipng.
Alternatively, oxipng can be built from source using the latest stable or nightly Rust:
```
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.27.0**. Oxipng may compile on earlier versions of Rust,
but there is no guarantee.
Oxipng follows Semantic Versioning.
## Usage
Oxipng is a command-line utility. An example usage, suitable for web, may be the following:
Oxipng is a command-line utility. Basic usage looks similar to the following:
```
oxipng -o 4 --strip safe --alpha *.png
oxipng -o 4 -i 1 --strip safe *.png
```
The most commonly used options are as follows:
* Optimization: `-o 1` through `-o 6`, lower is faster, higher is better compression.
The default (`-o 2`) is sufficiently fast on a modern CPU and provides 30-50% compression
gains over an unoptimized PNG. `-o 4` is 6 times slower than `-o 2` but can provide 5-10%
extra compression over `-o 2`. Using any setting higher than `-o 4` is unlikely
to give any extra compression gains and is not recommended.
* Interlacing: `-i 1` will enable [Adam7](https://en.wikipedia.org/wiki/Adam7_algorithm)
PNG interlacing on any images that are processed. `-i 0` will remove interlacing from all
processed images. Not specifying either will keep the same interlacing state as the
input image. Note: Interlacing can add 25-50% to the size of an optimized image. Only use
it if you believe the benefits outweight the costs for your use case.
* Strip: Used to remove metadata info from processed images. Used via `--strip [safe,all]`.
Can save a few kilobytes if you don't need the metadata. "Safe" removes only metadata that
will never affect rendering of the image. "All" removes all metadata that is not critical
to the image. You can also pass a comma-separated list of specific metadata chunks to remove.
`-s` can be used as a shorthand for `--strip safe`.
- Optimization: `-o 0` through `-o 6` (or `-o max`), lower is faster, higher is better compression.
The default (`-o 2`) is quite fast and provides good compression. Higher levels can be notably
better* but generally have increasingly diminishing returns.
- Strip: Used to remove metadata info from processed images. Used via `--strip [safe,all]`.
Can save a few kilobytes if you don't need the metadata. "Safe" removes only metadata that
will never affect rendering of the image. "All" removes all metadata that is not critical
to the image. You can also pass a comma-separated list of specific metadata chunks to remove.
`-s` can be used as a shorthand for `--strip safe`.
- Alpha: `--alpha` can improve compression of images with transparency, by altering the color
values of fully transparent pixels. This is generally recommended, but take care as this is
technically a lossy transformation and may be unsuitable for some specific applications.
More advanced options can be found by running `oxipng --help`, or viewed [here](MANUAL.txt).
Some options have both short (`-a`) and long (`--alpha`) forms. Which form you use is just a
matter of preference. Multiple short options can be combined together, e.g.:
`-savvo6` is equivalent to to `--strip safe --alpha --verbose --verbose --opt 6`.
All options are case-sensitive.
\* Note that oxipng is not a brute-force optimizer. This means that while higher optimization levels
are almost always better or equal to lower levels, this is not guaranteed and it is possible in
rare circumstances that a lower level may give a marginally smaller output. Similarly, using Zopfli
compression (`-z`) is not guaranteed to always be better than default compression.
## APNG support
Oxipng currently only supports limited optimization of animated PNGs (APNGs). It can perform
alpha-optimization, refiltering and recompression of all frames, but all transformations will be
disabled. For best results, it is recommended to use another tool such as
[apngopt](https://sourceforge.net/projects/apng/files/APNG_Optimizer/) before running Oxipng.
## Git integration via [pre-commit]
Create a `.pre-commit-config.yaml` file like this, or add the lines after the `repos` map
preamble to an already existing one:
```yaml
repos:
- repo: https://github.com/oxipng/oxipng
rev: v10.0.0
hooks:
- id: oxipng
args: ["-o", "4", "--strip", "safe", "--alpha"]
```
[pre-commit]: https://pre-commit.com/
## 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`.
You can use it the following way:
```bash
docker run --rm -v $(pwd):/work ghcr.io/oxipng/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).
More advanced options can be found by running `oxipng -h`.
## Library Usage
Although originally intended to be used as an executable, Oxipng can also be used as a library in
other Rust projects. To do so, simply add Oxipng as a dependency in your Cargo.toml. You should then
have access to all of the library functions [documented here](https://docs.rs/oxipng). The simplest
method of usage involves creating an [Options
struct](https://docs.rs/oxipng/latest/oxipng/struct.Options.html) and passing it, along with an
input filename, into the [optimize function](https://docs.rs/oxipng/latest/oxipng/fn.optimize.html).
It is recommended to disable the "binary" feature when including Oxipng as a library. Currently, there is
no simple way to just disable one feature in Cargo, it has to be done by disabling default features
and specifying the desired ones, for example:
`oxipng = { version = "10.0", features = ["parallel", "zopfli", "filetime"], default-features = false }`
## Software using Oxipng
- [ImageOptim](https://imageoptim.com): Mac app and web service for optimizing images
- [Squoosh](https://squoosh.app): Web app for optimizing images
- [FileOptimizer](https://nikkhokkho.sourceforge.io/?page=FileOptimizer): Windows app for optimizing files
- [Curtail](https://github.com/Huluti/Curtail): Linux app for optimizing images
- [pyoxipng](https://pypi.org/project/pyoxipng/): Python wrapper for Oxipng
- [jSquash](https://github.com/jamsinclair/jSquash): Collection of WebAssembly image codecs
- [Trunk](https://trunk.io): Developer experience toolkit for managing code
Although originally intended to be used as an executable, oxipng can also be used as a library in
other Rust projects. To do so, simply add oxipng as a dependency in your Cargo.toml,
then `extern crate oxipng` in your project. You should then have access to all of the library
functions [documented here](https://docs.rs/oxipng). The simplest
method of usage involves creating an
[Options struct](https://docs.rs/oxipng/0.13.0/oxipng/struct.Options.html) and
passing it, along with an input filename, into the
[optimize function](https://docs.rs/oxipng/0.13.0/oxipng/fn.optimize.html).
## 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.
This also served as an opportunity to choose a more modern, safer language (Rust).
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.
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.
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).
## Contributing
@ -154,11 +88,45 @@ 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 2.1.4 (compiled on rustc 1.29.1 (b801ae664 2018-09-20)) against OptiPNG version 0.7.7 on Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz with 8 logical cores
Benchmark #1: ./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 76.1 ms ± 2.2 ms [User: 165.1 ms, System: 26.9 ms]
Range (min … max): 71.1 ms … 80.4 ms
Benchmark #2: optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 274.9 ms ± 2.3 ms [User: 272.8 ms, System: 2.0 ms]
Range (min … max): 271.8 ms … 278.1 ms
Summary
'./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png' ran
3.61x 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 ± σ): 103.1 ms ± 3.1 ms [User: 444.9 ms, System: 25.2 ms]
Range (min … max): 98.6 ms … 111.4 ms
Benchmark #2: optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 931.3 ms ± 8.7 ms [User: 929.6 ms, System: 1.1 ms]
Range (min … max): 920.4 ms … 943.3 ms
Summary
'./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png' ran
9.03x faster than 'optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png'

96
README.template.md Normal file
View file

@ -0,0 +1,96 @@
# Oxipng
[![Build Status](https://travis-ci.org/shssoichiro/oxipng.svg?branch=master)](https://travis-ci.org/shssoichiro/oxipng)
[![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/shssoichiro/oxipng/blob/master/LICENSE)
## Overview
Oxipng is a multithreaded lossless PNG compression optimizer. It can be used via a command-line
interface or as a library in other Rust programs.
## Installing
Oxipng can be downloaded from the [Releases](https://github.com/shssoichiro/oxipng/releases) link on the GitHub page.
Oxipng can also be installed from Cargo, via the following command:
```
cargo install oxipng
```
Alternatively, oxipng can be built from source using the latest stable or nightly Rust:
```
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.27.0**. Oxipng may compile on earlier versions of Rust,
but there is no guarantee.
Oxipng follows Semantic Versioning.
## Usage
Oxipng is a command-line utility. Basic usage looks similar to the following:
```
oxipng -o 4 -i 1 --strip safe *.png
```
The most commonly used options are as follows:
* Optimization: `-o 1` through `-o 6`, lower is faster, higher is better compression.
The default (`-o 2`) is sufficiently fast on a modern CPU and provides 30-50% compression
gains over an unoptimized PNG. `-o 4` is 6 times slower than `-o 2` but can provide 5-10%
extra compression over `-o 2`. Using any setting higher than `-o 4` is unlikely
to give any extra compression gains and is not recommended.
* Interlacing: `-i 1` will enable [Adam7](https://en.wikipedia.org/wiki/Adam7_algorithm)
PNG interlacing on any images that are processed. `-i 0` will remove interlacing from all
processed images. Not specifying either will keep the same interlacing state as the
input image. Note: Interlacing can add 25-50% to the size of an optimized image. Only use
it if you believe the benefits outweight the costs for your use case.
* Strip: Used to remove metadata info from processed images. Used via `--strip [safe,all]`.
Can save a few kilobytes if you don't need the metadata. "Safe" removes only metadata that
will never affect rendering of the image. "All" removes all metadata that is not critical
to the image. You can also pass a comma-separated list of specific metadata chunks to remove.
`-s` can be used as a shorthand for `--strip safe`.
More advanced options can be found by running `oxipng -h`.
## Library Usage
Although originally intended to be used as an executable, oxipng can also be used as a library in
other Rust projects. To do so, simply add oxipng as a dependency in your Cargo.toml,
then `extern crate oxipng` in your project. You should then have access to all of the library
functions [documented here](https://docs.rs/oxipng). The simplest
method of usage involves creating an
[Options struct](https://docs.rs/oxipng/0.13.0/oxipng/struct.Options.html) and
passing it, along with an input filename, into the
[optimize function](https://docs.rs/oxipng/0.13.0/oxipng/fn.optimize.html).
## History
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).
## Contributing
Any contributions are welcome and will be accepted via pull request on GitHub. Bug reports can be
filed via GitHub issues. Please include as many details as possible. If you have the capability
to submit a fix with the bug report, it is preferred that you do so via pull request,
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.
## License
Oxipng is open-source software, distributed under the MIT license.
## Benchmarks

View file

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

64
appveyor.yml Normal file
View file

@ -0,0 +1,64 @@
environment:
global:
# This will be used as part of the zipfile name
PROJECT_NAME: oxipng
matrix:
# Stable channel
- TARGET: i686-pc-windows-msvc
CHANNEL: stable
- TARGET: x86_64-pc-windows-msvc
CHANNEL: stable
- TARGET: x86_64-pc-windows-msvc
CHANNEL: nightly
# Install Rust and Cargo
# (Based on from https://github.com/rust-lang/libc/blob/master/appveyor.yml)
install:
- curl -sSf -o rustup-init.exe https://win.rustup.rs
- rustup-init.exe --default-host %TARGET% --default-toolchain %CHANNEL% -y
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
- rustc -Vv
- cargo -V
# 'cargo test' takes care of building for us, so disable Appveyor's build stage. This prevents
# the "directory does not contain a project or solution file" error.
# source: https://github.com/starkat99/appveyor-rust/blob/master/appveyor.yml#L113
build: false
# Equivalent to Travis' `script` phase
test_script:
- cargo build
- cargo test %TESTFLAGS%
before_deploy:
# Generate artifacts for release
- SET RUSTFLAGS=-C target-feature=+crt-static
- cargo build --release
- mkdir staging
- copy target\release\oxipng.exe staging
- cd staging
# release zipfile will look like 'rust-everywhere-v1.2.3-x86_64-pc-windows-msvc'
- 7z a ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip *
- appveyor PushArtifact ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip
deploy:
description: 'Windows release'
# All the zipped artifacts will be deployed
artifact: /.*\.zip/
# - Go to 'https://github.com/settings/tokens/new' and generate a Token with only the
# `public_repo` scope enabled
# - Then go to 'https://ci.appveyor.com/tools/encrypt' and enter the newly generated token.
# - Enter the "encrypted value" below
auth_token:
secure: JT77kw2p+U5Z9yM/77QjEWRFgvyaGOO2SquE5OBgUNpqSt4VDg/Mdicy8pkSvSEu
provider: GitHub
# deploy when a new tag is pushed and only on the stable channel
on:
# channel to use to produce the release artifacts
# NOTE make sure you only release *once* per target
CHANNEL: stable
appveyor_repo_tag: true
branches:
only:
- master

View file

@ -3,61 +3,258 @@
extern crate oxipng;
extern crate test;
use oxipng::internal_tests::*;
use std::path::PathBuf;
use oxipng::{internal_tests::*, *};
use test::Bencher;
#[bench]
fn deflate_16_bits(b: &mut Bencher) {
fn deflate_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();
let png = PngData::new(&input, false).unwrap();
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 0, 15, &min)
});
}
#[bench]
fn deflate_8_bits(b: &mut Bencher) {
fn deflate_8_bits_strategy_0(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 0, 15, &min)
});
}
#[bench]
fn deflate_4_bits(b: &mut Bencher) {
fn deflate_4_bits_strategy_0(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_4_should_be_palette_4.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 0, 15, &min)
});
}
#[bench]
fn deflate_2_bits(b: &mut Bencher) {
fn deflate_2_bits_strategy_0(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_2_should_be_palette_2.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 0, 15, &min)
});
}
#[bench]
fn deflate_1_bits(b: &mut Bencher) {
fn deflate_1_bits_strategy_0(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_1_should_be_palette_1.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 0, 15, &min)
});
}
#[bench]
fn deflate_16_bits_strategy_1(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 1, 15, &min)
});
}
#[bench]
fn deflate_8_bits_strategy_1(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 1, 15, &min)
});
}
#[bench]
fn deflate_4_bits_strategy_1(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_4_should_be_palette_4.png",
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 1, 15, &min)
});
}
#[bench]
fn deflate_2_bits_strategy_1(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_2_should_be_palette_2.png",
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 1, 15, &min)
});
}
#[bench]
fn deflate_1_bits_strategy_1(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_1_should_be_palette_1.png",
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 1, 15, &min)
});
}
#[bench]
fn deflate_16_bits_strategy_2(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 2, 15, &min)
});
}
#[bench]
fn deflate_8_bits_strategy_2(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 2, 15, &min)
});
}
#[bench]
fn deflate_4_bits_strategy_2(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_4_should_be_palette_4.png",
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 2, 15, &min)
});
}
#[bench]
fn deflate_2_bits_strategy_2(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_2_should_be_palette_2.png",
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 2, 15, &min)
});
}
#[bench]
fn deflate_1_bits_strategy_2(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_1_should_be_palette_1.png",
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 2, 15, &min)
});
}
#[bench]
fn deflate_16_bits_strategy_3(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 3, 15, &min)
});
}
#[bench]
fn deflate_8_bits_strategy_3(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 3, 15, &min)
});
}
#[bench]
fn deflate_4_bits_strategy_3(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_4_should_be_palette_4.png",
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 3, 15, &min)
});
}
#[bench]
fn deflate_2_bits_strategy_3(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_2_should_be_palette_2.png",
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 3, 15, &min)
});
}
#[bench]
fn deflate_1_bits_strategy_3(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_1_should_be_palette_1.png",
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw_data.as_ref(), 9, 3, 15, &min)
});
}
#[bench]
fn inflate_generic(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| inflate(png.idat_data.as_ref(), png.raw.ihdr.raw_data_size()));
b.iter(|| inflate(png.idat_data.as_ref()));
}

View file

@ -3,25 +3,28 @@
extern crate oxipng;
extern crate test;
use oxipng::internal_tests::*;
use std::path::PathBuf;
use oxipng::{internal_tests::*, *};
use test::Bencher;
#[bench]
fn filters_16_bits_filter_0(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
b.iter(|| {
png.filter_image(0);
});
}
#[bench]
fn filters_8_bits_filter_0(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
b.iter(|| {
png.filter_image(0);
});
}
#[bench]
@ -29,9 +32,11 @@ fn filters_4_bits_filter_0(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_4_should_be_palette_4.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
b.iter(|| {
png.filter_image(0);
});
}
#[bench]
@ -39,9 +44,11 @@ fn filters_2_bits_filter_0(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_2_should_be_palette_2.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
b.iter(|| {
png.filter_image(0);
});
}
#[bench]
@ -49,25 +56,31 @@ fn filters_1_bits_filter_0(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_1_should_be_palette_1.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
b.iter(|| {
png.filter_image(0);
});
}
#[bench]
fn filters_16_bits_filter_1(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
b.iter(|| {
png.filter_image(1);
});
}
#[bench]
fn filters_8_bits_filter_1(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
b.iter(|| {
png.filter_image(1);
});
}
#[bench]
@ -75,9 +88,11 @@ fn filters_4_bits_filter_1(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_4_should_be_palette_4.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
b.iter(|| {
png.filter_image(1);
});
}
#[bench]
@ -85,9 +100,11 @@ fn filters_2_bits_filter_1(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_2_should_be_palette_2.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
b.iter(|| {
png.filter_image(1);
});
}
#[bench]
@ -95,25 +112,31 @@ fn filters_1_bits_filter_1(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_1_should_be_palette_1.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
b.iter(|| {
png.filter_image(1);
});
}
#[bench]
fn filters_16_bits_filter_2(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
b.iter(|| {
png.filter_image(2);
});
}
#[bench]
fn filters_8_bits_filter_2(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
b.iter(|| {
png.filter_image(2);
});
}
#[bench]
@ -121,9 +144,11 @@ fn filters_4_bits_filter_2(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_4_should_be_palette_4.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
b.iter(|| {
png.filter_image(2);
});
}
#[bench]
@ -131,9 +156,11 @@ fn filters_2_bits_filter_2(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_2_should_be_palette_2.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
b.iter(|| {
png.filter_image(2);
});
}
#[bench]
@ -141,25 +168,31 @@ fn filters_1_bits_filter_2(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_1_should_be_palette_1.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
b.iter(|| {
png.filter_image(2);
});
}
#[bench]
fn filters_16_bits_filter_3(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
b.iter(|| {
png.filter_image(3);
});
}
#[bench]
fn filters_8_bits_filter_3(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
b.iter(|| {
png.filter_image(3);
});
}
#[bench]
@ -167,9 +200,11 @@ fn filters_4_bits_filter_3(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_4_should_be_palette_4.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
b.iter(|| {
png.filter_image(3);
});
}
#[bench]
@ -177,9 +212,11 @@ fn filters_2_bits_filter_3(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_2_should_be_palette_2.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
b.iter(|| {
png.filter_image(3);
});
}
#[bench]
@ -187,25 +224,31 @@ fn filters_1_bits_filter_3(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_1_should_be_palette_1.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
b.iter(|| {
png.filter_image(3);
});
}
#[bench]
fn filters_16_bits_filter_4(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
b.iter(|| {
png.filter_image(4);
});
}
#[bench]
fn filters_8_bits_filter_4(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
b.iter(|| {
png.filter_image(4);
});
}
#[bench]
@ -213,9 +256,11 @@ fn filters_4_bits_filter_4(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_4_should_be_palette_4.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
b.iter(|| {
png.filter_image(4);
});
}
#[bench]
@ -223,9 +268,11 @@ fn filters_2_bits_filter_4(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_2_should_be_palette_2.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
b.iter(|| {
png.filter_image(4);
});
}
#[bench]
@ -233,25 +280,31 @@ fn filters_1_bits_filter_4(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_1_should_be_palette_1.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
b.iter(|| {
png.filter_image(4);
});
}
#[bench]
fn filters_16_bits_filter_5(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
b.iter(|| {
png.filter_image(5);
});
}
#[bench]
fn filters_8_bits_filter_5(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
b.iter(|| {
png.filter_image(5);
});
}
#[bench]
@ -259,9 +312,11 @@ fn filters_4_bits_filter_5(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_4_should_be_palette_4.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
b.iter(|| {
png.filter_image(5);
});
}
#[bench]
@ -269,9 +324,11 @@ fn filters_2_bits_filter_5(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_2_should_be_palette_2.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
b.iter(|| {
png.filter_image(5);
});
}
#[bench]
@ -279,7 +336,9 @@ fn filters_1_bits_filter_5(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_1_should_be_palette_1.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
b.iter(|| {
png.filter_image(5);
});
}

View file

@ -3,25 +3,30 @@
extern crate oxipng;
extern crate test;
use oxipng::internal_tests::*;
use std::path::PathBuf;
use oxipng::{internal_tests::*, *};
use test::Bencher;
#[bench]
fn interlacing_16_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.change_interlacing(true));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.change_interlacing(1);
});
}
#[bench]
fn interlacing_8_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.change_interlacing(true));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.change_interlacing(1);
});
}
#[bench]
@ -29,9 +34,12 @@ fn interlacing_4_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_4_should_be_palette_4.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.change_interlacing(true));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.change_interlacing(1);
});
}
#[bench]
@ -39,9 +47,12 @@ fn interlacing_2_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_2_should_be_palette_2.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.change_interlacing(true));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.change_interlacing(1);
});
}
#[bench]
@ -49,9 +60,12 @@ fn interlacing_1_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_1_should_be_palette_1.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.change_interlacing(true));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.change_interlacing(1);
});
}
#[bench]
@ -59,9 +73,12 @@ fn deinterlacing_16_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/interlaced_rgb_16_should_be_rgb_16.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.change_interlacing(false));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.change_interlacing(0);
});
}
#[bench]
@ -69,9 +86,12 @@ fn deinterlacing_8_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/interlaced_rgb_8_should_be_rgb_8.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.change_interlacing(false));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.change_interlacing(0);
});
}
#[bench]
@ -79,9 +99,12 @@ fn deinterlacing_4_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/interlaced_palette_4_should_be_palette_4.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.change_interlacing(false));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.change_interlacing(0);
});
}
#[bench]
@ -89,9 +112,12 @@ fn deinterlacing_2_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/interlaced_palette_2_should_be_palette_2.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.change_interlacing(false));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.change_interlacing(0);
});
}
#[bench]
@ -99,7 +125,10 @@ fn deinterlacing_1_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/interlaced_palette_1_should_be_palette_1.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| png.raw.change_interlacing(false));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.change_interlacing(0);
});
}

View file

@ -3,25 +3,19 @@
extern crate oxipng;
extern crate test;
use oxipng::internal_tests::*;
use std::path::PathBuf;
use oxipng::{internal_tests::*, *};
use test::Bencher;
#[bench]
fn reductions_16_to_8_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_16_to_8(&png.raw, false));
}
#[bench]
fn reductions_16_to_8_bits_scaled(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_16_to_8(&png.raw, true));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_bit_depth();
});
}
#[bench]
@ -29,9 +23,12 @@ fn reductions_8_to_4_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_8_should_be_palette_4.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_bit_depth();
});
}
#[bench]
@ -39,9 +36,12 @@ fn reductions_8_to_2_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_8_should_be_palette_2.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_bit_depth();
});
}
#[bench]
@ -49,63 +49,73 @@ fn reductions_8_to_1_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_8_should_be_palette_1.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_bit_depth();
});
}
#[bench]
fn reductions_grayscale_8_to_4_bits(b: &mut Bencher) {
fn reductions_4_to_2_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/grayscale_8_should_be_grayscale_4.png",
"tests/files/palette_4_should_be_palette_2.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_bit_depth();
});
}
#[bench]
fn reductions_grayscale_8_to_2_bits(b: &mut Bencher) {
fn reductions_4_to_1_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/grayscale_8_should_be_grayscale_2.png",
"tests/files/palette_4_should_be_palette_1.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_bit_depth();
});
}
#[bench]
fn reductions_grayscale_8_to_1_bits(b: &mut Bencher) {
fn reductions_2_to_1_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/grayscale_8_should_be_grayscale_1.png",
"tests/files/palette_2_should_be_palette_1.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_bit_depth();
});
}
#[bench]
fn reductions_rgba_to_rgb_16(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
});
}
#[bench]
fn reductions_rgba_to_rgb_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
}
#[bench]
fn reductions_rgba_to_rgb_trns_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_rgb_trns_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
});
}
#[bench]
@ -113,9 +123,12 @@ fn reductions_rgba_to_grayscale_alpha_16(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/rgba_16_should_be_grayscale_alpha_16.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
});
}
#[bench]
@ -123,9 +136,38 @@ fn reductions_rgba_to_grayscale_alpha_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/rgba_8_should_be_grayscale_alpha_8.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
});
}
#[bench]
fn reductions_rgba_to_grayscale_16(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/rgba_16_should_be_grayscale_16.png",
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
});
}
#[bench]
fn reductions_rgba_to_grayscale_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/rgba_8_should_be_grayscale_8.png",
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
});
}
#[bench]
@ -133,83 +175,45 @@ fn reductions_rgb_to_grayscale_16(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/rgb_16_should_be_grayscale_16.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
});
}
#[bench]
fn reductions_rgb_to_grayscale_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_grayscale_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
});
}
#[bench]
fn reductions_rgba_to_palette_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_palette_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| color::reduced_to_indexed(&png.raw, true));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
});
}
#[bench]
fn reductions_rgb_to_palette_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_palette_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| color::reduced_to_indexed(&png.raw, true));
}
#[bench]
fn reductions_grayscale_alpha_to_grayscale_16(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/grayscale_alpha_16_should_be_grayscale_16.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
}
#[bench]
fn reductions_grayscale_alpha_to_grayscale_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/grayscale_alpha_8_should_be_grayscale_8.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
}
#[bench]
fn reductions_grayscale_alpha_to_grayscale_trns_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/grayscale_alpha_8_should_be_grayscale_trns_8.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
}
#[bench]
fn reductions_grayscale_8_to_palette_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/grayscale_8_should_be_palette_8.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| color::reduced_to_indexed(&png.raw, true));
}
#[bench]
fn reductions_palette_8_to_grayscale_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_8_should_be_grayscale_8.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| color::indexed_to_channels(&png.raw, true, false));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_color_type();
});
}
#[bench]
@ -217,9 +221,12 @@ fn reductions_palette_duplicate_reduction(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_should_be_reduced_with_dupes.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| palette::reduced_palette(&png.raw, false));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_palette();
});
}
#[bench]
@ -227,9 +234,12 @@ fn reductions_palette_unused_reduction(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_should_be_reduced_with_unused.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| palette::reduced_palette(&png.raw, false));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_palette();
});
}
#[bench]
@ -237,45 +247,76 @@ fn reductions_palette_full_reduction(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_should_be_reduced_with_both.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| palette::reduced_palette(&png.raw, false));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_palette();
});
}
#[bench]
fn reductions_palette_sort(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_8_should_be_palette_8.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
fn reductions_alpha_black(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_black.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| palette::sorted_palette(&png.raw));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_alpha_channel(AlphaOptim::Black);
});
}
#[bench]
fn reductions_palette_sort_mzeng(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_8_should_be_palette_8.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
fn reductions_alpha_white(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_white.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| palette::sorted_palette_mzeng(&png.raw));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_alpha_channel(AlphaOptim::White);
});
}
#[bench]
fn reductions_palette_sort_battiato(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_8_should_be_palette_8.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
fn reductions_alpha_left(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_left.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| palette::sorted_palette_battiato(&png.raw));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_alpha_channel(AlphaOptim::Left);
});
}
#[bench]
fn reductions_alpha(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
fn reductions_alpha_right(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_right.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| alpha::cleaned_alpha_channel(&png.raw));
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_alpha_channel(AlphaOptim::Right);
});
}
#[bench]
fn reductions_alpha_up(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_up.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_alpha_channel(AlphaOptim::Up);
});
}
#[bench]
fn reductions_alpha_down(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_down.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
let mut safe_png = png.clone();
safe_png.reduce_alpha_channel(AlphaOptim::Down);
});
}

View file

@ -1,57 +0,0 @@
#![feature(test)]
extern crate oxipng;
extern crate test;
use std::path::PathBuf;
use oxipng::{internal_tests::*, *};
use test::Bencher;
#[bench]
fn filters_minsum(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
}
#[bench]
fn filters_entropy(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::Entropy, false));
}
#[bench]
fn filters_bigrams(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::Bigrams, false));
}
#[bench]
fn filters_bigent(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.filter_image(FilterStrategy::BigEnt, false));
}
#[bench]
fn filters_brute(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(
FilterStrategy::Brute {
num_lines: 4,
level: 1,
},
false,
)
});
}

View file

@ -3,28 +3,27 @@
extern crate oxipng;
extern crate test;
use oxipng::internal_tests::*;
use std::path::PathBuf;
use oxipng::{internal_tests::*, *};
use test::Bencher;
#[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();
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
zopfli_deflate(png.raw_data.as_ref()).ok();
});
}
#[bench]
fn zopfli_8_bits_strategy_0(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
zopfli_deflate(png.raw_data.as_ref()).ok();
});
}
@ -33,10 +32,10 @@ fn zopfli_4_bits_strategy_0(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_4_should_be_palette_4.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
zopfli_deflate(png.raw_data.as_ref()).ok();
});
}
@ -45,10 +44,10 @@ fn zopfli_2_bits_strategy_0(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_2_should_be_palette_2.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
zopfli_deflate(png.raw_data.as_ref()).ok();
});
}
@ -57,9 +56,9 @@ fn zopfli_1_bits_strategy_0(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_1_should_be_palette_1.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
zopfli_deflate(png.raw_data.as_ref()).ok();
});
}

1
index.html Normal file
View file

@ -0,0 +1 @@
<script>(function() {window.location.assign(window.location.href + "doc/oxipng/");})();</script>

386
package-lock.json generated Normal file
View file

@ -0,0 +1,386 @@
{
"name": "oxipng",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"array-find-index": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
"integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
"dev": true
},
"builtin-modules": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
"integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
"dev": true
},
"camelcase": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
"integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
"dev": true
},
"camelcase-keys": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
"integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
"dev": true,
"requires": {
"camelcase": "^2.0.0",
"map-obj": "^1.0.0"
}
},
"currently-unhandled": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
"integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
"dev": true,
"requires": {
"array-find-index": "^1.0.1"
}
},
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
"dev": true
},
"error-ex": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz",
"integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=",
"dev": true,
"requires": {
"is-arrayish": "^0.2.1"
}
},
"find-up": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
"integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
"dev": true,
"requires": {
"path-exists": "^2.0.0",
"pinkie-promise": "^2.0.0"
}
},
"get-stdin": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
"integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
"dev": true
},
"graceful-fs": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
"dev": true
},
"hosted-git-info": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz",
"integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==",
"dev": true
},
"indent-string": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
"integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
"dev": true,
"requires": {
"repeating": "^2.0.0"
}
},
"is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
"dev": true
},
"is-builtin-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
"integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
"dev": true,
"requires": {
"builtin-modules": "^1.0.0"
}
},
"is-finite": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
"integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
"dev": true,
"requires": {
"number-is-nan": "^1.0.0"
}
},
"is-utf8": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
"integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
"dev": true
},
"load-json-file": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"parse-json": "^2.2.0",
"pify": "^2.0.0",
"pinkie-promise": "^2.0.0",
"strip-bom": "^2.0.0"
}
},
"loud-rejection": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
"integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
"dev": true,
"requires": {
"currently-unhandled": "^0.4.1",
"signal-exit": "^3.0.0"
}
},
"map-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
"integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
"dev": true
},
"meow": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
"integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
"dev": true,
"requires": {
"camelcase-keys": "^2.0.0",
"decamelize": "^1.1.2",
"loud-rejection": "^1.0.0",
"map-obj": "^1.0.1",
"minimist": "^1.1.3",
"normalize-package-data": "^2.3.4",
"object-assign": "^4.0.1",
"read-pkg-up": "^1.0.1",
"redent": "^1.0.0",
"trim-newlines": "^1.0.0"
}
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
},
"normalize-package-data": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
"integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
"dev": true,
"requires": {
"hosted-git-info": "^2.1.4",
"is-builtin-module": "^1.0.0",
"semver": "2 || 3 || 4 || 5",
"validate-npm-package-license": "^3.0.1"
}
},
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true
},
"parse-json": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
"integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
"dev": true,
"requires": {
"error-ex": "^1.2.0"
}
},
"path-exists": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
"integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
"dev": true,
"requires": {
"pinkie-promise": "^2.0.0"
}
},
"path-type": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
"integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"pify": "^2.0.0",
"pinkie-promise": "^2.0.0"
}
},
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
},
"pinkie": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
"integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
"dev": true
},
"pinkie-promise": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
"integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
"dev": true,
"requires": {
"pinkie": "^2.0.0"
}
},
"read-pkg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
"integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
"dev": true,
"requires": {
"load-json-file": "^1.0.0",
"normalize-package-data": "^2.3.2",
"path-type": "^1.0.0"
}
},
"read-pkg-up": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
"integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
"dev": true,
"requires": {
"find-up": "^1.0.0",
"read-pkg": "^1.0.0"
}
},
"redent": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
"integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
"dev": true,
"requires": {
"indent-string": "^2.1.0",
"strip-indent": "^1.0.1"
}
},
"repeating": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
"integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
"dev": true,
"requires": {
"is-finite": "^1.0.0"
}
},
"semver": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
"dev": true
},
"signal-exit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
"dev": true
},
"spdx-correct": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz",
"integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=",
"dev": true,
"requires": {
"spdx-license-ids": "^1.0.2"
}
},
"spdx-expression-parse": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz",
"integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=",
"dev": true
},
"spdx-license-ids": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz",
"integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=",
"dev": true
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"dev": true,
"requires": {
"ansi-regex": "^3.0.0"
}
},
"strip-ansi-cli": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi-cli/-/strip-ansi-cli-2.0.0.tgz",
"integrity": "sha1-D91Cq86DJTm/cv+ROSpxqzVGIIw=",
"dev": true,
"requires": {
"meow": "^3.7.0",
"strip-ansi": "^4.0.0"
}
},
"strip-bom": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
"integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
"dev": true,
"requires": {
"is-utf8": "^0.2.0"
}
},
"strip-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
"integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
"dev": true,
"requires": {
"get-stdin": "^4.0.1"
}
},
"trim-newlines": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
"integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
"dev": true
},
"validate-npm-package-license": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz",
"integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=",
"dev": true,
"requires": {
"spdx-correct": "~1.0.0",
"spdx-expression-parse": "~1.0.0"
}
}
}
}

26
package.json Normal file
View file

@ -0,0 +1,26 @@
{
"name": "oxipng",
"version": "1.0.0",
"description": "[![Build Status](https://travis-ci.org/shssoichiro/oxipng.svg?branch=master)](https://travis-ci.org/shssoichiro/oxipng) [![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/shssoichiro/oxipng/blob/master/LICENSE)",
"main": "index.js",
"private": true,
"directories": {
"test": "tests"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/shssoichiro/oxipng.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/shssoichiro/oxipng/issues"
},
"homepage": "https://github.com/shssoichiro/oxipng#readme",
"devDependencies": {
"strip-ansi-cli": "^2.0.0"
}
}

1
rustfmt.toml Normal file
View file

@ -0,0 +1 @@
format_strings = false

View file

@ -1,19 +1,14 @@
#!/bin/bash
cargo build --release
sed -i.bak '/## Benchmarks/,$d' README.md
rm README.md.bak
cp README.template.md README.md
CORES=$(sysctl -n hw.ncpu 2>/dev/null || grep -c ^processor /proc/cpuinfo)
CPU=$(sysctl -n machdep.cpu.brand_string 2>/dev/null || grep '^model name' /proc/cpuinfo | sed 's/model name.\+: //g' | head -n 1)
OXIPNG_VERSION=$(./target/release/oxipng -V)
OPTIPNG_VERSION=$(optipng -v | head -n 1)
RUST_VERSION=$(rustc -V)
echo -e '## Benchmarks\n' >> README.md
echo "Tested $OXIPNG_VERSION (compiled on $RUST_VERSION) against $OPTIPNG_VERSION on $CPU with $CORES logical cores" >> README.md
echo -e '\n```\n' >> README.md
hyperfine --style basic --warmup 5 './target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png' 'optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png' >> README.md
echo -e '\n\n' >> README.md
hyperfine --style basic --warmup 5 './target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png' 'optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png' >> README.md
echo -e '\n```' >> README.md
hyperfine --warmup 5 './target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png' 'optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png' | ./node_modules/.bin/strip-ansi >> README.md
hyperfine --warmup 5 './target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png' 'optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png' | ./node_modules/.bin/strip-ansi >> README.md

View file

@ -1,7 +0,0 @@
#!/bin/sh -eu
cargo build
cargo xtask mangen
./target/debug/oxipng -V > MANUAL.txt
#Redirect all streams to prevent detection of the terminal width and force an internal default of 100
./target/debug/oxipng --help >> MANUAL.txt 2>/dev/null </dev/null

View file

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

View file

@ -1,25 +1,39 @@
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering::{Relaxed, SeqCst};
#[derive(Debug)]
pub struct AtomicMin {
val: AtomicUsize,
}
impl AtomicMin {
#[must_use]
pub fn new(init: Option<usize>) -> Self {
Self {
val: AtomicUsize::new(init.unwrap_or(usize::MAX)),
val: AtomicUsize::new(init.unwrap_or(usize::max_value())),
}
}
pub fn get(&self) -> Option<usize> {
let val = self.val.load(SeqCst);
if val == usize::MAX { None } else { Some(val) }
if val == usize::max_value() {
None
} else {
Some(val)
}
}
/// Try a new value, returning true if it is the new minimum
pub fn set_min(&self, new_val: usize) -> bool {
new_val < self.val.fetch_min(new_val, SeqCst)
pub fn set_min(&self, new_val: usize) {
let mut current_val = self.val.load(Relaxed);
loop {
if new_val < current_val {
if let Err(v) = self
.val
.compare_exchange(current_val, new_val, SeqCst, Relaxed)
{
current_val = v;
continue;
}
}
break;
}
}
}

View file

@ -1,450 +0,0 @@
use std::{num::NonZeroU64, 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;
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,
// so that they can fit on a single line in an 80 character terminal.
// Long help descriptions are soft wrapped here at 90 characters (column 91) but this does not
// affect output, it simply matches what is rendered when help is output to a file.
Command::new("oxipng")
.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)")
.index(1)
.num_args(1..)
.use_value_delimiter(false)
.required(true)
.value_parser(value_parser!(PathBuf)),
)
.arg(
Arg::new("optimization")
.help("Optimization level (0-6, or max)")
.long_help("\
Set the optimization level preset. The default level 2 is quite fast and provides good \
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)
Manually specifying a compression option (zc, f, etc.) will override the optimization \
preset, regardless of the order you write the arguments.")
.short('o')
.long("opt")
.value_name("level")
.default_value("2")
.value_parser(["0", "1", "2", "3", "4", "5", "6", "max"])
.hide_possible_values(true),
)
.arg(
Arg::new("recursive")
.help("Recurse input directories, optimizing all PNG files")
.long_help("\
When directories are given as input, traverse the directory trees and optimize all PNG \
files found (files with .png or .apng extension).")
.short('r')
.long("recursive")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("output_dir")
.help("Write output file(s) to <directory>")
.long_help("\
Write output file(s) to <directory>. If the directory does not exist, it will be created. \
Note that this will not preserve the directory structure of the input files when used with \
'--recursive'.")
.long("dir")
.value_name("directory")
.value_parser(value_parser!(PathBuf))
.conflicts_with("output_file")
.conflicts_with("stdout"),
)
.arg(
Arg::new("output_file")
.help("Write output file to <file>")
.long("out")
.value_name("file")
.value_parser(value_parser!(PathBuf))
.conflicts_with("output_dir")
.conflicts_with("stdout"),
)
.arg(
Arg::new("stdout")
.help("Write output to stdout")
.long("stdout")
.action(ArgAction::SetTrue)
.conflicts_with("output_dir")
.conflicts_with("output_file"),
)
.arg(
Arg::new("preserve")
.help("Preserve file permissions and timestamps if possible")
.short('p')
.long("preserve")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("dry-run")
.help("Do not write any files, only show compression results")
.short('d')
.long("dry-run")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("strip-safe")
.help("Strip safely-removable chunks, same as '--strip safe'")
.short('s')
.action(ArgAction::SetTrue)
.conflicts_with("strip"),
)
.arg(
Arg::new("strip")
.help("Strip metadata (safe, all, or comma-separated list)\nCAUTION: 'all' will convert APNGs to standard PNGs")
.long_help(format!("\
Strip metadata chunks, where <mode> is one of:
safe => Strip all non-critical chunks, except for the following:
{}
all => Strip all non-critical chunks
<list> => Strip chunks in the comma-separated list, e.g. 'bKGD,cHRM'
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.
The default when --strip is not passed is to keep all chunks that remain valid.",
DISPLAY_CHUNKS
.iter()
.map(|c| String::from_utf8_lossy(c))
.collect::<Vec<_>>()
.join(", ")))
.long("strip")
.value_name("mode")
.conflicts_with("strip-safe"),
)
.arg(
Arg::new("keep")
.help("Strip all metadata except in the comma-separated list")
.long_help("\
Strip all metadata chunks except those in the comma-separated list. The special value \
'display' includes chunks that affect the image appearance, equivalent to '--strip safe'.
E.g. '--keep eXIf,display' will strip chunks, keeping only eXIf and those that affect the \
image appearance.")
.long("keep")
.value_name("list")
.conflicts_with("strip")
.conflicts_with("strip-safe"),
)
.arg(
Arg::new("alpha")
.help("Perform additional alpha channel optimization")
.long_help("\
Perform additional optimization on images with an alpha channel, by altering the color \
values of fully transparent pixels. This is generally recommended for better compression, \
but take care as while this is visually lossless, it is technically a lossy \
transformation and may be unsuitable for some applications.")
.short('a')
.long("alpha")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("interlace")
.help("Set PNG interlacing (off, on, keep)")
.long_help("\
Set the PNG interlacing mode, where <mode> 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
Note that interlacing can add 25-50% to the size of an optimized image. Only use it if you \
believe the benefits outweigh the costs for your use case.")
.short('i')
.long("interlace")
.value_name("mode")
.value_parser(["off", "on", "keep", "0", "1"])
.default_value("off")
.default_value_if("no-reductions", ArgPredicate::IsPresent, "keep")
.hide_possible_values(true),
)
.arg(
Arg::new("scale16")
.help("Forcibly reduce 16-bit images to 8-bit (lossy)")
.long_help("\
Forcibly reduce images with 16 bits per channel to 8 bits per channel. This is a lossy \
operation but can provide significant savings when you have no need for higher depth. \
Reduction is performed by scaling the values such that, e.g. 0x00FF is reduced to 0x01 \
rather than 0x00.
Without this flag, 16-bit images will only be reduced in depth if it can be done \
losslessly.")
.long("scale16")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("verbose")
.help("Show per-file info (use multiple times for more detail)")
.short('v')
.long("verbose")
.action(ArgAction::Count)
.conflicts_with("quiet"),
)
.arg(
Arg::new("quiet")
.help("Suppress all output messages")
.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)")
.long_help("\
Perform compression trials with each of the given filter types. You can specify a \
comma-separated list, or a range of values. E.g. '-f 0-3' is the same as '-f 0,1,2,3'.
PNG delta filters (apply the same filter to every line)
0 => None (recommended to always include this filter)
1 => Sub
2 => Up
3 => Average
4 => Paeth
Heuristic strategies (try to find the best delta filter for each line)
5 => MinSum Minimum sum of absolute differences
6 => Entropy Smallest Shannon entropy
7 => Bigrams Lowest count of distinct bigrams
8 => BigEnt Smallest Shannon entropy of bigrams
9 => Brute Smallest compressed size (slow)
The default value depends on the optimization level preset.")
.short('f')
.long("filters")
.value_name("list"),
)
.arg(
Arg::new("fast")
.help("Use fast filter evaluation")
.long_help("\
Perform a fast compression evaluation of each enabled filter, followed by a single main \
compression trial of the best result. Recommended if you have more filters enabled than \
CPU cores.")
.long("fast")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("compression")
.help("Deflate compression level (0-12)")
.long_help("\
Deflate compression level (0-12) for main compression trials. The levels here are defined \
by the libdeflate compression library.
The default value depends on the optimization level preset.")
.long("zc")
.value_name("level")
.value_parser(0..=12)
.conflicts_with("zopfli"),
)
.arg(
Arg::new("no-bit-reduction")
.help("Do not change bit depth")
.long("nb")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("no-color-reduction")
.help("Do not change color type")
.long("nc")
.action(ArgAction::SetTrue),
)
.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),
)
.arg(
Arg::new("no-grayscale-reduction")
.help("Do not change to or from grayscale")
.long("ng")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("no-reductions")
.help("Do not perform any transformations")
.long_help("\
Do not perform any transformations and do not deinterlace by default.")
.long("nx")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("no-recoding")
.help("Do not recompress unless transformations occur")
.long_help("\
Do not recompress IDAT unless required due to transformations. Recompression of other \
compressed chunks (such as iCCP) will also be disabled. Note that the combination of \
'--nx' and '--nz' will fully disable all optimization.")
.long("nz")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("fix")
.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.")
.long("fix")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("force")
.help("Write the output even if it is larger than the input")
.long("force")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("zopfli")
.help("Use the much slower but stronger Zopfli compressor")
.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
.long("zopfli")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("iterations")
.help("Number of Zopfli iterations")
.long_help("\
Set the number of iterations to use for Zopfli compression. Using fewer iterations may \
speed up compression for large files. This option requires '--zopfli' to be set.")
.long("zi")
.value_name("iterations")
.default_value("15")
.value_parser(value_parser!(NonZeroU64))
.requires("zopfli"),
)
.arg(
Arg::new("iterations-without-improvement")
.hide_short_help(true)
.long_help("\
Stop Zopfli compression after this number of iterations without improvement. Use this in \
conjunction with a high value for '--zi' to achieve better compression in reasonable time.")
.long("ziwi")
.value_name("iterations")
.value_parser(value_parser!(NonZeroU64))
.requires("zopfli"),
)
.arg(
Arg::new("brute-level")
.hide_short_help(true)
.long_help("\
Set the libdeflate compression level to use with the Brute filter strategy. Sane values \
are 1-5. Higher values are not necessarily better.")
.long("brute-level")
.value_name("level")
.value_parser(1..=12),
)
.arg(
Arg::new("brute-lines")
.hide_short_help(true)
.long_help("\
Set the number of lines to compress at once with the Brute filter strategy. Sane values \
are 2-16. Higher values are not necessarily better.")
.long("brute-lines")
.value_name("lines")
.value_parser(value_parser!(usize)),
)
.arg(
Arg::new("timeout")
.help("Maximum amount of time to spend on optimizations")
.long_help("\
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 \
the file if the timeout is exceeded. Note that this does not cut short any operations that \
are already in progress, so it is currently of limited effectiveness for large files with \
high compression levels.")
.long("timeout")
.value_name("secs")
.value_parser(value_parser!(u64)),
)
.arg(
Arg::new("max-size")
.help("Skip image if the decompressed size exceeds this limit")
.long_help("\
Maximum size to allow for the input image. If the raw, decompressed image data (or the \
file size) of the image exceeds this size, it will be skipped. This is useful for limiting \
memory usage or avoiding long processing times on large images. The value may be specified \
with a unit suffix such as k, KB, m, MB, etc.
The decompressed size of an image is roughly equal to width * height * bit-depth / 8. E.g. \
a 1920x1080 image with 24-bit color depth would be roughly 6MB.")
.long("max-raw-size")
.value_name("bytes")
.value_parser(|s: &str| parse_size(s)),
)
.arg(
Arg::new("threads")
.help("Number of threads to use [default: num logical CPUs]")
.long_help("\
Set the maximum number of threads to use. Oxipng uses multithreading to evaluate multiple \
optimizations on the same file in parallel as well as process multiple files in parallel. \
You can set this to a lower value if you need to limit memory or CPU usage.
[default: num logical CPUs]")
.short('t')
.long("threads")
.value_name("num")
.value_parser(value_parser!(usize)),
)
.arg(
Arg::new("parallel-files")
.help("Process multiple files sequentially")
.long_help("\
Process multiple files sequentially rather than in parallel. Use this if you need \
determinism in the processing order. Note this is not necessary if using '--threads 1'.")
.long("sequential")
.action(ArgAction::SetFalse),
)
}

View file

@ -1,129 +1,145 @@
use std::{fmt, fmt::Display};
use std::fmt;
use rgb::{RGB16, RGBA8};
use crate::PngError;
#[derive(Debug, PartialEq, Eq, Clone)]
#[derive(Debug, PartialEq, Clone, Copy)]
/// The color type used to represent this image
pub enum ColorType {
/// Grayscale, with one color channel
Grayscale {
/// Optional shade of gray that should be rendered as transparent
transparent_shade: Option<u16>,
},
Grayscale,
/// RGB, with three color channels
RGB {
/// Optional color value that should be rendered as transparent
transparent_color: Option<RGB16>,
},
/// Indexed, with one byte per pixel representing a color from the palette
Indexed {
/// The palette containing the colors used, up to 256 entries
palette: Vec<RGBA8>,
},
RGB,
/// Indexed, with one byte per pixel representing one of up to 256 colors in the image
Indexed,
/// Grayscale + Alpha, with two color channels
GrayscaleAlpha,
/// RGBA, with four color channels
RGBA,
}
impl Display for ColorType {
impl fmt::Display for ColorType {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Grayscale { .. } => write!(f, "Grayscale"),
Self::RGB { .. } => write!(f, "RGB"),
Self::Indexed { palette } => write!(f, "Indexed ({} colors)", palette.len()),
Self::GrayscaleAlpha => write!(f, "Grayscale + Alpha"),
Self::RGBA => write!(f, "RGB + Alpha"),
}
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match *self {
ColorType::Grayscale => "Grayscale",
ColorType::RGB => "RGB",
ColorType::Indexed => "Indexed",
ColorType::GrayscaleAlpha => "Grayscale + Alpha",
ColorType::RGBA => "RGB + Alpha",
}
)
}
}
impl ColorType {
/// Get the code used by the PNG specification to denote this color type
#[inline]
#[must_use]
pub const fn png_header_code(&self) -> u8 {
pub fn png_header_code(self) -> u8 {
match self {
Self::Grayscale { .. } => 0,
Self::RGB { .. } => 2,
Self::Indexed { .. } => 3,
Self::GrayscaleAlpha => 4,
Self::RGBA => 6,
ColorType::Grayscale => 0,
ColorType::RGB => 2,
ColorType::Indexed => 3,
ColorType::GrayscaleAlpha => 4,
ColorType::RGBA => 6,
}
}
#[inline]
pub(crate) const fn channels_per_pixel(&self) -> u8 {
pub fn channels_per_pixel(self) -> u8 {
match self {
Self::Grayscale { .. } | Self::Indexed { .. } => 1,
Self::GrayscaleAlpha => 2,
Self::RGB { .. } => 3,
Self::RGBA => 4,
}
}
#[inline]
pub(crate) const fn is_rgb(&self) -> bool {
matches!(self, Self::RGB { .. } | Self::RGBA)
}
#[inline]
pub(crate) const fn is_gray(&self) -> bool {
matches!(self, Self::Grayscale { .. } | Self::GrayscaleAlpha)
}
#[inline]
pub(crate) const fn has_alpha(&self) -> bool {
matches!(self, Self::GrayscaleAlpha | Self::RGBA)
}
#[inline]
pub(crate) const fn has_trns(&self) -> bool {
match self {
Self::Grayscale { transparent_shade } => transparent_shade.is_some(),
Self::RGB { transparent_color } => transparent_color.is_some(),
_ => false,
ColorType::Grayscale | ColorType::Indexed => 1,
ColorType::GrayscaleAlpha => 2,
ColorType::RGB => 3,
ColorType::RGBA => 4,
}
}
}
#[repr(u8)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
#[derive(Debug, PartialEq, Clone, Copy)]
/// The number of bits to be used per channel per pixel
pub enum BitDepth {
/// One bit per channel per pixel
One = 1,
One,
/// Two bits per channel per pixel
Two = 2,
Two,
/// Four bits per channel per pixel
Four = 4,
Four,
/// Eight bits per channel per pixel
Eight = 8,
Eight,
/// Sixteen bits per channel per pixel
Sixteen = 16,
Sixteen,
}
impl TryFrom<u8> for BitDepth {
type Error = PngError;
impl fmt::Display for BitDepth {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match *self {
BitDepth::One => "1",
BitDepth::Two => "2",
BitDepth::Four => "4",
BitDepth::Eight => "8",
BitDepth::Sixteen => "16",
}
)
}
}
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
1 => Ok(Self::One),
2 => Ok(Self::Two),
4 => Ok(Self::Four),
8 => Ok(Self::Eight),
16 => Ok(Self::Sixteen),
_ => Err(PngError::InvalidData),
impl BitDepth {
/// Retrieve the number of bits per channel per pixel as a `u8`
#[inline]
pub fn as_u8(self) -> u8 {
match self {
BitDepth::One => 1,
BitDepth::Two => 2,
BitDepth::Four => 4,
BitDepth::Eight => 8,
BitDepth::Sixteen => 16,
}
}
/// Parse a number of bits per channel per pixel into a `BitDepth`
#[inline]
pub fn from_u8(depth: u8) -> BitDepth {
match depth {
1 => BitDepth::One,
2 => BitDepth::Two,
4 => BitDepth::Four,
8 => BitDepth::Eight,
16 => BitDepth::Sixteen,
_ => panic!("Unsupported bit depth"),
}
}
}
impl Display for BitDepth {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&(*self as u8).to_string(), f)
#[derive(Debug, PartialEq, Clone, Copy, Eq, Hash)]
/// Potential optimization methods for alpha channel
pub enum AlphaOptim {
NoOp,
Black,
White,
Up,
Right,
Down,
Left,
}
impl fmt::Display for AlphaOptim {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match *self {
AlphaOptim::NoOp => "_",
AlphaOptim::Black => "B",
AlphaOptim::White => "W",
AlphaOptim::Up => "U",
AlphaOptim::Right => "R",
AlphaOptim::Down => "D",
AlphaOptim::Left => "L",
}
)
}
}

View file

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

View file

@ -0,0 +1,67 @@
use atomicmin::AtomicMin;
use error::PngError;
use miniz_oxide::deflate::core::*;
use PngResult;
pub fn compress_to_vec_oxipng(
input: &[u8],
level: u8,
window_bits: i32,
strategy: i32,
max_size: &AtomicMin,
) -> PngResult<Vec<u8>> {
// The comp flags function sets the zlib flag if the window_bits parameter is > 0.
let flags = create_comp_flags_from_zip_params(level.into(), window_bits, strategy);
let mut compressor = CompressorOxide::new(flags);
// if max size is known, then expect that much data (but no more than input.len())
let mut output = Vec::with_capacity(max_size.get().unwrap_or(input.len() / 2).min(input.len()));
// # Unsafe
// We trust compress to not read the uninitialized bytes.
unsafe {
let cap = output.capacity();
output.set_len(cap);
}
let mut in_pos = 0;
let mut out_pos = 0;
loop {
let (status, bytes_in, bytes_out) = compress(
&mut compressor,
&input[in_pos..],
&mut output[out_pos..],
TDEFLFlush::Finish,
);
out_pos += bytes_out;
in_pos += bytes_in;
match status {
TDEFLStatus::Done => {
output.truncate(out_pos);
break;
}
TDEFLStatus::Okay => {
if let Some(max) = max_size.get() {
if output.len() > max {
return Err(PngError::DeflatedDataTooLong(output.len()));
}
}
// We need more space, so extend the vector.
if output.len().saturating_sub(out_pos) < 30 {
let current_len = output.len();
output.reserve(current_len);
// # Unsafe
// We trust compress to not read the uninitialized bytes.
unsafe {
let cap = output.capacity();
output.set_len(cap);
}
}
}
// Not supposed to happen unless there is a bug.
_ => panic!("Bug! Unexpectedly failed to compress!"),
}
}
Ok(output)
}

View file

@ -1,53 +1,123 @@
mod deflater;
use std::{fmt, fmt::Display};
use atomicmin::AtomicMin;
use error::PngError;
use miniz_oxide;
use std::cmp::max;
use zopfli;
use PngResult;
pub use deflater::{crc32, deflate, inflate};
#[doc(hidden)]
pub mod miniz_stream;
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 {
/// Use libdeflater.
Libdeflater {
/// Which compression level to use on the file (0-12)
compression: u8,
},
#[cfg(feature = "zopfli")]
/// Use the better but slower Zopfli implementation
Zopfli(ZopfliOptions),
/// Decompress a data stream using the DEFLATE algorithm
pub fn inflate(data: &[u8]) -> PngResult<Vec<u8>> {
miniz_oxide::inflate::decompress_to_vec_zlib(data)
.map_err(|e| PngError::new(&format!("Error on decompress: {:?}", e)))
}
impl Deflater {
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)?,
};
if let Some(max) = max_size {
if compressed.len() > max {
return Err(PngError::DeflatedDataTooLong(max));
/// Compress a data stream using the DEFLATE algorithm
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
pub fn deflate(data: &[u8], zc: u8, zs: u8, zw: u8, max_size: &AtomicMin) -> PngResult<Vec<u8>> {
if is_cfzlib_supported() {
return cfzlib_deflate(data, zc, zs, zw, max_size);
}
miniz_stream::compress_to_vec_oxipng(data, zc, zw.into(), zs.into(), max_size)
}
/// Compress a data stream using the DEFLATE algorithm
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
pub fn deflate(data: &[u8], zc: u8, zs: u8, zw: u8, max_size: &AtomicMin) -> PngResult<Vec<u8>> {
miniz_stream::compress_to_vec_oxipng(data, zc, zw.into(), zs.into(), max_size)
}
#[cfg(target_arch = "x86_64")]
fn is_cfzlib_supported() -> bool {
if is_x86_feature_detected!("sse4.2") && is_x86_feature_detected!("pclmulqdq") {
return true;
}
false
}
#[cfg(target_arch = "aarch64")]
fn is_cfzlib_supported() -> bool {
if is_arm_feature_detected!("neon") && is_arm_feature_detected!("crc") {
return true;
}
false
}
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
pub fn cfzlib_deflate(
data: &[u8],
level: u8,
strategy: u8,
window_bits: u8,
max_size: &AtomicMin,
) -> PngResult<Vec<u8>> {
use cloudflare_zlib_sys::*;
use std::mem;
assert!(data.len() < u32::max_value() as usize);
unsafe {
let mut stream = mem::zeroed();
if Z_OK != deflateInit2(
&mut stream,
level.into(),
Z_DEFLATED,
window_bits.into(),
MAX_MEM_LEVEL,
strategy.into(),
) {
return Err(PngError::new("deflateInit2"));
}
let upper_bound = deflateBound(&mut stream, data.len() as uLong) as usize;
let max_size = max_size.get().unwrap_or(upper_bound).min(upper_bound);
// it's important to have the capacity pre-allocated,
// as unsafe set_len is called later
let mut out = Vec::with_capacity(max_size);
stream.next_in = data.as_ptr() as *mut _;
stream.total_in = data.len() as uLong;
stream.avail_in = data.len() as uInt;
stream.next_out = out.as_mut_ptr();
stream.avail_out = out.capacity() as uInt;
match deflate(&mut stream, Z_FINISH) {
Z_STREAM_END => {}
Z_OK | Z_BUF_ERROR => {
deflateEnd(&mut stream);
return Err(PngError::DeflatedDataTooLong(stream.total_out as usize));
}
_ => {
deflateEnd(&mut stream);
return Err(PngError::new("deflate"));
}
}
Ok(compressed)
if Z_OK != deflateEnd(&mut stream) {
return Err(PngError::new("deflateEnd"));
}
debug_assert!(stream.total_out as usize <= out.capacity());
out.set_len(stream.total_out as usize);
Ok(out)
}
}
impl Display for Deflater {
#[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),
}
}
pub fn zopfli_deflate(data: &[u8]) -> PngResult<Vec<u8>> {
let mut output = Vec::with_capacity(max(1024, data.len() / 20));
let options = zopfli::Options::default();
match zopfli::compress(&options, &zopfli::Format::Zlib, data, &mut output) {
Ok(_) => (),
Err(_) => return Err(PngError::new("Failed to compress in zopfli")),
};
output.shrink_to_fit();
Ok(output)
}
#[derive(Clone, Copy, Debug, PartialEq)]
/// DEFLATE algorithms supported by oxipng
pub enum Deflaters {
/// Use the Zlib/Miniz DEFLATE implementation
Zlib,
/// Use the better but slower Zopfli implementation
Zopfli,
}

View file

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

View file

@ -1,4 +0,0 @@
/// List of chunks that affect image display and will be kept when using the `Safe` chunk strip option
pub const DISPLAY_CHUNKS: [[u8; 4]; 7] = [
*b"cICP", *b"iCCP", *b"sRGB", *b"pHYs", *b"acTL", *b"fcTL", *b"fdAT",
];

View file

@ -1,69 +1,49 @@
use std::{error::Error, fmt};
use std::error::Error;
use std::fmt;
use crate::colors::{BitDepth, ColorType};
#[derive(Debug)]
#[non_exhaustive]
// TODO: Use `#[non_exhaustive]` once stabilized
// https://github.com/rust-lang/rust/issues/44109
#[derive(Debug, Clone)]
pub enum PngError {
APNGOutOfOrder,
C2PAMetadataPreventsChanges,
ChunkMissing(&'static str),
CRCMismatch([u8; 4]),
DeflatedDataTooLong(usize),
IncorrectDataLength(usize, usize),
InflatedDataTooLong(usize),
InvalidData,
InvalidDepthForType(BitDepth, ColorType),
NotPNG,
ReadFailed(String, std::io::Error),
APNGNotSupported,
InvalidData,
TruncatedData,
WriteFailed(String, std::io::Error),
ChunkMissing(&'static str),
Other(Box<str>),
#[doc(hidden)]
_Nonexhaustive,
}
impl Error for PngError {}
impl Error for PngError {
// deprecated
fn description(&self) -> &str {
""
}
}
impl fmt::Display for PngError {
#[inline]
#[cold]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Self::APNGOutOfOrder => f.write_str("APNG chunks are out of order"),
Self::C2PAMetadataPreventsChanges => f.write_str(
"The image contains C2PA manifest that would be invalidated by any file changes",
),
Self::ChunkMissing(s) => write!(f, "Chunk {s} missing or empty"),
Self::CRCMismatch(ref c) => write!(
f,
"CRC mismatch in {} chunk; May be recoverable by using --fix",
String::from_utf8_lossy(c)
),
Self::DeflatedDataTooLong(_) => f.write_str("Deflated data too long"),
Self::IncorrectDataLength(l1, l2) => write!(
f,
"Data length {l1} does not match the expected length {l2}"
),
Self::InflatedDataTooLong(max) => write!(
f,
"Inflated data would exceed the maximum size ({max} bytes)"
),
Self::InvalidData => f.write_str("Invalid data found; unable to read PNG file"),
Self::InvalidDepthForType(d, ref c) => {
write!(f, "Invalid bit depth {d} for color type {c}")
PngError::DeflatedDataTooLong(_) => f.write_str("deflated data too long"),
PngError::NotPNG => f.write_str("Invalid header detected; Not a PNG file"),
PngError::InvalidData => f.write_str("Invalid data found; unable to read PNG file"),
PngError::TruncatedData => {
f.write_str("Missing data in the file; the file is truncated")
}
Self::NotPNG => f.write_str("Invalid header detected; Not a PNG file"),
Self::ReadFailed(ref s, ref e) => write!(f, "Failed to read from {s}: {e}"),
Self::TruncatedData => f.write_str("Missing data in the file; the file is truncated"),
Self::WriteFailed(ref s, ref e) => write!(f, "Failed to write to {s}: {e}"),
Self::Other(ref s) => f.write_str(s),
PngError::APNGNotSupported => f.write_str("APNG files are not (yet) supported"),
PngError::ChunkMissing(s) => write!(f, "Chunk {} missing or empty", s),
PngError::Other(ref s) => f.write_str(s),
PngError::_Nonexhaustive => unreachable!(),
}
}
}
impl PngError {
#[cold]
#[must_use]
pub fn new(description: &str) -> Self {
Self::Other(description.into())
#[inline]
pub fn new(description: &str) -> PngError {
PngError::Other(description.into())
}
}

View file

@ -1,202 +0,0 @@
//! Check if a reduction makes file smaller, and keep best reductions.
//! Works asynchronously when possible
#[cfg(not(feature = "parallel"))]
use std::cell::RefCell;
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering::*},
};
use deflate::Deflater;
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,
};
pub(crate) struct Candidate {
pub image: Arc<PngImage>,
pub idat_data: Option<Vec<u8>>,
pub estimated_output_size: usize,
/// The input filter, which is retained for printing and for APNG frames.
pub filter: FilterStrategy,
/// The filter returned by the filter function, which may be Predefined.
/// Use this for the next round to avoid recomputing the filter.
pub filter_used: FilterStrategy,
/// For determining tie-breaker
nth: usize,
}
impl Candidate {
fn cmp_key(&self) -> impl Ord + use<> {
(
self.estimated_output_size,
self.image.data.len(),
self.filter.clone(),
// Prefer the later image added (e.g. baseline, which is always added last)
usize::MAX - self.nth,
)
}
}
/// Collect image versions and pick one that compresses best
pub(crate) struct Evaluator {
deadline: Arc<Deadline>,
filters: IndexSet<FilterStrategy>,
deflater: Deflater,
optimize_alpha: bool,
final_round: bool,
nth: AtomicUsize,
executed: Arc<AtomicUsize>,
best_candidate_size: Arc<AtomicMin>,
/// images are sent to the caller thread for evaluation
#[cfg(feature = "parallel")]
eval_channel: (Sender<Candidate>, Receiver<Candidate>),
// in non-parallel mode, images are evaluated synchronously
#[cfg(not(feature = "parallel"))]
eval_best_candidate: RefCell<Option<Candidate>>,
}
impl Evaluator {
pub fn new(
deadline: Arc<Deadline>,
filters: IndexSet<FilterStrategy>,
deflater: Deflater,
optimize_alpha: bool,
final_round: bool,
) -> Self {
#[cfg(feature = "parallel")]
let eval_channel = channel();
Self {
deadline,
filters,
deflater,
optimize_alpha,
final_round,
nth: AtomicUsize::new(0),
executed: Arc::new(AtomicUsize::new(0)),
best_candidate_size: Arc::new(AtomicMin::new(None)),
#[cfg(feature = "parallel")]
eval_channel,
#[cfg(not(feature = "parallel"))]
eval_best_candidate: RefCell::new(None),
}
}
/// Wait for all evaluations to finish and return smallest reduction
/// Or `None` if the queue is empty.
#[cfg(feature = "parallel")]
pub fn get_best_candidate(self) -> Option<Candidate> {
let (eval_send, eval_recv) = self.eval_channel;
// Disconnect the sender, breaking the loop in the thread
drop(eval_send);
let nth = self.nth.load(SeqCst);
// Yield to ensure all evaluations are executed
// This can prevent deadlocks when run within an existing rayon thread pool
while self.executed.load(Relaxed) < nth {
rayon::yield_local();
}
eval_recv.into_iter().min_by_key(Candidate::cmp_key)
}
#[cfg(not(feature = "parallel"))]
pub fn get_best_candidate(self) -> Option<Candidate> {
self.eval_best_candidate.into_inner()
}
/// Set best size, if known in advance
pub fn set_best_size(&self, size: usize) {
self.best_candidate_size.set_min(size);
}
/// Check if the image is smaller than others
pub fn try_image(&self, image: Arc<PngImage>) {
let description = image.ihdr.color_type.to_string();
self.try_image_with_description(image, &description);
}
/// Check if the image is smaller than others, with a description for verbose mode
pub fn try_image_with_description(&self, image: Arc<PngImage>, description: &str) {
let nth = self.nth.fetch_add(1, SeqCst);
// These clones are only cheap refcounts
let deadline = self.deadline.clone();
let filters = self.filters.clone();
let deflater = self.deflater;
let optimize_alpha = self.optimize_alpha;
let final_round = self.final_round;
let executed = self.executed.clone();
let best_candidate_size = self.best_candidate_size.clone();
let description = description.to_string();
// sends it off asynchronously for compression,
// but results will be collected via the message queue
#[cfg(feature = "parallel")]
let eval_send = self.eval_channel.0.clone();
rayon::spawn(move || {
executed.fetch_add(1, Relaxed);
let filters_iter = filters.par_iter().with_max_len(1);
// Updating of best result inside the parallel loop would require locks,
// 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| {
if deadline.passed() {
return;
}
let (filtered, filter_used) = image.filter_image(filter.clone(), optimize_alpha);
let idat_data = deflater.deflate(&filtered, best_candidate_size.get());
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
let new = Candidate {
image: image.clone(),
idat_data: if final_round { Some(idat_data) } else { None },
estimated_output_size,
filter: filter.clone(),
filter_used,
nth,
};
best_candidate_size.set_min(estimated_output_size);
#[cfg(feature = "parallel")]
{
eval_send.send(new).expect("send");
}
#[cfg(not(feature = "parallel"))]
{
match &mut *self.eval_best_candidate.borrow_mut() {
Some(prev) if prev.cmp_key() < new.cmp_key() => {}
best => *best = Some(new),
}
}
} else if let Err(PngError::DeflatedDataTooLong(size)) = idat_data {
trace!(
"Eval: {}-bit {:23} {:8} >{} bytes",
image.ihdr.bit_depth, description, filter, size
);
}
});
});
}
}

View file

@ -1,278 +1,148 @@
use std::{fmt, fmt::Display, mem::transmute};
/// Filtering strategy for use in [`Options`][crate::Options]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
pub enum FilterStrategy {
/// Same filter for all rows
Basic(RowFilter),
/// Minimum sum of absolute differences
MinSum,
/// Shannon entropy
Entropy,
/// Count of distinct bigrams
Bigrams,
/// Shannon entropy of bigrams
BigEnt,
/// Deflate compression
Brute {
/// The number of lines to compress at once
num_lines: usize,
/// The compression level to use (1-12)
level: u8,
},
/// Predefined filter for each row
Predefined(Vec<RowFilter>),
}
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),
pub fn filter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec<u8> {
let mut filtered = Vec::with_capacity(data.len());
match filter {
0 => {
filtered.extend_from_slice(data);
}
}
}
/// PNG delta filters
#[repr(u8)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
pub enum RowFilter {
None,
Sub,
Up,
Average,
Paeth,
}
impl TryFrom<u8> for RowFilter {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
if value > 4 {
return Err(());
1 => {
filtered.extend_from_slice(&data[0..bpp]);
filtered.extend(
data.iter()
.skip(bpp)
.zip(data.iter())
.map(|(cur, last)| cur.wrapping_sub(*last)),
);
}
unsafe { transmute(value as i8) }
}
}
impl Display for RowFilter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(
match self {
Self::None => "None",
Self::Sub => "Sub",
Self::Up => "Up",
Self::Average => "Average",
Self::Paeth => "Paeth",
},
f,
)
}
}
impl RowFilter {
pub(crate) const ALL: [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(
self,
bpp: usize,
data: &mut [u8],
prev_line: &[u8],
buf: &mut Vec<u8>,
alpha_bytes: usize,
) {
assert!(data.len() >= bpp);
assert_eq!(data.len(), prev_line.len());
if alpha_bytes != 0 {
self.optimize_alpha(bpp, data, prev_line, bpp - alpha_bytes);
}
buf.clear();
buf.reserve(data.len() + 1);
buf.push(self as u8);
match self {
Self::None => {
buf.extend_from_slice(data);
}
Self::Sub => {
buf.extend_from_slice(&data[0..bpp]);
buf.extend(
2 => {
if last_line.is_empty() {
filtered.extend_from_slice(data);
} else {
filtered.extend(
data.iter()
.skip(bpp)
.zip(data.iter())
.zip(last_line.iter())
.map(|(cur, last)| cur.wrapping_sub(*last)),
);
}
Self::Up => {
buf.extend(
data.iter()
.zip(prev_line.iter())
.map(|(cur, last)| cur.wrapping_sub(*last)),
);
}
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,
)));
}
}
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]),
)));
}
}
};
}
3 => for (i, byte) in data.iter().enumerate() {
if last_line.is_empty() {
filtered.push(match i.checked_sub(bpp) {
Some(x) => byte.wrapping_sub(data[x] >> 1),
None => *byte,
});
} else {
filtered.push(match i.checked_sub(bpp) {
Some(x) => byte
.wrapping_sub(((u16::from(data[x]) + u16::from(last_line[i])) >> 1) as u8),
None => byte.wrapping_sub(last_line[i] >> 1),
});
};
},
4 => for (i, byte) in data.iter().enumerate() {
if last_line.is_empty() {
filtered.push(match i.checked_sub(bpp) {
Some(x) => byte.wrapping_sub(data[x]),
None => *byte,
});
} else {
filtered.push(match i.checked_sub(bpp) {
Some(x) => {
byte.wrapping_sub(paeth_predictor(data[x], last_line[i], last_line[x]))
}
None => byte.wrapping_sub(last_line[i]),
});
};
},
_ => unreachable!(),
}
filtered
}
// Optimize fully transparent pixels of a scanline such that they will be zeroed when filtered
fn optimize_alpha(self, bpp: usize, data: &mut [u8], prev_line: &[u8], color_bytes: usize) {
if self == Self::None {
// Assume transparent pixels already set to 0
return;
pub fn unfilter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec<u8> {
let mut unfiltered = Vec::with_capacity(data.len());
match filter {
0 => {
unfiltered.extend_from_slice(data);
}
let mut pixels: Vec<_> = data.chunks_exact_mut(bpp).collect();
let prev_pixels: Vec<_> = prev_line.chunks_exact(bpp).collect();
for i in 0..pixels.len() {
if pixels[i].iter().skip(color_bytes).all(|b| *b == 0) {
// If the first pixel in the row is transparent, find the next non-transparent pixel and pretend
// it is the previous one. This can help improve effectiveness of the Sub and Paeth filters.
let prev = match i {
0 => pixels
.iter()
.position(|px| px.iter().skip(color_bytes).any(|b| *b != 0))
.unwrap_or(i),
_ => i - 1,
1 => for (i, byte) in data.iter().enumerate() {
match i.checked_sub(bpp) {
Some(x) => {
let b = unfiltered[x];
unfiltered.push(byte.wrapping_add(b));
}
None => {
unfiltered.push(*byte);
}
};
},
2 => {
if last_line.is_empty() {
unfiltered.extend_from_slice(data);
} else {
unfiltered.extend(
data.iter()
.zip(last_line.iter())
.map(|(cur, last)| cur.wrapping_add(*last)),
);
};
}
3 => {
for (i, byte) in data.iter().enumerate() {
if last_line.is_empty() {
match i.checked_sub(bpp) {
Some(x) => {
let b = unfiltered[x];
unfiltered.push(byte.wrapping_add(b >> 1));
}
None => {
unfiltered.push(*byte);
}
};
} else {
match i.checked_sub(bpp) {
Some(x) => {
let b = unfiltered[x];
unfiltered.push(byte.wrapping_add(
((u16::from(b) + u16::from(last_line[i])) >> 1) as u8,
));
}
None => {
unfiltered.push(byte.wrapping_add(last_line[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
}
}
Self::Up => {
pixels[i][0..color_bytes].copy_from_slice(&prev_pixels[i][0..color_bytes]);
}
Self::Average => {
for j in 0..color_bytes {
pixels[i][j] = match i {
0 => prev_pixels[i][j] >> 1,
_ => {
((u16::from(pixels[i - 1][j]) + u16::from(prev_pixels[i][j]))
>> 1) as u8
}
};
}
}
Self::Paeth => {
for j in 0..color_bytes {
pixels[i][j] = match i {
0 => pixels[prev][j].min(prev_pixels[i][j]),
_ => paeth_predictor(
pixels[i - 1][j],
prev_pixels[i][j],
prev_pixels[i - 1][j],
),
};
}
}
}
}
}
}
pub(crate) fn unfilter_line(
self,
bpp: usize,
data: &[u8],
prev_line: &[u8],
buf: &mut Vec<u8>,
) {
buf.clear();
buf.reserve(data.len());
assert!(data.len() >= bpp);
assert_eq!(data.len(), prev_line.len());
match self {
Self::None => {
buf.extend_from_slice(data);
}
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)));
}
}
Self::Up => {
buf.extend(
data.iter()
.zip(prev_line)
.map(|(&cur, &last)| cur.wrapping_add(last)),
);
}
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,
)));
}
}
Self::Paeth => {
for (i, (&cur, &up)) in data.iter().zip(prev_line).enumerate() {
buf.push(
match i
.checked_sub(bpp)
.map(|x| (buf.get(x).copied(), prev_line.get(x).copied()))
{
Some((Some(left), Some(left_up))) => {
cur.wrapping_add(paeth_predictor(left, up, left_up))
}
_ => cur.wrapping_add(up),
},
);
}
}
}
4 => for (i, byte) in data.iter().enumerate() {
if last_line.is_empty() {
match i.checked_sub(bpp) {
Some(x) => {
let b = unfiltered[x];
unfiltered.push(byte.wrapping_add(b));
}
None => {
unfiltered.push(*byte);
}
};
} else {
match i.checked_sub(bpp) {
Some(x) => {
let b = unfiltered[x];
unfiltered.push(byte.wrapping_add(paeth_predictor(
b,
last_line[i],
last_line[x],
)));
}
None => {
unfiltered.push(byte.wrapping_add(last_line[i]));
}
};
};
},
_ => unreachable!(),
}
unfiltered
}
fn paeth_predictor(a: u8, b: u8, c: u8) -> u8 {

View file

@ -1,16 +1,12 @@
use indexmap::IndexSet;
use log::{debug, trace, warn};
use rgb::{RGB16, RGBA8};
use byteorder::{BigEndian, ReadBytesExt};
use colors::{BitDepth, ColorType};
use crc::crc32;
use error::PngError;
use std::collections::HashSet;
use std::io::Cursor;
use PngResult;
use crate::{
Deflater, Options, PngResult,
colors::{BitDepth, ColorType},
deflate::{crc32, inflate},
display_chunks::DISPLAY_CHUNKS,
error::PngError,
};
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
/// Headers from the IHDR chunk of the image
pub struct IhdrData {
/// The width of the image in pixels
@ -21,81 +17,38 @@ 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 compression method used for this image (0 for DEFLATE)
pub compression: u8,
/// The filter mode used for this image (currently only 0 is valid)
pub filter: u8,
/// The interlacing mode of the image (0 = None, 1 = Adam7)
pub interlaced: u8,
}
impl IhdrData {
/// Bits per pixel
#[must_use]
#[inline]
pub const fn bpp(&self) -> usize {
self.bit_depth as usize * self.color_type.channels_per_pixel() as usize
}
/// Byte length of IDAT that is correct for this IHDR
#[must_use]
pub const fn raw_data_size(&self) -> usize {
let w = self.width as usize;
let h = self.height as usize;
let bpp = self.bpp();
const fn bitmap_size(bpp: usize, w: usize, h: usize) -> usize {
(w * bpp).div_ceil(8) * h
}
if self.interlaced {
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);
}
size += bitmap_size(bpp, (w + 3) >> 2, (h + 3) >> 3) + ((h + 3) >> 3);
if w > 2 {
size += bitmap_size(bpp, (w + 1) >> 2, (h + 3) >> 2) + ((h + 3) >> 2);
}
size += bitmap_size(bpp, (w + 1) >> 1, (h + 1) >> 2) + ((h + 1) >> 2);
if w > 1 {
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
}
}
}
#[derive(Debug, Clone)]
pub struct Chunk {
pub name: [u8; 4],
pub data: Vec<u8>,
}
/// [`Options`][crate::Options] to use when stripping chunks (metadata)
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum StripChunks {
#[derive(Debug, PartialEq, Clone)]
/// Options to use for performing operations on headers (such as stripping)
pub enum Headers {
/// None
///
/// ...except caBX chunk if it contains a C2PA.org signature.
None,
/// Remove specific chunks
Strip(IndexSet<[u8; 4]>),
/// Remove all chunks that won't affect image display
Strip(Vec<String>),
/// Headers that won't affect rendering (all but cHRM, gAMA, iCCP, sBIT, sRGB, bKGD, hIST, pHYs, sPLT)
Safe,
/// Remove all non-critical chunks except these
Keep(IndexSet<[u8; 4]>),
/// All non-critical chunks
Keep(HashSet<String>),
/// All non-critical headers
All,
}
impl StripChunks {
pub(crate) fn keep(&self, name: &[u8; 4]) -> bool {
match &self {
Self::None => true,
Self::Keep(names) => names.contains(name),
Self::Strip(names) => !names.contains(name),
Self::Safe => DISPLAY_CHUNKS.contains(name),
Self::All => false,
}
#[derive(Debug, Clone)]
pub struct Header {
pub key: String,
pub data: Vec<u8>,
}
impl Header {
pub fn new(key: String, data: Vec<u8>) -> Self {
Header { key, data }
}
}
@ -107,319 +60,84 @@ pub fn file_header_is_valid(bytes: &[u8]) -> bool {
}
#[derive(Debug, Clone, Copy)]
pub struct RawChunk<'a> {
pub struct RawHeader<'a> {
pub name: [u8; 4],
pub data: &'a [u8],
}
impl RawChunk<'_> {
// Is it a chunk for C2PA/CAI JUMBF metadata
pub(crate) fn is_c2pa(&self) -> bool {
if self.name == *b"caBX" {
if let Some((b"jumb", data)) = parse_jumbf_box(self.data) {
if let Some((b"jumd", data)) = parse_jumbf_box(data) {
if data.get(..4) == Some(b"c2pa") {
return true;
}
}
}
}
false
}
}
fn parse_jumbf_box(data: &[u8]) -> Option<(&[u8], &[u8])> {
if data.len() < 8 {
return None;
}
let (len, rest) = data.split_at(4);
let len = read_be_u32(len) as usize;
if len < 8 || len > data.len() {
return None;
}
let (box_name, data) = rest.split_at(4);
let data = data.get(..len - 8)?;
Some((box_name, data))
}
pub fn parse_next_chunk<'a>(
pub fn parse_next_header<'a>(
byte_data: &'a [u8],
byte_offset: &mut usize,
fix_errors: bool,
) -> PngResult<Option<RawChunk<'a>>> {
let length = read_be_u32(
) -> PngResult<Option<RawHeader<'a>>> {
let mut rdr = Cursor::new(
byte_data
.get(*byte_offset..*byte_offset + 4)
.ok_or(PngError::TruncatedData)?,
);
if byte_data.len() < *byte_offset + 12 + length as usize {
return Err(PngError::TruncatedData);
}
let length = rdr.read_u32::<BigEndian>().unwrap();
*byte_offset += 4;
let chunk_start = *byte_offset;
let chunk_name = &byte_data[chunk_start..chunk_start + 4];
let header_start = *byte_offset;
let chunk_name = byte_data
.get(header_start..header_start + 4)
.ok_or(PngError::TruncatedData)?;
if chunk_name == b"IEND" {
// End of data
return Ok(None);
}
*byte_offset += 4;
let data = &byte_data[*byte_offset..*byte_offset + length as usize];
let data = byte_data
.get(*byte_offset..*byte_offset + length as usize)
.ok_or(PngError::TruncatedData)?;
*byte_offset += length as usize;
let crc = read_be_u32(&byte_data[*byte_offset..*byte_offset + 4]);
let mut rdr = Cursor::new(
byte_data
.get(*byte_offset..*byte_offset + 4)
.ok_or(PngError::TruncatedData)?,
);
let crc = rdr.read_u32::<BigEndian>().unwrap();
*byte_offset += 4;
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()));
let header_bytes = byte_data
.get(header_start..header_start + 4 + length as usize)
.ok_or(PngError::TruncatedData)?;
if !fix_errors && crc32::checksum_ieee(header_bytes) != crc {
return Err(PngError::new(&format!(
"CRC Mismatch in {} header; May be recoverable by using --fix",
String::from_utf8_lossy(chunk_name)
)));
}
let name: [u8; 4] = chunk_name.try_into().unwrap();
Ok(Some(RawChunk { name, data }))
let mut name = [0u8; 4];
name.copy_from_slice(chunk_name);
Ok(Some(RawHeader { name, data }))
}
pub fn parse_ihdr_chunk(
byte_data: &[u8],
palette_data: Option<Vec<u8>>,
trns_data: Option<Vec<u8>>,
) -> PngResult<IhdrData> {
// This eliminates bounds checks for the rest of the function
let interlaced = byte_data.get(12).copied().ok_or(PngError::TruncatedData)?;
pub fn parse_ihdr_header(byte_data: &[u8]) -> PngResult<IhdrData> {
let mut rdr = Cursor::new(&byte_data[0..8]);
Ok(IhdrData {
color_type: match byte_data[9] {
0 => ColorType::Grayscale {
transparent_shade: trns_data
.filter(|t| t.len() >= 2)
.map(|t| read_be_u16(&t[0..2])),
},
2 => ColorType::RGB {
transparent_color: trns_data.filter(|t| t.len() >= 6).map(|t| RGB16 {
r: read_be_u16(&t[0..2]),
g: read_be_u16(&t[2..4]),
b: read_be_u16(&t[4..6]),
}),
},
3 => ColorType::Indexed {
palette: palette_to_rgba(palette_data, trns_data).unwrap_or_default(),
},
0 => ColorType::Grayscale,
2 => ColorType::RGB,
3 => ColorType::Indexed,
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),
bit_depth: match byte_data[8] {
1 => BitDepth::One,
2 => BitDepth::Two,
4 => BitDepth::Four,
8 => BitDepth::Eight,
16 => BitDepth::Sixteen,
_ => return Err(PngError::new("Unexpected bit depth in header")),
},
width: rdr.read_u32::<BigEndian>().unwrap(),
height: rdr.read_u32::<BigEndian>().unwrap(),
compression: byte_data[10],
filter: byte_data[11],
interlaced: byte_data[12],
})
}
/// Construct an RGBA palette from the raw palette and transparency data
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 mut palette: Vec<_> = palette_data
.chunks_exact(3)
.map(|color| RGBA8::new(color[0], color[1], color[2], 255))
.collect();
if let Some(trns_data) = trns_data {
for (color, trns) in palette.iter_mut().zip(trns_data) {
color.a = trns;
}
}
Ok(palette)
}
#[inline]
pub fn read_be_u16(bytes: &[u8]) -> u16 {
u16::from_be_bytes(bytes.try_into().unwrap())
}
#[inline]
pub fn read_be_u32(bytes: &[u8]) -> u32 {
u32::from_be_bytes(bytes.try_into().unwrap())
}
/// Extract and decompress the ICC profile from an iCCP chunk
pub fn extract_icc(iccp: &Chunk, max_size: Option<usize>) -> Option<Vec<u8>> {
// Skip (useless) profile name
let mut data = iccp.data.as_slice();
loop {
let (&n, rest) = data.split_first()?;
data = rest;
if n == 0 {
break;
}
}
let (&compression_method, compressed_data) = data.split_first()?;
if compression_method != 0 {
return None; // The profile is supposed to be compressed (method 0)
}
// 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) {
Ok(icc) => Some(icc),
Err(e) => {
// Log the error so we can know if the buffer size needs to be adjusted
warn!("Failed to decompress icc: {e}");
None
}
}
}
/// Make an iCCP chunk by compressing the ICC profile
pub fn make_iccp(icc: &[u8], deflater: Deflater, max_size: Option<usize>) -> PngResult<Chunk> {
let mut compressed = deflater.deflate(icc, max_size)?;
let mut data = Vec::with_capacity(compressed.len() + 5);
data.extend(b"icc"); // Profile name - generally unused, can be anything
data.extend([0, 0]); // Null separator, zlib compression method
data.append(&mut compressed);
Ok(Chunk {
name: *b"iCCP",
data,
})
}
/// If the profile is sRGB, extracts the rendering intent value from it
pub fn srgb_rendering_intent(icc_data: &[u8]) -> Option<u8> {
let rendering_intent = *icc_data.get(67)?;
// The known profiles are the same as in libpng's `png_sRGB_checks`.
// The Profile ID header of ICC has a fixed layout,
// and is supposed to contain MD5 of profile data at this offset
match icc_data.get(84..100)? {
b"\x29\xf8\x3d\xde\xaf\xf2\x55\xae\x78\x42\xfa\xe4\xca\x83\x39\x0d"
| b"\xc9\x5b\xd6\x37\xe9\x5d\x8a\x3b\x0d\xf3\x8f\x99\xc1\x32\x03\x89"
| b"\xfc\x66\x33\x78\x37\xe2\x88\x6b\xfd\x72\xe9\x83\x82\x28\xf1\xb8"
| b"\x34\x56\x2a\xbf\x99\x4c\xcd\x06\x6d\x2c\x57\x21\xd0\xd6\x8c\x5d" => {
Some(rendering_intent)
}
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" => {
// Known-bad profiles are identified by their CRC
match (crc32(icc_data), icc_data.len()) {
(0x5d51_29ce, 3024) | (0x182e_a552, 3144) | (0xf29e_526d, 3144) => {
Some(rendering_intent)
}
_ => None,
}
}
_ => None,
}
}
/// Process aux chunks and potentially adjust options before optimizing
pub fn preprocess_chunks(aux_chunks: &mut Vec<Chunk>, opts: &mut Options) {
let has_srgb = aux_chunks.iter().any(|c| &c.name == b"sRGB");
// Grayscale conversion should not be performed if the image is not in the sRGB colorspace
// An sRGB profile would need to be stripped on conversion, so disallow if stripping is disabled
let mut allow_grayscale = !has_srgb || opts.strip != StripChunks::None;
if let Some(iccp_idx) = aux_chunks.iter().position(|c| &c.name == b"iCCP") {
allow_grayscale = false;
// See if we can replace an iCCP chunk with an sRGB chunk
let may_replace_iccp = opts.strip != StripChunks::None && opts.strip.keep(b"sRGB");
if may_replace_iccp && has_srgb {
// Files aren't supposed to have both chunks, so we chose to honor sRGB
trace!("Removing iCCP chunk due to conflict with sRGB chunk");
aux_chunks.remove(iccp_idx);
allow_grayscale = true;
} else if let Some(icc) = extract_icc(&aux_chunks[iccp_idx], opts.max_decompressed_size) {
let intent = if may_replace_iccp {
srgb_rendering_intent(&icc)
} else {
None
};
// sRGB-like profile can be replaced with an sRGB chunk with the same rendering intent
if let Some(intent) = intent {
trace!("Replacing iCCP chunk with equivalent sRGB chunk");
aux_chunks[iccp_idx] = Chunk {
name: *b"sRGB",
data: vec![intent],
};
allow_grayscale = true;
} else if opts.idat_recoding {
// Try recompressing the profile
let cur_len = aux_chunks[iccp_idx].data.len();
if let Ok(iccp) = make_iccp(&icc, opts.deflater, Some(cur_len - 1)) {
debug!(
"Recompressed iCCP chunk: {} ({} bytes decrease)",
iccp.data.len(),
cur_len - iccp.data.len()
);
aux_chunks[iccp_idx] = iccp;
}
}
}
}
if !allow_grayscale && opts.grayscale_reduction {
debug!("Disabling grayscale reduction due to presence of sRGB or iCCP chunk");
opts.grayscale_reduction = false;
}
// Check for APNG by presence of acTL chunk
if aux_chunks.iter().any(|c| &c.name == b"acTL") {
warn!("APNG detected, disabling all reductions");
opts.interlace = None;
opts.bit_depth_reduction = false;
opts.color_type_reduction = false;
opts.palette_reduction = false;
opts.grayscale_reduction = false;
}
}
/// Perform cleanup of certain aux chunks after optimization has been completed
pub fn postprocess_chunks(aux_chunks: &mut Vec<Chunk>, ihdr: &IhdrData, orig_ihdr: &IhdrData) {
// If the depth/color type has changed, some chunks may be invalid and should be dropped
// While these could potentially be converted, they have no known use case today and are
// generally more trouble than they're worth
if orig_ihdr.bit_depth != ihdr.bit_depth || orig_ihdr.color_type != ihdr.color_type {
aux_chunks.retain(|c| {
let invalid = &c.name == b"bKGD" || &c.name == b"sBIT" || &c.name == b"hIST";
if invalid {
warn!(
"Removing {} chunk as it no longer matches the image data",
std::str::from_utf8(&c.name).unwrap()
);
}
!invalid
});
}
// Remove any sRGB or iCCP chunks if the image was converted to or from grayscale
if orig_ihdr.color_type.is_gray() != ihdr.color_type.is_gray() {
aux_chunks.retain(|c| {
let invalid = &c.name == b"sRGB" || &c.name == b"iCCP";
if invalid {
trace!(
"Removing {} chunk as it no longer matches the color type",
std::str::from_utf8(&c.name).unwrap()
);
}
!invalid
});
}
// Remove iDOT which will necessarily be invalid after successful optimization
aux_chunks.retain(|c| {
let invalid = &c.name == b"iDOT";
if invalid {
trace!(
"Removing {} chunk as it no longer matches the IDAT",
std::str::from_utf8(&c.name).unwrap()
);
}
!invalid
});
}

View file

@ -1,20 +1,51 @@
use bitvec::prelude::*;
use bit_vec::BitVec;
use png::PngData;
use crate::{headers::IhdrData, png::PngImage};
#[must_use]
pub fn interlace_image(png: &PngImage) -> PngImage {
let mut passes: Vec<BitVec<u8, Msb0>> = vec![BitVec::new(); 7];
let bits_per_pixel = png.ihdr.bpp();
for (index, line) in png.scan_lines(false).enumerate() {
let bit_vec = line.data.view_bits::<Msb0>();
for (i, bit) in bit_vec.iter().by_vals().enumerate() {
pub fn interlace_image(png: &mut PngData) {
let mut passes: Vec<BitVec> = vec![BitVec::new(); 7];
let bits_per_pixel = png.ihdr_data.bit_depth.as_u8() * png.channels_per_pixel();
for (index, line) in png.scan_lines().enumerate() {
match index % 8 {
// Add filter bytes to passes that will be in the output image
0 => {
passes[0].extend(BitVec::from_elem(8, false));
if png.ihdr_data.width >= 5 {
passes[1].extend(BitVec::from_elem(8, false));
}
if png.ihdr_data.width >= 3 {
passes[3].extend(BitVec::from_elem(8, false));
}
if png.ihdr_data.width >= 2 {
passes[5].extend(BitVec::from_elem(8, false));
}
}
4 => {
passes[2].extend(BitVec::from_elem(8, false));
if png.ihdr_data.width >= 3 {
passes[3].extend(BitVec::from_elem(8, false));
}
if png.ihdr_data.width >= 2 {
passes[5].extend(BitVec::from_elem(8, false));
}
}
2 | 6 => {
passes[4].extend(BitVec::from_elem(8, false));
if png.ihdr_data.width >= 2 {
passes[5].extend(BitVec::from_elem(8, false));
}
}
_ => {
passes[6].extend(BitVec::from_elem(8, false));
}
}
let bit_vec = BitVec::from_bytes(&line.data);
for (i, bit) in bit_vec.iter().enumerate() {
// Avoid moving padded 0's into new image
if i >= (png.ihdr.width as usize * bits_per_pixel) {
if i >= (png.ihdr_data.width * u32::from(bits_per_pixel)) as usize {
break;
}
// Copy pixels into interlaced passes
let pix_modulo = (i / bits_per_pixel) % 8;
let pix_modulo = (i / bits_per_pixel as usize) % 8;
match index % 8 {
0 => match pix_modulo {
0 => passes[0].push(bit),
@ -43,136 +74,64 @@ pub fn interlace_image(png: &PngImage) -> PngImage {
}
}
}
let mut output = Vec::with_capacity(png.data.len());
let mut output = Vec::new();
for pass in &passes {
output.extend_from_slice(pass.as_raw_slice());
}
PngImage {
data: output,
ihdr: IhdrData {
color_type: png.ihdr.color_type.clone(),
interlaced: true,
..png.ihdr
},
output.extend(pass.to_bytes());
}
png.raw_data = output;
}
pub fn deinterlace_image(png: &PngImage) -> PngImage {
PngImage {
data: match png.ihdr.bpp() {
8.. => deinterlace_bytes(png),
_ => deinterlace_bits(png),
},
ihdr: IhdrData {
color_type: png.ihdr.color_type.clone(),
interlaced: false,
..png.ihdr
},
}
}
/// Deinterlace by bits, for images with less than 8bpp
fn deinterlace_bits(png: &PngImage) -> Vec<u8> {
let bits_per_pixel = png.ihdr.bpp();
let bits_per_line = bits_per_pixel * png.ihdr.width as usize;
// Initialize each output line with blank data
let mut lines: Vec<BitVec<u8, Msb0>> =
vec![bitvec![u8, Msb0; 0; bits_per_line]; png.ihdr.height as usize];
pub fn deinterlace_image(png: &mut PngData) {
let bits_per_pixel = png.ihdr_data.bit_depth.as_u8() * png.channels_per_pixel();
let bits_per_line = 8 + bits_per_pixel as usize * png.ihdr_data.width as usize;
// Initialize each output line with a starting filter byte of 0
// as well as some blank data
let mut lines: Vec<BitVec> =
vec![BitVec::from_elem(bits_per_line, false); png.ihdr_data.height as usize];
let mut current_pass = 1;
let mut pass_constants = interlaced_constants(current_pass);
let mut current_y: usize = pass_constants.y_shift as usize;
for line in png.scan_lines(false) {
let bit_vec = line.data.view_bits::<Msb0>();
let bits_in_line = (png.ihdr.width - u32::from(pass_constants.x_shift))
.div_ceil(u32::from(pass_constants.x_step)) as usize
* bits_per_pixel;
for (i, bit) in bit_vec.iter().by_vals().enumerate() {
for line in png.scan_lines() {
let bit_vec = BitVec::from_bytes(&line.data);
let bits_in_line = ((png.ihdr_data.width - u32::from(pass_constants.x_shift)) as f32
/ f32::from(pass_constants.x_step)).ceil() as usize
* bits_per_pixel as usize;
for (i, bit) in bit_vec.iter().enumerate() {
// Avoid moving padded 0's into new image
if i >= bits_in_line {
break;
}
let current_x: usize = pass_constants.x_shift as usize
+ (i / bits_per_pixel) * pass_constants.x_step as usize;
// Copy this bit into the output line
let index = (i % bits_per_pixel) + current_x * bits_per_pixel;
+ (i / bits_per_pixel as usize) * pass_constants.x_step as usize;
// Copy this bit into the output line, offset by 8 because of filter byte
let index = 8 + (i % bits_per_pixel as usize) + current_x * bits_per_pixel as usize;
lines[current_y].set(index, bit);
}
// Calculate the next line and move to next pass if necessary
current_y += pass_constants.y_step as usize;
if current_y >= png.ihdr.height as usize {
if !increment_pass(&mut current_pass, &png.ihdr) {
if current_y >= png.ihdr_data.height as usize {
if current_pass == 7 {
break;
}
current_pass += 1;
if current_pass == 2 && png.ihdr_data.width <= 4 {
current_pass += 1;
}
if current_pass == 3 && png.ihdr_data.height <= 4 {
current_pass += 1;
}
pass_constants = interlaced_constants(current_pass);
current_y = pass_constants.y_shift as usize;
}
}
let mut output = Vec::with_capacity(png.data.len());
let mut output = Vec::new();
for line in &mut lines {
while line.len() % 8 != 0 {
line.push(false);
}
output.extend_from_slice(line.as_raw_slice());
output.extend(line.to_bytes());
}
output
}
/// Deinterlace by bytes, for images with at least 8bpp
fn deinterlace_bytes(png: &PngImage) -> Vec<u8> {
let bytes_per_pixel = png.ihdr.bpp() / 8;
let bytes_per_line = bytes_per_pixel * png.ihdr.width as usize;
// Initialize each output line with some blank data
let mut lines: Vec<Vec<u8>> = vec![vec![0; bytes_per_line]; png.ihdr.height as usize];
let mut current_pass = 1;
let mut pass_constants = interlaced_constants(current_pass);
let mut current_y: usize = pass_constants.y_shift as usize;
for line in png.scan_lines(false) {
for (i, byte) in line.data.iter().enumerate() {
let current_x: usize = pass_constants.x_shift as usize
+ (i / bytes_per_pixel) * pass_constants.x_step as usize;
// Copy this byte into the output line
let index = (i % bytes_per_pixel) + current_x * bytes_per_pixel;
lines[current_y][index] = *byte;
}
// Calculate the next line and move to next pass if necessary
current_y += pass_constants.y_step as usize;
if current_y >= png.ihdr.height as usize {
if !increment_pass(&mut current_pass, &png.ihdr) {
break;
}
pass_constants = interlaced_constants(current_pass);
current_y = pass_constants.y_shift as usize;
}
}
lines.concat()
}
const fn increment_pass(current_pass: &mut u8, ihdr: &IhdrData) -> bool {
if *current_pass == 7 {
return false;
}
*current_pass += 1;
if *current_pass == 2 && ihdr.width <= 4 {
*current_pass += 1;
}
if *current_pass == 3 && ihdr.height <= 4 {
*current_pass += 1;
}
if *current_pass == 4 && ihdr.width <= 2 {
*current_pass += 1;
}
if *current_pass == 5 && ihdr.height <= 2 {
*current_pass += 1;
}
if *current_pass == 6 && ihdr.width == 1 {
*current_pass += 1;
}
if *current_pass == 7 && ihdr.height == 1 {
return false;
}
true
png.raw_data = output;
}
#[derive(Clone, Copy)]

1301
src/lib.rs

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,303 +0,0 @@
use std::{
fmt,
path::{Path, PathBuf},
time::Duration,
};
use indexmap::{IndexSet, indexset};
use log::warn;
use crate::{deflate::Deflater, filters::FilterStrategy, headers::StripChunks};
/// Write destination for [`optimize`][crate::optimize].
/// You can use [`optimize_from_memory`](crate::optimize_from_memory) to avoid external I/O.
#[derive(Clone, Debug)]
pub enum OutFile {
/// Don't actually write any output, just calculate the best results.
None,
/// Write output to a file.
///
/// * `path`: Path to write the output file. `None` means same as input.
/// * `preserve_attrs`: Ensure the output file has the same permissions & timestamps as the input file.
Path {
path: Option<PathBuf>,
preserve_attrs: bool,
},
/// Write to standard output.
StdOut,
}
impl OutFile {
/// Construct a new `OutFile` with the given path.
///
/// This is a convenience method for `OutFile::Path { path: Some(path), preserve_attrs: false }`.
#[must_use]
pub const fn from_path(path: PathBuf) -> Self {
Self::Path {
path: Some(path),
preserve_attrs: false,
}
}
#[must_use]
pub fn path(&self) -> Option<&Path> {
match *self {
Self::Path {
path: Some(ref p), ..
} => Some(p.as_path()),
_ => None,
}
}
}
/// Where to read images from in [`optimize`][crate::optimize].
/// You can use [`optimize_from_memory`](crate::optimize_from_memory) to avoid external I/O.
#[derive(Clone, Debug)]
pub enum InFile {
Path(PathBuf),
StdIn,
}
impl InFile {
#[must_use]
pub fn path(&self) -> Option<&Path> {
match *self {
Self::Path(ref p) => Some(p.as_path()),
Self::StdIn => None,
}
}
}
impl fmt::Display for InFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::Path(ref p) => write!(f, "{}", p.display()),
Self::StdIn => f.write_str("stdin"),
}
}
}
impl<T: Into<PathBuf>> From<T> for InFile {
fn from(s: T) -> Self {
Self::Path(s.into())
}
}
#[derive(Clone, Debug)]
/// Options controlling the output of the `optimize` function
pub struct Options {
/// Attempt to fix errors when decoding the input file rather than returning an `Err`.
///
/// Default: `false`
pub fix_errors: bool,
/// Write to output even if there was no improvement in compression.
///
/// Default: `false`
pub force: bool,
/// Which `FilterStrategy` to try on the file
///
/// Default: `None,Sub,Entropy,Bigrams`
pub filters: IndexSet<FilterStrategy>,
/// Whether to change the interlacing of the file.
///
/// - `None` will not change the current interlacing.
/// - `Some(x)` will turn interlacing on or off.
///
/// Default: `Some(false)`
pub interlace: Option<bool>,
/// Whether to allow transparent pixels to be altered to improve compression.
///
/// Default: `false`
pub optimize_alpha: bool,
/// Whether to attempt bit depth reduction
///
/// Default: `true`
pub bit_depth_reduction: bool,
/// Whether to attempt color type reduction
///
/// Default: `true`
pub color_type_reduction: bool,
/// Whether to attempt palette reduction
///
/// Default: `true`
pub palette_reduction: bool,
/// Whether to attempt grayscale reduction
///
/// Default: `true`
pub grayscale_reduction: bool,
/// Whether to perform recoding of IDAT and other compressed chunks
///
/// If any type of reduction is performed, IDAT recoding will be performed
/// regardless of this setting
///
/// Default: `true`
pub idat_recoding: bool,
/// Whether to forcibly reduce 16-bit to 8-bit by scaling
///
/// Default: `false`
pub scale_16: bool,
/// Which chunks to strip from the PNG file, if any
///
/// Default: `None`
pub strip: StripChunks,
/// Which DEFLATE (zlib) algorithm to use
#[cfg_attr(feature = "zopfli", doc = "(e.g. Zopfli)")]
///
/// Default: `Libdeflater`
pub deflater: Deflater,
/// Whether to use fast evaluation to pick the best filter
///
/// Default: `true`
pub fast_evaluation: bool,
/// Maximum amount of time to spend on optimizations.
/// Further potential optimizations are skipped if the timeout is exceeded.
///
/// Default: `None`
pub timeout: Option<Duration>,
/// Maximum decompressed size of the input IDAT.
/// If decompression would exceed this size, it will be rejected.
///
/// Default: `None`
pub max_decompressed_size: Option<usize>,
}
impl Options {
#[must_use]
pub fn from_preset(level: u8) -> Self {
let opts = Self::default();
match level {
0 => opts.apply_preset_0(),
1 => opts.apply_preset_1(),
2 => opts.apply_preset_2(),
3 => opts.apply_preset_3(),
4 => opts.apply_preset_4(),
5 => opts.apply_preset_5(),
6 => opts.apply_preset_6(),
_ => {
warn!("Level 7 and above don't exist yet and are identical to level 6");
opts.apply_preset_6()
}
}
}
#[must_use]
pub fn max_compression() -> Self {
Self::from_preset(6)
}
// The following methods make assumptions that they are operating
// on an `Options` struct generated by the `default` method.
fn apply_preset_0(mut self) -> Self {
self.filters.clear();
self.deflater = Deflater::Libdeflater { compression: 5 };
self
}
fn apply_preset_1(mut self) -> Self {
self.filters.clear();
self.deflater = Deflater::Libdeflater { compression: 10 };
self
}
const 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
}
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
}
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
}
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
}
}
impl Default for Options {
fn default() -> Self {
// Default settings based on -o 2 from the CLI interface
Self {
fix_errors: false,
force: false,
filters: indexset! {
FilterStrategy::NONE,
FilterStrategy::SUB,
FilterStrategy::Entropy,
FilterStrategy::Bigrams
},
interlace: Some(false),
optimize_alpha: false,
bit_depth_reduction: true,
color_type_reduction: true,
palette_reduction: true,
grayscale_reduction: true,
idat_recoding: true,
scale_16: false,
strip: StripChunks::None,
deflater: Deflater::Libdeflater { compression: 11 },
fast_evaluation: true,
timeout: None,
max_decompressed_size: None,
}
}
}

81
src/png/apng.rs Normal file
View file

@ -0,0 +1,81 @@
use byteorder::{BigEndian, ReadBytesExt};
use std::io::Cursor;
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum DisposalType {
None = 0,
Background = 1,
Previous = 2,
}
impl From<u8> for DisposalType {
fn from(val: u8) -> Self {
match val {
0 => DisposalType::None,
1 => DisposalType::Background,
2 => DisposalType::Previous,
_ => panic!("Unrecognized disposal type"),
}
}
}
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum BlendType {
Source = 0,
Over = 1,
}
impl From<u8> for BlendType {
fn from(val: u8) -> Self {
match val {
0 => BlendType::Source,
1 => BlendType::Over,
_ => panic!("Unrecognized blend type"),
}
}
}
#[derive(Debug, Clone)]
pub struct ApngFrame {
pub sequence_number: u32,
pub width: u32,
pub height: u32,
pub x_offset: u32,
pub y_offset: u32,
pub delay_num: u16,
pub delay_den: u16,
pub dispose_op: DisposalType,
pub blend_op: BlendType,
/// The compressed, filtered data from the fdAT chunks
pub frame_data: Vec<u8>,
/// The uncompressed, optionally filtered data from the fdAT chunks
pub raw_data: Vec<u8>,
}
impl<'a> From<&'a [u8]> for ApngFrame {
/// Converts a fcTL header to an `ApngFrame`. Will panic if `data` is less than 26 bytes.
fn from(data: &[u8]) -> Self {
let mut cursor = Cursor::new(data);
ApngFrame {
sequence_number: cursor.read_u32::<BigEndian>().unwrap(),
width: cursor.read_u32::<BigEndian>().unwrap(),
height: cursor.read_u32::<BigEndian>().unwrap(),
x_offset: cursor.read_u32::<BigEndian>().unwrap(),
y_offset: cursor.read_u32::<BigEndian>().unwrap(),
delay_num: cursor.read_u16::<BigEndian>().unwrap(),
delay_den: cursor.read_u16::<BigEndian>().unwrap(),
dispose_op: cursor.read_u8().unwrap().into(),
blend_op: cursor.read_u8().unwrap().into(),
frame_data: Vec::new(),
raw_data: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ApngHeaders {
pub frames: u32,
pub plays: u32,
}

File diff suppressed because it is too large Load diff

View file

@ -1,168 +1,131 @@
use crate::png::PngImage;
use super::PngData;
/// An iterator over the scan lines of a PNG image
#[derive(Debug, Clone)]
/// An iterator over the scan lines of a PNG image
pub struct ScanLines<'a> {
iter: ScanLineRanges,
/// A reference to the PNG image being iterated upon
raw_data: &'a [u8],
/// Whether the raw data contains filter bytes
has_filter: bool,
}
impl<'a> ScanLines<'a> {
#[must_use]
pub fn new(png: &'a PngImage, has_filter: bool) -> Self {
Self {
iter: ScanLineRanges::new(png, has_filter),
raw_data: &png.data,
has_filter,
}
}
pub png: &'a PngData,
pub start: usize,
pub end: usize,
/// Current pass number, and 0-indexed row within the pass
pub pass: Option<(u8, u32)>,
}
impl<'a> Iterator for ScanLines<'a> {
type Item = ScanLine<'a>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let (len, pass, num_pixels) = self.iter.next()?;
debug_assert!(self.raw_data.len() >= len);
debug_assert!(!self.has_filter || len > 1);
// The data length should always be correct here but this check assures
// the compiler that it doesn't need to account for a potential panic
if self.raw_data.len() < len {
return None;
}
let (data, rest) = self.raw_data.split_at(len);
self.raw_data = rest;
let (&filter, data) = if self.has_filter {
data.split_first().unwrap()
} else {
(&0, data)
};
Some(ScanLine {
filter,
data,
pass,
num_pixels,
})
}
}
#[derive(Debug, Clone)]
/// An iterator over the scan line locations of a PNG image
struct ScanLineRanges {
/// Current pass number, and 0-indexed row within the pass
pass: Option<(u8, u32)>,
bits_per_pixel: usize,
width: u32,
height: u32,
left: usize,
has_filter: bool,
}
impl ScanLineRanges {
pub fn new(png: &PngImage, has_filter: bool) -> Self {
Self {
bits_per_pixel: png.ihdr.bpp(),
width: png.ihdr.width,
height: png.ihdr.height,
left: png.data.len(),
pass: if png.ihdr.interlaced {
Some((1, 0))
} else {
None
},
has_filter,
}
}
}
impl Iterator for ScanLineRanges {
type Item = (usize, Option<u8>, usize);
fn next(&mut self) -> Option<Self::Item> {
if self.left == 0 {
return None;
}
let (pixels_per_line, current_pass) = if let Some(ref mut pass) = self.pass {
if self.end == self.png.raw_data.len() {
None
} else if self.png.ihdr_data.interlaced == 1 {
// Scanlines for interlaced PNG files
if self.pass.is_none() {
self.pass = Some((1, 0));
}
// Handle edge cases for images smaller than 5 pixels in either direction
// No extra case needed for skipping pass 7 as this is already handled by the
// self.left == 0 check above
if self.width < 5 && pass.0 == 2 {
pass.0 = 3;
pass.1 = 4;
if self.png.ihdr_data.width < 5 && self.pass.unwrap().0 == 2 {
if let Some(pass) = self.pass.as_mut() {
pass.0 = 3;
pass.1 = 4;
}
}
// Intentionally keep these separate so that they can be applied one after another
if self.height < 5 && pass.0 == 3 {
pass.0 = 4;
pass.1 = 0;
if self.png.ihdr_data.height < 5 && self.pass.unwrap().0 == 3 {
if let Some(pass) = self.pass.as_mut() {
pass.0 = 4;
pass.1 = 0;
}
}
if self.width < 3 && pass.0 == 4 {
pass.0 = 5;
pass.1 = 2;
}
if self.height < 3 && pass.0 == 5 {
pass.0 = 6;
pass.1 = 0;
}
if self.width == 1 && pass.0 == 6 {
pass.0 = 7;
pass.1 = 1;
}
let (pixels_factor, y_steps) = match pass.0 {
1 | 2 => (8, 8),
3 => (4, 8),
4 => (4, 4),
5 => (2, 4),
6 => (2, 2),
7 => (1, 2),
let bits_per_pixel = u32::from(self.png.ihdr_data.bit_depth.as_u8())
* u32::from(self.png.channels_per_pixel());
let y_steps;
let pixels_factor;
match self.pass {
Some((1, _)) | Some((2, _)) => {
pixels_factor = 8;
y_steps = 8;
}
Some((3, _)) => {
pixels_factor = 4;
y_steps = 8;
}
Some((4, _)) => {
pixels_factor = 4;
y_steps = 4;
}
Some((5, _)) => {
pixels_factor = 2;
y_steps = 4;
}
Some((6, _)) => {
pixels_factor = 2;
y_steps = 2;
}
Some((7, _)) => {
pixels_factor = 1;
y_steps = 2;
}
_ => unreachable!(),
};
let mut pixels_per_line = self.width / pixels_factor;
}
let mut pixels_per_line = self.png.ihdr_data.width / pixels_factor as u32;
// Determine whether to add pixels if there is a final, incomplete 8x8 block
let gap = self.width % pixels_factor;
match pass.0 {
1 | 3 | 5 if gap > 0 => {
pixels_per_line += 1;
}
2 if gap >= 5 => {
pixels_per_line += 1;
}
4 if gap >= 3 => {
pixels_per_line += 1;
}
6 if gap >= 2 => {
pixels_per_line += 1;
}
_ => (),
}
let current_pass = Some(pass.0);
if pass.1 + y_steps >= self.height {
pass.0 += 1;
pass.1 = match pass.0 {
3 => 4,
5 => 2,
7 => 1,
_ => 0,
let gap = self.png.ihdr_data.width % pixels_factor;
if gap > 0 {
match self.pass.unwrap().0 {
1 | 3 | 5 => {
pixels_per_line += 1;
}
2 if gap >= 5 => {
pixels_per_line += 1;
}
4 if gap >= 3 => {
pixels_per_line += 1;
}
6 if gap >= 2 => {
pixels_per_line += 1;
}
_ => (),
};
} else {
pass.1 += y_steps;
}
(pixels_per_line, current_pass)
let current_pass = if let Some(pass) = self.pass {
Some(pass.0)
} else {
None
};
let bytes_per_line = ((pixels_per_line * bits_per_pixel + 7) / 8) as usize;
self.start = self.end;
self.end = self.start + bytes_per_line + 1;
if let Some(pass) = self.pass.as_mut() {
if pass.1 + y_steps >= self.png.ihdr_data.height {
pass.0 += 1;
pass.1 = match pass.0 {
3 => 4,
5 => 2,
7 => 1,
_ => 0,
};
} else {
pass.1 += y_steps;
}
}
Some(ScanLine {
filter: self.png.raw_data[self.start],
data: &self.png.raw_data[(self.start + 1)..self.end],
pass: current_pass,
})
} else {
// Standard, non-interlaced PNG scanlines
(self.width, None)
};
let bits_per_line = pixels_per_line as usize * self.bits_per_pixel;
let mut len = bits_per_line.div_ceil(8);
if self.has_filter {
len += 1;
let bits_per_line = self.png.ihdr_data.width as usize
* self.png.ihdr_data.bit_depth.as_u8() as usize
* self.png.channels_per_pixel() as usize;
let bytes_per_line = (bits_per_line + 7) / 8 as usize;
self.start = self.end;
self.end = self.start + bytes_per_line + 1;
Some(ScanLine {
filter: self.png.raw_data[self.start],
data: &self.png.raw_data[(self.start + 1)..self.end],
pass: None,
})
}
self.left = self.left.checked_sub(len)?;
Some((len, current_pass, pixels_per_line as usize))
}
}
@ -175,6 +138,4 @@ pub struct ScanLine<'a> {
pub data: &'a [u8],
/// The current pass if the image is interlaced
pub pass: Option<u8>,
/// The number of pixels in the current scan line
pub num_pixels: usize,
}

View file

@ -1,81 +0,0 @@
#![allow(dead_code)]
pub mod prelude {
pub use super::*;
}
pub trait ParallelIterator: Iterator + Sized {
fn with_max_len(self, _l: usize) -> Self {
self
}
fn reduce_with<OP>(mut self, op: OP) -> Option<Self::Item>
where
OP: Fn(Self::Item, Self::Item) -> Self::Item + Sync,
{
self.next().map(|a| self.fold(a, op))
}
}
pub trait IntoParallelIterator {
type Iter: Iterator<Item = Self::Item>;
type Item: Send;
fn into_par_iter(self) -> Self::Iter;
}
pub trait IntoParallelRefIterator<'data> {
type Iter: Iterator<Item = Self::Item>;
type Item: Send + 'data;
fn par_iter(&'data self) -> Self::Iter;
}
pub trait IntoParallelRefMutIterator<'data> {
type Iter: ParallelIterator<Item = Self::Item>;
type Item: Send + 'data;
fn par_iter_mut(&'data mut self) -> Self::Iter;
}
impl<I: IntoIterator> IntoParallelIterator for I
where
I::Item: Send,
{
type Iter = I::IntoIter;
type Item = I::Item;
fn into_par_iter(self) -> Self::Iter {
self.into_iter()
}
}
impl<'data, I: 'data + ?Sized> IntoParallelRefIterator<'data> for I
where
&'data I: IntoParallelIterator,
{
type Iter = <&'data I as IntoParallelIterator>::Iter;
type Item = <&'data I as IntoParallelIterator>::Item;
fn par_iter(&'data self) -> Self::Iter {
self.into_par_iter()
}
}
impl<'data, I: 'data + ?Sized> IntoParallelRefMutIterator<'data> for I
where
&'data mut I: IntoParallelIterator,
{
type Iter = <&'data mut I as IntoParallelIterator>::Iter;
type Item = <&'data mut I as IntoParallelIterator>::Item;
fn par_iter_mut(&'data mut self) -> Self::Iter {
self.into_par_iter()
}
}
impl<I: Iterator> ParallelIterator for I {}
pub fn join<A, B>(a: impl FnOnce() -> A, b: impl FnOnce() -> B) -> (A, B) {
(a(), b())
}
pub fn spawn<A>(a: impl FnOnce() -> A) -> A {
a()
}

View file

@ -1,108 +1,37 @@
use rgb::RGB16;
use png::PngData;
use crate::{
colors::{BitDepth, ColorType},
headers::IhdrData,
png::PngImage,
};
/// Clean the alpha channel by setting the color of all fully transparent pixels to black
#[must_use]
pub fn cleaned_alpha_channel(png: &PngImage) -> Option<PngImage> {
if !png.ihdr.color_type.has_alpha() {
return None;
}
let byte_depth = png.bytes_per_channel();
let bpp = png.channels_per_pixel() * byte_depth;
pub fn reduce_alpha_channel(png: &mut PngData, channels: u8) -> Option<Vec<u8>> {
let byte_depth = png.ihdr_data.bit_depth.as_u8() >> 3;
let bpp = channels * byte_depth;
let bpp_mask = bpp - 1;
assert_eq!(0, bpp & bpp_mask);
let colored_bytes = bpp - byte_depth;
let mut reduced = Vec::with_capacity(png.data.len());
for pixel in png.data.chunks_exact(bpp) {
if pixel.iter().skip(colored_bytes).all(|b| *b == 0) {
reduced.resize(reduced.len() + bpp, 0);
} else {
reduced.extend_from_slice(pixel);
}
}
Some(PngImage {
data: reduced,
ihdr: png.ihdr.clone(),
})
}
#[must_use]
pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option<PngImage> {
if !png.ihdr.color_type.has_alpha() {
return None;
}
let byte_depth = png.bytes_per_channel();
let bpp = png.channels_per_pixel() * byte_depth;
let colored_bytes = bpp - byte_depth;
// If alpha optimisation is enabled, see if the image contains only fully opaque and fully transparent pixels.
// In case this occurs, we want to try and find an unused color we can use for the tRNS chunk.
// Rather than an exhaustive search, we will just keep track of 256 shades of gray, which should cover many cases.
let mut has_transparency = false;
let mut used_colors = vec![false; 256];
for pixel in png.data.chunks_exact(bpp) {
if optimize_alpha && pixel.iter().skip(colored_bytes).all(|b| *b == 0) {
// Fully transparent, we may be able to reduce with tRNS
has_transparency = true;
} else if pixel.iter().skip(colored_bytes).any(|b| *b != 255) {
// Partially transparent, the image is not reducible
return None;
} else if optimize_alpha && pixel.iter().take(colored_bytes).all(|b| *b == pixel[0]) {
// Opaque shade of gray, we can't use this color for tRNS
used_colors[pixel[0] as usize] = true;
}
}
let transparency_pixel = if has_transparency {
// For grayscale, start by checking 4 specific values in the hope that we may reduce depth
let unused = match png.ihdr.color_type {
ColorType::GrayscaleAlpha => [0x00, 0xFF, 0x55, 0xAA]
.into_iter()
.find(|&v| !used_colors[v as usize]),
_ => None,
}
.or_else(|| used_colors.iter().position(|&u| !u).map(|v| v as u8));
// If no unused color was found we will have to fail here
Some(unused?)
} else {
None
};
let mut raw_data = Vec::with_capacity(png.data.len());
for pixel in png.data.chunks_exact(bpp) {
match transparency_pixel {
Some(trns) if pixel.iter().skip(colored_bytes).all(|b| *b == 0) => {
raw_data.resize(raw_data.len() + colored_bytes, trns);
for line in png.scan_lines() {
for (i, &byte) in line.data.iter().enumerate() {
if i as u8 & bpp_mask >= colored_bytes && byte != 255 {
return None;
}
_ => raw_data.extend_from_slice(&pixel[0..colored_bytes]),
}
}
// Construct the color type with appropriate transparency data
let transparent = transparency_pixel.map(|trns| match png.ihdr.bit_depth {
BitDepth::Sixteen => (u16::from(trns) << 8) | u16::from(trns),
_ => u16::from(trns),
});
let target_color_type = match png.ihdr.color_type {
ColorType::GrayscaleAlpha => ColorType::Grayscale {
transparent_shade: transparent,
},
_ => ColorType::RGB {
transparent_color: transparent.map(|t| RGB16::new(t, t, t)),
},
};
let mut reduced = Vec::with_capacity(png.raw_data.len());
for line in png.scan_lines() {
reduced.push(line.filter);
for (i, &byte) in line.data.iter().enumerate() {
if i as u8 & bpp_mask >= colored_bytes {
continue;
} else {
reduced.push(byte);
}
}
}
Some(PngImage {
data: raw_data,
ihdr: IhdrData {
color_type: target_color_type,
..png.ihdr
},
})
// sBIT contains information about alpha channel's original depth,
// and alpha has just been removed
if let Some(sbit_header) = png.aux_headers.get_mut(b"sBIT") {
assert_eq!(sbit_header.len(), channels as usize);
sbit_header.pop();
}
Some(reduced)
}

View file

@ -1,230 +1,84 @@
use crate::{
colors::{BitDepth, ColorType},
headers::IhdrData,
png::PngImage,
};
use bit_vec::BitVec;
use colors::{BitDepth, ColorType};
use png::PngData;
/// Attempt to reduce a 16-bit image to 8-bit, returning the reduced image if successful
#[must_use]
pub fn reduced_bit_depth_16_to_8(png: &PngImage, force_scale: bool) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Sixteen {
return None;
}
const ONE_BIT_PERMUTATIONS: [u8; 2] = [0b0000_0000, 0b1111_1111];
const TWO_BIT_PERMUTATIONS: [u8; 5] = [
0b0000_0000,
0b0000_1111,
0b0011_1100,
0b1111_0000,
0b1111_1111,
];
const FOUR_BIT_PERMUTATIONS: [u8; 11] = [
0b0000_0000,
0b0000_0011,
0b0000_1100,
0b0011_0000,
0b1100_0000,
0b0000_1111,
0b0011_1100,
0b1111_0000,
0b0011_1111,
0b1111_1100,
0b1111_1111,
];
if force_scale {
return scaled_bit_depth_16_to_8(png);
}
// Reduce from 16 to 8 bits per channel per pixel
if png.data.chunks_exact(2).any(|pair| pair[0] != pair[1]) {
// Can't reduce
return None;
}
Some(PngImage {
data: png.data.chunks_exact(2).map(|pair| pair[0]).collect(),
ihdr: IhdrData {
color_type: png.ihdr.color_type.clone(),
bit_depth: BitDepth::Eight,
..png.ihdr
},
})
}
/// Forcibly reduce a 16-bit image to 8-bit by scaling, returning the reduced image if successful
#[must_use]
pub fn scaled_bit_depth_16_to_8(png: &PngImage) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Sixteen {
return None;
}
// Reduce from 16 to 8 bits per channel per pixel by scaling when necessary
let data = png
.data
.chunks_exact(2)
.map(|pair| {
if pair[0] == pair[1] {
return pair[0];
}
// See: http://www.libpng.org/pub/png/spec/1.2/PNG-Decoders.html#D.Sample-depth-rescaling
// This allows values such as 0x00FF to be rounded to 0x01 rather than truncated to 0x00
let val = f32::from(u16::from_be_bytes([pair[0], pair[1]]));
(val * (255.0 / 65535.0)).round() as u8
})
.collect();
Some(PngImage {
data,
ihdr: IhdrData {
color_type: png.ihdr.color_type.clone(),
bit_depth: BitDepth::Eight,
..png.ihdr
},
})
}
/// Attempt to reduce an 8-bit image to a lower bit depth, returning the reduced image if successful
#[must_use]
pub fn reduced_bit_depth_8_or_less(png: &PngImage) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight || png.channels_per_pixel() != 1 {
return None;
}
let minimum_bits = if let ColorType::Indexed { palette } = &png.ihdr.color_type {
// We can easily determine minimum depth by the palette size
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;
for &b in &png.data {
if b == 0 || b == 255 {
continue;
}
'try_depth: loop {
// Align the first pixel division with the mask
let mut byte = b.rotate_left(minimum_bits as u32);
// Each potential division of this pixel must be identical to successfully reduce
let compare = byte & mask;
for _ in divisions.clone() {
// Align the next division with the mask
byte = byte.rotate_left(minimum_bits as u32);
if byte & mask != compare {
// This depth is not possible, try the next one up
minimum_bits <<= 1;
if minimum_bits == 8 {
return None;
}
mask = (1 << minimum_bits) - 1;
divisions = 1..(8 / minimum_bits);
continue 'try_depth;
pub fn reduce_bit_depth_8_or_less(png: &mut PngData) -> bool {
let mut reduced = BitVec::with_capacity(png.raw_data.len() * 8);
let bit_depth: usize = png.ihdr_data.bit_depth.as_u8() as usize;
let mut allowed_bits = 1;
for line in png.scan_lines() {
let bit_vec = BitVec::from_bytes(&line.data);
if png.ihdr_data.color_type == ColorType::Indexed {
for (i, bit) in bit_vec.iter().enumerate() {
let bit_index = bit_depth - (i % bit_depth);
if bit && bit_index > allowed_bits {
allowed_bits = bit_index.next_power_of_two();
if allowed_bits == bit_depth {
// Not reducable
return false;
}
}
break;
}
}
minimum_bits
};
let mut reduced = Vec::with_capacity(png.data.len());
let mask = (1 << minimum_bits) - 1;
for line in png.scan_lines(false) {
// Loop over the data in chunks that will produce 1 byte of output
for chunk in line.data.chunks(8 / minimum_bits) {
let mut new_byte = 0;
let mut shift = 8;
for byte in chunk {
shift -= minimum_bits;
// Take the low bits of the pixel and shift them into the output byte
new_byte |= (byte & mask) << shift;
}
reduced.push(new_byte);
}
}
// If the image is grayscale we also need to reduce the transparency pixel
let color_type = if let ColorType::Grayscale {
transparent_shade: Some(trans),
} = png.ihdr.color_type
{
let reduced_trans = (trans & 0xFF) >> (8 - minimum_bits);
// Verify the reduction is valid by restoring back to original bit depth
let mut check = reduced_trans;
let mut bits = minimum_bits;
while bits < 8 {
check = (check << bits) | check;
bits <<= 1;
}
// If the transparency doesn't fit the new bit depth it is therefore unused - set it to None
ColorType::Grayscale {
transparent_shade: if trans == check {
Some(reduced_trans)
} else {
None
},
}
} else {
png.ihdr.color_type.clone()
};
Some(PngImage {
data: reduced,
ihdr: IhdrData {
color_type,
bit_depth: (minimum_bits as u8).try_into().unwrap(),
..png.ihdr
},
})
}
/// Expand a 1/2/4-bit image to 8-bit, returning the expanded image if successful
#[must_use]
pub fn expanded_bit_depth_to_8(png: &PngImage) -> Option<PngImage> {
let bit_depth = png.ihdr.bit_depth as u32;
if bit_depth >= 8 {
return None;
}
// Calculate the current number of pixels per byte
let ppb = 8 / bit_depth;
let is_gray = matches!(png.ihdr.color_type, ColorType::Grayscale { .. });
let mut reduced = Vec::with_capacity((png.ihdr.width * png.ihdr.height) as usize);
let mut length = 0;
let mask = (1 << bit_depth) - 1;
for line in png.scan_lines(false) {
for &(mut byte) in line.data {
// Loop over each pixel in the byte
for _ in 0..ppb {
// Align the current pixel with the mask
byte = byte.rotate_left(bit_depth);
let mut val = byte & mask;
if is_gray {
// Expand gray by repeating the bits
let mut bits = bit_depth;
while bits < 8 {
val = (val << bits) | val;
bits <<= 1;
} else {
for byte in bit_vec.to_bytes() {
while allowed_bits < bit_depth {
let permutations: &[u8] = if allowed_bits == 1 {
&ONE_BIT_PERMUTATIONS
} else if allowed_bits == 2 {
&TWO_BIT_PERMUTATIONS
} else if allowed_bits == 4 {
&FOUR_BIT_PERMUTATIONS
} else {
return false;
};
if permutations.iter().any(|perm| *perm == byte) {
break;
} else {
allowed_bits <<= 1;
}
}
reduced.push(val);
}
}
// Trim any overflow
length += line.num_pixels;
reduced.truncate(length);
}
// If the image is grayscale we also need to expand the transparency pixel
let color_type = if let ColorType::Grayscale {
transparent_shade: Some(mut trans),
} = png.ihdr.color_type
{
let mut bits = bit_depth;
while bits < 8 {
trans = (trans << bits) | trans;
bits <<= 1;
for line in png.scan_lines() {
reduced.extend(BitVec::from_bytes(&[line.filter]));
let bit_vec = BitVec::from_bytes(&line.data);
for (i, bit) in bit_vec.iter().enumerate() {
let bit_index = bit_depth - (i % bit_depth);
if bit_index <= allowed_bits {
reduced.push(bit);
}
}
ColorType::Grayscale {
transparent_shade: Some(trans),
// Pad end of line to get 8 bits per byte
while reduced.len() % 8 != 0 {
reduced.push(false);
}
} else {
png.ihdr.color_type.clone()
};
}
Some(PngImage {
data: reduced,
ihdr: IhdrData {
color_type,
bit_depth: BitDepth::Eight,
..png.ihdr
},
})
png.raw_data = reduced.to_bytes();
png.ihdr_data.bit_depth = BitDepth::from_u8(allowed_bits as u8);
true
}

View file

@ -1,206 +1,306 @@
use std::hash::{BuildHasherDefault, Hash};
use colors::{BitDepth, ColorType};
use itertools::Itertools;
use png::PngData;
use indexmap::IndexSet;
use rgb::{ComponentMap, FromSlice, RGB, RGBA, alt::Gray};
use rustc_hash::FxHasher;
use super::alpha::reduce_alpha_channel;
use crate::{
colors::{BitDepth, ColorType},
headers::IhdrData,
png::PngImage,
};
type FxIndexSet<V> = IndexSet<V, BuildHasherDefault<FxHasher>>;
/// Maximum size difference between indexed and channels to consider a candidate for evaluation
pub const INDEXED_MAX_DIFF: usize = 20000;
fn build_palette<T>(
iter: impl IntoIterator<Item = T>,
reduced: &mut Vec<u8>,
) -> Option<FxIndexSet<T>>
where
T: Eq + Hash,
{
let mut palette = FxIndexSet::default();
palette.reserve(257);
for pixel in iter {
let (idx, _) = palette.insert_full(pixel);
if idx == 256 {
return None;
}
reduced.push(idx as u8);
}
Some(palette)
}
#[must_use]
pub fn reduced_to_indexed(png: &PngImage, allow_grayscale: bool) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight {
return None;
}
if matches!(png.ihdr.color_type, ColorType::Indexed { .. }) {
return None;
}
if !allow_grayscale && png.ihdr.color_type.is_gray() {
return None;
}
let mut raw_data = Vec::with_capacity(png.data.len() / png.channels_per_pixel());
let palette: Vec<_> = match png.ihdr.color_type {
ColorType::Grayscale { transparent_shade } => {
let pmap = build_palette(png.data.as_gray().iter().copied(), &mut raw_data)?;
// Convert the Gray16 transparency to Gray8
let transparency_pixel = transparent_shade.map(|t| Gray::from(t as u8));
pmap.into_iter()
.map(|px| {
RGB::from(px).with_alpha(if Some(px) == transparency_pixel {
0
} else {
255
})
})
.collect()
}
ColorType::RGB { transparent_color } => {
let pmap = build_palette(png.data.as_rgb().iter().copied(), &mut raw_data)?;
// Convert the RGB16 transparency to RGB8
let transparency_pixel = transparent_color.map(|t| t.map(|c| c as u8));
pmap.into_iter()
.map(|px| {
px.with_alpha(if Some(px) == transparency_pixel {
0
} else {
255
})
})
.collect()
}
ColorType::GrayscaleAlpha => {
let pmap = build_palette(png.data.as_gray_alpha().iter().copied(), &mut raw_data)?;
pmap.into_iter().map(RGBA::from).collect()
}
ColorType::RGBA => {
let pmap = build_palette(png.data.as_rgba().iter().copied(), &mut raw_data)?;
pmap.into_iter().collect()
}
_ => return None,
};
Some(PngImage {
data: raw_data,
ihdr: IhdrData {
color_type: ColorType::Indexed { palette },
..png.ihdr
},
})
}
#[must_use]
pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
if !png.ihdr.color_type.is_rgb() {
return None;
}
let mut reduced = Vec::with_capacity(png.data.len());
let byte_depth = png.bytes_per_channel();
let bpp = png.channels_per_pixel() * byte_depth;
let last_color = 2 * byte_depth;
for pixel in png.data.chunks_exact(bpp) {
if byte_depth == 1 {
if pixel[0] != pixel[1] || pixel[1] != pixel[2] {
return None;
}
} else if pixel[0..2] != pixel[2..4] || pixel[2..4] != pixel[4..6] {
return None;
}
reduced.extend_from_slice(&pixel[last_color..]);
}
let color_type = match png.ihdr.color_type {
ColorType::RGB { transparent_color } => ColorType::Grayscale {
// Copy the transparent component if it is also gray
transparent_shade: transparent_color
.filter(|t| t.r == t.g && t.g == t.b)
.map(|t| t.r),
},
_ => ColorType::GrayscaleAlpha,
};
Some(PngImage {
data: reduced,
ihdr: IhdrData {
color_type,
..png.ihdr
},
})
}
/// Attempt to convert indexed to a different color type, returning the resulting image if successful
#[must_use]
pub fn indexed_to_channels(
png: &PngImage,
allow_grayscale: bool,
optimize_alpha: bool,
) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight {
return None;
}
let mut palette = match &png.ihdr.color_type {
ColorType::Indexed { palette } => palette.clone(),
_ => return None,
};
// Ensure fully transparent colors are black, which can help with grayscale conversion
if optimize_alpha {
for color in &mut palette {
if color.a == 0 {
color.r = 0;
color.g = 0;
color.b = 0;
}
}
}
// Determine which channels are required
let is_gray = if allow_grayscale {
palette.iter().all(|c| c.r == c.g && c.g == c.b)
pub fn reduce_rgba_to_rgb(png: &mut PngData) -> bool {
if let Some(reduced) = reduce_alpha_channel(png, 4) {
png.raw_data = reduced;
png.ihdr_data.color_type = ColorType::RGB;
true
} else {
false
}
}
pub fn reduce_rgba_to_grayscale_alpha(png: &mut PngData) -> bool {
let mut reduced = Vec::with_capacity(png.raw_data.len());
let byte_depth = png.ihdr_data.bit_depth.as_u8() >> 3;
let bpp = 4 * byte_depth;
let bpp_mask = bpp - 1;
assert_eq!(0, bpp & bpp_mask);
let colored_bytes = bpp - byte_depth;
for line in png.scan_lines() {
reduced.push(line.filter);
let mut low_bytes = Vec::with_capacity(4);
let mut high_bytes = Vec::with_capacity(4);
let mut trans_bytes = Vec::with_capacity(byte_depth as usize);
for (i, byte) in line.data.iter().enumerate() {
if i as u8 & bpp_mask < colored_bytes {
if byte_depth == 1 || i % 2 == 1 {
low_bytes.push(*byte);
} else {
high_bytes.push(*byte);
}
} else {
trans_bytes.push(*byte);
}
if (i as u8 & bpp_mask) == bpp - 1 {
if low_bytes.iter().unique().count() > 1 {
return false;
}
if byte_depth == 2 {
if high_bytes.iter().unique().count() > 1 {
return false;
}
reduced.push(high_bytes[0]);
high_bytes.clear();
}
reduced.push(low_bytes[0]);
low_bytes.clear();
reduced.extend_from_slice(&trans_bytes);
trans_bytes.clear();
}
}
}
if let Some(sbit_header) = png.aux_headers.get_mut(b"sBIT") {
assert_eq!(sbit_header.len(), 4);
sbit_header.remove(1);
sbit_header.remove(1);
}
if let Some(bkgd_header) = png.aux_headers.get_mut(b"bKGD") {
assert_eq!(bkgd_header.len(), 6);
bkgd_header.truncate(2);
}
png.raw_data = reduced;
png.ihdr_data.color_type = ColorType::GrayscaleAlpha;
true
}
pub fn reduce_rgba_to_palette(png: &mut PngData) -> bool {
if png.ihdr_data.bit_depth != BitDepth::Eight {
return false;
}
let mut reduced = Vec::with_capacity(png.raw_data.len());
let mut palette = Vec::with_capacity(256);
let bpp: usize = (4 * png.ihdr_data.bit_depth.as_u8() as usize) >> 3;
for line in png.scan_lines() {
reduced.push(line.filter);
let mut cur_pixel = Vec::with_capacity(bpp);
for (i, byte) in line.data.iter().enumerate() {
cur_pixel.push(*byte);
if i % bpp == bpp - 1 {
if let Some(idx) = palette.iter().position(|x| x == &cur_pixel) {
reduced.push(idx as u8);
} else {
let len = palette.len();
if len == 256 {
return false;
}
palette.push(cur_pixel);
reduced.push(len as u8);
}
cur_pixel = Vec::with_capacity(bpp);
}
}
}
let mut color_palette = Vec::with_capacity(
palette.len() * 3 + if png.aux_headers.contains_key(b"bKGD") {
6
} else {
0
},
);
let mut trans_palette = Vec::with_capacity(palette.len());
for color in &palette {
for (i, byte) in color.iter().enumerate() {
if i < 3 {
color_palette.push(*byte);
} else {
trans_palette.push(*byte);
}
}
}
let headers_size = color_palette.len() + trans_palette.len() + 8;
if reduced.len() + headers_size > png.raw_data.len() * 4 {
// Reduction would result in a larger image
return false;
}
if let Some(bkgd_header) = png.aux_headers.get_mut(b"bKGD") {
assert_eq!(bkgd_header.len(), 6);
let header_pixels = bkgd_header
.iter()
.skip(1)
.step(2)
.cloned()
.collect::<Vec<u8>>();
if let Some(entry) = color_palette
.chunks(3)
.position(|x| x == header_pixels.as_slice())
{
*bkgd_header = vec![entry as u8];
} else if color_palette.len() / 3 == 256 {
return false;
} else {
let entry = color_palette.len() / 3;
color_palette.extend_from_slice(&header_pixels);
*bkgd_header = vec![entry as u8];
}
}
if let Some(sbit_header) = png.aux_headers.get_mut(b"sBIT") {
assert_eq!(sbit_header.len(), 4);
sbit_header.pop();
}
png.raw_data = reduced;
png.palette = Some(color_palette);
if trans_palette.iter().any(|x| *x != 255) {
while let Some(255) = trans_palette.last().cloned() {
trans_palette.pop();
}
png.transparency_palette = Some(trans_palette);
} else {
png.transparency_palette = None;
}
png.ihdr_data.color_type = ColorType::Indexed;
true
}
pub fn reduce_rgb_to_palette(png: &mut PngData) -> bool {
if png.ihdr_data.bit_depth != BitDepth::Eight {
return false;
}
let mut reduced = Vec::with_capacity(png.raw_data.len());
let mut palette = Vec::with_capacity(256);
if let Some(ref trns) = png.transparency_pixel {
assert_eq!(trns.len(), 6);
if trns[0] != trns[1] || trns[2] != trns[3] || trns[4] != trns[5] {
return false;
}
palette.push(vec![trns[0], trns[2], trns[4]]);
}
let bpp: usize = (3 * png.ihdr_data.bit_depth.as_u8() as usize) >> 3;
for line in png.scan_lines() {
reduced.push(line.filter);
let mut cur_pixel = Vec::with_capacity(bpp);
for (i, byte) in line.data.iter().enumerate() {
cur_pixel.push(*byte);
if i % bpp == bpp - 1 {
if let Some(idx) = palette.iter().position(|x| x == &cur_pixel) {
reduced.push(idx as u8);
} else {
let len = palette.len();
if len == 256 {
return false;
}
palette.push(cur_pixel);
reduced.push(len as u8);
}
cur_pixel = Vec::with_capacity(bpp);
}
}
}
let mut color_palette = Vec::with_capacity(palette.len() * 3);
for color in &palette {
color_palette.extend_from_slice(color);
}
let headers_size = color_palette.len() + 4;
if reduced.len() + headers_size > png.raw_data.len() * 3 {
// Reduction would result in a larger image
return false;
}
if let Some(bkgd_header) = png.aux_headers.get_mut(b"bKGD") {
assert_eq!(bkgd_header.len(), 6);
let header_pixels = bkgd_header
.iter()
.skip(1)
.step(2)
.cloned()
.collect::<Vec<u8>>();
if let Some(entry) = color_palette
.chunks(3)
.position(|x| x == header_pixels.as_slice())
{
*bkgd_header = vec![entry as u8];
} else if color_palette.len() == 255 {
return false;
} else {
let entry = color_palette.len() / 3;
color_palette.extend_from_slice(&header_pixels);
*bkgd_header = vec![entry as u8];
}
}
png.raw_data = reduced;
png.palette = Some(color_palette);
png.ihdr_data.color_type = ColorType::Indexed;
if png.transparency_pixel.is_some() {
png.transparency_pixel = None;
png.transparency_palette = Some(vec![0]);
};
true
}
pub fn reduce_rgb_to_grayscale(png: &mut PngData) -> bool {
let mut reduced = Vec::with_capacity(png.raw_data.len());
let byte_depth: u8 = png.ihdr_data.bit_depth.as_u8() >> 3;
let bpp: usize = 3 * byte_depth as usize;
let mut cur_pixel = Vec::with_capacity(bpp);
for line in png.scan_lines() {
reduced.push(line.filter);
for (i, byte) in line.data.iter().enumerate() {
cur_pixel.push(*byte);
if i % bpp == bpp - 1 {
if bpp == 3 {
if cur_pixel.iter().unique().count() > 1 {
return false;
}
reduced.push(cur_pixel[0]);
} else {
let pixel_bytes = cur_pixel
.iter()
.step(2)
.cloned()
.zip(cur_pixel.iter().skip(1).step(2).cloned())
.unique()
.collect::<Vec<(u8, u8)>>();
if pixel_bytes.len() > 1 {
return false;
}
reduced.push(pixel_bytes[0].0);
reduced.push(pixel_bytes[0].1);
}
cur_pixel.clear();
}
}
}
if let Some(ref mut trns) = png.transparency_pixel {
assert_eq!(trns.len(), 6);
if trns[0..2] != trns[2..4] || trns[2..4] != trns[4..6] {
return false;
}
*trns = trns[0..2].to_owned();
}
if let Some(sbit_header) = png.aux_headers.get_mut(b"sBIT") {
assert_eq!(sbit_header.len(), 3);
sbit_header.truncate(1);
}
if let Some(bkgd_header) = png.aux_headers.get_mut(b"bKGD") {
assert_eq!(bkgd_header.len(), 6);
bkgd_header.truncate(2);
}
png.raw_data = reduced;
png.ihdr_data.color_type = ColorType::Grayscale;
true
}
pub fn reduce_grayscale_alpha_to_grayscale(png: &mut PngData) -> bool {
if let Some(reduced) = reduce_alpha_channel(png, 2) {
png.raw_data = reduced;
png.ihdr_data.color_type = ColorType::Grayscale;
true
} else {
false
};
let has_alpha = palette.iter().any(|c| c.a != 255);
let color_type = match (is_gray, has_alpha) {
(false, true) => ColorType::RGBA,
(false, false) => ColorType::RGB {
transparent_color: None,
},
(true, true) => ColorType::GrayscaleAlpha,
(true, false) => ColorType::Grayscale {
transparent_shade: None,
},
};
// Don't proceed if output would be too much larger
let out_size = color_type.channels_per_pixel() as usize * png.data.len();
if out_size - png.data.len() > INDEXED_MAX_DIFF {
return None;
}
// Construct the new data
let black = RGBA::new(0, 0, 0, 255);
let ch_start = if is_gray { 2 } else { 0 };
let ch_end = if has_alpha { 3 } else { 2 };
let mut data = Vec::with_capacity(out_size);
for b in &png.data {
let color = palette.get(*b as usize).unwrap_or(&black);
data.extend_from_slice(&color.as_ref()[ch_start..=ch_end]);
}
Some(PngImage {
ihdr: IhdrData {
color_type,
..png.ihdr
},
data,
})
}

View file

@ -1,203 +1,3 @@
use std::sync::Arc;
use crate::{ColorType, Deadline, Deflater, Options, evaluate::Evaluator, png::PngImage};
pub mod alpha;
use crate::alpha::*;
mod alpha;
pub mod bit_depth;
use crate::bit_depth::*;
pub mod color;
use crate::color::*;
pub mod palette;
use crate::palette::*;
pub(crate) fn perform_reductions(
mut png: Arc<PngImage>,
opts: &Options,
deadline: &Deadline,
eval: &Evaluator,
) -> Arc<PngImage> {
let mut evaluation_added = false;
// 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,
_ => false,
};
// Interlacing must be processed first in order to evaluate the rest correctly
if let Some(interlacing) = opts.interlace {
if let Some(reduced) = png.change_interlacing(interlacing) {
png = Arc::new(reduced);
}
}
// If alpha optimization is enabled, clean the alpha channel before continuing
// This can allow some color type reductions which may not have been possible otherwise
if opts.optimize_alpha && !deadline.passed() {
if let Some(reduced) = cleaned_alpha_channel(&png) {
png = Arc::new(reduced);
}
}
// Attempt to reduce 16-bit to 8-bit
// This is just removal of bytes and does not need to be evaluated
if opts.bit_depth_reduction && !deadline.passed() {
if let Some(reduced) = reduced_bit_depth_16_to_8(&png, opts.scale_16) {
png = Arc::new(reduced);
}
}
// Attempt to reduce RGB to grayscale
// This is just removal of bytes and does not need to be evaluated
if opts.color_type_reduction && opts.grayscale_reduction && !deadline.passed() {
if let Some(reduced) = reduced_rgb_to_grayscale(&png) {
png = Arc::new(reduced);
}
}
// Attempt to expand the bit depth to 8
// This does need to be evaluated but will be done so later when it gets reduced again
if opts.bit_depth_reduction && !deadline.passed() {
if let Some(reduced) = expanded_bit_depth_to_8(&png) {
png = Arc::new(reduced);
}
}
// Now retain the current png for the evaluator baseline
// It will only be entered into the evaluator if there are also others to evaluate
let mut baseline = png.clone();
// Attempt to reduce and sort the palette
if opts.palette_reduction && !deadline.passed() {
if let Some(reduced) = reduced_palette(&png, opts.optimize_alpha) {
png = Arc::new(reduced);
// If the palette was reduced but the data is unchanged then this should become the baseline
if png.data == baseline.data {
baseline = png.clone();
}
}
if let Some(reduced) = sorted_palette(&png) {
png = Arc::new(reduced);
}
// If either action changed the data then enter this into the evaluator
if !Arc::ptr_eq(&png, &baseline) {
eval.try_image_with_description(png.clone(), "Indexed (luma sort)");
evaluation_added = true;
}
}
// Attempt alpha removal
if opts.color_type_reduction && !deadline.passed() {
if let Some(reduced) = reduced_alpha_channel(&png, opts.optimize_alpha) {
png = Arc::new(reduced);
// For small differences, if a tRNS chunk is required then enter this into the evaluator
// Otherwise it is mostly just removal of bytes and should become the baseline
if png.ihdr.color_type.has_trns() && baseline.data.len() - png.data.len() <= 1000 {
eval.try_image(png.clone());
evaluation_added = true;
} else {
baseline = png.clone();
}
}
}
// Attempt to convert from indexed to channels
// This may give a better result due to dropping the PLTE chunk
if !cheap && opts.color_type_reduction && !deadline.passed() {
if let Some(reduced) =
indexed_to_channels(&png, opts.grayscale_reduction, opts.optimize_alpha)
{
// This result should not be passed on to subsequent reductions
eval.try_image(Arc::new(reduced));
evaluation_added = true;
}
}
// 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 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));
// For relatively small differences, enter this into the evaluator
// Otherwise we're confident enough for it to become the baseline
if png.data.len() - new.data.len() <= INDEXED_MAX_DIFF {
eval.try_image_with_description(new.clone(), "Indexed (luma sort)");
evaluation_added = true;
} else {
baseline = new.clone();
}
indexed = Some(new);
}
}
// Attempt additional palette sorting techniques
if !cheap && opts.palette_reduction {
// Collect a list of palettes so we can avoid evaluating the same one twice
let mut palettes = Vec::new();
if let ColorType::Indexed { palette } = &baseline.ihdr.color_type {
palettes.push(palette.clone());
}
// Make sure we use the `indexed` var as input if it exists
// This one doesn't need to be kept in the palette list as the sorters will fail if there's no change
let input = indexed.as_ref().unwrap_or(&png);
// Attempt to sort the palette using the battiato method
if !deadline.passed() {
if let Some(reduced) = sorted_palette_battiato(input) {
if let ColorType::Indexed { palette } = &reduced.ihdr.color_type {
if !palettes.contains(palette) {
palettes.push(palette.clone());
eval.try_image_with_description(
Arc::new(reduced),
"Indexed (battiato sort)",
);
evaluation_added = true;
}
}
}
}
// Attempt to sort the palette using the mzeng method
if !deadline.passed() {
if let Some(reduced) = sorted_palette_mzeng(input) {
if let ColorType::Indexed { palette } = &reduced.ihdr.color_type {
if !palettes.contains(palette) {
palettes.push(palette.clone());
eval.try_image_with_description(Arc::new(reduced), "Indexed (mzeng sort)");
evaluation_added = true;
}
}
}
}
}
// Attempt to reduce to a lower bit depth
if opts.bit_depth_reduction && !deadline.passed() {
// First try the `png` var
let reduced = reduced_bit_depth_8_or_less(&png);
// Then try the `indexed` var, unless we're doing cheap evaluations and already have a reduction
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) {
eval.try_image(Arc::new(indexed));
evaluation_added = true;
}
}
}
// Enter the first result into the evaluator
if let Some(reduced) = reduced {
eval.try_image(Arc::new(reduced));
evaluation_added = true;
}
}
if evaluation_added {
eval.try_image(baseline.clone());
}
baseline
}

View file

@ -1,439 +0,0 @@
use indexmap::IndexSet;
use rgb::RGBA8;
use crate::{
colors::{BitDepth, ColorType},
headers::IhdrData,
png::{PngImage, scan_lines::ScanLine},
};
/// Attempt to reduce the number of colors in the palette, returning the reduced image if successful
#[must_use]
pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight {
return None;
}
let ColorType::Indexed { palette } = &png.ihdr.color_type else {
return None;
};
let mut used = [false; 256];
for &byte in &png.data {
used[byte as usize] = true;
}
let black = RGBA8::new(0, 0, 0, 255);
let mut condensed = IndexSet::with_capacity(palette.len());
let mut byte_map = [0; 256];
let mut did_change = false;
for (i, used) in used.iter().enumerate() {
if !used {
continue;
}
// There are invalid files that use pixel indices beyond palette size
let color = *palette.get(i).unwrap_or(&black);
byte_map[i] = add_color_to_set(color, &mut condensed, optimize_alpha);
if byte_map[i] as usize != i {
did_change = true;
}
}
let data = if did_change {
// Reassign data bytes to new indices
png.data.iter().map(|b| byte_map[*b as usize]).collect()
} else if condensed.len() != palette.len() {
// Data is unchanged but palette is different size
// Note the new palette could potentially be larger if the original had a missing entry
png.data.clone()
} else {
// Nothing has changed
return None;
};
let palette: Vec<_> = condensed.into_iter().collect();
Some(PngImage {
ihdr: IhdrData {
color_type: ColorType::Indexed { palette },
..png.ihdr
},
data,
})
}
fn add_color_to_set(mut color: RGBA8, set: &mut IndexSet<RGBA8>, optimize_alpha: bool) -> u8 {
// If there are multiple fully transparent entries, reduce them into one
if optimize_alpha && color.a == 0 {
color.r = 0;
color.g = 0;
color.b = 0;
}
let (idx, _) = set.insert_full(color);
idx as u8
}
/// Attempt to sort the colors in the palette by luma, returning the sorted image if successful
#[must_use]
pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight {
return None;
}
let palette = match &png.ihdr.color_type {
ColorType::Indexed { palette } if palette.len() > 1 => palette,
_ => return None,
};
let mut enumerated: Vec<_> = palette.iter().enumerate().collect();
// Put the most popular edge color first, which can help slightly if the filter bytes are 0
let keep_first = most_popular_edge_color(palette.len(), png);
let first = keep_first.map(|f| enumerated.remove(f));
// Sort the palette
enumerated.sort_by(|a, b| {
// Sort by ascending alpha and descending luma
let color_val = |color: &RGBA8| {
let a = i32::from(color.a);
// Put 7 high bits of alpha first, then luma, then low bit of alpha
// This provides notable improvement in images with a lot of alpha
((a & 0xFE) << 18) + (a & 0x01)
// These are coefficients for standard sRGB to luma conversion
- i32::from(color.r) * 299
- i32::from(color.g) * 587
- i32::from(color.b) * 114
};
color_val(a.1).cmp(&color_val(b.1))
});
if let Some(first) = first {
enumerated.insert(0, first);
}
// Extract the new palette and determine if anything changed
let (remapping, palette): (Vec<_>, Vec<RGBA8>) = enumerated.into_iter().unzip();
if remapping.iter().enumerate().all(|(a, b)| a == *b) {
return None;
}
// Construct the new mapping and convert the data
let mut byte_map = [0; 256];
for (i, &v) in remapping.iter().enumerate() {
byte_map[v] = i as u8;
}
let data = png.data.iter().map(|&b| byte_map[b as usize]).collect();
Some(PngImage {
ihdr: IhdrData {
color_type: ColorType::Indexed { palette },
..png.ihdr
},
data,
})
}
/// Sort the colors in the palette using the mzeng technique, returning the sorted image if successful
#[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 {
return None;
}
let palette = match &png.ihdr.color_type {
// Images with only two colors will remain unchanged from previous luma sort
ColorType::Indexed { palette } if palette.len() > 2 => palette,
_ => return None,
};
let matrix = co_occurrence_matrix(palette.len(), png);
let edges = weighted_edges(&matrix);
let mut remapping = mzeng_reindex(palette.len(), edges, &matrix);
apply_most_popular_color(png, &mut remapping);
apply_palette_reorder(png, &remapping)
}
/// Sort the colors in the palette using the battiato technique, returning the sorted image if successful
#[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 {
return None;
}
let palette = match &png.ihdr.color_type {
// Images with only two colors will remain unchanged from previous luma sort
ColorType::Indexed { palette } if palette.len() > 2 => palette,
_ => return None,
};
let matrix = co_occurrence_matrix(palette.len(), png);
let edges = weighted_edges(&matrix);
let mut remapping = battiato_reindex(palette.len(), edges);
apply_most_popular_color(png, &mut remapping);
apply_palette_reorder(png, &remapping)
}
// Apply the palette reordering to the image data
fn apply_palette_reorder(png: &PngImage, remapping: &[usize]) -> Option<PngImage> {
let ColorType::Indexed { palette } = &png.ihdr.color_type else {
return None;
};
// Check if anything changed
if remapping.iter().enumerate().all(|(a, b)| a == *b) {
return None;
}
// Construct the palette and byte maps and convert the data
let mut new_palette = Vec::new();
let mut byte_map = [0; 256];
for (i, &v) in remapping.iter().enumerate() {
new_palette.push(palette[v]);
byte_map[v] = i as u8;
}
let data = png.data.iter().map(|&b| byte_map[b as usize]).collect();
Some(PngImage {
ihdr: IhdrData {
color_type: ColorType::Indexed {
palette: new_palette,
},
..png.ihdr
},
data,
})
}
// 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];
for line in png.scan_lines(false) {
if let &[first, .., last] = line.data {
counts[first as usize] += 1;
counts[last as usize] += 1;
}
}
let max = counts
.iter()
.take(num_colors)
.enumerate()
.max_by_key(|&(_, v)| v)
.unwrap();
// Ensure there's a clear winner - return None if multiple colors are tied
let max_equal = counts.iter().filter(|&v| v == max.1).count();
if max_equal > 1 {
return None;
}
Some(max.0)
}
// 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];
for &val in &png.data {
counts[val as usize] += 1;
}
counts
.iter()
.copied()
.take(num_colors)
.enumerate()
.max_by_key(|&(_, v)| v)
.unwrap_or_default()
}
// Put the most popular color first
fn apply_most_popular_color(png: &PngImage, remapping: &mut [usize]) {
let most_popular = most_popular_color(remapping.len(), png);
// If the most popular color is less than 15% of the image, don't use it
if most_popular.1 < png.data.len() as u32 * 3 / 20 {
return;
}
let first_idx = remapping.iter().position(|&i| i == most_popular.0).unwrap();
// If the index is past halfway, reverse the order so as to minimize the change
if first_idx >= remapping.len() / 2 {
remapping.reverse();
remapping.rotate_right(first_idx + 1);
} else {
remapping.rotate_left(first_idx);
}
}
// 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 prev: Option<ScanLine> = None;
let mut prev_val = None;
for line in png.scan_lines(false) {
for i in 0..line.data.len() {
let val = line.data[i] as usize;
if val > num_colors {
continue;
}
if let Some(prev_val) = prev_val.replace(val) {
matrix[prev_val][val] += 1;
matrix[val][prev_val] += 1;
}
if let Some(prev) = &prev {
let prev_val = prev.data[i] as usize;
if prev_val > num_colors {
continue;
}
matrix[prev_val][val] += 1;
matrix[val][prev_val] += 1;
}
}
prev = Some(line);
}
matrix
}
// Calculate edge list sorted by weight
fn weighted_edges(matrix: &[Vec<u32>]) -> Vec<(usize, usize)> {
let mut edges = Vec::new();
for (i, m_row) in matrix.iter().enumerate() {
for (j, val) in m_row.iter().enumerate().take(i) {
edges.push(((j, i), val));
}
}
edges.sort_by(|(_, w1), (_, w2)| w2.cmp(w1));
edges.into_iter().map(|(e, _)| e).collect()
}
// Apply a greedy index assignment using the modified version of Zeng's techinque from
// "A note on Zeng's technique for color reindexing of palette-based images" by Pinho et al
// https://ieeexplore.ieee.org/document/1261987
// Based on the C implementation in libwebp
fn mzeng_reindex(num_colors: usize, edges: Vec<(usize, usize)>, matrix: &[Vec<u32>]) -> Vec<usize> {
// Initialize the mapping list with the two best indices.
let mut remapping = vec![edges[0].0, edges[0].1];
// Initialize the sums with the first two remappings and find the best one
let mut sums = Vec::new();
let mut best_sum_pos = 0;
let mut best_sum = (0, 0);
for (i, m_row) in matrix.iter().enumerate() {
if i == remapping[0] || i == remapping[1] {
continue;
}
let sum = (i, m_row[remapping[0]] + m_row[remapping[1]]);
if sum.1 > best_sum.1 {
best_sum_pos = sums.len();
best_sum = sum;
}
sums.push(sum);
}
while !sums.is_empty() {
let best_index = best_sum.0;
// Compute delta to know if we need to prepend or append the best index.
let mut delta: isize = 0;
let n = (num_colors - sums.len()) as isize;
for (i, &index) in remapping.iter().enumerate() {
delta += (n - 1 - 2 * i as isize) * matrix[best_index][index] as isize;
}
if delta > 0 {
remapping.insert(0, best_index);
} else {
remapping.push(best_index);
}
// Remove best_sum from sums.
sums.swap_remove(best_sum_pos);
if !sums.is_empty() {
// Update all the sums and find the best one.
best_sum_pos = 0;
best_sum = (0, 0);
for (i, sum) in sums.iter_mut().enumerate() {
sum.1 += matrix[best_index][sum.0];
if sum.1 > best_sum.1 {
best_sum_pos = i;
best_sum = *sum;
}
}
}
}
// Return the completed remapping
remapping
}
// Calculate an approximate solution of the Traveling Salesman Problem using the algorithm
// from "An efficient Re-indexing algorithm for color-mapped images" by Battiato et al
// https://ieeexplore.ieee.org/document/1344033
fn battiato_reindex(num_colors: usize, edges: Vec<(usize, usize)>) -> Vec<usize> {
let mut chains = Vec::new();
// Keep track of the state of each vertex (.0) and it's chain number (.1)
// 0 = an unvisited vertex (White)
// 1 = an endpoint of a chain (Red)
// 2 = part of the middle of a chain (Black)
let mut vx = vec![(0, 0); num_colors];
// Iterate the edges and assemble them into a chain
for (i, j) in edges {
let vi = vx[i];
let vj = vx[j];
if vi.0 == 0 && vj.0 == 0 {
// Two unvisited vertices - create a new chain
vx[i].0 = 1;
vx[i].1 = chains.len();
vx[j].0 = 1;
vx[j].1 = chains.len();
chains.push(vec![i, j]);
} else if vi.0 == 0 && vj.0 == 1 {
// An unvisited vertex connects with an endpoint of an existing chain
vx[i].0 = 1;
vx[i].1 = vj.1;
vx[j].0 = 2;
let chain = &mut chains[vj.1];
if chain[0] == j {
chain.insert(0, i);
} else {
chain.push(i);
}
} else if vi.0 == 1 && vj.0 == 0 {
// An unvisited vertex connects with an endpoint of an existing chain
vx[j].0 = 1;
vx[j].1 = vi.1;
vx[i].0 = 2;
let chain = &mut chains[vi.1];
if chain[0] == i {
chain.insert(0, j);
} else {
chain.push(j);
}
} else if vi.0 == 1 && vj.0 == 1 && vi.1 != vj.1 {
// Two endpoints of different chains are connected together
vx[i].0 = 2;
vx[j].0 = 2;
let (a, b) = if vi.1 < vj.1 { (i, j) } else { (j, i) };
let ca = vx[a].1;
let cb = vx[b].1;
let chainb = std::mem::take(&mut chains[cb]);
for &v in &chainb {
vx[v].1 = ca;
}
let chaina = &mut chains[ca];
if chaina[0] == a && chainb[0] == b {
for v in chainb {
chaina.insert(0, v);
}
} else if chaina[0] == a {
chaina.splice(0..0, chainb);
} else if chainb[0] == b {
chaina.extend(chainb);
} else {
let pos = chaina.len();
for v in chainb {
chaina.insert(pos, v);
}
}
}
if chains[0].len() == num_colors {
break;
}
}
// Return the completed chain
chains.swap_remove(0)
}

View file

@ -1,65 +0,0 @@
use std::io::Cursor;
use image::{codecs::png::PngDecoder, *};
use log::{error, warn};
#[cfg(not(feature = "parallel"))]
use crate::rayon;
/// Validate that the output png data still matches the original image
pub fn validate_output(output: &[u8], original_data: &[u8]) -> bool {
let (old_frames, new_frames) = rayon::join(
|| load_png_image_from_memory(original_data),
|| load_png_image_from_memory(output),
);
match (new_frames, old_frames) {
(Err(new_err), _) => {
error!("Failed to read output image for validation: {}", new_err);
false
}
(_, Err(old_err)) => {
// The original image might be invalid if, for example, there is a CRC error,
// and we set fix_errors to true. In that case, all we can do is check that the
// new image is decodable.
warn!("Failed to read input image for validation: {}", old_err);
true
}
(Ok(new_frames), Ok(old_frames)) if new_frames.len() != old_frames.len() => false,
(Ok(new_frames), Ok(old_frames)) => {
for (a, b) in old_frames.iter().zip(new_frames) {
if !images_equal(a, &b) {
return false;
}
}
true
}
}
}
/// Loads a PNG image from memory to frames of [RgbaImage]
fn load_png_image_from_memory(png_data: &[u8]) -> Result<Vec<RgbaImage>, image::ImageError> {
let decoder = PngDecoder::new(Cursor::new(png_data))?;
if decoder.is_apng()? {
decoder
.apng()?
.into_frames()
.map(|f| f.map(image::Frame::into_buffer))
.collect()
} else {
DynamicImage::from_decoder(decoder).map(|i| vec![i.into_rgba8()])
}
}
/// Compares images pixel by pixel for equivalent content
fn images_equal(old_png: &RgbaImage, new_png: &RgbaImage) -> bool {
let a = old_png.pixels().filter(|x| {
let p = x.channels();
!(p.len() == 4 && p[3] == 0)
});
let b = new_png.pixels().filter(|x| {
let p = x.channels();
!(p.len() == 4 && p[3] == 0)
});
a.eq(b)
}

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: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 B

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 B

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 B

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 B

After

Width:  |  Height:  |  Size: 1 KiB

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