Compare commits
157 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0466546981 | ||
|
|
4e80dd009a | ||
|
|
028294cc8c | ||
|
|
628e241e23 | ||
|
|
e167b04b85 | ||
|
|
fbae35922d | ||
|
|
55b9758575 | ||
|
|
010b76eada | ||
|
|
d239592b58 | ||
|
|
0cf8b782ff | ||
|
|
25e147f6a0 | ||
|
|
a75343800c | ||
|
|
7d52fd66d0 | ||
|
|
5bbd826ae3 | ||
|
|
e4a7d0fd99 | ||
|
|
a1a18ffd21 | ||
|
|
587fd03d0e | ||
|
|
ce52ab8748 | ||
|
|
494edba245 | ||
|
|
2e861b95db | ||
|
|
7834e4709e | ||
|
|
8284676f50 | ||
|
|
2a79fa4c6c | ||
|
|
00b9e33508 | ||
|
|
2585529089 | ||
|
|
6a99181fec | ||
|
|
ea9caac249 | ||
|
|
764d057c32 | ||
|
|
4a537c087d | ||
|
|
015d31e6a1 | ||
|
|
854510a864 | ||
|
|
8466e390b8 | ||
|
|
baaafee4e4 | ||
|
|
9169f99674 | ||
|
|
47d5628a68 | ||
|
|
d54e7c04ea | ||
|
|
ff168f5d2d | ||
|
|
a8edb2871c | ||
|
|
d48b147b3d | ||
|
|
155eeb3637 | ||
|
|
8665b2da8c | ||
|
|
e5e1568196 | ||
|
|
8754013cbc | ||
|
|
a0f4e751bf | ||
|
|
fa2c69071c | ||
|
|
f99ce277da | ||
|
|
0b68c08852 | ||
|
|
58eec39cc8 | ||
|
|
6a2379ce78 | ||
|
|
33cea5603e | ||
|
|
285b0aae10 | ||
|
|
e868af9106 | ||
|
|
1a9e0d79e5 | ||
|
|
cb132b1cc4 | ||
|
|
99eccf3f30 | ||
|
|
fb9be2f5d2 | ||
|
|
779336f60c | ||
|
|
e091767278 | ||
|
|
aebddf443a | ||
|
|
7cc91b1308 | ||
|
|
14532e8bf5 | ||
|
|
2a3aa83928 | ||
|
|
82cae58794 | ||
|
|
71290acc62 | ||
|
|
ff9beb00fe | ||
|
|
c60dbd12a5 | ||
|
|
54c49a1644 | ||
|
|
9d3e6e6f33 | ||
|
|
5ee3c2afa0 | ||
|
|
1c02ee9299 | ||
|
|
0ff97448c2 | ||
|
|
420ea940bc | ||
|
|
fe1f5a0889 | ||
|
|
fd5b7a4aa8 | ||
|
|
8631eb1c79 | ||
|
|
d57d880770 | ||
|
|
bf276f29b8 | ||
|
|
54a31a65a7 | ||
|
|
788997c437 | ||
|
|
a451711e96 | ||
|
|
ef1d87427d | ||
|
|
fa7d3d9067 | ||
|
|
8c6532ca1b | ||
|
|
c7d462f909 | ||
|
|
a44ba5f88b | ||
|
|
d168fffb12 | ||
|
|
748d5987a5 | ||
|
|
26b084c161 | ||
|
|
f7a66f2075 | ||
|
|
7650953faf | ||
|
|
6cf42fbefa | ||
|
|
1e3f4520ef | ||
|
|
d649beacac | ||
|
|
bc8c364568 | ||
|
|
30387b26d9 | ||
|
|
1188b9a91e | ||
|
|
5a4cfa9505 | ||
|
|
174f36ad8f | ||
|
|
e6f84264e5 | ||
|
|
8f298be7fb | ||
|
|
5f0a0d6938 | ||
|
|
2632be2034 | ||
|
|
bbde68ddce | ||
|
|
8a44cdbc84 | ||
|
|
14b8b0e93a | ||
|
|
cca23b5479 | ||
|
|
09c4851118 | ||
|
|
5fdaa12c96 | ||
|
|
fec6b2b9f3 | ||
|
|
60911977f6 | ||
|
|
6c7ab5d79f | ||
|
|
3a6b792ca1 | ||
|
|
ffbd927d82 | ||
|
|
a72bc945fc | ||
|
|
8270c6a034 | ||
|
|
910e779b09 | ||
|
|
8740f475d7 | ||
|
|
4df1464f39 | ||
|
|
8ebb5e6dfd | ||
|
|
3b7467a5da | ||
|
|
262082f546 | ||
|
|
f8c84c258d | ||
|
|
e8e8309c2d | ||
|
|
e638807d33 | ||
|
|
ca05d99be4 | ||
|
|
1efacace9f | ||
|
|
1936861b91 | ||
|
|
f602e84fa6 | ||
|
|
e367f23c01 | ||
|
|
cb97e69074 | ||
|
|
490a4691cb | ||
|
|
ef64dd0768 | ||
|
|
c81a863e69 | ||
|
|
e7f1c04488 | ||
|
|
e83c6725dc | ||
|
|
95eea28c82 | ||
|
|
ac943da875 | ||
|
|
59e0509a58 | ||
|
|
44619fd24d | ||
|
|
7a29741f18 | ||
|
|
0d60e8b7c6 | ||
|
|
0f24120c9a | ||
|
|
1bb7109804 | ||
|
|
e2d58c112b | ||
|
|
61deab369b | ||
|
|
d30da6e8a0 | ||
|
|
e8366882ad | ||
|
|
664b27c4ca | ||
|
|
0506418157 | ||
|
|
a8846b897d | ||
|
|
f7b837af69 | ||
|
|
f6e3614fe9 | ||
|
|
f70fe61e05 | ||
|
|
a2dedcfed2 | ||
|
|
3759ca85ec | ||
|
|
90ceef9796 | ||
|
|
e2c5b45f4b |
|
|
@ -1,9 +1,2 @@
|
||||||
# Remove this if targeting AArch64 from an AArch64 Linux box
|
[alias]
|
||||||
[target.'cfg(all(target_os = "linux", target_arch = "aarch64"))']
|
xtask = "run --manifest-path xtask/Cargo.toml --"
|
||||||
runner = 'qemu-aarch64'
|
|
||||||
|
|
||||||
[target.aarch64-unknown-linux-gnu]
|
|
||||||
linker = 'aarch64-linux-gnu-gcc'
|
|
||||||
|
|
||||||
[target.aarch64-unknown-linux-musl]
|
|
||||||
linker = 'aarch64-linux-musl-gcc'
|
|
||||||
|
|
|
||||||
7
.dockerignore
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
.editorconfig
|
||||||
|
.gitignore
|
||||||
|
/.git/
|
||||||
|
/.github/
|
||||||
|
/.pre-commit-hooks.yaml
|
||||||
|
/scripts/
|
||||||
|
/target/
|
||||||
20
.github/dependabot.yml
vendored
|
|
@ -1,6 +1,24 @@
|
||||||
version: 2
|
version: 2
|
||||||
updates:
|
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"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: monthly
|
interval: "weekly"
|
||||||
|
|
|
||||||
54
.github/workflows/deploy.yml
vendored
|
|
@ -3,7 +3,12 @@ name: deploy
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*.*.*'
|
- 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:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
|
|
@ -20,6 +25,10 @@ jobs:
|
||||||
if: ${{ !github.event.repository.fork }}
|
if: ${{ !github.event.repository.fork }}
|
||||||
|
|
||||||
strategy:
|
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:
|
matrix:
|
||||||
target:
|
target:
|
||||||
- x86_64-unknown-linux-gnu
|
- x86_64-unknown-linux-gnu
|
||||||
|
|
@ -33,18 +42,32 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source
|
- name: Checkout source
|
||||||
uses: actions/checkout@v4
|
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
|
- name: Get the Oxipng version
|
||||||
id: oxipngMeta
|
id: oxipngMeta
|
||||||
run: echo "version=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "oxipng").version')"
|
run: >
|
||||||
>> "$GITHUB_OUTPUT"
|
echo "version=$(cargo metadata --format-version 1 --no-deps |
|
||||||
|
jq -r '.packages[] | select(.name == "oxipng").version')" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Retrieve ${{ matrix.target }} binary
|
- name: Retrieve ${{ matrix.target }} binary
|
||||||
uses: dawidd6/action-download-artifact@v3
|
uses: dawidd6/action-download-artifact@v21
|
||||||
with:
|
with:
|
||||||
workflow: oxipng.yml
|
workflow: oxipng.yml
|
||||||
commit: ${{ env.GITHUB_SHA }}
|
commit: ${{ github.sha }}
|
||||||
name: Oxipng binary (${{ matrix.target }})
|
name: Oxipng binary (${{ matrix.target }})
|
||||||
path: target
|
path: target
|
||||||
|
|
||||||
|
|
@ -71,12 +94,12 @@ jobs:
|
||||||
# so make the binary world-executable to meet user
|
# so make the binary world-executable to meet user
|
||||||
# expectations set by preceding releases.
|
# expectations set by preceding releases.
|
||||||
# Related issue:
|
# Related issue:
|
||||||
# https://github.com/shssoichiro/oxipng/issues/575
|
# https://github.com/oxipng/oxipng/issues/575
|
||||||
chmod ugo+x "$ARCHIVE_NAME"/oxipng
|
chmod ugo+x "$ARCHIVE_NAME"/oxipng
|
||||||
tar -vczf "${ARCHIVE_NAME}.tar.gz" "$ARCHIVE_NAME"/*;;
|
tar -vczf "${ARCHIVE_NAME}.tar.gz" "$ARCHIVE_NAME"/*;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
- name: Install QEMU and AArch64 cross compiler
|
- name: Install AArch64 libc components
|
||||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get -yq update
|
sudo apt-get -yq update
|
||||||
|
|
@ -85,20 +108,27 @@ jobs:
|
||||||
# properly
|
# properly
|
||||||
sudo apt-get -yq install libc6-arm64-cross libgcc-s1-arm64-cross
|
sudo apt-get -yq install libc6-arm64-cross libgcc-s1-arm64-cross
|
||||||
|
|
||||||
- name: Build deb archives
|
- name: Build Debian packages
|
||||||
if: endsWith(matrix.target, '-linux-gnu')
|
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: |
|
run: |
|
||||||
mkdir -p "target/${{ matrix.target }}/release"
|
mkdir -p "target/${{ matrix.target }}/release"
|
||||||
mv target/oxipng "target/${{ matrix.target }}/release"
|
mv target/oxipng "target/${{ matrix.target }}/release"
|
||||||
mv target/debug/assets "target/${{ matrix.target }}/release"
|
|
||||||
cargo install --locked cargo-deb
|
|
||||||
cargo deb --target "${{ matrix.target }}" --no-build --no-strip
|
cargo deb --target "${{ matrix.target }}" --no-build --no-strip
|
||||||
|
|
||||||
- name: Create release notes
|
- name: Create release notes
|
||||||
run: tail -n +3 CHANGELOG.md | sed -e '/^$/,$d' > RELEASE_NOTES.txt
|
run: tail -n +3 CHANGELOG.md | sed -e '/^$/,$d' > RELEASE_NOTES.txt
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v3
|
||||||
with:
|
with:
|
||||||
name: v${{ steps.oxipngMeta.outputs.version }}
|
name: v${{ steps.oxipngMeta.outputs.version }}
|
||||||
body_path: RELEASE_NOTES.txt
|
body_path: RELEASE_NOTES.txt
|
||||||
|
|
|
||||||
60
.github/workflows/docker.yml
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
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 }}
|
||||||
115
.github/workflows/oxipng.yml
vendored
|
|
@ -8,6 +8,11 @@ on:
|
||||||
- synchronize
|
- synchronize
|
||||||
workflow_dispatch:
|
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:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
name: CI
|
name: CI
|
||||||
|
|
@ -33,94 +38,83 @@ jobs:
|
||||||
|
|
||||||
include:
|
include:
|
||||||
- target: x86_64-unknown-linux-gnu
|
- target: x86_64-unknown-linux-gnu
|
||||||
os: ubuntu-22.04
|
os: ubuntu-24.04
|
||||||
target-apt-arch: amd64
|
target-apt-arch: amd64
|
||||||
- target: x86_64-unknown-linux-musl
|
- target: x86_64-unknown-linux-musl
|
||||||
os: ubuntu-22.04
|
os: ubuntu-24.04
|
||||||
target-apt-arch: amd64
|
target-apt-arch: amd64
|
||||||
- target: aarch64-unknown-linux-gnu
|
- target: aarch64-unknown-linux-gnu
|
||||||
os: ubuntu-22.04
|
os: ubuntu-24.04-arm
|
||||||
target-apt-arch: arm64
|
target-apt-arch: arm64
|
||||||
- target: aarch64-unknown-linux-musl
|
- target: aarch64-unknown-linux-musl
|
||||||
os: ubuntu-22.04
|
os: ubuntu-24.04-arm
|
||||||
target-apt-arch: arm64
|
target-apt-arch: arm64
|
||||||
- target: x86_64-pc-windows-msvc
|
- target: x86_64-pc-windows-msvc
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
- target: i686-pc-windows-msvc
|
- target: i686-pc-windows-msvc
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
- target: x86_64-apple-darwin
|
- target: x86_64-apple-darwin
|
||||||
os: macos-12
|
os: macos-15
|
||||||
- target: aarch64-apple-darwin
|
- target: aarch64-apple-darwin
|
||||||
os: macos-14 # ARM64 runner
|
os: macos-15
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_BUILD_TARGET: ${{ matrix.target }}
|
CARGO_BUILD_TARGET: ${{ matrix.target }}
|
||||||
RUSTFLAGS: -Zlocation-detail=none
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source
|
- name: Checkout source
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Ubuntu multiarch
|
|
||||||
if: startsWith(matrix.os, 'ubuntu') && matrix.target-apt-arch != 'amd64'
|
|
||||||
run: |
|
|
||||||
readonly DISTRO_CODENAME=jammy
|
|
||||||
sudo dpkg --add-architecture "${{ matrix.target-apt-arch }}"
|
|
||||||
sudo sed -i "s/^deb http/deb [arch=$(dpkg-architecture -q DEB_HOST_ARCH)] http/" /etc/apt/sources.list
|
|
||||||
sudo sed -i "s/^deb mirror/deb [arch=$(dpkg-architecture -q DEB_HOST_ARCH)] mirror/" /etc/apt/sources.list
|
|
||||||
for suite in '' '-updates' '-backports' '-security'; do
|
|
||||||
echo "deb [arch=${{ matrix.target-apt-arch }}] http://ports.ubuntu.com/ $DISTRO_CODENAME$suite main universe multiverse" | \
|
|
||||||
sudo tee -a /etc/apt/sources.list >/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Install musl development files
|
- name: Install musl development files
|
||||||
if: endsWith(matrix.target, '-musl')
|
if: endsWith(matrix.target, '-musl')
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get -yq update
|
sudo apt-get -yq update
|
||||||
sudo apt-get -yq install musl-tools musl-dev:${{ matrix.target-apt-arch }}
|
sudo apt-get -yq install musl-tools musl-dev:${{ matrix.target-apt-arch }}
|
||||||
|
|
||||||
- name: Install QEMU and AArch64 cross compiler
|
|
||||||
if: startsWith(matrix.target, 'aarch64-unknown-linux')
|
|
||||||
run: |
|
|
||||||
sudo apt-get -yq update
|
|
||||||
# libc6 must be present to run executables dynamically linked
|
|
||||||
# against glibc for the target architecture
|
|
||||||
sudo apt-get -yq install qemu-user gcc-aarch64-linux-gnu libc6:arm64
|
|
||||||
|
|
||||||
- name: Cache Cargo artifacts
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
targets: ${{ env.CARGO_BUILD_TARGET }}
|
target: ${{ env.CARGO_BUILD_TARGET }}
|
||||||
components: clippy, rustfmt
|
components: clippy, rustfmt, rust-src
|
||||||
|
cache-bin: false
|
||||||
|
cache-shared-key: cache
|
||||||
|
|
||||||
- name: Install nextest
|
- name: Install nextest
|
||||||
uses: taiki-e/install-action@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
|
- name: Run rustfmt
|
||||||
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
||||||
run: cargo fmt --check
|
run: cargo fmt --check
|
||||||
|
|
||||||
- name: Run Clippy (no default features)
|
- name: Run Clippy for all feature combinations
|
||||||
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
||||||
uses: giraffate/clippy-action@v1
|
run: >
|
||||||
with:
|
set -o pipefail;
|
||||||
clippy_flags: --no-deps --all-targets --no-default-features -- -D warnings
|
cargo hack clippy --no-deps --all-targets --feature-powerset \
|
||||||
reporter: github-check
|
--exclude-features sanity-checks,system-libdeflate \
|
||||||
fail_on_error: true
|
--message-format=json -- -D warnings \
|
||||||
|
| clippy-sarif
|
||||||
- name: Run Clippy (all features)
|
| tee clippy-results.sarif
|
||||||
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
| sarif-fmt
|
||||||
uses: giraffate/clippy-action@v1
|
|
||||||
with:
|
|
||||||
clippy_flags: --no-deps --all-targets --all-features -- -D warnings
|
|
||||||
reporter: github-check
|
|
||||||
fail_on_error: true
|
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -135,16 +129,26 @@ jobs:
|
||||||
run: cargo doc --release --no-deps
|
run: cargo doc --release --no-deps
|
||||||
|
|
||||||
- name: Build CLI binary
|
- name: Build CLI binary
|
||||||
run: cargo build --release
|
run: cargo build --release -Zbuild-std
|
||||||
|
env:
|
||||||
|
RUSTFLAGS: "-Zlocation-detail=none -Zunstable-options -Cpanic=immediate-abort"
|
||||||
|
|
||||||
- name: Upload CLI binary as artifact
|
- name: Upload CLI binary as artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: Oxipng binary (${{ matrix.target }})
|
name: Oxipng binary (${{ matrix.target }})
|
||||||
path: |
|
path: |
|
||||||
target/${{ env.CARGO_BUILD_TARGET }}/release/oxipng
|
target/${{ env.CARGO_BUILD_TARGET }}/release/oxipng
|
||||||
target/${{ env.CARGO_BUILD_TARGET }}/release/oxipng.exe
|
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:
|
msrv-check:
|
||||||
name: MSRV check
|
name: MSRV check
|
||||||
|
|
||||||
|
|
@ -156,15 +160,16 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source
|
- name: Checkout source
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Cache Cargo artifacts
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
|
|
||||||
- name: Install MSRV Rust toolchain
|
- name: Install MSRV Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@1.74.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: 1.85.1
|
||||||
|
cache-bin: false
|
||||||
|
cache-shared-key: cache
|
||||||
|
|
||||||
- name: Install nextest
|
- name: Install nextest
|
||||||
uses: taiki-e/install-action@nextest
|
uses: taiki-e/install-action@nextest
|
||||||
|
|
|
||||||
8
.gitignore
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
target
|
|
||||||
*.bk
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
*.bk
|
||||||
*.out.png
|
*.out.png
|
||||||
/.idea
|
/.idea/
|
||||||
/node_modules
|
/node_modules/
|
||||||
|
/target/
|
||||||
|
|
|
||||||
194
CHANGELOG.md
|
|
@ -1,3 +1,65 @@
|
||||||
|
## 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
|
## Version 9.1.1
|
||||||
|
|
||||||
- [Build] Change man page generation path to resolve issue with cargo publish.
|
- [Build] Change man page generation path to resolve issue with cargo publish.
|
||||||
|
|
@ -137,51 +199,51 @@
|
||||||
### Version 3.0.0
|
### Version 3.0.0
|
||||||
|
|
||||||
- [Breaking] Bump minimum Rust version to 1.41.0
|
- [Breaking] Bump minimum Rust version to 1.41.0
|
||||||
- [Breaking] Use IndexMap/IndexSet to provide more consistent performance ([#202](https://github.com/shssoichiro/oxipng/pull/202))
|
- [Breaking] Use IndexMap/IndexSet to provide more consistent performance ([#202](https://github.com/oxipng/oxipng/pull/202))
|
||||||
- This changes some public-facing types.
|
- This changes some public-facing types.
|
||||||
`IndexMap` and `IndexSet` are reexported
|
`IndexMap` and `IndexSet` are reexported
|
||||||
at the crate root to aid migration.
|
at the crate root to aid migration.
|
||||||
- [Breaking] Remove fields from the `Options` struct which were never used ([#211](https://github.com/shssoichiro/oxipng/pull/211/files#diff-b4aea3e418ccdb71239b96952d9cddb6L217), [#212](https://github.com/shssoichiro/oxipng/pull/212/files#diff-b4aea3e418ccdb71239b96952d9cddb6L134))
|
- [Breaking] 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/shssoichiro/oxipng/pull/210/files))
|
- [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/shssoichiro/oxipng/pull/203))
|
- [Feature] Add libdeflater as an option ([#203](https://github.com/oxipng/oxipng/pull/203))
|
||||||
- [Feature] Use standard `log` library ([#218](https://github.com/shssoichiro/oxipng/pull/218))
|
- [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/shssoichiro/oxipng/pull/224))
|
- [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.
|
- [Deprecated] `-o 4` was found to be equivalent to `-o 3` and is deprecated.
|
||||||
It will likely be removed in a future release.
|
It will likely be removed in a future release.
|
||||||
For now it remains equivalent to `-o 3`. ([#224](https://github.com/shssoichiro/oxipng/pull/224))
|
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/shssoichiro/oxipng/pull/199))
|
- [Bugfix] Ensure output is deterministic ([#199](https://github.com/oxipng/oxipng/pull/199))
|
||||||
- Update `image` crate to 0.23
|
- Update `image` crate to 0.23
|
||||||
- Update `itertools` crate to 0.9
|
- Update `itertools` crate to 0.9
|
||||||
- Various performance and internal improvements
|
- Various performance and internal improvements
|
||||||
|
|
||||||
### Version 2.3.0
|
### Version 2.3.0
|
||||||
|
|
||||||
- Allow disabling all alpha optimizations ([#181](https://github.com/shssoichiro/oxipng/pull/181))
|
- Allow disabling all alpha optimizations ([#181](https://github.com/oxipng/oxipng/pull/181))
|
||||||
- Fix interlacing issues on tiny images ([#182](https://github.com/shssoichiro/oxipng/pull/182))
|
- Fix interlacing issues on tiny images ([#182](https://github.com/oxipng/oxipng/pull/182))
|
||||||
- Reduce memory usage in filtering ([#191](https://github.com/shssoichiro/oxipng/pull/191))
|
- Reduce memory usage in filtering ([#191](https://github.com/oxipng/oxipng/pull/191))
|
||||||
- Implement palette sorting to improve compression ([#193](https://github.com/shssoichiro/oxipng/pull/193))
|
- Implement palette sorting to improve compression ([#193](https://github.com/oxipng/oxipng/pull/193))
|
||||||
- Disable alpha optimizations by default ([#187](https://github.com/shssoichiro/oxipng/pull/187))
|
- Disable alpha optimizations by default ([#187](https://github.com/oxipng/oxipng/pull/187))
|
||||||
- Add support for WASM ([#194](https://github.com/shssoichiro/oxipng/pull/194))
|
- Add support for WASM ([#194](https://github.com/oxipng/oxipng/pull/194))
|
||||||
|
|
||||||
### Version 2.2.2
|
### Version 2.2.2
|
||||||
|
|
||||||
- Fix grayscale bit-depth reduction ([#171](https://github.com/shssoichiro/oxipng/pull/171))
|
- Fix grayscale bit-depth reduction ([#171](https://github.com/oxipng/oxipng/pull/171))
|
||||||
- Fix typos and incorrect log message ([#172](https://github.com/shssoichiro/oxipng/pull/172))
|
- Fix typos and incorrect log message ([#172](https://github.com/oxipng/oxipng/pull/172))
|
||||||
- Make metadata order deterministic ([#174](https://github.com/shssoichiro/oxipng/pull/174))
|
- Make metadata order deterministic ([#174](https://github.com/oxipng/oxipng/pull/174))
|
||||||
- Fix 32-bit builds ([#176](https://github.com/shssoichiro/oxipng/pull/176))
|
- Fix 32-bit builds ([#176](https://github.com/oxipng/oxipng/pull/176))
|
||||||
- Enable LTO in release builds ([#177](https://github.com/shssoichiro/oxipng/pull/177))
|
- Enable LTO in release builds ([#177](https://github.com/oxipng/oxipng/pull/177))
|
||||||
- Use deterministic compression strategy ([#179](https://github.com/shssoichiro/oxipng/pull/179))
|
- 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/shssoichiro/oxipng/pull/175))
|
- 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/shssoichiro/oxipng/pull/180))
|
- Preallocate memory in reduced_alpha_to_up ([#180](https://github.com/oxipng/oxipng/pull/180))
|
||||||
- Update `bit-vec` crate to 0.6
|
- Update `bit-vec` crate to 0.6
|
||||||
|
|
||||||
### Version 2.2.1
|
### Version 2.2.1
|
||||||
|
|
||||||
- Fix compression of very large files ([#167](https://github.com/shssoichiro/oxipng/pull/167)) ([#168](https://github.com/shssoichiro/oxipng/pull/168))
|
- 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
|
### Version 2.2.0
|
||||||
|
|
||||||
- Various internal improvements ([#154](https://github.com/shssoichiro/oxipng/pull/154)) ([#158](https://github.com/shssoichiro/oxipng/pull/158)) ([#160](https://github.com/shssoichiro/oxipng/pull/160)) ([#161](https://github.com/shssoichiro/oxipng/pull/161)) ([#162](https://github.com/shssoichiro/oxipng/pull/162)) ([#163](https://github.com/shssoichiro/oxipng/pull/163))
|
- 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 `image` crate to 0.21.0
|
||||||
- Update `itertools` crate to 0.8.0
|
- Update `itertools` crate to 0.8.0
|
||||||
- Update `zopfli` crate to 0.4.0
|
- Update `zopfli` crate to 0.4.0
|
||||||
|
|
@ -190,24 +252,24 @@
|
||||||
|
|
||||||
### Version 2.1.8
|
### Version 2.1.8
|
||||||
|
|
||||||
- Fix non-standard sBIT headers in other code locations ([#153](https://github.com/shssoichiro/oxipng/issues/153))
|
- Fix non-standard sBIT headers in other code locations ([#153](https://github.com/oxipng/oxipng/issues/153))
|
||||||
|
|
||||||
### Version 2.1.7
|
### Version 2.1.7
|
||||||
|
|
||||||
- 80x faster palette reduction ([#150](https://github.com/shssoichiro/oxipng/pull/150))
|
- 80x faster palette reduction ([#150](https://github.com/oxipng/oxipng/pull/150))
|
||||||
- Optimize RGB to palette conversion ([#148](https://github.com/shssoichiro/oxipng/pull/148))
|
- Optimize RGB to palette conversion ([#148](https://github.com/oxipng/oxipng/pull/148))
|
||||||
- Various microoptimizations ([#146](https://github.com/shssoichiro/oxipng/pull/146))
|
- Various microoptimizations ([#146](https://github.com/oxipng/oxipng/pull/146))
|
||||||
- Introduce third-party safe wrapper around cloudflare-zlib ([#149](https://github.com/shssoichiro/oxipng/pull/149))
|
- Introduce third-party safe wrapper around cloudflare-zlib ([#149](https://github.com/oxipng/oxipng/pull/149))
|
||||||
|
|
||||||
### Version 2.1.6
|
### Version 2.1.6
|
||||||
|
|
||||||
- Identify and drop useless sRGB profiles ([#143](https://github.com/shssoichiro/oxipng/pull/143))
|
- Identify and drop useless sRGB profiles ([#143](https://github.com/oxipng/oxipng/pull/143))
|
||||||
- Alpha heuristic improvements ([#144](https://github.com/shssoichiro/oxipng/pull/144))
|
- Alpha heuristic improvements ([#144](https://github.com/oxipng/oxipng/pull/144))
|
||||||
- Bump `miniz_oxide` and `cloudflare-zlib-sys` to 0.2.0
|
- Bump `miniz_oxide` and `cloudflare-zlib-sys` to 0.2.0
|
||||||
|
|
||||||
### Version 2.1.5
|
### Version 2.1.5
|
||||||
|
|
||||||
- Fix issue where some images will incorrectly reduce bit depth ([#140](https://github.com/shssoichiro/oxipng/issues/140))
|
- Fix issue where some images will incorrectly reduce bit depth ([#140](https://github.com/oxipng/oxipng/issues/140))
|
||||||
|
|
||||||
### Version 2.1.4
|
### Version 2.1.4
|
||||||
|
|
||||||
|
|
@ -220,22 +282,22 @@
|
||||||
|
|
||||||
### Version 2.1.2
|
### Version 2.1.2
|
||||||
|
|
||||||
- Fix issue with PNG to Indexed reduction on images with transparency pixel ([#129](https://github.com/shssoichiro/oxipng/issues/129))
|
- Fix issue with PNG to Indexed reduction on images with transparency pixel ([#129](https://github.com/oxipng/oxipng/issues/129))
|
||||||
|
|
||||||
### Version 2.1.1
|
### Version 2.1.1
|
||||||
|
|
||||||
- More fixes for alpha optimization on interlaced images ([#133](https://github.com/shssoichiro/oxipng/issues/133))
|
- More fixes for alpha optimization on interlaced images ([#133](https://github.com/oxipng/oxipng/issues/133))
|
||||||
|
|
||||||
### Version 2.1.0
|
### Version 2.1.0
|
||||||
|
|
||||||
- [SEMVER_MINOR] Bump minimum Rust version to 1.27.0
|
- [SEMVER_MINOR] Bump minimum Rust version to 1.27.0
|
||||||
- [SEMVER_MINOR] Reenable faster Cloudflare zlib compression on platforms that support it
|
- [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))
|
- Fix memory leak with Cloudflare zlib ([#126](https://github.com/oxipng/oxipng/issues/126))
|
||||||
- Minor fixes and cleanup
|
- Minor fixes and cleanup
|
||||||
|
|
||||||
### Version 2.0.2
|
### Version 2.0.2
|
||||||
|
|
||||||
- Fix an issue in alpha optimization on interlaced images ([#113](https://github.com/shssoichiro/oxipng/issues/113))
|
- Fix an issue in alpha optimization on interlaced images ([#113](https://github.com/oxipng/oxipng/issues/113))
|
||||||
|
|
||||||
### Version 2.0.1
|
### Version 2.0.1
|
||||||
|
|
||||||
|
|
@ -272,12 +334,12 @@
|
||||||
|
|
||||||
### Version 1.0.1
|
### Version 1.0.1
|
||||||
|
|
||||||
- Bump rayon to 1.0 ([#99](https://github.com/shssoichiro/oxipng/pull/99) @cuviper)
|
- Bump rayon to 1.0 ([#99](https://github.com/oxipng/oxipng/pull/99) @cuviper)
|
||||||
- Bump minor versions of other dependencies for binary distribution
|
- Bump minor versions of other dependencies for binary distribution
|
||||||
|
|
||||||
### Version 1.0.0
|
### Version 1.0.0
|
||||||
|
|
||||||
- Remove the C dependency on miniz, and replace it with a Rust version ([#57](https://github.com/shssoichiro/oxipng/issues/57))
|
- 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.
|
- 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.
|
- [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.
|
- Presets will be updated automatically. This means that presets 3 and higher will run significantly more quickly.
|
||||||
|
|
@ -304,30 +366,30 @@
|
||||||
|
|
||||||
### Version 0.18.3
|
### 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/shssoichiro/oxipng/issues/93))
|
- 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))
|
||||||
|
|
||||||
### Version 0.18.2
|
### Version 0.18.2
|
||||||
|
|
||||||
- Bump `image` to 0.18
|
- Bump `image` to 0.18
|
||||||
- Fix unfiltering of scan lines in interlaced images ([#92](https://github.com/shssoichiro/oxipng/issues/92))
|
- Fix unfiltering of scan lines in interlaced images ([#92](https://github.com/oxipng/oxipng/issues/92))
|
||||||
|
|
||||||
### Version 0.18.1
|
### Version 0.18.1
|
||||||
|
|
||||||
- Bump `rayon` to 0.9
|
- Bump `rayon` to 0.9
|
||||||
- Fix failure to optimize on certain grayscale images ([#89](https://github.com/shssoichiro/oxipng/issues/89))
|
- Fix failure to optimize on certain grayscale images ([#89](https://github.com/oxipng/oxipng/issues/89))
|
||||||
|
|
||||||
### Version 0.18.0
|
### Version 0.18.0
|
||||||
|
|
||||||
- Bump `itertools` to 0.7
|
- Bump `itertools` to 0.7
|
||||||
- Bump `image` to 0.17
|
- Bump `image` to 0.17
|
||||||
- [SEMVER_MAJOR] Bump minimum rustc version to 1.20.0
|
- [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))
|
- Fix parsing of glob paths on Windows ([#90](https://github.com/oxipng/oxipng/issues/90))
|
||||||
|
|
||||||
### Version 0.17.2
|
### Version 0.17.2
|
||||||
|
|
||||||
- Bump `image` to 0.16
|
- 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)
|
- 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/shssoichiro/oxipng/issues/83) @emielbeinema)
|
- Return an error instead of crashing on APNG files ([#83](https://github.com/oxipng/oxipng/issues/83) @emielbeinema)
|
||||||
|
|
||||||
### Version 0.17.1
|
### Version 0.17.1
|
||||||
|
|
||||||
|
|
@ -343,14 +405,14 @@
|
||||||
trials for optimizing the alpha channel, using the previously mentioned fast heuristic.
|
trials for optimizing the alpha channel, using the previously mentioned fast heuristic.
|
||||||
This option will make optimization of images with transparency somewhat slower,
|
This option will make optimization of images with transparency somewhat slower,
|
||||||
but may improve compression.
|
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 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/shssoichiro/oxipng/issues/82))
|
- Fixed another bug in reducing palettes for images with bit depth less than eight ([#82](https://github.com/oxipng/oxipng/issues/82))
|
||||||
- Code cleanup
|
- Code cleanup
|
||||||
- Bump `image` to 0.15
|
- Bump `image` to 0.15
|
||||||
|
|
||||||
### Version 0.16.3
|
### Version 0.16.3
|
||||||
|
|
||||||
- Fix command-line help text ([#70](https://github.com/shssoichiro/oxipng/issues/70))
|
- Fix command-line help text ([#70](https://github.com/oxipng/oxipng/issues/70))
|
||||||
|
|
||||||
### Version 0.16.2
|
### Version 0.16.2
|
||||||
|
|
||||||
|
|
@ -368,18 +430,18 @@
|
||||||
|
|
||||||
### Version 0.15.2
|
### Version 0.15.2
|
||||||
|
|
||||||
- Bump `image` to 0.13 ([#65](https://github.com/shssoichiro/oxipng/pull/65))
|
- Bump `image` to 0.13 ([#65](https://github.com/oxipng/oxipng/pull/65))
|
||||||
- Bump `rayon` to 0.7
|
- Bump `rayon` to 0.7
|
||||||
- Bump `itertools` to 0.6
|
- Bump `itertools` to 0.6
|
||||||
|
|
||||||
### Version 0.15.1
|
### Version 0.15.1
|
||||||
|
|
||||||
- Ignore color reductions that would increase file size ([#61](https://github.com/shssoichiro/oxipng/issues/61))
|
- Ignore color reductions that would increase file size ([#61](https://github.com/oxipng/oxipng/issues/61))
|
||||||
|
|
||||||
### Version 0.15.0
|
### Version 0.15.0
|
||||||
|
|
||||||
- [SEMVER_MINOR] Check images for correctness before writing result ([#60](https://github.com/shssoichiro/oxipng/issues/60))
|
- [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/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/oxipng/oxipng/issues/60))
|
||||||
- Don't write new file if moving from interlaced to non-interlaced if new file would be larger
|
- Don't write new file if moving from interlaced to non-interlaced if new file would be larger
|
||||||
|
|
||||||
### Version 0.14.4
|
### Version 0.14.4
|
||||||
|
|
@ -413,14 +475,14 @@
|
||||||
|
|
||||||
### Version 0.13.0
|
### Version 0.13.0
|
||||||
|
|
||||||
- Fix bug in certain PNG headers when reducing color type ([#52](https://github.com/shssoichiro/oxipng/issues/52))
|
- 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
|
- [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
|
- [SMEVER_MAJOR] Bump minimum required rustc version to 1.12.0
|
||||||
|
|
||||||
### Version 0.12.0
|
### Version 0.12.0
|
||||||
|
|
||||||
- Performance optimizations
|
- Performance optimizations
|
||||||
- Fix processing filenames that contain commas (@aliceatlas [#50](https://github.com/shssoichiro/oxipng/pull/50))
|
- 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.
|
- [SEMVER_MINOR] Add zopfli option (-Z), disabled by default. Gives about 10% better compression, but is currently 50-100x slower.
|
||||||
|
|
||||||
### Version 0.11.0
|
### Version 0.11.0
|
||||||
|
|
@ -445,7 +507,7 @@
|
||||||
|
|
||||||
### Version 0.8.2
|
### Version 0.8.2
|
||||||
|
|
||||||
- Fix issue where images smaller than 4px width would crash on interlacing ([#42](https://github.com/shssoichiro/oxipng/issues/42))
|
- Fix issue where images smaller than 4px width would crash on interlacing ([#42](https://github.com/oxipng/oxipng/issues/42))
|
||||||
|
|
||||||
### Version 0.8.1
|
### Version 0.8.1
|
||||||
|
|
||||||
|
|
@ -461,7 +523,7 @@
|
||||||
- Minor compression improvement on interlaced images
|
- Minor compression improvement on interlaced images
|
||||||
- Performance optimizations
|
- Performance optimizations
|
||||||
- [SEMVER_MINOR] Move default Options into a Default impl
|
- [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))
|
- [SEMVER_MINOR] Add option for setting number of threads ([#39](https://github.com/oxipng/oxipng/issues/39))
|
||||||
|
|
||||||
### Version 0.6.0
|
### Version 0.6.0
|
||||||
|
|
||||||
|
|
@ -472,47 +534,47 @@
|
||||||
|
|
||||||
### Version 0.5.0
|
### Version 0.5.0
|
||||||
|
|
||||||
- [SEMVER_MINOR] Palette entries can now reduced, on by default ([#11](https://github.com/shssoichiro/oxipng/issues/11))
|
- [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
|
- 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))
|
- Add cargo bench suite ([#7](https://github.com/oxipng/oxipng/issues/7))
|
||||||
|
|
||||||
### Version 0.4.0
|
### Version 0.4.0
|
||||||
|
|
||||||
- Performance optimizations
|
- Performance optimizations
|
||||||
- [SEMVER_MAJOR] `-s` automatically infers `--strip safe` ([#31](https://github.com/shssoichiro/oxipng/issues/31))
|
- [SEMVER_MAJOR] `-s` automatically infers `--strip safe` ([#31](https://github.com/oxipng/oxipng/issues/31))
|
||||||
- Update byteorder and clap crates
|
- Update byteorder and clap crates
|
||||||
- Fix issue where interlaced images incorrectly applied filters on the first line of a pass
|
- Fix issue where interlaced images incorrectly applied filters on the first line of a pass
|
||||||
|
|
||||||
### Version 0.3.0
|
### Version 0.3.0
|
||||||
|
|
||||||
- Properly decode interlaced images
|
- Properly decode interlaced images
|
||||||
- [SEMVER_MINOR] Allow converting between progressive and interlaced images ([#3](https://github.com/shssoichiro/oxipng/issues/3))
|
- [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
|
- Fix a bug that would cause oxipng to crash on very small images
|
||||||
|
|
||||||
### Version 0.2.2
|
### Version 0.2.2
|
||||||
|
|
||||||
- Limit number of threads to 1.5x number of cores
|
- 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))
|
- Significantly improve memory usage, especially with high optimization levels. ([#32](https://github.com/oxipng/oxipng/issues/32))
|
||||||
- Refactor output code ([#19](https://github.com/shssoichiro/oxipng/issues/19))
|
- Refactor output code ([#19](https://github.com/oxipng/oxipng/issues/19))
|
||||||
|
|
||||||
### Version 0.2.1
|
### Version 0.2.1
|
||||||
|
|
||||||
- Add rustdoc for public methods and structs
|
- Add rustdoc for public methods and structs
|
||||||
- Improve filter mode 5 heuristic ([#16](https://github.com/shssoichiro/oxipng/issues/16))
|
- 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/shssoichiro/oxipng/issues/29))
|
- Add tests for edge-case images with subtitles ([#29](https://github.com/oxipng/oxipng/issues/29))
|
||||||
|
|
||||||
### Version 0.2.0
|
### Version 0.2.0
|
||||||
|
|
||||||
- Fix program version that is displayed when running `oxipng -V`
|
- 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))
|
- Ensure `--quiet` mode is actually quiet (@SethDusek [#20](https://github.com/oxipng/oxipng/pull/20))
|
||||||
- Write status/debug information to stderr instead of stdout
|
- 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))
|
- 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_MAJOR] Allow 'safe', 'all', or comma-separated list as options for `--strip`
|
||||||
- [SEMVER_MINOR] Add `-s` alias for `--strip`
|
- [SEMVER_MINOR] Add `-s` alias for `--strip`
|
||||||
|
|
||||||
### Version 0.1.1
|
### Version 0.1.1
|
||||||
|
|
||||||
- Fix `oxipng *` writing all input files to one output file ([#15](https://github.com/shssoichiro/oxipng/issues/15))
|
- Fix `oxipng *` writing all input files to one output file ([#15](https://github.com/oxipng/oxipng/issues/15))
|
||||||
|
|
||||||
### Version 0.1.0
|
### Version 0.1.0
|
||||||
|
|
||||||
|
|
|
||||||
588
Cargo.lock
generated
|
|
@ -1,78 +1,74 @@
|
||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "adler"
|
name = "adler2"
|
||||||
version = "1.0.2"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.13"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
|
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"anstyle-parse",
|
"anstyle-parse",
|
||||||
"anstyle-query",
|
"anstyle-query",
|
||||||
"anstyle-wincon",
|
"anstyle-wincon",
|
||||||
"colorchoice",
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
"utf8parse",
|
"utf8parse",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle"
|
name = "anstyle"
|
||||||
version = "1.0.6"
|
version = "1.0.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
|
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-parse"
|
name = "anstyle-parse"
|
||||||
version = "0.2.3"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
|
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"utf8parse",
|
"utf8parse",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-query"
|
name = "anstyle-query"
|
||||||
version = "1.0.2"
|
version = "1.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-wincon"
|
name = "anstyle-wincon"
|
||||||
version = "3.0.2"
|
version = "3.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
|
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"windows-sys 0.52.0",
|
"once_cell_polyfill",
|
||||||
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.2.0"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "2.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bitflags"
|
|
||||||
version = "2.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitvec"
|
name = "bitvec"
|
||||||
|
|
@ -87,43 +83,53 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bumpalo"
|
||||||
version = "1.15.0"
|
version = "3.20.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15"
|
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "bytemuck"
|
||||||
version = "1.5.0"
|
version = "1.25.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder-lite"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.92"
|
version = "1.2.60"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41"
|
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.0"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.4"
|
version = "4.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
|
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.2"
|
version = "4.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
|
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
|
|
@ -134,49 +140,30 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_lex"
|
name = "clap_lex"
|
||||||
version = "0.7.0"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
|
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clap_mangen"
|
|
||||||
version = "0.2.20"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e1dd95b5ebb5c1c54581dd6346f3ed6a79a3eef95dd372fc2ac13d535535300e"
|
|
||||||
dependencies = [
|
|
||||||
"clap",
|
|
||||||
"roff",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.0"
|
version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.4.0"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
|
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-channel"
|
|
||||||
version = "0.5.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-deque"
|
name = "crossbeam-deque"
|
||||||
version = "0.8.5"
|
version = "0.8.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
|
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-epoch",
|
"crossbeam-epoch",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
|
|
@ -193,30 +180,30 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.19"
|
version = "0.8.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.10.0"
|
version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_filter"
|
name = "env_filter"
|
||||||
version = "0.1.0"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea"
|
checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_logger"
|
name = "env_logger"
|
||||||
version = "0.11.3"
|
version = "0.11.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9"
|
checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
|
|
@ -226,46 +213,40 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.8"
|
version = "0.3.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fdeflate"
|
name = "fdeflate"
|
||||||
version = "0.3.4"
|
version = "0.3.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645"
|
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filetime"
|
name = "find-msvc-tools"
|
||||||
version = "0.2.23"
|
version = "0.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"redox_syscall",
|
|
||||||
"windows-sys 0.52.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.0.28"
|
version = "1.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
|
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
|
|
@ -279,33 +260,34 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glob"
|
name = "glob"
|
||||||
version = "0.3.1"
|
version = "0.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.3"
|
version = "0.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
version = "0.25.1"
|
version = "0.25.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11"
|
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"byteorder",
|
"byteorder-lite",
|
||||||
|
"moxcms",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"png",
|
"png",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.2.6"
|
version = "2.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
|
|
@ -313,95 +295,164 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "is_terminal_polyfill"
|
||||||
version = "0.2.153"
|
version = "1.70.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.185"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libdeflate-sys"
|
name = "libdeflate-sys"
|
||||||
version = "1.20.0"
|
version = "1.25.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "669ea17f9257bcb48c09c7ee4bef3957777504acffac557263e20c11001977bc"
|
checksum = "72753e0008ea87963d2f0770042d0df7abe51fafbb8dcaf618ac440f2f1fec0a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libdeflater"
|
name = "libdeflater"
|
||||||
version = "1.20.0"
|
version = "1.25.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8dfd6424f7010ee0a3416f1d796d0450e3ad3ac237a237644f728277c4ded016"
|
checksum = "d1ee41cf6fb1bb6030dfb59ffb7bc01ab26aade44142084c87f0fc7a1658fe71"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libdeflate-sys",
|
"libdeflate-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.13"
|
version = "0.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.21"
|
version = "0.4.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.7.2"
|
version = "0.8.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adler",
|
"adler2",
|
||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "moxcms"
|
||||||
version = "0.2.18"
|
version = "0.7.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
|
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
"pxfm",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxipng"
|
name = "oxipng"
|
||||||
version = "9.1.1"
|
version = "10.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitvec",
|
"bitvec",
|
||||||
"clap",
|
"clap",
|
||||||
"clap_mangen",
|
|
||||||
"crossbeam-channel",
|
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"filetime",
|
|
||||||
"glob",
|
"glob",
|
||||||
"image",
|
"image",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"libdeflater",
|
"libdeflater",
|
||||||
"log",
|
"log",
|
||||||
|
"parse-size",
|
||||||
"rayon",
|
"rayon",
|
||||||
"rgb",
|
"rgb",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustc_version",
|
"serde_json",
|
||||||
"zopfli",
|
"zopfli",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "png"
|
name = "parse-size"
|
||||||
version = "0.17.13"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1"
|
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"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags",
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"fdeflate",
|
"fdeflate",
|
||||||
"flate2",
|
"flate2",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pxfm"
|
||||||
|
version = "0.1.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "radium"
|
name = "radium"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
|
@ -410,9 +461,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon"
|
name = "rayon"
|
||||||
version = "1.10.0"
|
version = "1.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
|
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"either",
|
"either",
|
||||||
"rayon-core",
|
"rayon-core",
|
||||||
|
|
@ -420,77 +471,95 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon-core"
|
name = "rayon-core"
|
||||||
version = "1.12.1"
|
version = "1.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
|
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-deque",
|
"crossbeam-deque",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redox_syscall"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 1.3.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rgb"
|
name = "rgb"
|
||||||
version = "0.8.37"
|
version = "0.8.53"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8"
|
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "roff"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "1.1.0"
|
version = "2.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustc_version"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
|
|
||||||
dependencies = [
|
|
||||||
"semver",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.32"
|
version = "1.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.5.0",
|
"bitflags",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "serde"
|
||||||
version = "1.0.22"
|
version = "1.0.228"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
|
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]]
|
[[package]]
|
||||||
name = "simd-adler32"
|
name = "simd-adler32"
|
||||||
version = "0.3.7"
|
version = "0.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
|
|
@ -498,6 +567,17 @@ version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tap"
|
name = "tap"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
@ -506,165 +586,41 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "terminal_size"
|
name = "terminal_size"
|
||||||
version = "0.3.0"
|
version = "0.4.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
|
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typed-arena"
|
name = "unicode-ident"
|
||||||
version = "2.0.2"
|
version = "1.0.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.48.0"
|
version = "0.61.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-targets 0.48.5",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.52.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets 0.52.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-targets"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
|
||||||
dependencies = [
|
|
||||||
"windows_aarch64_gnullvm 0.48.5",
|
|
||||||
"windows_aarch64_msvc 0.48.5",
|
|
||||||
"windows_i686_gnu 0.48.5",
|
|
||||||
"windows_i686_msvc 0.48.5",
|
|
||||||
"windows_x86_64_gnu 0.48.5",
|
|
||||||
"windows_x86_64_gnullvm 0.48.5",
|
|
||||||
"windows_x86_64_msvc 0.48.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-targets"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
|
|
||||||
dependencies = [
|
|
||||||
"windows_aarch64_gnullvm 0.52.5",
|
|
||||||
"windows_aarch64_msvc 0.52.5",
|
|
||||||
"windows_i686_gnu 0.52.5",
|
|
||||||
"windows_i686_gnullvm",
|
|
||||||
"windows_i686_msvc 0.52.5",
|
|
||||||
"windows_x86_64_gnu 0.52.5",
|
|
||||||
"windows_x86_64_gnullvm 0.52.5",
|
|
||||||
"windows_x86_64_msvc 0.52.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_gnullvm"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_gnullvm"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnullvm"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnullvm"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnullvm"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.48.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wyz"
|
name = "wyz"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|
@ -675,13 +631,19 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zopfli"
|
name = "zmij"
|
||||||
version = "0.8.0"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c1f48f3508a3a3f2faee01629564400bc12260f6214a056d06a3aaaa6ef0736"
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zopfli"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"log",
|
"log",
|
||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
"typed-arena",
|
|
||||||
]
|
]
|
||||||
|
|
|
||||||
117
Cargo.toml
|
|
@ -2,8 +2,9 @@
|
||||||
authors = ["Joshua Holmer <jholmer.in@gmail.com>"]
|
authors = ["Joshua Holmer <jholmer.in@gmail.com>"]
|
||||||
categories = ["command-line-utilities", "compression"]
|
categories = ["command-line-utilities", "compression"]
|
||||||
description = "A lossless PNG compression optimizer"
|
description = "A lossless PNG compression optimizer"
|
||||||
|
keywords = ["png", "image-compression", "optimization", "multi-threading", "lossless"]
|
||||||
documentation = "https://docs.rs/oxipng"
|
documentation = "https://docs.rs/oxipng"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
exclude = [
|
exclude = [
|
||||||
".editorconfig",
|
".editorconfig",
|
||||||
".gitattributes",
|
".gitattributes",
|
||||||
|
|
@ -13,16 +14,17 @@ exclude = [
|
||||||
"Dockerfile",
|
"Dockerfile",
|
||||||
"scripts/*",
|
"scripts/*",
|
||||||
"tests/*",
|
"tests/*",
|
||||||
|
"xtask/*",
|
||||||
]
|
]
|
||||||
homepage = "https://github.com/shssoichiro/oxipng"
|
homepage = "https://github.com/oxipng/oxipng"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "oxipng"
|
name = "oxipng"
|
||||||
repository = "https://github.com/shssoichiro/oxipng"
|
repository = "https://github.com/oxipng/oxipng"
|
||||||
version = "9.1.1"
|
version = "10.1.1"
|
||||||
rust-version = "1.74.0"
|
rust-version = "1.85.1"
|
||||||
|
|
||||||
[badges]
|
[badges]
|
||||||
travis-ci = { repository = "shssoichiro/oxipng", branch = "master" }
|
travis-ci = { repository = "oxipng/oxipng", branch = "master" }
|
||||||
maintenance = { status = "actively-developed" }
|
maintenance = { status = "actively-developed" }
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|
@ -36,58 +38,33 @@ name = "zopfli"
|
||||||
required-features = ["zopfli"]
|
required-features = ["zopfli"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
zopfli = { version = "0.8.0", optional = true, default-features = false, features = ["std", "zlib"] }
|
|
||||||
rgb = "0.8.37"
|
|
||||||
indexmap = "2.2.6"
|
|
||||||
libdeflater = "1.20.0"
|
|
||||||
log = "0.4.21"
|
|
||||||
bitvec = "1.0.1"
|
bitvec = "1.0.1"
|
||||||
rustc-hash = "1.1.0"
|
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"] }
|
||||||
|
|
||||||
[dependencies.env_logger]
|
[target.'cfg(windows)'.dependencies]
|
||||||
optional = true
|
glob = { version = "0.3.3", optional = true }
|
||||||
default-features = false
|
|
||||||
features = ["auto-color"]
|
|
||||||
version = "0.11.3"
|
|
||||||
|
|
||||||
[dependencies.crossbeam-channel]
|
[dev-dependencies]
|
||||||
optional = true
|
serde_json = "1.0.150"
|
||||||
version = "0.5.12"
|
|
||||||
|
|
||||||
[dependencies.filetime]
|
|
||||||
optional = true
|
|
||||||
version = "0.2.23"
|
|
||||||
|
|
||||||
[dependencies.rayon]
|
|
||||||
optional = true
|
|
||||||
version = "1.10.0"
|
|
||||||
|
|
||||||
[dependencies.clap]
|
|
||||||
optional = true
|
|
||||||
version = "4.5.4"
|
|
||||||
features = ["wrap_help"]
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies.glob]
|
|
||||||
optional = true
|
|
||||||
version = "0.3.1"
|
|
||||||
|
|
||||||
[dependencies.image]
|
|
||||||
optional = true
|
|
||||||
default-features = false
|
|
||||||
features = ["png"]
|
|
||||||
version = "0.25.1"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
clap = "4.5.4"
|
|
||||||
clap_mangen = "0.2.20"
|
|
||||||
rustc_version = "0.4.0"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
binary = ["clap", "glob", "env_logger"]
|
binary = ["dep:clap", "dep:glob", "dep:env_logger", "dep:parse-size"]
|
||||||
default = ["binary", "filetime", "parallel", "zopfli"]
|
default = ["binary", "parallel", "zopfli"]
|
||||||
parallel = ["rayon", "indexmap/rayon", "crossbeam-channel"]
|
parallel = ["dep:rayon", "indexmap/rayon"]
|
||||||
freestanding = ["libdeflater/freestanding"]
|
freestanding = ["libdeflater/freestanding"]
|
||||||
sanity-checks = ["image"]
|
sanity-checks = ["dep:image"]
|
||||||
|
zopfli = ["dep:zopfli"]
|
||||||
|
system-libdeflate = ["libdeflater/dynamic"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "oxipng"
|
name = "oxipng"
|
||||||
|
|
@ -104,7 +81,41 @@ panic = "abort"
|
||||||
[package.metadata.deb]
|
[package.metadata.deb]
|
||||||
assets = [
|
assets = [
|
||||||
["target/release/oxipng", "usr/bin/", "755"],
|
["target/release/oxipng", "usr/bin/", "755"],
|
||||||
["target/release/assets/oxipng.1", "usr/share/man/man1/", "644"],
|
["target/xtask/mangen/manpages/oxipng.1", "usr/share/man/man1/", "644"],
|
||||||
["README.md", "usr/share/doc/oxipng/", "644"],
|
["README.md", "usr/share/doc/oxipng/", "644"],
|
||||||
["CHANGELOG.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"
|
||||||
|
|
|
||||||
46
Dockerfile
|
|
@ -1,20 +1,46 @@
|
||||||
FROM rust:alpine as base
|
# syntax=docker/dockerfile:1
|
||||||
|
# check=error=true
|
||||||
|
FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
|
||||||
|
|
||||||
COPY . /src
|
FROM --platform=$BUILDPLATFORM rust:1.85.1-alpine AS base
|
||||||
|
|
||||||
RUN rustup update 1.74 && rustup default 1.74
|
RUN apk update && \
|
||||||
|
apk add \
|
||||||
RUN apk update \
|
|
||||||
&& apk add \
|
|
||||||
gcc \
|
gcc \
|
||||||
g++
|
g++ \
|
||||||
|
clang
|
||||||
|
|
||||||
RUN cd /src && cargo build --release
|
COPY --from=xx / /
|
||||||
|
|
||||||
FROM alpine as tool
|
ARG TARGETPLATFORM
|
||||||
|
RUN xx-info env
|
||||||
|
|
||||||
COPY --from=base /src/target/release/oxipng /usr/local/bin
|
RUN xx-apk add \
|
||||||
|
gcc \
|
||||||
|
musl-dev \
|
||||||
|
libdeflate
|
||||||
|
|
||||||
WORKDIR /src
|
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" ]
|
ENTRYPOINT [ "oxipng" ]
|
||||||
CMD [ "--help" ]
|
CMD [ "--help" ]
|
||||||
|
|
|
||||||
96
MANUAL.txt
|
|
@ -1,4 +1,4 @@
|
||||||
oxipng 9.1.1
|
oxipng 10.1.0
|
||||||
Losslessly improve compression of PNG files
|
Losslessly improve compression of PNG files
|
||||||
|
|
||||||
Usage: oxipng [OPTIONS] <files>...
|
Usage: oxipng [OPTIONS] <files>...
|
||||||
|
|
@ -13,14 +13,14 @@ Options:
|
||||||
compression. Lower levels are faster, higher levels provide better compression, though
|
compression. Lower levels are faster, higher levels provide better compression, though
|
||||||
with increasingly diminishing returns.
|
with increasingly diminishing returns.
|
||||||
|
|
||||||
0 => --zc 5 --fast (1 trial, determined heuristically)
|
0 => --zc 5 --fast (filter chosen heuristically)
|
||||||
1 => --zc 10 --fast (1 trial, determined heuristically)
|
1 => --zc 10 --fast (filter chosen heuristically)
|
||||||
2 => --zc 11 -f 0,1,6,7 --fast (4 fast trials, 1 main trial)
|
2 => --zc 11 -f 0,1,6,7 --fast
|
||||||
3 => --zc 11 -f 0,7,8,9 (4 trials)
|
3 => --zc 11 -f 0,7,8,9 --brute-level 1 --brute-lines 3
|
||||||
4 => --zc 12 -f 0,7,8,9 (4 trials)
|
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 (8 trials)
|
5 => --zc 12 -f 0,1,2,5,6,7,8,9 --brute-level 4 --brute-lines 4
|
||||||
6 => --zc 12 -f 0-9 (10 trials)
|
6 => --zc 12 -f 0-9 --brute-level 5 --brute-lines 8
|
||||||
max => (stable alias for the max level)
|
max => (stable alias for the maximum level)
|
||||||
|
|
||||||
Manually specifying a compression option (zc, f, etc.) will override the optimization
|
Manually specifying a compression option (zc, f, etc.) will override the optimization
|
||||||
preset, regardless of the order you write the arguments.
|
preset, regardless of the order you write the arguments.
|
||||||
|
|
@ -45,7 +45,7 @@ Options:
|
||||||
-p, --preserve
|
-p, --preserve
|
||||||
Preserve file permissions and timestamps if possible
|
Preserve file permissions and timestamps if possible
|
||||||
|
|
||||||
-P, --pretend
|
-d, --dry-run
|
||||||
Do not write any files, only show compression results
|
Do not write any files, only show compression results
|
||||||
|
|
||||||
-s
|
-s
|
||||||
|
|
@ -61,10 +61,14 @@ Options:
|
||||||
|
|
||||||
CAUTION: 'all' will convert APNGs to standard PNGs.
|
CAUTION: 'all' will convert APNGs to standard PNGs.
|
||||||
|
|
||||||
Note that 'bKGD', 'sBIT' and 'hIST' will be forcibly stripped if the color type or bit
|
Please note that regardless of any options set, some chunks will necessarily be stripped
|
||||||
depth is changed, regardless of any options set.
|
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 metadata.
|
The default when --strip is not passed is to keep all chunks that remain valid.
|
||||||
|
|
||||||
--keep <list>
|
--keep <list>
|
||||||
Strip all metadata chunks except those in the comma-separated list. The special value
|
Strip all metadata chunks except those in the comma-separated list. The special value
|
||||||
|
|
@ -79,17 +83,17 @@ Options:
|
||||||
but take care as while this is “visually lossless”, it is technically a lossy
|
but take care as while this is “visually lossless”, it is technically a lossy
|
||||||
transformation and may be unsuitable for some applications.
|
transformation and may be unsuitable for some applications.
|
||||||
|
|
||||||
-i, --interlace <type>
|
-i, --interlace <mode>
|
||||||
Set the PNG interlacing type, where <type> is one of:
|
Set the PNG interlacing mode, where <mode> is one of:
|
||||||
|
|
||||||
0 => Remove interlacing from all images that are processed
|
off => Remove interlacing from all images that are processed
|
||||||
1 => Apply Adam7 interlacing on all images that are processed
|
on => Apply Adam7 interlacing on all images that are processed
|
||||||
keep => Keep the existing interlacing type of each image
|
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
|
Note that interlacing can add 25-50% to the size of an optimized image. Only use it if you
|
||||||
believe the benefits outweigh the costs for your use case.
|
believe the benefits outweigh the costs for your use case.
|
||||||
|
|
||||||
[default: 0]
|
[default: off]
|
||||||
|
|
||||||
--scale16
|
--scale16
|
||||||
Forcibly reduce images with 16 bits per channel to 8 bits per channel. This is a lossy
|
Forcibly reduce images with 16 bits per channel to 8 bits per channel. This is a lossy
|
||||||
|
|
@ -101,10 +105,13 @@ Options:
|
||||||
losslessly.
|
losslessly.
|
||||||
|
|
||||||
-v, --verbose...
|
-v, --verbose...
|
||||||
Run in verbose mode (use twice to increase verbosity)
|
Show per-file info (use multiple times for more detail)
|
||||||
|
|
||||||
-q, --quiet
|
-q, --quiet
|
||||||
Run in quiet mode
|
Suppress all output messages
|
||||||
|
|
||||||
|
-j, --json
|
||||||
|
Print results as JSON
|
||||||
|
|
||||||
-f, --filters <list>
|
-f, --filters <list>
|
||||||
Perform compression trials with each of the given filter types. You can specify a
|
Perform compression trials with each of the given filter types. You can specify a
|
||||||
|
|
@ -119,9 +126,9 @@ Options:
|
||||||
|
|
||||||
Heuristic strategies (try to find the best delta filter for each line)
|
Heuristic strategies (try to find the best delta filter for each line)
|
||||||
5 => MinSum Minimum sum of absolute differences
|
5 => MinSum Minimum sum of absolute differences
|
||||||
6 => Entropy Highest Shannon entropy
|
6 => Entropy Smallest Shannon entropy
|
||||||
7 => Bigrams Lowest count of distinct bigrams
|
7 => Bigrams Lowest count of distinct bigrams
|
||||||
8 => BigEnt Highest Shannon entropy of bigrams
|
8 => BigEnt Smallest Shannon entropy of bigrams
|
||||||
9 => Brute Smallest compressed size (slow)
|
9 => Brute Smallest compressed size (slow)
|
||||||
|
|
||||||
The default value depends on the optimization level preset.
|
The default value depends on the optimization level preset.
|
||||||
|
|
@ -132,7 +139,7 @@ Options:
|
||||||
CPU cores.
|
CPU cores.
|
||||||
|
|
||||||
--zc <level>
|
--zc <level>
|
||||||
Deflate compression level (1-12) for main compression trials. The levels here are defined
|
Deflate compression level (0-12) for main compression trials. The levels here are defined
|
||||||
by the libdeflate compression library.
|
by the libdeflate compression library.
|
||||||
|
|
||||||
The default value depends on the optimization level preset.
|
The default value depends on the optimization level preset.
|
||||||
|
|
@ -159,15 +166,33 @@ Options:
|
||||||
|
|
||||||
--fix
|
--fix
|
||||||
Do not perform checksum validation of PNG chunks. This may allow some files with errors to
|
Do not perform checksum validation of PNG chunks. This may allow some files with errors to
|
||||||
be processed successfully.
|
be processed successfully. The output will always have correct checksums.
|
||||||
|
|
||||||
--force
|
--force
|
||||||
Write the output even if it is larger than the input
|
Write the output even if it is larger than the input
|
||||||
|
|
||||||
-Z, --zopfli
|
-z, --zopfli
|
||||||
Use the much slower but stronger Zopfli compressor for main compression trials.
|
Use the much slower but stronger Zopfli compressor for main compression trials.
|
||||||
Recommended use is with '-o max' and '--fast'.
|
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>
|
--timeout <secs>
|
||||||
Maximum amount of time, in seconds, to spend on optimizations. Oxipng will check the
|
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
|
timeout before each transformation or compression trial, and will stop trying to optimize
|
||||||
|
|
@ -175,8 +200,25 @@ Options:
|
||||||
are already in progress, so it is currently of limited effectiveness for large files with
|
are already in progress, so it is currently of limited effectiveness for large files with
|
||||||
high compression levels.
|
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>
|
-t, --threads <num>
|
||||||
Set number of threads to use [default: num CPU cores]
|
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
|
-h, --help
|
||||||
Print help (see a summary with '-h')
|
Print help (see a summary with '-h')
|
||||||
|
|
|
||||||
204
README.md
|
|
@ -1,41 +1,46 @@
|
||||||
# Oxipng
|
# Oxipng
|
||||||
|
|
||||||
[](https://github.com/shssoichiro/oxipng/actions?query=branch%3Amaster)
|
[](https://github.com/oxipng/oxipng/actions?query=branch%3Amaster)
|
||||||
[](https://crates.io/crates/oxipng)
|
[](https://crates.io/crates/oxipng)
|
||||||
[](https://github.com/shssoichiro/oxipng/blob/master/LICENSE)
|
[](https://github.com/oxipng/oxipng/blob/master/LICENSE)
|
||||||
[](https://docs.rs/oxipng)
|
[](https://docs.rs/oxipng)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Oxipng is a multithreaded lossless PNG/APNG compression optimizer. It can be used via a command-line
|
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.
|
interface or as a library in other Rust programs. It is fast and highly effective.
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
|
|
||||||
Oxipng for Windows can be downloaded from the [Releases](https://github.com/shssoichiro/oxipng/releases) link on the GitHub page.
|
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
|
For MacOS or Linux, it is recommended to install from your distro's package repository, provided
|
||||||
repository, if possible. Oxipng is known to be packaged for the environments listed below.
|
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.
|
||||||
|
|
||||||
[](https://repology.org/project/oxipng/versions)
|
[](https://repology.org/project/oxipng/versions)
|
||||||
|
|
||||||
Alternatively, oxipng can be installed from Cargo, via the following command:
|
Alternatively, Oxipng can be installed from Cargo, via the following command:
|
||||||
|
|
||||||
```
|
```
|
||||||
cargo install oxipng
|
cargo install oxipng
|
||||||
```
|
```
|
||||||
|
|
||||||
Oxipng can be built from source using the latest stable or nightly Rust.
|
Oxipng can also be built from source using the latest stable or nightly Rust.
|
||||||
This is primarily useful for developing on oxipng.
|
This is primarily useful for developing on Oxipng.
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/shssoichiro/oxipng.git
|
git clone https://github.com/oxipng/oxipng.git
|
||||||
cd oxipng
|
cd oxipng
|
||||||
cargo build --release
|
cargo build --release
|
||||||
cp target/release/oxipng /usr/local/bin
|
cp target/release/oxipng /usr/local/bin
|
||||||
```
|
```
|
||||||
|
|
||||||
The current minimum supported Rust version is **1.74.0**.
|
The current minimum supported Rust version is **1.85.1**.
|
||||||
|
|
||||||
Oxipng follows Semantic Versioning.
|
Oxipng follows Semantic Versioning.
|
||||||
|
|
||||||
|
|
@ -51,7 +56,7 @@ The most commonly used options are as follows:
|
||||||
|
|
||||||
- Optimization: `-o 0` through `-o 6` (or `-o max`), lower is faster, higher is better compression.
|
- 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
|
The default (`-o 2`) is quite fast and provides good compression. Higher levels can be notably
|
||||||
better but generally have increasingly diminishing returns.
|
better* but generally have increasingly diminishing returns.
|
||||||
- Strip: Used to remove metadata info from processed images. Used via `--strip [safe,all]`.
|
- 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
|
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
|
will never affect rendering of the image. "All" removes all metadata that is not critical
|
||||||
|
|
@ -66,7 +71,19 @@ More advanced options can be found by running `oxipng --help`, or viewed [here](
|
||||||
Some options have both short (`-a`) and long (`--alpha`) forms. Which form you use is just a
|
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.:
|
matter of preference. Multiple short options can be combined together, e.g.:
|
||||||
`-savvo6` is equivalent to to `--strip safe --alpha --verbose --verbose --opt 6`.
|
`-savvo6` is equivalent to to `--strip safe --alpha --verbose --verbose --opt 6`.
|
||||||
Note that all options are case-sensitive.
|
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]
|
## Git integration via [pre-commit]
|
||||||
|
|
||||||
|
|
@ -75,83 +92,59 @@ preamble to an already existing one:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/shssoichiro/oxipng
|
- repo: https://github.com/oxipng/oxipng
|
||||||
rev: v9.0.0
|
rev: v10.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: oxipng
|
- id: oxipng
|
||||||
args: ["-o", "4", "--strip", "safe", "--alpha"]
|
args: ["-o", "4", "--strip", "safe", "--alpha"]
|
||||||
```
|
```
|
||||||
[pre-commit]: https://pre-commit.com/
|
[pre-commit]: https://pre-commit.com/
|
||||||
|
|
||||||
## Git integration via [Trunk]
|
## Docker
|
||||||
|
|
||||||
[Trunk] is an extendable superlinter which can be used to run `oxipng` to automatically optimize `png`s when committing them into a git repo, or to gate any `png`s being added to a git repo on whether they are optimized. The [trunk] oxipng integration is [here](https://github.com/trunk-io/plugins/tree/main/linters/oxipng).
|
A Docker image is availlable at [`ghcr.io/oxipng/oxipng`](https://github.com/oxipng/oxipng/pkgs/container/oxipng) for `linux/amd64` and `linux/arm64`.
|
||||||
|
|
||||||
To enable oxipng via [trunk]:
|
You can use it the following way:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# to get the latest version:
|
docker run --rm -v $(pwd):/work ghcr.io/oxipng/oxipng -o 4 /work/file.png
|
||||||
trunk check enable oxipng
|
|
||||||
|
|
||||||
# to get a specific version:
|
|
||||||
trunk check enable oxipng@9.0.0
|
|
||||||
```
|
```
|
||||||
|
|
||||||
or modify `.trunk/trunk.yaml` in your repo to contain:
|
Some older images are also available at [`ghcr.io/shssoichiro/oxipng`](https://github.com/users/shssoichiro/packages/container/package/oxipng).
|
||||||
|
|
||||||
```
|
|
||||||
lint:
|
|
||||||
enabled:
|
|
||||||
- oxipng@9.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
Then just run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# to optimize a png:
|
|
||||||
trunk fmt <file>
|
|
||||||
|
|
||||||
# to check if a png is already optimized:
|
|
||||||
trunk check <file>
|
|
||||||
```
|
|
||||||
|
|
||||||
You can setup [trunk] to [manage your git hooks](https://docs.trunk.io/docs/actions-git-hooks) and automatically optimize any `png`s you commit to git, _when_ you `git commit`. To enable this, run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
trunk actions enable trunk-fmt-pre-commit
|
|
||||||
```
|
|
||||||
|
|
||||||
[trunk]: https://docs.trunk.io
|
|
||||||
|
|
||||||
## Library Usage
|
## Library Usage
|
||||||
|
|
||||||
Although originally intended to be used as an executable, oxipng can also be used as a library in
|
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,
|
other Rust projects. To do so, simply add Oxipng as a dependency in your Cargo.toml. You should then
|
||||||
then `extern crate oxipng` in your project. You should then have access to all of the library
|
have access to all of the library functions [documented here](https://docs.rs/oxipng). The simplest
|
||||||
functions [documented here](https://docs.rs/oxipng). The simplest
|
method of usage involves creating an [Options
|
||||||
method of usage involves creating an
|
struct](https://docs.rs/oxipng/latest/oxipng/struct.Options.html) and passing it, along with an
|
||||||
[Options struct](https://docs.rs/oxipng/latest/oxipng/struct.Options.html) and
|
input filename, into the [optimize function](https://docs.rs/oxipng/latest/oxipng/fn.optimize.html).
|
||||||
passing it, along with an input filename, into the
|
|
||||||
[optimize function](https://docs.rs/oxipng/latest/oxipng/fn.optimize.html).
|
|
||||||
|
|
||||||
It is recommended to disable the "binary" feature when including oxipng as a library. Currently, there is
|
It is recommended to disable the "binary" feature when including Oxipng as a library. Currently, there is
|
||||||
no simple way to just disable one feature in Cargo, it has to be done by disabling default features
|
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:
|
and specifying the desired ones, for example:
|
||||||
`oxipng = { version = "9.0", features = ["parallel", "zopfli", "filetime"], default-features = false }`
|
`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
|
||||||
|
|
||||||
## History
|
## History
|
||||||
|
|
||||||
Oxipng began as a complete rewrite of the OptiPNG project,
|
Oxipng began in 2015 as a rewrite of the OptiPNG project. The core goal was to implement
|
||||||
which was assumed to be dead as no commit had been made to it since March 2014.
|
multithreading, which would have been very difficult to do within the existing C codebase of OptiPNG.
|
||||||
(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).
|
This also served as an opportunity to choose a more modern, safer language (Rust).
|
||||||
|
|
||||||
Note that, while similar, Oxipng is not a drop-in replacement for OptiPNG.
|
However, Oxipng has evolved considerably since then. While some of the options remain similar to
|
||||||
If you are migrating from OptiPNG, please check the [help](MANUAL.txt) before using.
|
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.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
@ -161,78 +154,11 @@ 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.
|
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.
|
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
|
## License
|
||||||
|
|
||||||
Oxipng is open-source software, distributed under the MIT license.
|
Oxipng is open-source software, distributed under the MIT license.
|
||||||
|
|
||||||
## Benchmarks
|
|
||||||
|
|
||||||
Tested OxiPNG 9.0.0 (commit `c16519b38b0519988db625913be919d4f0e42f5d`, compiled
|
|
||||||
on `rustc 1.74.0-nightly (7b4d9e155 2023-09-28)`) against OptiPNG version 0.7.7,
|
|
||||||
as packaged by Debian unstable, on a Linux 6.5.0-2-amd64 kernel, Intel Core
|
|
||||||
i7-12700 CPU (8 performance cores, 4 efficiency cores, 20 threads), DDR5-5200
|
|
||||||
RAM in dual channel configuration.
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Benchmark 1: ./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png
|
|
||||||
Time (mean ± σ): 59.6 ms ± 7.7 ms [User: 77.4 ms, System: 3.6 ms]
|
|
||||||
Range (min … max): 53.3 ms … 89.9 ms 32 runs
|
|
||||||
|
|
||||||
Benchmark 2: optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
|
|
||||||
Time (mean ± σ): 132.4 ms ± 0.8 ms [User: 132.5 ms, System: 0.6 ms]
|
|
||||||
Range (min … max): 131.8 ms … 134.4 ms 22 runs
|
|
||||||
|
|
||||||
Summary
|
|
||||||
./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png ran
|
|
||||||
2.22 ± 0.29 times faster than optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
|
|
||||||
|
|
||||||
Benchmark 1: ./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png
|
|
||||||
Time (mean ± σ): 88.7 ms ± 4.3 ms [User: 270.3 ms, System: 11.0 ms]
|
|
||||||
Range (min … max): 86.8 ms … 109.4 ms 26 runs
|
|
||||||
|
|
||||||
Benchmark 2: optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
|
|
||||||
Time (mean ± σ): 444.9 ms ± 0.3 ms [User: 444.8 ms, System: 0.7 ms]
|
|
||||||
Range (min … max): 444.4 ms … 445.6 ms 10 runs
|
|
||||||
|
|
||||||
Summary
|
|
||||||
./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png ran
|
|
||||||
5.01 ± 0.25 times faster than optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Older benchmark</summary>
|
|
||||||
|
|
||||||
Tested oxipng 5.0.0 (compiled on rustc 1.55.0-nightly (7a16cfcff 2021-07-11)) against OptiPNG version 0.7.7 on AMD Ryzen 7 4800H with Radeon Graphics with 16 logical cores
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Benchmark #1: ./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png
|
|
||||||
Time (mean ± σ): 128.8 ms ± 14.2 ms [User: 296.0 ms, System: 14.3 ms]
|
|
||||||
Range (min … max): 98.8 ms … 152.3 ms 21 runs
|
|
||||||
|
|
||||||
Benchmark #2: optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
|
|
||||||
Time (mean ± σ): 254.2 ms ± 16.0 ms [User: 252.8 ms, System: 1.2 ms]
|
|
||||||
Range (min … max): 208.4 ms … 263.8 ms 14 runs
|
|
||||||
|
|
||||||
Summary
|
|
||||||
'./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png' ran
|
|
||||||
1.97 ± 0.25 times faster than 'optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Benchmark #1: ./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png
|
|
||||||
Time (mean ± σ): 141.4 ms ± 14.9 ms [User: 611.7 ms, System: 21.1 ms]
|
|
||||||
Range (min … max): 100.2 ms … 160.4 ms 23 runs
|
|
||||||
|
|
||||||
Benchmark #2: optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
|
|
||||||
Time (mean ± σ): 730.0 ms ± 25.9 ms [User: 728.0 ms, System: 1.2 ms]
|
|
||||||
Range (min … max): 713.3 ms … 768.2 ms 10 runs
|
|
||||||
|
|
||||||
Summary
|
|
||||||
'./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png' ran
|
|
||||||
5.16 ± 0.58 times faster than 'optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png'
|
|
||||||
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
|
||||||
13
SECURITY.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# 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.
|
||||||
|
|
@ -13,10 +13,7 @@ fn deflate_16_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
|
||||||
let min = AtomicMin::new(None);
|
|
||||||
deflate(png.raw.data.as_ref(), 12, &min)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -24,10 +21,7 @@ fn deflate_8_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
|
||||||
let min = AtomicMin::new(None);
|
|
||||||
deflate(png.raw.data.as_ref(), 12, &min)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -37,10 +31,7 @@ fn deflate_4_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
|
||||||
let min = AtomicMin::new(None);
|
|
||||||
deflate(png.raw.data.as_ref(), 12, &min)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -50,10 +41,7 @@ fn deflate_2_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
|
||||||
let min = AtomicMin::new(None);
|
|
||||||
deflate(png.raw.data.as_ref(), 12, &min)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -63,10 +51,7 @@ fn deflate_1_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
|
||||||
let min = AtomicMin::new(None);
|
|
||||||
deflate(png.raw.data.as_ref(), 12, &min)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,7 @@ fn filters_16_bits_filter_0(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
|
||||||
png.raw.filter_image(RowFilter::None, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -23,9 +21,7 @@ fn filters_8_bits_filter_0(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
|
||||||
png.raw.filter_image(RowFilter::None, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -35,9 +31,7 @@ fn filters_4_bits_filter_0(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
|
||||||
png.raw.filter_image(RowFilter::None, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -47,9 +41,7 @@ fn filters_2_bits_filter_0(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
|
||||||
png.raw.filter_image(RowFilter::None, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -59,9 +51,7 @@ fn filters_1_bits_filter_0(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
|
||||||
png.raw.filter_image(RowFilter::None, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -69,9 +59,7 @@ fn filters_16_bits_filter_1(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
|
||||||
png.raw.filter_image(RowFilter::Sub, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -79,9 +67,7 @@ fn filters_8_bits_filter_1(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
|
||||||
png.raw.filter_image(RowFilter::Sub, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -91,9 +77,7 @@ fn filters_4_bits_filter_1(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
|
||||||
png.raw.filter_image(RowFilter::Sub, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -103,9 +87,7 @@ fn filters_2_bits_filter_1(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
|
||||||
png.raw.filter_image(RowFilter::Sub, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -115,9 +97,7 @@ fn filters_1_bits_filter_1(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
|
||||||
png.raw.filter_image(RowFilter::Sub, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -125,9 +105,7 @@ fn filters_16_bits_filter_2(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
|
||||||
png.raw.filter_image(RowFilter::Up, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -135,9 +113,7 @@ fn filters_8_bits_filter_2(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
|
||||||
png.raw.filter_image(RowFilter::Up, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -147,9 +123,7 @@ fn filters_4_bits_filter_2(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
|
||||||
png.raw.filter_image(RowFilter::Up, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -159,9 +133,7 @@ fn filters_2_bits_filter_2(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
|
||||||
png.raw.filter_image(RowFilter::Up, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -171,9 +143,7 @@ fn filters_1_bits_filter_2(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
|
||||||
png.raw.filter_image(RowFilter::Up, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -181,9 +151,7 @@ fn filters_16_bits_filter_3(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
|
||||||
png.raw.filter_image(RowFilter::Average, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -191,9 +159,7 @@ fn filters_8_bits_filter_3(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
|
||||||
png.raw.filter_image(RowFilter::Average, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -203,9 +169,7 @@ fn filters_4_bits_filter_3(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
|
||||||
png.raw.filter_image(RowFilter::Average, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -215,9 +179,7 @@ fn filters_2_bits_filter_3(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
|
||||||
png.raw.filter_image(RowFilter::Average, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -227,9 +189,7 @@ fn filters_1_bits_filter_3(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
|
||||||
png.raw.filter_image(RowFilter::Average, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -237,9 +197,7 @@ fn filters_16_bits_filter_4(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
|
||||||
png.raw.filter_image(RowFilter::Paeth, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -247,9 +205,7 @@ fn filters_8_bits_filter_4(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
|
||||||
png.raw.filter_image(RowFilter::Paeth, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -259,9 +215,7 @@ fn filters_4_bits_filter_4(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
|
||||||
png.raw.filter_image(RowFilter::Paeth, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -271,9 +225,7 @@ fn filters_2_bits_filter_4(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
|
||||||
png.raw.filter_image(RowFilter::Paeth, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -283,9 +235,7 @@ fn filters_1_bits_filter_4(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
|
||||||
png.raw.filter_image(RowFilter::Paeth, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -293,9 +243,7 @@ fn filters_16_bits_filter_5(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
|
||||||
png.raw.filter_image(RowFilter::MinSum, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -303,9 +251,7 @@ fn filters_8_bits_filter_5(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
|
||||||
png.raw.filter_image(RowFilter::MinSum, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -315,9 +261,7 @@ fn filters_4_bits_filter_5(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
|
||||||
png.raw.filter_image(RowFilter::MinSum, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -327,9 +271,7 @@ fn filters_2_bits_filter_5(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
|
||||||
png.raw.filter_image(RowFilter::MinSum, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -339,7 +281,5 @@ fn filters_1_bits_filter_5(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
|
||||||
png.raw.filter_image(RowFilter::MinSum, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ fn interlacing_16_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
|
b.iter(|| png.raw.change_interlacing(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -21,7 +21,7 @@ fn interlacing_8_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
|
b.iter(|| png.raw.change_interlacing(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -31,7 +31,7 @@ fn interlacing_4_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
|
b.iter(|| png.raw.change_interlacing(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -41,7 +41,7 @@ fn interlacing_2_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
|
b.iter(|| png.raw.change_interlacing(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -51,7 +51,7 @@ fn interlacing_1_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
|
b.iter(|| png.raw.change_interlacing(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -61,7 +61,7 @@ fn deinterlacing_16_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::None));
|
b.iter(|| png.raw.change_interlacing(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -71,7 +71,7 @@ fn deinterlacing_8_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::None));
|
b.iter(|| png.raw.change_interlacing(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -81,7 +81,7 @@ fn deinterlacing_4_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::None));
|
b.iter(|| png.raw.change_interlacing(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -91,7 +91,7 @@ fn deinterlacing_2_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::None));
|
b.iter(|| png.raw.change_interlacing(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -101,5 +101,5 @@ fn deinterlacing_1_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::None));
|
b.iter(|| png.raw.change_interlacing(false));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,36 +54,6 @@ fn reductions_8_to_1_bits(b: &mut Bencher) {
|
||||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn reductions_4_to_2_bits(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from(
|
|
||||||
"tests/files/palette_4_should_be_palette_2.png",
|
|
||||||
));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn reductions_4_to_1_bits(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from(
|
|
||||||
"tests/files/palette_4_should_be_palette_1.png",
|
|
||||||
));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn reductions_2_to_1_bits(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from(
|
|
||||||
"tests/files/palette_2_should_be_palette_1.png",
|
|
||||||
));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn reductions_grayscale_8_to_4_bits(b: &mut Bencher) {
|
fn reductions_grayscale_8_to_4_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
|
|
@ -114,42 +84,12 @@ fn reductions_grayscale_8_to_1_bits(b: &mut Bencher) {
|
||||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn reductions_grayscale_4_to_2_bits(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from(
|
|
||||||
"tests/files/grayscale_4_should_be_grayscale_2.png",
|
|
||||||
));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn reductions_grayscale_4_to_1_bits(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from(
|
|
||||||
"tests/files/grayscale_4_should_be_grayscale_1.png",
|
|
||||||
));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn reductions_grayscale_2_to_1_bits(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from(
|
|
||||||
"tests/files/grayscale_2_should_be_grayscale_1.png",
|
|
||||||
));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn reductions_rgba_to_rgb_16(b: &mut Bencher) {
|
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 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| alpha::reduced_alpha_channel(&png.raw, false));
|
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -157,7 +97,15 @@ 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 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| alpha::reduced_alpha_channel(&png.raw, false));
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -180,32 +128,6 @@ fn reductions_rgba_to_grayscale_alpha_8(b: &mut Bencher) {
|
||||||
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
|
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn reductions_rgba_to_grayscale_16(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from(
|
|
||||||
"tests/files/rgba_16_should_be_grayscale_16.png",
|
|
||||||
));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| {
|
|
||||||
color::reduced_rgb_to_grayscale(&png.raw)
|
|
||||||
.and_then(|r| alpha::reduced_alpha_channel(&r, false))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn reductions_rgba_to_grayscale_8(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from(
|
|
||||||
"tests/files/rgba_8_should_be_grayscale_8.png",
|
|
||||||
));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| {
|
|
||||||
color::reduced_rgb_to_grayscale(&png.raw)
|
|
||||||
.and_then(|r| alpha::reduced_alpha_channel(&r, false))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn reductions_rgb_to_grayscale_16(b: &mut Bencher) {
|
fn reductions_rgb_to_grayscale_16(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
|
|
@ -240,6 +162,36 @@ fn reductions_rgb_to_palette_8(b: &mut Bencher) {
|
||||||
b.iter(|| color::reduced_to_indexed(&png.raw, true));
|
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]
|
#[bench]
|
||||||
fn reductions_grayscale_8_to_palette_8(b: &mut Bencher) {
|
fn reductions_grayscale_8_to_palette_8(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
|
|
@ -257,7 +209,7 @@ fn reductions_palette_8_to_grayscale_8(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| color::indexed_to_channels(&png.raw, true));
|
b.iter(|| color::indexed_to_channels(&png.raw, true, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,7 @@ fn filters_minsum(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
|
||||||
png.raw.filter_image(RowFilter::MinSum, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -23,9 +21,7 @@ fn filters_entropy(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::Entropy, false));
|
||||||
png.raw.filter_image(RowFilter::Entropy, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -33,9 +29,7 @@ fn filters_bigrams(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::Bigrams, false));
|
||||||
png.raw.filter_image(RowFilter::Bigrams, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -43,9 +37,7 @@ fn filters_bigent(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
let 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| png.raw.filter_image(FilterStrategy::BigEnt, false));
|
||||||
png.raw.filter_image(RowFilter::BigEnt, false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -54,6 +46,12 @@ fn filters_brute(b: &mut Bencher) {
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
png.raw.filter_image(RowFilter::Brute, false);
|
png.raw.filter_image(
|
||||||
|
FilterStrategy::Brute {
|
||||||
|
num_lines: 4,
|
||||||
|
level: 1,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,18 @@
|
||||||
extern crate oxipng;
|
extern crate oxipng;
|
||||||
extern crate test;
|
extern crate test;
|
||||||
|
|
||||||
use std::{num::NonZeroU8, path::PathBuf};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use oxipng::{internal_tests::*, *};
|
use oxipng::{internal_tests::*, *};
|
||||||
use test::Bencher;
|
use test::Bencher;
|
||||||
|
|
||||||
// SAFETY: trivially safe. Stopgap solution until const unwrap is stabilized.
|
|
||||||
const DEFAULT_ZOPFLI_ITERATIONS: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(15) };
|
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn zopfli_16_bits_strategy_0(b: &mut Bencher) {
|
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 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, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
|
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,7 +24,7 @@ fn zopfli_8_bits_strategy_0(b: &mut Bencher) {
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
|
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,7 +36,7 @@ fn zopfli_4_bits_strategy_0(b: &mut Bencher) {
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
|
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,7 +48,7 @@ fn zopfli_2_bits_strategy_0(b: &mut Bencher) {
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
|
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,6 +60,6 @@ fn zopfli_1_bits_strategy_0(b: &mut Bencher) {
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
|
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
40
build.rs
|
|
@ -1,40 +0,0 @@
|
||||||
use std::{
|
|
||||||
env,
|
|
||||||
fs::File,
|
|
||||||
io::{BufWriter, Error},
|
|
||||||
path::Path,
|
|
||||||
};
|
|
||||||
|
|
||||||
use clap_mangen::Man;
|
|
||||||
|
|
||||||
include!("src/cli.rs");
|
|
||||||
|
|
||||||
fn build_manpages(outdir: &Path) -> Result<(), Error> {
|
|
||||||
let app = build_command();
|
|
||||||
|
|
||||||
let file = Path::new(&outdir).join("oxipng.1");
|
|
||||||
let mut file = BufWriter::new(File::create(file)?);
|
|
||||||
|
|
||||||
Man::new(app).render(&mut file)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), Error> {
|
|
||||||
println!("cargo:rerun-if-changed=src/cli.rs");
|
|
||||||
println!("cargo:rerun-if-changed=src/display_chunks.rs");
|
|
||||||
|
|
||||||
// Create `target/<debug|release>/assets/` folder.
|
|
||||||
let outdir = match env::var_os("OUT_DIR") {
|
|
||||||
None => return Ok(()),
|
|
||||||
Some(outdir) => outdir,
|
|
||||||
};
|
|
||||||
let out_path = PathBuf::from(outdir);
|
|
||||||
let mut path = out_path.ancestors().nth(3).unwrap().to_owned();
|
|
||||||
path.push("assets");
|
|
||||||
std::fs::create_dir_all(&path).unwrap();
|
|
||||||
|
|
||||||
build_manpages(&path)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#!/bin/bash
|
#!/bin/sh -eu
|
||||||
cargo build
|
cargo build
|
||||||
|
cargo xtask mangen
|
||||||
|
|
||||||
./target/debug/oxipng -V > MANUAL.txt
|
./target/debug/oxipng -V > MANUAL.txt
|
||||||
#Redirect all streams to prevent detection of the terminal width and force an internal default of 100
|
#Redirect all streams to prevent detection of the terminal width and force an internal default of 100
|
||||||
|
|
|
||||||
72
src/apng.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ pub struct AtomicMin {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AtomicMin {
|
impl AtomicMin {
|
||||||
|
#[must_use]
|
||||||
pub fn new(init: Option<usize>) -> Self {
|
pub fn new(init: Option<usize>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
val: AtomicUsize::new(init.unwrap_or(usize::MAX)),
|
val: AtomicUsize::new(init.unwrap_or(usize::MAX)),
|
||||||
|
|
@ -14,16 +15,7 @@ impl AtomicMin {
|
||||||
|
|
||||||
pub fn get(&self) -> Option<usize> {
|
pub fn get(&self) -> Option<usize> {
|
||||||
let val = self.val.load(SeqCst);
|
let val = self.val.load(SeqCst);
|
||||||
if val == usize::MAX {
|
if val == usize::MAX { None } else { Some(val) }
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unset value is usize_max
|
|
||||||
pub fn as_atomic_usize(&self) -> &AtomicUsize {
|
|
||||||
&self.val
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try a new value, returning true if it is the new minimum
|
/// Try a new value, returning true if it is the new minimum
|
||||||
|
|
|
||||||
179
src/cli.rs
|
|
@ -1,9 +1,18 @@
|
||||||
use std::path::PathBuf;
|
use std::{num::NonZeroU64, path::PathBuf};
|
||||||
|
|
||||||
use clap::{value_parser, Arg, ArgAction, Command};
|
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");
|
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 {
|
pub fn build_command() -> Command {
|
||||||
// Note: clap 'wrap_help' is enabled to automatically wrap lines according to terminal width.
|
// 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,
|
// To keep things tidy though, short help descriptions should be no more than 54 characters,
|
||||||
|
|
@ -14,6 +23,7 @@ pub fn build_command() -> Command {
|
||||||
.version(env!("CARGO_PKG_VERSION"))
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
.author("Joshua Holmer <jholmer.in@gmail.com>")
|
.author("Joshua Holmer <jholmer.in@gmail.com>")
|
||||||
.about("Losslessly improve compression of PNG files")
|
.about("Losslessly improve compression of PNG files")
|
||||||
|
.styles(STYLES)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("files")
|
Arg::new("files")
|
||||||
.help("File(s) to compress (use '-' for stdin)")
|
.help("File(s) to compress (use '-' for stdin)")
|
||||||
|
|
@ -31,14 +41,14 @@ Set the optimization level preset. The default level 2 is quite fast and provide
|
||||||
compression. Lower levels are faster, higher levels provide better compression, though \
|
compression. Lower levels are faster, higher levels provide better compression, though \
|
||||||
with increasingly diminishing returns.
|
with increasingly diminishing returns.
|
||||||
|
|
||||||
0 => --zc 5 --fast (1 trial, determined heuristically)
|
0 => --zc 5 --fast (filter chosen heuristically)
|
||||||
1 => --zc 10 --fast (1 trial, determined heuristically)
|
1 => --zc 10 --fast (filter chosen heuristically)
|
||||||
2 => --zc 11 -f 0,1,6,7 --fast (4 fast trials, 1 main trial)
|
2 => --zc 11 -f 0,1,6,7 --fast
|
||||||
3 => --zc 11 -f 0,7,8,9 (4 trials)
|
3 => --zc 11 -f 0,7,8,9 --brute-level 1 --brute-lines 3
|
||||||
4 => --zc 12 -f 0,7,8,9 (4 trials)
|
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 (8 trials)
|
5 => --zc 12 -f 0,1,2,5,6,7,8,9 --brute-level 4 --brute-lines 4
|
||||||
6 => --zc 12 -f 0-9 (10 trials)
|
6 => --zc 12 -f 0-9 --brute-level 5 --brute-lines 8
|
||||||
max => (stable alias for the max level)
|
max => (stable alias for the maximum level)
|
||||||
|
|
||||||
Manually specifying a compression option (zc, f, etc.) will override the optimization \
|
Manually specifying a compression option (zc, f, etc.) will override the optimization \
|
||||||
preset, regardless of the order you write the arguments.")
|
preset, regardless of the order you write the arguments.")
|
||||||
|
|
@ -49,14 +59,6 @@ preset, regardless of the order you write the arguments.")
|
||||||
.value_parser(["0", "1", "2", "3", "4", "5", "6", "max"])
|
.value_parser(["0", "1", "2", "3", "4", "5", "6", "max"])
|
||||||
.hide_possible_values(true),
|
.hide_possible_values(true),
|
||||||
)
|
)
|
||||||
.arg(
|
|
||||||
Arg::new("backup")
|
|
||||||
.help("Back up modified files")
|
|
||||||
.short('b')
|
|
||||||
.long("backup")
|
|
||||||
.hide(true)
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("recursive")
|
Arg::new("recursive")
|
||||||
.help("Recurse input directories, optimizing all PNG files")
|
.help("Recurse input directories, optimizing all PNG files")
|
||||||
|
|
@ -105,10 +107,10 @@ Note that this will not preserve the directory structure of the input files when
|
||||||
.action(ArgAction::SetTrue),
|
.action(ArgAction::SetTrue),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("pretend")
|
Arg::new("dry-run")
|
||||||
.help("Do not write any files, only show compression results")
|
.help("Do not write any files, only show compression results")
|
||||||
.short('P')
|
.short('d')
|
||||||
.long("pretend")
|
.long("dry-run")
|
||||||
.action(ArgAction::SetTrue),
|
.action(ArgAction::SetTrue),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
|
|
@ -131,10 +133,14 @@ Strip metadata chunks, where <mode> is one of:
|
||||||
|
|
||||||
CAUTION: 'all' will convert APNGs to standard PNGs.
|
CAUTION: 'all' will convert APNGs to standard PNGs.
|
||||||
|
|
||||||
Note that 'bKGD', 'sBIT' and 'hIST' will be forcibly stripped if the color type or bit \
|
Please note that regardless of any options set, some chunks will necessarily be stripped \
|
||||||
depth is changed, regardless of any options set.
|
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 metadata.",
|
The default when --strip is not passed is to keep all chunks that remain valid.",
|
||||||
DISPLAY_CHUNKS
|
DISPLAY_CHUNKS
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| String::from_utf8_lossy(c))
|
.map(|c| String::from_utf8_lossy(c))
|
||||||
|
|
@ -172,21 +178,22 @@ transformation and may be unsuitable for some applications.")
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("interlace")
|
Arg::new("interlace")
|
||||||
.help("Set PNG interlacing type (0, 1, keep)")
|
.help("Set PNG interlacing (off, on, keep)")
|
||||||
.long_help("\
|
.long_help("\
|
||||||
Set the PNG interlacing type, where <type> is one of:
|
Set the PNG interlacing mode, where <mode> is one of:
|
||||||
|
|
||||||
0 => Remove interlacing from all images that are processed
|
off => Remove interlacing from all images that are processed
|
||||||
1 => Apply Adam7 interlacing on all images that are processed
|
on => Apply Adam7 interlacing on all images that are processed
|
||||||
keep => Keep the existing interlacing type of each image
|
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 \
|
Note that interlacing can add 25-50% to the size of an optimized image. Only use it if you \
|
||||||
believe the benefits outweigh the costs for your use case.")
|
believe the benefits outweigh the costs for your use case.")
|
||||||
.short('i')
|
.short('i')
|
||||||
.long("interlace")
|
.long("interlace")
|
||||||
.value_name("type")
|
.value_name("mode")
|
||||||
.default_value("0")
|
.value_parser(["off", "on", "keep", "0", "1"])
|
||||||
.value_parser(["0", "1", "keep"])
|
.default_value("off")
|
||||||
|
.default_value_if("no-reductions", ArgPredicate::IsPresent, "keep")
|
||||||
.hide_possible_values(true),
|
.hide_possible_values(true),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
|
|
@ -205,7 +212,7 @@ losslessly.")
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("verbose")
|
Arg::new("verbose")
|
||||||
.help("Run in verbose mode (use twice to increase verbosity)")
|
.help("Show per-file info (use multiple times for more detail)")
|
||||||
.short('v')
|
.short('v')
|
||||||
.long("verbose")
|
.long("verbose")
|
||||||
.action(ArgAction::Count)
|
.action(ArgAction::Count)
|
||||||
|
|
@ -213,12 +220,20 @@ losslessly.")
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("quiet")
|
Arg::new("quiet")
|
||||||
.help("Run in quiet mode")
|
.help("Suppress all output messages")
|
||||||
.short('q')
|
.short('q')
|
||||||
.long("quiet")
|
.long("quiet")
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.conflicts_with("verbose"),
|
.conflicts_with("verbose"),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("json")
|
||||||
|
.help("Print results as JSON")
|
||||||
|
.short('j')
|
||||||
|
.long("json")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.conflicts_with("stdout"),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("filters")
|
Arg::new("filters")
|
||||||
.help("Filters to try (0-9; see '--help' for details)")
|
.help("Filters to try (0-9; see '--help' for details)")
|
||||||
|
|
@ -235,9 +250,9 @@ PNG delta filters (apply the same filter to every line)
|
||||||
|
|
||||||
Heuristic strategies (try to find the best delta filter for each line)
|
Heuristic strategies (try to find the best delta filter for each line)
|
||||||
5 => MinSum Minimum sum of absolute differences
|
5 => MinSum Minimum sum of absolute differences
|
||||||
6 => Entropy Highest Shannon entropy
|
6 => Entropy Smallest Shannon entropy
|
||||||
7 => Bigrams Lowest count of distinct bigrams
|
7 => Bigrams Lowest count of distinct bigrams
|
||||||
8 => BigEnt Highest Shannon entropy of bigrams
|
8 => BigEnt Smallest Shannon entropy of bigrams
|
||||||
9 => Brute Smallest compressed size (slow)
|
9 => Brute Smallest compressed size (slow)
|
||||||
|
|
||||||
The default value depends on the optimization level preset.")
|
The default value depends on the optimization level preset.")
|
||||||
|
|
@ -257,15 +272,15 @@ CPU cores.")
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("compression")
|
Arg::new("compression")
|
||||||
.help("Deflate compression level (1-12)")
|
.help("Deflate compression level (0-12)")
|
||||||
.long_help("\
|
.long_help("\
|
||||||
Deflate compression level (1-12) for main compression trials. The levels here are defined \
|
Deflate compression level (0-12) for main compression trials. The levels here are defined \
|
||||||
by the libdeflate compression library.
|
by the libdeflate compression library.
|
||||||
|
|
||||||
The default value depends on the optimization level preset.")
|
The default value depends on the optimization level preset.")
|
||||||
.long("zc")
|
.long("zc")
|
||||||
.value_name("level")
|
.value_name("level")
|
||||||
.value_parser(1..=12)
|
.value_parser(0..=12)
|
||||||
.conflicts_with("zopfli"),
|
.conflicts_with("zopfli"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
|
|
@ -283,6 +298,8 @@ The default value depends on the optimization level preset.")
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("no-palette-reduction")
|
Arg::new("no-palette-reduction")
|
||||||
.help("Do not change color palette")
|
.help("Do not change color palette")
|
||||||
|
.long_help("\
|
||||||
|
Do not convert to indexed and do not modify an existing color palette.")
|
||||||
.long("np")
|
.long("np")
|
||||||
.action(ArgAction::SetTrue),
|
.action(ArgAction::SetTrue),
|
||||||
)
|
)
|
||||||
|
|
@ -315,7 +332,7 @@ compressed chunks (such as iCCP) will also be disabled. Note that the combinatio
|
||||||
.help("Disable checksum validation")
|
.help("Disable checksum validation")
|
||||||
.long_help("\
|
.long_help("\
|
||||||
Do not perform checksum validation of PNG chunks. This may allow some files with errors to \
|
Do not perform checksum validation of PNG chunks. This may allow some files with errors to \
|
||||||
be processed successfully.")
|
be processed successfully. The output will always have correct checksums.")
|
||||||
.long("fix")
|
.long("fix")
|
||||||
.action(ArgAction::SetTrue),
|
.action(ArgAction::SetTrue),
|
||||||
)
|
)
|
||||||
|
|
@ -331,10 +348,54 @@ be processed successfully.")
|
||||||
.long_help("\
|
.long_help("\
|
||||||
Use the much slower but stronger Zopfli compressor for main compression trials. \
|
Use the much slower but stronger Zopfli compressor for main compression trials. \
|
||||||
Recommended use is with '-o max' and '--fast'.")
|
Recommended use is with '-o max' and '--fast'.")
|
||||||
.short('Z')
|
.short('z')
|
||||||
|
.short_alias('Z') // Kept for backwards compatibility
|
||||||
.long("zopfli")
|
.long("zopfli")
|
||||||
.action(ArgAction::SetTrue),
|
.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(
|
||||||
Arg::new("timeout")
|
Arg::new("timeout")
|
||||||
.help("Maximum amount of time to spend on optimizations")
|
.help("Maximum amount of time to spend on optimizations")
|
||||||
|
|
@ -344,16 +405,46 @@ timeout before each transformation or compression trial, and will stop trying to
|
||||||
the file if the timeout is exceeded. Note that this does not cut short any operations that \
|
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 \
|
are already in progress, so it is currently of limited effectiveness for large files with \
|
||||||
high compression levels.")
|
high compression levels.")
|
||||||
.value_name("secs")
|
|
||||||
.long("timeout")
|
.long("timeout")
|
||||||
|
.value_name("secs")
|
||||||
.value_parser(value_parser!(u64)),
|
.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(
|
||||||
Arg::new("threads")
|
Arg::new("threads")
|
||||||
.help("Set number of threads to use [default: num CPU cores]")
|
.help("Number of threads to use [default: num logical CPUs]")
|
||||||
.long("threads")
|
.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')
|
.short('t')
|
||||||
|
.long("threads")
|
||||||
.value_name("num")
|
.value_name("num")
|
||||||
.value_parser(value_parser!(usize)),
|
.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),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,11 @@ impl Display for ColorType {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
ColorType::Grayscale { .. } => Display::fmt("Grayscale", f),
|
Self::Grayscale { .. } => write!(f, "Grayscale"),
|
||||||
ColorType::RGB { .. } => Display::fmt("RGB", f),
|
Self::RGB { .. } => write!(f, "RGB"),
|
||||||
ColorType::Indexed { palette } => {
|
Self::Indexed { palette } => write!(f, "Indexed ({} colors)", palette.len()),
|
||||||
Display::fmt(&format!("Indexed ({} colors)", palette.len()), f)
|
Self::GrayscaleAlpha => write!(f, "Grayscale + Alpha"),
|
||||||
}
|
Self::RGBA => write!(f, "RGB + Alpha"),
|
||||||
ColorType::GrayscaleAlpha => Display::fmt("Grayscale + Alpha", f),
|
|
||||||
ColorType::RGBA => Display::fmt("RGB + Alpha", f),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -46,49 +44,47 @@ impl Display for ColorType {
|
||||||
impl ColorType {
|
impl ColorType {
|
||||||
/// Get the code used by the PNG specification to denote this color type
|
/// Get the code used by the PNG specification to denote this color type
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn png_header_code(&self) -> u8 {
|
#[must_use]
|
||||||
|
pub const fn png_header_code(&self) -> u8 {
|
||||||
match self {
|
match self {
|
||||||
ColorType::Grayscale { .. } => 0,
|
Self::Grayscale { .. } => 0,
|
||||||
ColorType::RGB { .. } => 2,
|
Self::RGB { .. } => 2,
|
||||||
ColorType::Indexed { .. } => 3,
|
Self::Indexed { .. } => 3,
|
||||||
ColorType::GrayscaleAlpha => 4,
|
Self::GrayscaleAlpha => 4,
|
||||||
ColorType::RGBA => 6,
|
Self::RGBA => 6,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn channels_per_pixel(&self) -> u8 {
|
pub(crate) const fn channels_per_pixel(&self) -> u8 {
|
||||||
match self {
|
match self {
|
||||||
ColorType::Grayscale { .. } | ColorType::Indexed { .. } => 1,
|
Self::Grayscale { .. } | Self::Indexed { .. } => 1,
|
||||||
ColorType::GrayscaleAlpha => 2,
|
Self::GrayscaleAlpha => 2,
|
||||||
ColorType::RGB { .. } => 3,
|
Self::RGB { .. } => 3,
|
||||||
ColorType::RGBA => 4,
|
Self::RGBA => 4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn is_rgb(&self) -> bool {
|
pub(crate) const fn is_rgb(&self) -> bool {
|
||||||
matches!(self, ColorType::RGB { .. } | ColorType::RGBA)
|
matches!(self, Self::RGB { .. } | Self::RGBA)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn is_gray(&self) -> bool {
|
pub(crate) const fn is_gray(&self) -> bool {
|
||||||
matches!(
|
matches!(self, Self::Grayscale { .. } | Self::GrayscaleAlpha)
|
||||||
self,
|
|
||||||
ColorType::Grayscale { .. } | ColorType::GrayscaleAlpha
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn has_alpha(&self) -> bool {
|
pub(crate) const fn has_alpha(&self) -> bool {
|
||||||
matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA)
|
matches!(self, Self::GrayscaleAlpha | Self::RGBA)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn has_trns(&self) -> bool {
|
pub(crate) const fn has_trns(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
ColorType::Grayscale { transparent_shade } => transparent_shade.is_some(),
|
Self::Grayscale { transparent_shade } => transparent_shade.is_some(),
|
||||||
ColorType::RGB { transparent_color } => transparent_color.is_some(),
|
Self::RGB { transparent_color } => transparent_color.is_some(),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -120,7 +116,7 @@ impl TryFrom<u8> for BitDepth {
|
||||||
4 => Ok(Self::Four),
|
4 => Ok(Self::Four),
|
||||||
8 => Ok(Self::Eight),
|
8 => Ok(Self::Eight),
|
||||||
16 => Ok(Self::Sixteen),
|
16 => Ok(Self::Sixteen),
|
||||||
_ => Err(PngError::new("Unexpected bit depth")),
|
_ => Err(PngError::InvalidData),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
use libdeflater::*;
|
use libdeflater::*;
|
||||||
|
|
||||||
use crate::{atomicmin::AtomicMin, PngError, PngResult};
|
use crate::{PngError, PngResult};
|
||||||
|
|
||||||
pub fn deflate(data: &[u8], level: u8, max_size: &AtomicMin) -> PngResult<Vec<u8>> {
|
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 mut compressor = Compressor::new(CompressionLvl::new(level.into()).unwrap());
|
||||||
let capacity = max_size
|
let capacity = max_size.unwrap_or_else(|| compressor.zlib_compress_bound(data.len()));
|
||||||
.get()
|
|
||||||
.unwrap_or_else(|| compressor.zlib_compress_bound(data.len()));
|
|
||||||
let mut dest = vec![0; capacity];
|
let mut dest = vec![0; capacity];
|
||||||
let len = compressor
|
let len = compressor
|
||||||
.zlib_compress(data, &mut dest)
|
.zlib_compress(data, &mut dest)
|
||||||
|
|
@ -24,12 +22,13 @@ pub fn inflate(data: &[u8], out_size: usize) -> PngResult<Vec<u8>> {
|
||||||
.zlib_decompress(data, &mut dest)
|
.zlib_decompress(data, &mut dest)
|
||||||
.map_err(|err| match err {
|
.map_err(|err| match err {
|
||||||
DecompressionError::BadData => PngError::InvalidData,
|
DecompressionError::BadData => PngError::InvalidData,
|
||||||
DecompressionError::InsufficientSpace => PngError::new("inflated data too long"),
|
DecompressionError::InsufficientSpace => PngError::InflatedDataTooLong(out_size),
|
||||||
})?;
|
})?;
|
||||||
dest.truncate(len);
|
dest.truncate(len);
|
||||||
Ok(dest)
|
Ok(dest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn crc32(data: &[u8]) -> u32 {
|
pub fn crc32(data: &[u8]) -> u32 {
|
||||||
let mut crc = Crc::new();
|
let mut crc = Crc::new();
|
||||||
crc.update(data);
|
crc.update(data);
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,38 @@
|
||||||
mod deflater;
|
mod deflater;
|
||||||
#[cfg(feature = "zopfli")]
|
|
||||||
use std::num::NonZeroU8;
|
|
||||||
use std::{fmt, fmt::Display};
|
use std::{fmt, fmt::Display};
|
||||||
|
|
||||||
pub use deflater::{crc32, deflate, inflate};
|
pub use deflater::{crc32, deflate, inflate};
|
||||||
|
|
||||||
use crate::{AtomicMin, PngError, PngResult};
|
use crate::{PngError, PngResult};
|
||||||
|
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
mod zopfli_oxipng;
|
mod zopfli_oxipng;
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
|
pub use zopfli::Options as ZopfliOptions;
|
||||||
|
#[cfg(feature = "zopfli")]
|
||||||
pub use zopfli_oxipng::deflate as zopfli_deflate;
|
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)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
/// DEFLATE algorithms supported by oxipng
|
pub enum Deflater {
|
||||||
pub enum Deflaters {
|
|
||||||
/// Use libdeflater.
|
/// Use libdeflater.
|
||||||
Libdeflater {
|
Libdeflater {
|
||||||
/// Which compression level to use on the file (1-12)
|
/// Which compression level to use on the file (0-12)
|
||||||
compression: u8,
|
compression: u8,
|
||||||
},
|
},
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
/// Use the better but slower Zopfli implementation
|
/// Use the better but slower Zopfli implementation
|
||||||
Zopfli {
|
Zopfli(ZopfliOptions),
|
||||||
/// The number of compression iterations to do. 15 iterations are fine
|
|
||||||
/// for small files, but bigger files will need to be compressed with
|
|
||||||
/// less iterations, or else they will be too slow.
|
|
||||||
iterations: NonZeroU8,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deflaters {
|
impl Deflater {
|
||||||
pub(crate) fn deflate(self, data: &[u8], max_size: &AtomicMin) -> PngResult<Vec<u8>> {
|
pub(crate) fn deflate(self, data: &[u8], max_size: Option<usize>) -> PngResult<Vec<u8>> {
|
||||||
let compressed = match self {
|
let compressed = match self {
|
||||||
Self::Libdeflater { compression } => deflate(data, compression, max_size)?,
|
Self::Libdeflater { compression } => deflate(data, compression, max_size)?,
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
Self::Zopfli { iterations } => zopfli_deflate(data, iterations)?,
|
Self::Zopfli(options) => zopfli_deflate(data, options)?,
|
||||||
};
|
};
|
||||||
if let Some(max) = max_size.get() {
|
if let Some(max) = max_size {
|
||||||
if compressed.len() > max {
|
if compressed.len() > max {
|
||||||
return Err(PngError::DeflatedDataTooLong(max));
|
return Err(PngError::DeflatedDataTooLong(max));
|
||||||
}
|
}
|
||||||
|
|
@ -45,13 +41,13 @@ impl Deflaters {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Deflaters {
|
impl Display for Deflater {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Libdeflater { compression } => Display::fmt(compression, f),
|
Self::Libdeflater { compression } => write!(f, "zc = {compression}"),
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
Self::Zopfli { .. } => Display::fmt("zopfli", f),
|
Self::Zopfli(options) => write!(f, "zopfli, zi = {}", options.iteration_count),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,13 @@
|
||||||
use std::num::NonZeroU8;
|
|
||||||
|
|
||||||
use crate::{PngError, PngResult};
|
use crate::{PngError, PngResult};
|
||||||
|
|
||||||
pub fn deflate(data: &[u8], iterations: NonZeroU8) -> PngResult<Vec<u8>> {
|
pub fn deflate(data: &[u8], options: zopfli::Options) -> PngResult<Vec<u8>> {
|
||||||
let mut output = Vec::with_capacity(data.len());
|
let mut output = Vec::with_capacity(data.len());
|
||||||
let options = zopfli::Options {
|
// Since Rust v1.74, passing &[u8] directly into zopfli causes a regression in compressed size
|
||||||
iteration_count: iterations.into(),
|
// for some files. Wrapping the slice in another Read implementer such as Box fixes it for now.
|
||||||
..Default::default()
|
match zopfli::compress(options, zopfli::Format::Zlib, Box::new(data), &mut output) {
|
||||||
};
|
Ok(()) => (),
|
||||||
match zopfli::compress(options, zopfli::Format::Zlib, data, &mut output) {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(_) => return Err(PngError::new("Failed to compress in zopfli")),
|
Err(_) => return Err(PngError::new("Failed to compress in zopfli")),
|
||||||
};
|
}
|
||||||
output.shrink_to_fit();
|
output.shrink_to_fit();
|
||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
69
src/error.rs
|
|
@ -2,18 +2,22 @@ use std::{error::Error, fmt};
|
||||||
|
|
||||||
use crate::colors::{BitDepth, ColorType};
|
use crate::colors::{BitDepth, ColorType};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum PngError {
|
pub enum PngError {
|
||||||
DeflatedDataTooLong(usize),
|
APNGOutOfOrder,
|
||||||
TimedOut,
|
C2PAMetadataPreventsChanges,
|
||||||
NotPNG,
|
|
||||||
APNGNotSupported,
|
|
||||||
InvalidData,
|
|
||||||
TruncatedData,
|
|
||||||
ChunkMissing(&'static str),
|
ChunkMissing(&'static str),
|
||||||
InvalidDepthForType(BitDepth, ColorType),
|
CRCMismatch([u8; 4]),
|
||||||
|
DeflatedDataTooLong(usize),
|
||||||
IncorrectDataLength(usize, usize),
|
IncorrectDataLength(usize, usize),
|
||||||
|
InflatedDataTooLong(usize),
|
||||||
|
InvalidData,
|
||||||
|
InvalidDepthForType(BitDepth, ColorType),
|
||||||
|
NotPNG,
|
||||||
|
ReadFailed(String, std::io::Error),
|
||||||
|
TruncatedData,
|
||||||
|
WriteFailed(String, std::io::Error),
|
||||||
Other(Box<str>),
|
Other(Box<str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,31 +28,42 @@ impl fmt::Display for PngError {
|
||||||
#[cold]
|
#[cold]
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match *self {
|
match *self {
|
||||||
PngError::DeflatedDataTooLong(_) => f.write_str("deflated data too long"),
|
Self::APNGOutOfOrder => f.write_str("APNG chunks are out of order"),
|
||||||
PngError::TimedOut => f.write_str("timed out"),
|
Self::C2PAMetadataPreventsChanges => f.write_str(
|
||||||
PngError::NotPNG => f.write_str("Invalid header detected; Not a PNG file"),
|
"The image contains C2PA manifest that would be invalidated by any file changes",
|
||||||
PngError::InvalidData => f.write_str("Invalid data found; unable to read PNG file"),
|
|
||||||
PngError::TruncatedData => {
|
|
||||||
f.write_str("Missing data in the file; the file is truncated")
|
|
||||||
}
|
|
||||||
PngError::APNGNotSupported => f.write_str("APNG files are not (yet) supported"),
|
|
||||||
PngError::ChunkMissing(s) => write!(f, "Chunk {} missing or empty", s),
|
|
||||||
PngError::InvalidDepthForType(d, ref c) => {
|
|
||||||
write!(f, "Invalid bit depth {} for color type {}", d, c)
|
|
||||||
}
|
|
||||||
PngError::IncorrectDataLength(l1, l2) => write!(
|
|
||||||
f,
|
|
||||||
"Data length {} does not match the expected length {}",
|
|
||||||
l1, l2
|
|
||||||
),
|
),
|
||||||
PngError::Other(ref s) => f.write_str(s),
|
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}")
|
||||||
|
}
|
||||||
|
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PngError {
|
impl PngError {
|
||||||
#[cold]
|
#[cold]
|
||||||
pub fn new(description: &str) -> PngError {
|
#[must_use]
|
||||||
PngError::Other(description.into())
|
pub fn new(description: &str) -> Self {
|
||||||
|
Self::Other(description.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,37 +4,44 @@
|
||||||
#[cfg(not(feature = "parallel"))]
|
#[cfg(not(feature = "parallel"))]
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicUsize, Ordering::*},
|
|
||||||
Arc,
|
Arc,
|
||||||
|
atomic::{AtomicUsize, Ordering::*},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "parallel")]
|
use deflate::Deflater;
|
||||||
use crossbeam_channel::{unbounded, Receiver, Sender};
|
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use log::trace;
|
use log::trace;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
#[cfg(feature = "parallel")]
|
||||||
|
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||||
|
|
||||||
#[cfg(not(feature = "parallel"))]
|
#[cfg(not(feature = "parallel"))]
|
||||||
use crate::rayon;
|
use crate::rayon;
|
||||||
use crate::{atomicmin::AtomicMin, deflate, filters::RowFilter, png::PngImage, Deadline, PngError};
|
use crate::{
|
||||||
|
Deadline, PngError, atomicmin::AtomicMin, deflate, filters::FilterStrategy, png::PngImage,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct Candidate {
|
pub(crate) struct Candidate {
|
||||||
pub image: Arc<PngImage>,
|
pub image: Arc<PngImage>,
|
||||||
pub idat_data: Vec<u8>,
|
pub idat_data: Option<Vec<u8>>,
|
||||||
pub filtered: Vec<u8>,
|
pub estimated_output_size: usize,
|
||||||
pub filter: RowFilter,
|
/// The input filter, which is retained for printing and for APNG frames.
|
||||||
// first wins tie-breaker
|
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,
|
nth: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Candidate {
|
impl Candidate {
|
||||||
fn cmp_key(&self) -> impl Ord {
|
fn cmp_key(&self) -> impl Ord + use<> {
|
||||||
(
|
(
|
||||||
self.idat_data.len() + self.image.key_chunks_size(),
|
self.estimated_output_size,
|
||||||
self.image.data.len(),
|
self.image.data.len(),
|
||||||
self.image.ihdr.bit_depth,
|
self.filter.clone(),
|
||||||
self.filter,
|
// Prefer the later image added (e.g. baseline, which is always added last)
|
||||||
self.nth,
|
usize::MAX - self.nth,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -42,9 +49,10 @@ impl Candidate {
|
||||||
/// Collect image versions and pick one that compresses best
|
/// Collect image versions and pick one that compresses best
|
||||||
pub(crate) struct Evaluator {
|
pub(crate) struct Evaluator {
|
||||||
deadline: Arc<Deadline>,
|
deadline: Arc<Deadline>,
|
||||||
filters: IndexSet<RowFilter>,
|
filters: IndexSet<FilterStrategy>,
|
||||||
compression: u8,
|
deflater: Deflater,
|
||||||
optimize_alpha: bool,
|
optimize_alpha: bool,
|
||||||
|
final_round: bool,
|
||||||
nth: AtomicUsize,
|
nth: AtomicUsize,
|
||||||
executed: Arc<AtomicUsize>,
|
executed: Arc<AtomicUsize>,
|
||||||
best_candidate_size: Arc<AtomicMin>,
|
best_candidate_size: Arc<AtomicMin>,
|
||||||
|
|
@ -59,17 +67,19 @@ pub(crate) struct Evaluator {
|
||||||
impl Evaluator {
|
impl Evaluator {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
deadline: Arc<Deadline>,
|
deadline: Arc<Deadline>,
|
||||||
filters: IndexSet<RowFilter>,
|
filters: IndexSet<FilterStrategy>,
|
||||||
compression: u8,
|
deflater: Deflater,
|
||||||
optimize_alpha: bool,
|
optimize_alpha: bool,
|
||||||
|
final_round: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
#[cfg(feature = "parallel")]
|
#[cfg(feature = "parallel")]
|
||||||
let eval_channel = unbounded();
|
let eval_channel = channel();
|
||||||
Self {
|
Self {
|
||||||
deadline,
|
deadline,
|
||||||
filters,
|
filters,
|
||||||
compression,
|
deflater,
|
||||||
optimize_alpha,
|
optimize_alpha,
|
||||||
|
final_round,
|
||||||
nth: AtomicUsize::new(0),
|
nth: AtomicUsize::new(0),
|
||||||
executed: Arc::new(AtomicUsize::new(0)),
|
executed: Arc::new(AtomicUsize::new(0)),
|
||||||
best_candidate_size: Arc::new(AtomicMin::new(None)),
|
best_candidate_size: Arc::new(AtomicMin::new(None)),
|
||||||
|
|
@ -108,7 +118,7 @@ impl Evaluator {
|
||||||
|
|
||||||
/// Check if the image is smaller than others
|
/// Check if the image is smaller than others
|
||||||
pub fn try_image(&self, image: Arc<PngImage>) {
|
pub fn try_image(&self, image: Arc<PngImage>) {
|
||||||
let description = format!("{}", image.ihdr.color_type);
|
let description = image.ihdr.color_type.to_string();
|
||||||
self.try_image_with_description(image, &description);
|
self.try_image_with_description(image, &description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,8 +128,9 @@ impl Evaluator {
|
||||||
// These clones are only cheap refcounts
|
// These clones are only cheap refcounts
|
||||||
let deadline = self.deadline.clone();
|
let deadline = self.deadline.clone();
|
||||||
let filters = self.filters.clone();
|
let filters = self.filters.clone();
|
||||||
let compression = self.compression;
|
let deflater = self.deflater;
|
||||||
let optimize_alpha = self.optimize_alpha;
|
let optimize_alpha = self.optimize_alpha;
|
||||||
|
let final_round = self.final_round;
|
||||||
let executed = self.executed.clone();
|
let executed = self.executed.clone();
|
||||||
let best_candidate_size = self.best_candidate_size.clone();
|
let best_candidate_size = self.best_candidate_size.clone();
|
||||||
let description = description.to_string();
|
let description = description.to_string();
|
||||||
|
|
@ -135,29 +146,37 @@ impl Evaluator {
|
||||||
// which are dangerous to do in side Rayon's loop.
|
// which are dangerous to do in side Rayon's loop.
|
||||||
// Instead, only update (atomic) best size in real time,
|
// Instead, only update (atomic) best size in real time,
|
||||||
// and the best result later without need for locks.
|
// and the best result later without need for locks.
|
||||||
filters_iter.for_each(|&filter| {
|
filters_iter.for_each(|filter| {
|
||||||
if deadline.passed() {
|
if deadline.passed() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let filtered = image.filter_image(filter, optimize_alpha);
|
let (filtered, filter_used) = image.filter_image(filter.clone(), optimize_alpha);
|
||||||
let idat_data = deflate::deflate(&filtered, compression, &best_candidate_size);
|
let idat_data = deflater.deflate(&filtered, best_candidate_size.get());
|
||||||
if let Ok(idat_data) = idat_data {
|
if let Ok(idat_data) = idat_data {
|
||||||
let size = idat_data.len() + image.key_chunks_size();
|
let estimated_output_size = image.estimated_output_size(&idat_data);
|
||||||
best_candidate_size.set_min(size);
|
|
||||||
trace!(
|
trace!(
|
||||||
"Eval: {}-bit {:23} {:8} {} bytes",
|
"Eval: {}-bit {:23} {:8} {} bytes",
|
||||||
image.ihdr.bit_depth,
|
image.ihdr.bit_depth, description, filter, estimated_output_size
|
||||||
description,
|
|
||||||
filter,
|
|
||||||
size
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Skip if it exceeds best known size. (This is important to ensure
|
||||||
|
// the evaluator returns no result when all candidates are too large.)
|
||||||
|
if let Some(max) = best_candidate_size.get() {
|
||||||
|
if estimated_output_size > max {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only need to retain the IDAT data in the final round
|
||||||
let new = Candidate {
|
let new = Candidate {
|
||||||
image: image.clone(),
|
image: image.clone(),
|
||||||
idat_data,
|
idat_data: if final_round { Some(idat_data) } else { None },
|
||||||
filtered,
|
estimated_output_size,
|
||||||
filter,
|
filter: filter.clone(),
|
||||||
|
filter_used,
|
||||||
nth,
|
nth,
|
||||||
};
|
};
|
||||||
|
best_candidate_size.set_min(estimated_output_size);
|
||||||
|
|
||||||
#[cfg(feature = "parallel")]
|
#[cfg(feature = "parallel")]
|
||||||
{
|
{
|
||||||
|
|
@ -174,10 +193,7 @@ impl Evaluator {
|
||||||
} else if let Err(PngError::DeflatedDataTooLong(size)) = idat_data {
|
} else if let Err(PngError::DeflatedDataTooLong(size)) = idat_data {
|
||||||
trace!(
|
trace!(
|
||||||
"Eval: {}-bit {:23} {:8} >{} bytes",
|
"Eval: {}-bit {:23} {:8} >{} bytes",
|
||||||
image.ihdr.bit_depth,
|
image.ihdr.bit_depth, description, filter, size
|
||||||
description,
|
|
||||||
filter,
|
|
||||||
size
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
130
src/filters.rs
|
|
@ -1,29 +1,67 @@
|
||||||
use std::{fmt, fmt::Display, mem::transmute};
|
use std::{fmt, fmt::Display, mem::transmute};
|
||||||
|
|
||||||
use crate::error::PngError;
|
/// Filtering strategy for use in [`Options`][crate::Options]
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
|
||||||
|
pub enum FilterStrategy {
|
||||||
|
/// Same filter for all rows
|
||||||
|
Basic(RowFilter),
|
||||||
|
/// Minimum sum of absolute differences
|
||||||
|
MinSum,
|
||||||
|
/// Shannon entropy
|
||||||
|
Entropy,
|
||||||
|
/// Count of distinct bigrams
|
||||||
|
Bigrams,
|
||||||
|
/// Shannon entropy of bigrams
|
||||||
|
BigEnt,
|
||||||
|
/// Deflate compression
|
||||||
|
Brute {
|
||||||
|
/// The number of lines to compress at once
|
||||||
|
num_lines: usize,
|
||||||
|
/// The compression level to use (1-12)
|
||||||
|
level: u8,
|
||||||
|
},
|
||||||
|
/// Predefined filter for each row
|
||||||
|
Predefined(Vec<RowFilter>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilterStrategy {
|
||||||
|
pub const NONE: Self = Self::Basic(RowFilter::None);
|
||||||
|
pub const SUB: Self = Self::Basic(RowFilter::Sub);
|
||||||
|
pub const UP: Self = Self::Basic(RowFilter::Up);
|
||||||
|
pub const AVERAGE: Self = Self::Basic(RowFilter::Average);
|
||||||
|
pub const PAETH: Self = Self::Basic(RowFilter::Paeth);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for FilterStrategy {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Basic(filter) => filter.fmt(f),
|
||||||
|
Self::MinSum => "MinSum".fmt(f),
|
||||||
|
Self::Entropy => "Entropy".fmt(f),
|
||||||
|
Self::Bigrams => "Bigrams".fmt(f),
|
||||||
|
Self::BigEnt => "BigEnt".fmt(f),
|
||||||
|
Self::Brute { .. } => "Brute".fmt(f),
|
||||||
|
Self::Predefined(_) => "Predefined".fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PNG delta filters
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
|
||||||
pub enum RowFilter {
|
pub enum RowFilter {
|
||||||
// Standard filter types
|
|
||||||
None,
|
None,
|
||||||
Sub,
|
Sub,
|
||||||
Up,
|
Up,
|
||||||
Average,
|
Average,
|
||||||
Paeth,
|
Paeth,
|
||||||
// Heuristic strategies
|
|
||||||
MinSum,
|
|
||||||
Entropy,
|
|
||||||
Bigrams,
|
|
||||||
BigEnt,
|
|
||||||
Brute,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for RowFilter {
|
impl TryFrom<u8> for RowFilter {
|
||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
if value > Self::LAST {
|
if value > 4 {
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
unsafe { transmute(value as i8) }
|
unsafe { transmute(value as i8) }
|
||||||
|
|
@ -39,11 +77,6 @@ impl Display for RowFilter {
|
||||||
Self::Up => "Up",
|
Self::Up => "Up",
|
||||||
Self::Average => "Average",
|
Self::Average => "Average",
|
||||||
Self::Paeth => "Paeth",
|
Self::Paeth => "Paeth",
|
||||||
Self::MinSum => "MinSum",
|
|
||||||
Self::Entropy => "Entropy",
|
|
||||||
Self::Bigrams => "Bigrams",
|
|
||||||
Self::BigEnt => "BigEnt",
|
|
||||||
Self::Brute => "Brute",
|
|
||||||
},
|
},
|
||||||
f,
|
f,
|
||||||
)
|
)
|
||||||
|
|
@ -51,9 +84,7 @@ impl Display for RowFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RowFilter {
|
impl RowFilter {
|
||||||
pub const LAST: u8 = Self::Brute as u8;
|
pub(crate) const ALL: [Self; 5] = [Self::None, Self::Sub, Self::Up, Self::Average, Self::Paeth];
|
||||||
pub(crate) const STANDARD: [Self; 5] =
|
|
||||||
[Self::None, Self::Sub, Self::Up, Self::Average, Self::Paeth];
|
|
||||||
pub(crate) const SINGLE_LINE: [Self; 2] = [Self::None, Self::Sub];
|
pub(crate) const SINGLE_LINE: [Self; 2] = [Self::None, Self::Sub];
|
||||||
|
|
||||||
pub(crate) fn filter_line(
|
pub(crate) fn filter_line(
|
||||||
|
|
@ -96,25 +127,20 @@ impl RowFilter {
|
||||||
}
|
}
|
||||||
Self::Average => {
|
Self::Average => {
|
||||||
for (i, byte) in data.iter().enumerate() {
|
for (i, byte) in data.iter().enumerate() {
|
||||||
buf.push(match i.checked_sub(bpp) {
|
buf.push(byte.wrapping_sub(i.checked_sub(bpp).map_or_else(
|
||||||
Some(x) => byte.wrapping_sub(
|
|| prev_line[i] >> 1,
|
||||||
((u16::from(data[x]) + u16::from(prev_line[i])) >> 1) as u8,
|
|x| ((u16::from(data[x]) + u16::from(prev_line[i])) >> 1) as u8,
|
||||||
),
|
)));
|
||||||
None => byte.wrapping_sub(prev_line[i] >> 1),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Paeth => {
|
Self::Paeth => {
|
||||||
for (i, byte) in data.iter().enumerate() {
|
for (i, byte) in data.iter().enumerate() {
|
||||||
buf.push(match i.checked_sub(bpp) {
|
buf.push(byte.wrapping_sub(i.checked_sub(bpp).map_or_else(
|
||||||
Some(x) => {
|
|| prev_line[i],
|
||||||
byte.wrapping_sub(paeth_predictor(data[x], prev_line[i], prev_line[x]))
|
|x| paeth_predictor(data[x], prev_line[i], prev_line[x]),
|
||||||
}
|
)));
|
||||||
None => byte.wrapping_sub(prev_line[i]),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,8 +151,8 @@ impl RowFilter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut pixels: Vec<_> = data.chunks_mut(bpp).collect();
|
let mut pixels: Vec<_> = data.chunks_exact_mut(bpp).collect();
|
||||||
let prev_pixels: Vec<_> = prev_line.chunks(bpp).collect();
|
let prev_pixels: Vec<_> = prev_line.chunks_exact(bpp).collect();
|
||||||
for i in 0..pixels.len() {
|
for i in 0..pixels.len() {
|
||||||
if pixels[i].iter().skip(color_bytes).all(|b| *b == 0) {
|
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
|
// If the first pixel in the row is transparent, find the next non-transparent pixel and pretend
|
||||||
|
|
@ -138,10 +164,28 @@ impl RowFilter {
|
||||||
.unwrap_or(i),
|
.unwrap_or(i),
|
||||||
_ => i - 1,
|
_ => 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 {
|
match self {
|
||||||
|
Self::None => unreachable!(),
|
||||||
Self::Sub => {
|
Self::Sub => {
|
||||||
for j in 0..color_bytes {
|
// The code below is roughly equivalent to pixels[i][0..color_bytes].copy_from_slice(&pixels[prev][0..color_bytes]),
|
||||||
pixels[i][j] = pixels[prev][j];
|
// 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 => {
|
Self::Up => {
|
||||||
|
|
@ -170,7 +214,6 @@ impl RowFilter {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -182,7 +225,7 @@ impl RowFilter {
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
prev_line: &[u8],
|
prev_line: &[u8],
|
||||||
buf: &mut Vec<u8>,
|
buf: &mut Vec<u8>,
|
||||||
) -> Result<(), PngError> {
|
) {
|
||||||
buf.clear();
|
buf.clear();
|
||||||
buf.reserve(data.len());
|
buf.reserve(data.len());
|
||||||
assert!(data.len() >= bpp);
|
assert!(data.len() >= bpp);
|
||||||
|
|
@ -194,10 +237,7 @@ impl RowFilter {
|
||||||
Self::Sub => {
|
Self::Sub => {
|
||||||
for (i, &cur) in data.iter().enumerate() {
|
for (i, &cur) in data.iter().enumerate() {
|
||||||
let prev_byte = i.checked_sub(bpp).and_then(|x| buf.get(x).copied());
|
let prev_byte = i.checked_sub(bpp).and_then(|x| buf.get(x).copied());
|
||||||
buf.push(match prev_byte {
|
buf.push(prev_byte.map_or(cur, |b| cur.wrapping_add(b)));
|
||||||
Some(b) => cur.wrapping_add(b),
|
|
||||||
None => cur,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Up => {
|
Self::Up => {
|
||||||
|
|
@ -210,10 +250,10 @@ impl RowFilter {
|
||||||
Self::Average => {
|
Self::Average => {
|
||||||
for (i, (&cur, &last)) in data.iter().zip(prev_line).enumerate() {
|
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());
|
let prev_byte = i.checked_sub(bpp).and_then(|x| buf.get(x).copied());
|
||||||
buf.push(match prev_byte {
|
buf.push(cur.wrapping_add(prev_byte.map_or_else(
|
||||||
Some(b) => cur.wrapping_add(((u16::from(b) + u16::from(last)) >> 1) as u8),
|
|| last >> 1,
|
||||||
None => cur.wrapping_add(last >> 1),
|
|b| ((u16::from(b) + u16::from(last)) >> 1) as u8,
|
||||||
});
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Paeth => {
|
Self::Paeth => {
|
||||||
|
|
@ -231,9 +271,7 @@ impl RowFilter {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => return Err(PngError::InvalidData),
|
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
245
src/headers.rs
|
|
@ -1,14 +1,13 @@
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use log::warn;
|
use log::{debug, trace, warn};
|
||||||
use rgb::{RGB16, RGBA8};
|
use rgb::{RGB16, RGBA8};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
Deflater, Options, PngResult,
|
||||||
colors::{BitDepth, ColorType},
|
colors::{BitDepth, ColorType},
|
||||||
deflate::{crc32, inflate},
|
deflate::{crc32, inflate},
|
||||||
display_chunks::DISPLAY_CHUNKS,
|
display_chunks::DISPLAY_CHUNKS,
|
||||||
error::PngError,
|
error::PngError,
|
||||||
interlace::Interlacing,
|
|
||||||
AtomicMin, Deflaters, PngResult,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -22,32 +21,30 @@ pub struct IhdrData {
|
||||||
pub color_type: ColorType,
|
pub color_type: ColorType,
|
||||||
/// The bit depth of the image
|
/// The bit depth of the image
|
||||||
pub bit_depth: BitDepth,
|
pub bit_depth: BitDepth,
|
||||||
/// The interlacing mode of the image
|
/// Whether the image is interlaced
|
||||||
pub interlaced: Interlacing,
|
pub interlaced: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IhdrData {
|
impl IhdrData {
|
||||||
/// Bits per pixel
|
/// Bits per pixel
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn bpp(&self) -> usize {
|
pub const fn bpp(&self) -> usize {
|
||||||
self.bit_depth as usize * self.color_type.channels_per_pixel() as usize
|
self.bit_depth as usize * self.color_type.channels_per_pixel() as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Byte length of IDAT that is correct for this IHDR
|
/// Byte length of IDAT that is correct for this IHDR
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn raw_data_size(&self) -> usize {
|
pub const fn raw_data_size(&self) -> usize {
|
||||||
let w = self.width as usize;
|
let w = self.width as usize;
|
||||||
let h = self.height as usize;
|
let h = self.height as usize;
|
||||||
let bpp = self.bpp();
|
let bpp = self.bpp();
|
||||||
|
|
||||||
fn bitmap_size(bpp: usize, w: usize, h: usize) -> usize {
|
const fn bitmap_size(bpp: usize, w: usize, h: usize) -> usize {
|
||||||
((w * bpp + 7) / 8) * h
|
(w * bpp).div_ceil(8) * h
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.interlaced == Interlacing::None {
|
if self.interlaced {
|
||||||
bitmap_size(bpp, w, h) + h
|
|
||||||
} else {
|
|
||||||
let mut size = bitmap_size(bpp, (w + 7) >> 3, (h + 7) >> 3) + ((h + 7) >> 3);
|
let mut size = bitmap_size(bpp, (w + 7) >> 3, (h + 7) >> 3) + ((h + 7) >> 3);
|
||||||
if w > 4 {
|
if w > 4 {
|
||||||
size += bitmap_size(bpp, (w + 3) >> 3, (h + 7) >> 3) + ((h + 7) >> 3);
|
size += bitmap_size(bpp, (w + 3) >> 3, (h + 7) >> 3) + ((h + 7) >> 3);
|
||||||
|
|
@ -61,6 +58,8 @@ impl IhdrData {
|
||||||
size += bitmap_size(bpp, w >> 1, (h + 1) >> 1) + ((h + 1) >> 1);
|
size += bitmap_size(bpp, w >> 1, (h + 1) >> 1) + ((h + 1) >> 1);
|
||||||
}
|
}
|
||||||
size + bitmap_size(bpp, w, h >> 1) + (h >> 1)
|
size + bitmap_size(bpp, w, h >> 1) + (h >> 1)
|
||||||
|
} else {
|
||||||
|
bitmap_size(bpp, w, h) + h
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -71,10 +70,12 @@ pub struct Chunk {
|
||||||
pub data: Vec<u8>,
|
pub data: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`Options`][crate::Options] to use when stripping chunks (metadata)
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
/// Options to use when stripping chunks
|
|
||||||
pub enum StripChunks {
|
pub enum StripChunks {
|
||||||
/// None
|
/// None
|
||||||
|
///
|
||||||
|
/// ...except caBX chunk if it contains a C2PA.org signature.
|
||||||
None,
|
None,
|
||||||
/// Remove specific chunks
|
/// Remove specific chunks
|
||||||
Strip(IndexSet<[u8; 4]>),
|
Strip(IndexSet<[u8; 4]>),
|
||||||
|
|
@ -89,11 +90,11 @@ pub enum StripChunks {
|
||||||
impl StripChunks {
|
impl StripChunks {
|
||||||
pub(crate) fn keep(&self, name: &[u8; 4]) -> bool {
|
pub(crate) fn keep(&self, name: &[u8; 4]) -> bool {
|
||||||
match &self {
|
match &self {
|
||||||
StripChunks::None => true,
|
Self::None => true,
|
||||||
StripChunks::Keep(names) => names.contains(name),
|
Self::Keep(names) => names.contains(name),
|
||||||
StripChunks::Strip(names) => !names.contains(name),
|
Self::Strip(names) => !names.contains(name),
|
||||||
StripChunks::Safe => DISPLAY_CHUNKS.contains(name),
|
Self::Safe => DISPLAY_CHUNKS.contains(name),
|
||||||
StripChunks::All => false,
|
Self::All => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,6 +112,36 @@ pub struct RawChunk<'a> {
|
||||||
pub data: &'a [u8],
|
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_chunk<'a>(
|
||||||
byte_data: &'a [u8],
|
byte_data: &'a [u8],
|
||||||
byte_offset: &mut usize,
|
byte_offset: &mut usize,
|
||||||
|
|
@ -121,37 +152,27 @@ pub fn parse_next_chunk<'a>(
|
||||||
.get(*byte_offset..*byte_offset + 4)
|
.get(*byte_offset..*byte_offset + 4)
|
||||||
.ok_or(PngError::TruncatedData)?,
|
.ok_or(PngError::TruncatedData)?,
|
||||||
);
|
);
|
||||||
|
if byte_data.len() < *byte_offset + 12 + length as usize {
|
||||||
|
return Err(PngError::TruncatedData);
|
||||||
|
}
|
||||||
*byte_offset += 4;
|
*byte_offset += 4;
|
||||||
|
|
||||||
let chunk_start = *byte_offset;
|
let chunk_start = *byte_offset;
|
||||||
let chunk_name = byte_data
|
let chunk_name = &byte_data[chunk_start..chunk_start + 4];
|
||||||
.get(chunk_start..chunk_start + 4)
|
|
||||||
.ok_or(PngError::TruncatedData)?;
|
|
||||||
if chunk_name == b"IEND" {
|
if chunk_name == b"IEND" {
|
||||||
// End of data
|
// End of data
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
*byte_offset += 4;
|
*byte_offset += 4;
|
||||||
|
|
||||||
let data = byte_data
|
let data = &byte_data[*byte_offset..*byte_offset + length as usize];
|
||||||
.get(*byte_offset..*byte_offset + length as usize)
|
|
||||||
.ok_or(PngError::TruncatedData)?;
|
|
||||||
*byte_offset += length as usize;
|
*byte_offset += length as usize;
|
||||||
let crc = read_be_u32(
|
let crc = read_be_u32(&byte_data[*byte_offset..*byte_offset + 4]);
|
||||||
byte_data
|
|
||||||
.get(*byte_offset..*byte_offset + 4)
|
|
||||||
.ok_or(PngError::TruncatedData)?,
|
|
||||||
);
|
|
||||||
*byte_offset += 4;
|
*byte_offset += 4;
|
||||||
|
|
||||||
let chunk_bytes = byte_data
|
let chunk_bytes = &byte_data[chunk_start..chunk_start + 4 + length as usize];
|
||||||
.get(chunk_start..chunk_start + 4 + length as usize)
|
|
||||||
.ok_or(PngError::TruncatedData)?;
|
|
||||||
if !fix_errors && crc32(chunk_bytes) != crc {
|
if !fix_errors && crc32(chunk_bytes) != crc {
|
||||||
return Err(PngError::new(&format!(
|
return Err(PngError::CRCMismatch(chunk_name.try_into().unwrap()));
|
||||||
"CRC Mismatch in {} chunk; May be recoverable by using --fix",
|
|
||||||
String::from_utf8_lossy(chunk_name)
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let name: [u8; 4] = chunk_name.try_into().unwrap();
|
let name: [u8; 4] = chunk_name.try_into().unwrap();
|
||||||
|
|
@ -170,13 +191,13 @@ pub fn parse_ihdr_chunk(
|
||||||
0 => ColorType::Grayscale {
|
0 => ColorType::Grayscale {
|
||||||
transparent_shade: trns_data
|
transparent_shade: trns_data
|
||||||
.filter(|t| t.len() >= 2)
|
.filter(|t| t.len() >= 2)
|
||||||
.map(|t| u16::from_be_bytes([t[0], t[1]])),
|
.map(|t| read_be_u16(&t[0..2])),
|
||||||
},
|
},
|
||||||
2 => ColorType::RGB {
|
2 => ColorType::RGB {
|
||||||
transparent_color: trns_data.filter(|t| t.len() >= 6).map(|t| RGB16 {
|
transparent_color: trns_data.filter(|t| t.len() >= 6).map(|t| RGB16 {
|
||||||
r: u16::from_be_bytes([t[0], t[1]]),
|
r: read_be_u16(&t[0..2]),
|
||||||
g: u16::from_be_bytes([t[2], t[3]]),
|
g: read_be_u16(&t[2..4]),
|
||||||
b: u16::from_be_bytes([t[4], t[5]]),
|
b: read_be_u16(&t[4..6]),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
3 => ColorType::Indexed {
|
3 => ColorType::Indexed {
|
||||||
|
|
@ -184,12 +205,16 @@ pub fn parse_ihdr_chunk(
|
||||||
},
|
},
|
||||||
4 => ColorType::GrayscaleAlpha,
|
4 => ColorType::GrayscaleAlpha,
|
||||||
6 => ColorType::RGBA,
|
6 => ColorType::RGBA,
|
||||||
_ => return Err(PngError::new("Unexpected color type in header")),
|
_ => return Err(PngError::InvalidData),
|
||||||
},
|
},
|
||||||
bit_depth: byte_data[8].try_into()?,
|
bit_depth: byte_data[8].try_into()?,
|
||||||
width: read_be_u32(&byte_data[0..4]),
|
width: read_be_u32(&byte_data[0..4]),
|
||||||
height: read_be_u32(&byte_data[4..8]),
|
height: read_be_u32(&byte_data[4..8]),
|
||||||
interlaced: interlaced.try_into()?,
|
interlaced: match interlaced {
|
||||||
|
0 => false,
|
||||||
|
1 => true,
|
||||||
|
_ => return Err(PngError::InvalidData),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,9 +223,9 @@ fn palette_to_rgba(
|
||||||
palette_data: Option<Vec<u8>>,
|
palette_data: Option<Vec<u8>>,
|
||||||
trns_data: Option<Vec<u8>>,
|
trns_data: Option<Vec<u8>>,
|
||||||
) -> Result<Vec<RGBA8>, PngError> {
|
) -> Result<Vec<RGBA8>, PngError> {
|
||||||
let palette_data = palette_data.ok_or_else(|| PngError::new("no palette in indexed image"))?;
|
let palette_data = palette_data.ok_or(PngError::ChunkMissing("PLTE"))?;
|
||||||
let mut palette: Vec<_> = palette_data
|
let mut palette: Vec<_> = palette_data
|
||||||
.chunks(3)
|
.chunks_exact(3)
|
||||||
.map(|color| RGBA8::new(color[0], color[1], color[2], 255))
|
.map(|color| RGBA8::new(color[0], color[1], color[2], 255))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
@ -213,12 +238,17 @@ fn palette_to_rgba(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn read_be_u32(bytes: &[u8]) -> u32 {
|
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())
|
u32::from_be_bytes(bytes.try_into().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract and decompress the ICC profile from an iCCP chunk
|
/// Extract and decompress the ICC profile from an iCCP chunk
|
||||||
pub fn extract_icc(iccp: &Chunk) -> Option<Vec<u8>> {
|
pub fn extract_icc(iccp: &Chunk, max_size: Option<usize>) -> Option<Vec<u8>> {
|
||||||
// Skip (useless) profile name
|
// Skip (useless) profile name
|
||||||
let mut data = iccp.data.as_slice();
|
let mut data = iccp.data.as_slice();
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -233,21 +263,24 @@ pub fn extract_icc(iccp: &Chunk) -> Option<Vec<u8>> {
|
||||||
if compression_method != 0 {
|
if compression_method != 0 {
|
||||||
return None; // The profile is supposed to be compressed (method 0)
|
return None; // The profile is supposed to be compressed (method 0)
|
||||||
}
|
}
|
||||||
// The decompressed size is unknown so we have to guess the required buffer size
|
// Libdeflate works with a fixed size buffer. Since the decompressed size is unknown we have to
|
||||||
let max_size = compressed_data.len() * 2 + 1000;
|
// guess the required buffer size. We allow a fairly generous 10x factor with a minimum of 1000.
|
||||||
match inflate(compressed_data, max_size) {
|
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),
|
Ok(icc) => Some(icc),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Log the error so we can know if the buffer size needs to be adjusted
|
// Log the error so we can know if the buffer size needs to be adjusted
|
||||||
warn!("Failed to decompress icc: {}", e);
|
warn!("Failed to decompress icc: {e}");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct an iCCP chunk by compressing the ICC profile
|
/// Make an iCCP chunk by compressing the ICC profile
|
||||||
pub fn construct_iccp(icc: &[u8], deflater: Deflaters) -> PngResult<Chunk> {
|
pub fn make_iccp(icc: &[u8], deflater: Deflater, max_size: Option<usize>) -> PngResult<Chunk> {
|
||||||
let mut compressed = deflater.deflate(icc, &AtomicMin::new(None))?;
|
let mut compressed = deflater.deflate(icc, max_size)?;
|
||||||
let mut data = Vec::with_capacity(compressed.len() + 5);
|
let mut data = Vec::with_capacity(compressed.len() + 5);
|
||||||
data.extend(b"icc"); // Profile name - generally unused, can be anything
|
data.extend(b"icc"); // Profile name - generally unused, can be anything
|
||||||
data.extend([0, 0]); // Null separator, zlib compression method
|
data.extend([0, 0]); // Null separator, zlib compression method
|
||||||
|
|
@ -284,3 +317,109 @@ pub fn srgb_rendering_intent(icc_data: &[u8]) -> Option<u8> {
|
||||||
_ => 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,6 @@
|
||||||
use std::{fmt, fmt::Display};
|
|
||||||
|
|
||||||
use bitvec::prelude::*;
|
use bitvec::prelude::*;
|
||||||
|
|
||||||
use crate::{headers::IhdrData, png::PngImage, PngError};
|
use crate::{headers::IhdrData, png::PngImage};
|
||||||
|
|
||||||
#[repr(u8)]
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
||||||
pub enum Interlacing {
|
|
||||||
None,
|
|
||||||
Adam7,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<u8> for Interlacing {
|
|
||||||
type Error = PngError;
|
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
|
||||||
match value {
|
|
||||||
0 => Ok(Self::None),
|
|
||||||
1 => Ok(Self::Adam7),
|
|
||||||
_ => Err(PngError::new("Unexpected interlacing in header")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Interlacing {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
Display::fmt(
|
|
||||||
match self {
|
|
||||||
Self::None => "non-interlaced",
|
|
||||||
Self::Adam7 => "interlaced",
|
|
||||||
},
|
|
||||||
f,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn interlace_image(png: &PngImage) -> PngImage {
|
pub fn interlace_image(png: &PngImage) -> PngImage {
|
||||||
|
|
@ -86,7 +53,7 @@ pub fn interlace_image(png: &PngImage) -> PngImage {
|
||||||
data: output,
|
data: output,
|
||||||
ihdr: IhdrData {
|
ihdr: IhdrData {
|
||||||
color_type: png.ihdr.color_type.clone(),
|
color_type: png.ihdr.color_type.clone(),
|
||||||
interlaced: Interlacing::Adam7,
|
interlaced: true,
|
||||||
..png.ihdr
|
..png.ihdr
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +67,7 @@ pub fn deinterlace_image(png: &PngImage) -> PngImage {
|
||||||
},
|
},
|
||||||
ihdr: IhdrData {
|
ihdr: IhdrData {
|
||||||
color_type: png.ihdr.color_type.clone(),
|
color_type: png.ihdr.color_type.clone(),
|
||||||
interlaced: Interlacing::None,
|
interlaced: false,
|
||||||
..png.ihdr
|
..png.ihdr
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -118,10 +85,8 @@ fn deinterlace_bits(png: &PngImage) -> Vec<u8> {
|
||||||
let mut current_y: usize = pass_constants.y_shift as usize;
|
let mut current_y: usize = pass_constants.y_shift as usize;
|
||||||
for line in png.scan_lines(false) {
|
for line in png.scan_lines(false) {
|
||||||
let bit_vec = line.data.view_bits::<Msb0>();
|
let bit_vec = line.data.view_bits::<Msb0>();
|
||||||
let bits_in_line = ((png.ihdr.width - u32::from(pass_constants.x_shift)
|
let bits_in_line = (png.ihdr.width - u32::from(pass_constants.x_shift))
|
||||||
+ u32::from(pass_constants.x_step)
|
.div_ceil(u32::from(pass_constants.x_step)) as usize
|
||||||
- 1)
|
|
||||||
/ u32::from(pass_constants.x_step)) as usize
|
|
||||||
* bits_per_pixel;
|
* bits_per_pixel;
|
||||||
for (i, bit) in bit_vec.iter().by_vals().enumerate() {
|
for (i, bit) in bit_vec.iter().by_vals().enumerate() {
|
||||||
// Avoid moving padded 0's into new image
|
// Avoid moving padded 0's into new image
|
||||||
|
|
@ -184,7 +149,7 @@ fn deinterlace_bytes(png: &PngImage) -> Vec<u8> {
|
||||||
lines.concat()
|
lines.concat()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn increment_pass(current_pass: &mut u8, ihdr: &IhdrData) -> bool {
|
const fn increment_pass(current_pass: &mut u8, ihdr: &IhdrData) -> bool {
|
||||||
if *current_pass == 7 {
|
if *current_pass == 7 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
665
src/lib.rs
|
|
@ -1,23 +1,4 @@
|
||||||
#![warn(trivial_casts, trivial_numeric_casts, unused_import_braces)]
|
#![cfg_attr(not(feature = "zopfli"), allow(unreachable_patterns))]
|
||||||
#![deny(missing_debug_implementations, missing_copy_implementations)]
|
|
||||||
#![warn(clippy::expl_impl_clone_on_copy)]
|
|
||||||
#![warn(clippy::float_cmp_const)]
|
|
||||||
#![warn(clippy::linkedlist)]
|
|
||||||
#![warn(clippy::map_flatten)]
|
|
||||||
#![warn(clippy::match_same_arms)]
|
|
||||||
#![warn(clippy::mem_forget)]
|
|
||||||
#![warn(clippy::mut_mut)]
|
|
||||||
#![warn(clippy::mutex_integer)]
|
|
||||||
#![warn(clippy::needless_continue)]
|
|
||||||
#![warn(clippy::path_buf_push_overwrite)]
|
|
||||||
#![warn(clippy::range_plus_one)]
|
|
||||||
#![allow(clippy::cognitive_complexity)]
|
|
||||||
#![allow(clippy::upper_case_acronyms)]
|
|
||||||
#![cfg_attr(
|
|
||||||
not(feature = "zopfli"),
|
|
||||||
allow(irrefutable_let_patterns),
|
|
||||||
allow(unreachable_patterns)
|
|
||||||
)]
|
|
||||||
|
|
||||||
#[cfg(feature = "parallel")]
|
#[cfg(feature = "parallel")]
|
||||||
extern crate rayon;
|
extern crate rayon;
|
||||||
|
|
@ -26,39 +7,39 @@ extern crate rayon;
|
||||||
mod rayon;
|
mod rayon;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
fs::File,
|
||||||
fs::{File, Metadata},
|
io::{BufWriter, Read, Write, stdin, stdout},
|
||||||
io::{stdin, stdout, BufWriter, Read, Write},
|
path::PathBuf,
|
||||||
path::Path,
|
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
|
||||||
Arc,
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
},
|
},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use indexmap::{indexset, IndexSet};
|
pub use indexmap::{IndexSet, indexset};
|
||||||
use log::{debug, info, trace, warn};
|
use log::{debug, info, trace, warn};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
pub use rgb::{RGB16, RGBA8};
|
pub use rgb::{RGB16, RGBA8};
|
||||||
|
|
||||||
|
#[cfg(feature = "zopfli")]
|
||||||
|
pub use crate::deflate::ZopfliOptions;
|
||||||
|
pub use crate::{
|
||||||
|
colors::{BitDepth, ColorType},
|
||||||
|
deflate::Deflater,
|
||||||
|
error::PngError,
|
||||||
|
filters::{FilterStrategy, RowFilter},
|
||||||
|
headers::StripChunks,
|
||||||
|
options::{InFile, Options, OutFile},
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
atomicmin::AtomicMin,
|
evaluate::{Candidate, Evaluator},
|
||||||
evaluate::Evaluator,
|
|
||||||
headers::*,
|
headers::*,
|
||||||
png::{PngData, PngImage},
|
png::{PngData, PngImage},
|
||||||
reduction::*,
|
reduction::*,
|
||||||
};
|
};
|
||||||
pub use crate::{
|
|
||||||
colors::{BitDepth, ColorType},
|
|
||||||
deflate::Deflaters,
|
|
||||||
error::PngError,
|
|
||||||
filters::RowFilter,
|
|
||||||
headers::StripChunks,
|
|
||||||
interlace::Interlacing,
|
|
||||||
options::{InFile, Options, OutFile},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
mod apng;
|
||||||
mod atomicmin;
|
mod atomicmin;
|
||||||
mod colors;
|
mod colors;
|
||||||
mod deflate;
|
mod deflate;
|
||||||
|
|
@ -79,10 +60,11 @@ mod sanity_checks;
|
||||||
pub mod internal_tests {
|
pub mod internal_tests {
|
||||||
#[cfg(feature = "sanity-checks")]
|
#[cfg(feature = "sanity-checks")]
|
||||||
pub use crate::sanity_checks::*;
|
pub use crate::sanity_checks::*;
|
||||||
pub use crate::{atomicmin::*, deflate::*, png::*, reduction::*};
|
pub use crate::{deflate::*, png::*, reduction::*};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type PngResult<T> = Result<T, PngError>;
|
pub type PngResult<T> = Result<T, PngError>;
|
||||||
|
pub type OptimizationResult = PngResult<(usize, usize)>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
/// A raw image definition which can be used to create an optimized png
|
/// A raw image definition which can be used to create an optimized png
|
||||||
|
|
@ -105,7 +87,7 @@ impl RawImage {
|
||||||
color_type: ColorType,
|
color_type: ColorType,
|
||||||
bit_depth: BitDepth,
|
bit_depth: BitDepth,
|
||||||
data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
) -> Result<Self, PngError> {
|
) -> PngResult<Self> {
|
||||||
// Validate bit depth
|
// Validate bit depth
|
||||||
let valid_depth = match color_type {
|
let valid_depth = match color_type {
|
||||||
ColorType::Grayscale { .. } => true,
|
ColorType::Grayscale { .. } => true,
|
||||||
|
|
@ -118,7 +100,7 @@ impl RawImage {
|
||||||
|
|
||||||
// Validate data length
|
// Validate data length
|
||||||
let bpp = bit_depth as usize * color_type.channels_per_pixel() as usize;
|
let bpp = bit_depth as usize * color_type.channels_per_pixel() as usize;
|
||||||
let row_bytes = (bpp * width as usize + 7) / 8;
|
let row_bytes = (bpp * width as usize).div_ceil(8);
|
||||||
let expected_len = row_bytes * height as usize;
|
let expected_len = row_bytes * height as usize;
|
||||||
if data.len() != expected_len {
|
if data.len() != expected_len {
|
||||||
return Err(PngError::IncorrectDataLength(data.len(), expected_len));
|
return Err(PngError::IncorrectDataLength(data.len(), expected_len));
|
||||||
|
|
@ -131,7 +113,7 @@ impl RawImage {
|
||||||
height,
|
height,
|
||||||
color_type,
|
color_type,
|
||||||
bit_depth,
|
bit_depth,
|
||||||
interlaced: Interlacing::None,
|
interlaced: false,
|
||||||
},
|
},
|
||||||
data,
|
data,
|
||||||
}),
|
}),
|
||||||
|
|
@ -147,71 +129,56 @@ impl RawImage {
|
||||||
/// Add an ICC profile for the image
|
/// Add an ICC profile for the image
|
||||||
pub fn add_icc_profile(&mut self, data: &[u8]) {
|
pub fn add_icc_profile(&mut self, data: &[u8]) {
|
||||||
// Compress with fastest compression level - will be recompressed during optimization
|
// Compress with fastest compression level - will be recompressed during optimization
|
||||||
let deflater = Deflaters::Libdeflater { compression: 1 };
|
let deflater = Deflater::Libdeflater { compression: 1 };
|
||||||
if let Ok(iccp) = construct_iccp(data, deflater) {
|
if let Ok(iccp) = make_iccp(data, deflater, None) {
|
||||||
self.aux_chunks.push(iccp);
|
self.aux_chunks.push(iccp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an optimized png from the raw image data using the options provided
|
/// Create an optimized png from the raw image data using the options provided
|
||||||
pub fn create_optimized_png(&self, opts: &Options) -> PngResult<Vec<u8>> {
|
pub fn create_optimized_png(&self, opts: &Options) -> PngResult<Vec<u8>> {
|
||||||
let deadline = Arc::new(Deadline::new(opts.timeout));
|
let mut opts = opts.to_owned();
|
||||||
let mut png = optimize_raw(self.png.clone(), opts, deadline.clone(), None)
|
let mut aux_chunks: Vec<_> = self
|
||||||
.ok_or_else(|| PngError::new("Failed to optimize input data"))?;
|
|
||||||
|
|
||||||
// Process aux chunks
|
|
||||||
png.aux_chunks = self
|
|
||||||
.aux_chunks
|
.aux_chunks
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|c| opts.strip.keep(&c.name))
|
.filter(|c| opts.strip.keep(&c.name))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
postprocess_chunks(&mut png, opts, deadline, &self.png.ihdr);
|
preprocess_chunks(&mut aux_chunks, &mut opts);
|
||||||
|
|
||||||
|
let deadline = Arc::new(Deadline::new(opts.timeout));
|
||||||
|
let Some(result) = optimize_raw(self.png.clone(), &opts, deadline, None) else {
|
||||||
|
return Err(PngError::new("Failed to optimize input data"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut png = PngData {
|
||||||
|
raw: result.image,
|
||||||
|
idat_data: result.idat_data.unwrap(),
|
||||||
|
aux_chunks,
|
||||||
|
frames: Vec::new(),
|
||||||
|
};
|
||||||
|
postprocess_chunks(&mut png.aux_chunks, &png.raw.ihdr, &self.png.ihdr);
|
||||||
|
|
||||||
Ok(png.output())
|
Ok(png.output())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform optimization on the input file using the options provided
|
/// Perform optimization on the input file using the options provided
|
||||||
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<()> {
|
///
|
||||||
|
/// Returns the original and optimized file sizes
|
||||||
|
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> OptimizationResult {
|
||||||
// Read in the file and try to decode as PNG.
|
// Read in the file and try to decode as PNG.
|
||||||
info!("Processing: {}", input);
|
info!("Processing: {input}");
|
||||||
|
|
||||||
let deadline = Arc::new(Deadline::new(opts.timeout));
|
let deadline = Arc::new(Deadline::new(opts.timeout));
|
||||||
|
|
||||||
// grab metadata before even opening input file to preserve atime
|
|
||||||
let opt_metadata_preserved;
|
|
||||||
let in_data = match *input {
|
let in_data = match *input {
|
||||||
InFile::Path(ref input_path) => {
|
InFile::Path(ref input_path) => PngData::read_file(input_path)?,
|
||||||
if matches!(
|
|
||||||
output,
|
|
||||||
OutFile::Path {
|
|
||||||
preserve_attrs: true,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
opt_metadata_preserved = input_path
|
|
||||||
.metadata()
|
|
||||||
.map_err(|err| {
|
|
||||||
// Fail if metadata cannot be preserved
|
|
||||||
PngError::new(&format!(
|
|
||||||
"Unable to read metadata from input file {:?}: {}",
|
|
||||||
input_path, err
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.map(Some)?;
|
|
||||||
trace!("preserving metadata: {:?}", opt_metadata_preserved);
|
|
||||||
} else {
|
|
||||||
opt_metadata_preserved = None;
|
|
||||||
}
|
|
||||||
PngData::read_file(input_path)?
|
|
||||||
}
|
|
||||||
InFile::StdIn => {
|
InFile::StdIn => {
|
||||||
opt_metadata_preserved = None;
|
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
stdin()
|
stdin()
|
||||||
.read_to_end(&mut data)
|
.read_to_end(&mut data)
|
||||||
.map_err(|e| PngError::new(&format!("Error reading stdin: {}", e)))?;
|
.map_err(|e| PngError::ReadFailed("stdin".into(), e))?;
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -223,14 +190,14 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
|
||||||
|
|
||||||
let in_length = in_data.len();
|
let in_length = in_data.len();
|
||||||
|
|
||||||
if is_fully_optimized(in_data.len(), optimized_output.len(), opts) {
|
if is_fully_optimized(in_length, optimized_output.len(), opts) {
|
||||||
match (output, input) {
|
match (output, input) {
|
||||||
// if p is None, it also means same as the input path
|
// If output path is None, it also means same as the input path
|
||||||
(OutFile::Path { path, .. }, InFile::Path(ref input_path))
|
(OutFile::Path { path, .. }, InFile::Path(input_path))
|
||||||
if path.as_ref().map_or(true, |p| p == input_path) =>
|
if path.as_ref().is_none_or(|p| p == input_path) =>
|
||||||
{
|
{
|
||||||
info!("{}: Could not optimize further, no change written", input);
|
info!("Could not optimize further, no change written: {input}");
|
||||||
return Ok(());
|
return Ok((in_length, in_length));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
optimized_output = in_data;
|
optimized_output = in_data;
|
||||||
|
|
@ -254,51 +221,65 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
|
||||||
|
|
||||||
match (output, input) {
|
match (output, input) {
|
||||||
(OutFile::None, _) => {
|
(OutFile::None, _) => {
|
||||||
info!("{}: Running in pretend mode, no output", savings);
|
info!("{savings}: Dry run, no output");
|
||||||
}
|
}
|
||||||
(&OutFile::StdOut, _) | (&OutFile::Path { path: None, .. }, &InFile::StdIn) => {
|
(&OutFile::StdOut, _) | (&OutFile::Path { path: None, .. }, &InFile::StdIn) => {
|
||||||
let mut buffer = BufWriter::new(stdout());
|
let mut buffer = BufWriter::new(stdout());
|
||||||
buffer
|
buffer
|
||||||
.write_all(&optimized_output)
|
.write_all(&optimized_output)
|
||||||
.map_err(|e| PngError::new(&format!("Unable to write to stdout: {}", e)))?;
|
.map_err(|e| PngError::WriteFailed("stdout".into(), e))?;
|
||||||
|
info!("{savings}: stdout");
|
||||||
}
|
}
|
||||||
(OutFile::Path { path, .. }, _) => {
|
(
|
||||||
|
OutFile::Path {
|
||||||
|
path,
|
||||||
|
preserve_attrs,
|
||||||
|
},
|
||||||
|
_,
|
||||||
|
) => {
|
||||||
|
let input_metadata = if *preserve_attrs {
|
||||||
|
input.path().and_then(|in_path| {
|
||||||
|
let meta = in_path.metadata();
|
||||||
|
if let Err(e) = &meta {
|
||||||
|
warn!("Unable to read metadata from {in_path:?}: {e}");
|
||||||
|
}
|
||||||
|
meta.ok()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let output_path = path
|
let output_path = path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|p| p.as_path())
|
.map_or_else(|| input.path().unwrap(), PathBuf::as_path);
|
||||||
.unwrap_or_else(|| input.path().unwrap());
|
let out_file = File::create(output_path)
|
||||||
let out_file = File::create(output_path).map_err(|err| {
|
.map_err(|err| PngError::WriteFailed(output_path.display().to_string(), err))?;
|
||||||
PngError::new(&format!(
|
|
||||||
"Unable to write to file {}: {}",
|
|
||||||
output_path.display(),
|
|
||||||
err
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
if let Some(metadata_input) = &opt_metadata_preserved {
|
|
||||||
copy_permissions(metadata_input, &out_file)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut buffer = BufWriter::new(out_file);
|
let mut buffer = BufWriter::new(&out_file);
|
||||||
buffer
|
buffer
|
||||||
.write_all(&optimized_output)
|
.write_all(&optimized_output)
|
||||||
// flush BufWriter so IO errors don't get swallowed silently on close() by drop!
|
// flush BufWriter so IO errors don't get swallowed silently on close() by drop!
|
||||||
.and_then(|()| buffer.flush())
|
.and_then(|()| buffer.flush())
|
||||||
.map_err(|e| {
|
.map_err(|e| PngError::WriteFailed(output_path.display().to_string(), e))?;
|
||||||
PngError::new(&format!(
|
|
||||||
"Unable to write to {}: {}",
|
|
||||||
output_path.display(),
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
// force drop and thereby closing of file handle before modifying any timestamp
|
// force drop and thereby closing of file handle before modifying any timestamp
|
||||||
std::mem::drop(buffer);
|
std::mem::drop(buffer);
|
||||||
if let Some(metadata_input) = &opt_metadata_preserved {
|
|
||||||
copy_times(metadata_input, output_path)?;
|
if let Some(metadata_input) = &input_metadata {
|
||||||
|
let set_time = metadata_input
|
||||||
|
.modified()
|
||||||
|
.and_then(|m| out_file.set_modified(m));
|
||||||
|
if let Err(e) = set_time {
|
||||||
|
warn!("Unable to set modification time on {output_path:?}: {e}");
|
||||||
|
}
|
||||||
|
let set_perm = out_file.set_permissions(metadata_input.permissions());
|
||||||
|
if let Err(e) = set_perm {
|
||||||
|
warn!("Unable to set permissions on {output_path:?}: {e}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
info!("{}: {}", savings, output_path.display());
|
info!("{}: {}", savings, output_path.display());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok((in_length, optimized_output.len()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform optimization on the input file using the options provided, where the file is already
|
/// Perform optimization on the input file using the options provided, where the file is already
|
||||||
|
|
@ -323,8 +304,6 @@ pub fn optimize_from_memory(data: &[u8], opts: &Options) -> PngResult<Vec<u8>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrialResult = (RowFilter, Vec<u8>);
|
|
||||||
|
|
||||||
/// Perform optimization on the input PNG object using the options provided
|
/// Perform optimization on the input PNG object using the options provided
|
||||||
fn optimize_png(
|
fn optimize_png(
|
||||||
png: &mut PngData,
|
png: &mut PngData,
|
||||||
|
|
@ -341,34 +320,24 @@ fn optimize_png(
|
||||||
raw.ihdr.width, raw.ihdr.height
|
raw.ihdr.width, raw.ihdr.height
|
||||||
);
|
);
|
||||||
report_format(" ", &raw);
|
report_format(" ", &raw);
|
||||||
debug!(" IDAT size = {} bytes", idat_original_size);
|
debug!(" IDAT size = {idat_original_size} bytes");
|
||||||
debug!(" File size = {} bytes", file_original_size);
|
debug!(" File size = {file_original_size} bytes");
|
||||||
|
|
||||||
|
let mut opts = opts.to_owned();
|
||||||
|
preprocess_chunks(&mut png.aux_chunks, &mut opts);
|
||||||
|
|
||||||
// Check for APNG by presence of acTL chunk
|
|
||||||
let opts = if png.aux_chunks.iter().any(|c| &c.name == b"acTL") {
|
|
||||||
warn!("APNG detected, disabling all reductions");
|
|
||||||
let mut opts = opts.to_owned();
|
|
||||||
opts.interlace = None;
|
|
||||||
opts.bit_depth_reduction = false;
|
|
||||||
opts.color_type_reduction = false;
|
|
||||||
opts.palette_reduction = false;
|
|
||||||
opts.grayscale_reduction = false;
|
|
||||||
Cow::Owned(opts)
|
|
||||||
} else {
|
|
||||||
Cow::Borrowed(opts)
|
|
||||||
};
|
|
||||||
let max_size = if opts.force {
|
let max_size = if opts.force {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(png.estimated_output_size())
|
Some(png.raw.estimated_output_size(&png.idat_data))
|
||||||
};
|
};
|
||||||
if let Some(new_png) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_size) {
|
if let Some(result) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_size) {
|
||||||
png.raw = new_png.raw;
|
png.raw = result.image;
|
||||||
png.idat_data = new_png.idat_data;
|
png.idat_data = result.idat_data.unwrap();
|
||||||
|
recompress_frames(png, &opts, deadline, result.filter)?;
|
||||||
|
postprocess_chunks(&mut png.aux_chunks, &png.raw.ihdr, &raw.ihdr);
|
||||||
}
|
}
|
||||||
|
|
||||||
postprocess_chunks(png, &opts, deadline, &raw.ihdr);
|
|
||||||
|
|
||||||
let output = png.output();
|
let output = png.output();
|
||||||
|
|
||||||
if idat_original_size >= png.idat_data.len() {
|
if idat_original_size >= png.idat_data.len() {
|
||||||
|
|
@ -400,6 +369,12 @@ fn optimize_png(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.interlace == Some(true) && !png.raw.ihdr.interlaced {
|
||||||
|
warn!(
|
||||||
|
"Interlacing was not enabled as it would result in a larger file. To override this, use `--force`."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "sanity-checks")]
|
#[cfg(feature = "sanity-checks")]
|
||||||
assert!(sanity_checks::validate_output(&output, original_data));
|
assert!(sanity_checks::validate_output(&output, original_data));
|
||||||
|
|
||||||
|
|
@ -412,186 +387,155 @@ fn optimize_raw(
|
||||||
opts: &Options,
|
opts: &Options,
|
||||||
deadline: Arc<Deadline>,
|
deadline: Arc<Deadline>,
|
||||||
max_size: Option<usize>,
|
max_size: Option<usize>,
|
||||||
) -> Option<PngData> {
|
) -> Option<Candidate> {
|
||||||
// Libdeflate has four algorithms: 1-4 = 'greedy', 5-7 = 'lazy', 8-9 = 'lazy2', 10-12 = 'near-optimal'
|
// Libdeflate has four algorithms: 0 = 'uncompressed', 1-4 = 'greedy', 5-7 = 'lazy', 8-9 = 'lazy2', 10-12 = 'near-optimal'
|
||||||
// 5 is the minimumm required for a decent evaluation result
|
// 5 is the minimumm required for a decent evaluation result
|
||||||
// 7 is not noticeably slower than 5 and improves evaluation of filters in 'fast' mode (o2 and lower)
|
// 7 is not noticeably slower than 5 and improves evaluation of filters in 'fast' mode (o2 and lower)
|
||||||
// 8 is a little slower but not noticeably when used only for reductions (o3 and higher)
|
// 8 is a little slower but not noticeably when used only for reductions (o3 and higher)
|
||||||
// 9 is not appreciably better than 8
|
// 9 is not appreciably better than 8
|
||||||
// 10 and higher are quite slow - good for filters but only good for reductions if matching the main zc level
|
// 10 and higher are quite slow - good for filters but only good for reductions if matching the main zc level
|
||||||
let eval_compression = match opts.deflate {
|
let compression = match opts.deflater {
|
||||||
Deflaters::Libdeflater { compression } => {
|
Deflater::Libdeflater { compression } => {
|
||||||
if opts.fast_evaluation { 7 } else { 8 }.min(compression)
|
if opts.fast_evaluation { 7 } else { 8 }.min(compression)
|
||||||
}
|
}
|
||||||
_ => 8,
|
_ => 8,
|
||||||
};
|
};
|
||||||
|
let eval_deflater = Deflater::Libdeflater { compression };
|
||||||
// If only one filter is selected, use this for evaluations
|
// If only one filter is selected, use this for evaluations
|
||||||
let eval_filters = if opts.filter.len() == 1 {
|
let eval_filters = if opts.filters.len() == 1 {
|
||||||
opts.filter.clone()
|
opts.filters.clone()
|
||||||
} else {
|
} else {
|
||||||
// None and Bigrams work well together, especially for alpha reductions
|
// None and Bigrams work well together, especially for alpha reductions
|
||||||
indexset! {RowFilter::None, RowFilter::Bigrams}
|
indexset! {FilterStrategy::NONE, FilterStrategy::Bigrams}
|
||||||
};
|
};
|
||||||
// This will collect all versions of images and pick one that compresses best
|
// This will collect all versions of images and pick one that compresses best
|
||||||
let eval = Evaluator::new(
|
let eval = Evaluator::new(
|
||||||
deadline.clone(),
|
deadline.clone(),
|
||||||
eval_filters.clone(),
|
eval_filters.clone(),
|
||||||
eval_compression,
|
eval_deflater,
|
||||||
false,
|
false,
|
||||||
|
opts.deflater == eval_deflater,
|
||||||
);
|
);
|
||||||
let mut png = perform_reductions(image.clone(), opts, &deadline, &eval);
|
let mut new_image = perform_reductions(image.clone(), opts, &deadline, &eval);
|
||||||
let mut eval_result = eval.get_best_candidate();
|
let eval_result = eval.get_best_candidate();
|
||||||
if let Some(ref result) = eval_result {
|
if let Some(ref result) = eval_result {
|
||||||
png = result.image.clone();
|
new_image = result.image.clone();
|
||||||
}
|
}
|
||||||
let reduction_occurred = png.ihdr.color_type != image.ihdr.color_type
|
let reduction_occurred = new_image.ihdr.color_type != image.ihdr.color_type
|
||||||
|| png.ihdr.bit_depth != image.ihdr.bit_depth
|
|| new_image.ihdr.bit_depth != image.ihdr.bit_depth
|
||||||
|| png.ihdr.interlaced != image.ihdr.interlaced;
|
|| new_image.ihdr.interlaced != image.ihdr.interlaced;
|
||||||
|
|
||||||
if reduction_occurred {
|
if reduction_occurred {
|
||||||
report_format("Transformed image to ", &png);
|
report_format("Transformed image to ", &new_image);
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.idat_recoding || reduction_occurred {
|
let (result, deflater) = if opts.idat_recoding || reduction_occurred {
|
||||||
let mut filters = opts.filter.clone();
|
let result = perform_trials(
|
||||||
let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_result.is_some());
|
new_image,
|
||||||
let best: Option<TrialResult> = if fast_eval {
|
opts,
|
||||||
// Perform a fast evaluation of selected filters followed by a single main compression trial
|
deadline,
|
||||||
|
max_size,
|
||||||
if eval_result.is_some() {
|
eval_result,
|
||||||
// Some filters have already been evaluated, we don't need to try them again
|
eval_filters,
|
||||||
filters = filters.difference(&eval_filters).cloned().collect();
|
eval_deflater,
|
||||||
}
|
);
|
||||||
|
(result?, opts.deflater)
|
||||||
if !filters.is_empty() {
|
} else {
|
||||||
trace!("Evaluating: {} filters", filters.len());
|
|
||||||
let eval = Evaluator::new(deadline, filters, eval_compression, opts.optimize_alpha);
|
|
||||||
if let Some(ref result) = eval_result {
|
|
||||||
eval.set_best_size(result.idat_data.len());
|
|
||||||
}
|
|
||||||
eval.try_image(png.clone());
|
|
||||||
if let Some(result) = eval.get_best_candidate() {
|
|
||||||
eval_result = Some(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// We should have a result here - fail if not (e.g. deadline passed)
|
|
||||||
let result = eval_result?;
|
|
||||||
|
|
||||||
match opts.deflate {
|
|
||||||
Deflaters::Libdeflater { compression } if compression <= eval_compression => {
|
|
||||||
// No further compression required
|
|
||||||
Some((result.filter, result.idat_data))
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
debug!("Trying: {}", result.filter);
|
|
||||||
let best_size = AtomicMin::new(max_size);
|
|
||||||
perform_trial(&result.filtered, opts, result.filter, &best_size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Perform full compression trials of selected filters and determine the best
|
|
||||||
|
|
||||||
if filters.is_empty() {
|
|
||||||
// Pick a filter automatically
|
|
||||||
if png.ihdr.bit_depth as u8 >= 8 {
|
|
||||||
// Bigrams is the best all-rounder when there's at least one byte per pixel
|
|
||||||
filters.insert(RowFilter::Bigrams);
|
|
||||||
} else {
|
|
||||||
// Otherwise delta filters generally don't work well, so just stick with None
|
|
||||||
filters.insert(RowFilter::None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("Trying: {} filters", filters.len());
|
|
||||||
|
|
||||||
let best_size = AtomicMin::new(max_size);
|
|
||||||
let results_iter = filters.into_par_iter().with_max_len(1);
|
|
||||||
let best = results_iter.filter_map(|filter| {
|
|
||||||
if deadline.passed() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let filtered = &png.filter_image(filter, opts.optimize_alpha);
|
|
||||||
perform_trial(filtered, opts, filter, &best_size)
|
|
||||||
});
|
|
||||||
best.reduce_with(|i, j| {
|
|
||||||
if i.1.len() < j.1.len() || (i.1.len() == j.1.len() && i.0 < j.0) {
|
|
||||||
i
|
|
||||||
} else {
|
|
||||||
j
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some((filter, idat_data)) = best {
|
|
||||||
let image = PngData {
|
|
||||||
raw: png,
|
|
||||||
idat_data,
|
|
||||||
aux_chunks: Vec::new(),
|
|
||||||
};
|
|
||||||
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
|
|
||||||
debug!("Found better combination:");
|
|
||||||
debug!(
|
|
||||||
" zc = {} f = {:8} {} bytes",
|
|
||||||
opts.deflate,
|
|
||||||
filter,
|
|
||||||
image.idat_data.len()
|
|
||||||
);
|
|
||||||
return Some(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if let Some(result) = eval_result {
|
|
||||||
// If idat_recoding is off and reductions were attempted but ended up choosing the baseline,
|
// If idat_recoding is off and reductions were attempted but ended up choosing the baseline,
|
||||||
// we should still check if the evaluator compressed the baseline smaller than the original.
|
// we should still check if the evaluator compressed the baseline smaller than the original.
|
||||||
let image = PngData {
|
(eval_result?, eval_deflater)
|
||||||
raw: result.image,
|
};
|
||||||
idat_data: result.idat_data,
|
|
||||||
aux_chunks: Vec::new(),
|
|
||||||
};
|
|
||||||
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
|
|
||||||
debug!("Found better combination:");
|
|
||||||
debug!(
|
|
||||||
" zc = {} f = {:8} {} bytes",
|
|
||||||
eval_compression,
|
|
||||||
result.filter,
|
|
||||||
image.idat_data.len()
|
|
||||||
);
|
|
||||||
return Some(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if result.idat_data.is_some()
|
||||||
|
&& max_size.is_none_or(|max_size| result.estimated_output_size < max_size)
|
||||||
|
{
|
||||||
|
debug!("Found better result:");
|
||||||
|
debug!(" {}, f = {}", deflater, result.filter);
|
||||||
|
return Some(result);
|
||||||
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute a compression trial
|
/// Perform compression trials
|
||||||
fn perform_trial(
|
fn perform_trials(
|
||||||
filtered: &[u8],
|
image: Arc<PngImage>,
|
||||||
opts: &Options,
|
opts: &Options,
|
||||||
filter: RowFilter,
|
deadline: Arc<Deadline>,
|
||||||
best_size: &AtomicMin,
|
max_size: Option<usize>,
|
||||||
) -> Option<TrialResult> {
|
mut eval_result: Option<Candidate>,
|
||||||
match opts.deflate.deflate(filtered, best_size) {
|
eval_filters: IndexSet<FilterStrategy>,
|
||||||
Ok(new_idat) => {
|
eval_deflater: Deflater,
|
||||||
let bytes = new_idat.len();
|
) -> Option<Candidate> {
|
||||||
best_size.set_min(bytes);
|
let mut filters = opts.filters.clone();
|
||||||
trace!(
|
let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_result.is_some());
|
||||||
" zc = {} f = {:8} {} bytes",
|
if fast_eval {
|
||||||
opts.deflate,
|
// Perform a fast evaluation of selected filters followed by a single main compression trial
|
||||||
filter,
|
|
||||||
bytes
|
if eval_result.is_some() {
|
||||||
);
|
// Some filters have already been evaluated, we don't need to try them again
|
||||||
Some((filter, new_idat))
|
filters = filters.difference(&eval_filters).cloned().collect();
|
||||||
}
|
}
|
||||||
Err(PngError::DeflatedDataTooLong(bytes)) => {
|
|
||||||
trace!(
|
if !filters.is_empty() {
|
||||||
" zc = {} f = {:8} >{} bytes",
|
trace!("Evaluating {} filters", filters.len());
|
||||||
opts.deflate,
|
let eval = Evaluator::new(
|
||||||
filter,
|
deadline,
|
||||||
bytes,
|
filters,
|
||||||
|
eval_deflater,
|
||||||
|
opts.optimize_alpha,
|
||||||
|
opts.deflater == eval_deflater,
|
||||||
);
|
);
|
||||||
None
|
if let Some(result) = &eval_result {
|
||||||
|
eval.set_best_size(result.estimated_output_size);
|
||||||
|
}
|
||||||
|
eval.try_image(image.clone());
|
||||||
|
if let Some(result) = eval.get_best_candidate() {
|
||||||
|
eval_result = Some(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
|
||||||
|
// We should have a result here - fail if not (e.g. deadline passed)
|
||||||
|
let mut result = eval_result?;
|
||||||
|
|
||||||
|
if result.idat_data.is_none() {
|
||||||
|
// Compress with the main deflater
|
||||||
|
debug!("Trying filter {} with {}", result.filter, opts.deflater);
|
||||||
|
let (data, _) = image.filter_image(result.filter_used.clone(), opts.optimize_alpha);
|
||||||
|
match opts.deflater.deflate(&data, max_size) {
|
||||||
|
Ok(idat_data) => {
|
||||||
|
result.estimated_output_size = result.image.estimated_output_size(&idat_data);
|
||||||
|
result.idat_data = Some(idat_data);
|
||||||
|
trace!("{} bytes", result.estimated_output_size);
|
||||||
|
}
|
||||||
|
Err(PngError::DeflatedDataTooLong(bytes)) => {
|
||||||
|
trace!(">{bytes} bytes");
|
||||||
|
}
|
||||||
|
Err(_) => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Some(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Perform full compression trials of selected filters and determine the best
|
||||||
|
|
||||||
|
if filters.is_empty() {
|
||||||
|
// Pick a filter automatically
|
||||||
|
if image.ihdr.bit_depth as u8 >= 8 {
|
||||||
|
// Bigrams is the best all-rounder when there's at least one byte per pixel
|
||||||
|
filters.insert(FilterStrategy::Bigrams);
|
||||||
|
} else {
|
||||||
|
// Otherwise delta filters generally don't work well, so just stick with None
|
||||||
|
filters.insert(FilterStrategy::NONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Trying {} filters with {}", filters.len(), opts.deflater);
|
||||||
|
let eval = Evaluator::new(deadline, filters, opts.deflater, opts.optimize_alpha, true);
|
||||||
|
if let Some(max_size) = max_size {
|
||||||
|
eval.set_best_size(max_size);
|
||||||
|
}
|
||||||
|
eval.try_image(image);
|
||||||
|
eval.get_best_candidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -609,6 +553,7 @@ pub struct Deadline {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deadline {
|
impl Deadline {
|
||||||
|
#[must_use]
|
||||||
pub fn new(timeout: Option<Duration>) -> Self {
|
pub fn new(timeout: Option<Duration>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
imp: timeout.map(|timeout| DeadlineImp {
|
imp: timeout.map(|timeout| DeadlineImp {
|
||||||
|
|
@ -645,141 +590,57 @@ impl Deadline {
|
||||||
|
|
||||||
/// Display the format of the image data
|
/// Display the format of the image data
|
||||||
fn report_format(prefix: &str, png: &PngImage) {
|
fn report_format(prefix: &str, png: &PngImage) {
|
||||||
|
let interlaced = if png.ihdr.interlaced {
|
||||||
|
"interlaced"
|
||||||
|
} else {
|
||||||
|
"non-interlaced"
|
||||||
|
};
|
||||||
debug!(
|
debug!(
|
||||||
"{}{}-bit {}, {}",
|
"{}{}-bit {}, {}",
|
||||||
prefix, png.ihdr.bit_depth, png.ihdr.color_type, png.ihdr.interlaced
|
prefix, png.ihdr.bit_depth, png.ihdr.color_type, interlaced
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform cleanup of certain chunks from the `PngData` object, after optimization has been completed
|
/// Recompress the additional frames of an APNG
|
||||||
fn postprocess_chunks(
|
fn recompress_frames(
|
||||||
png: &mut PngData,
|
png: &mut PngData,
|
||||||
opts: &Options,
|
opts: &Options,
|
||||||
deadline: Arc<Deadline>,
|
deadline: Arc<Deadline>,
|
||||||
orig_ihdr: &IhdrData,
|
filter: FilterStrategy,
|
||||||
) {
|
) -> PngResult<()> {
|
||||||
if let Some(iccp_idx) = png.aux_chunks.iter().position(|c| &c.name == b"iCCP") {
|
if !opts.idat_recoding || png.frames.is_empty() {
|
||||||
// See if we can replace an iCCP chunk with an sRGB chunk
|
return Ok(());
|
||||||
let may_replace_iccp = opts.strip != StripChunks::None && opts.strip.keep(b"sRGB");
|
|
||||||
if may_replace_iccp && png.aux_chunks.iter().any(|c| &c.name == b"sRGB") {
|
|
||||||
// Files aren't supposed to have both chunks, so we chose to honor sRGB
|
|
||||||
trace!("Removing iCCP chunk due to conflict with sRGB chunk");
|
|
||||||
png.aux_chunks.remove(iccp_idx);
|
|
||||||
} else if let Some(icc) = extract_icc(&png.aux_chunks[iccp_idx]) {
|
|
||||||
let intent = if may_replace_iccp {
|
|
||||||
srgb_rendering_intent(&icc)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
// sRGB-like profile can be replaced with an sRGB chunk with the same rendering intent
|
|
||||||
if let Some(intent) = intent {
|
|
||||||
trace!("Replacing iCCP chunk with equivalent sRGB chunk");
|
|
||||||
png.aux_chunks[iccp_idx] = Chunk {
|
|
||||||
name: *b"sRGB",
|
|
||||||
data: vec![intent],
|
|
||||||
};
|
|
||||||
} else if opts.idat_recoding {
|
|
||||||
// Try recompressing the profile
|
|
||||||
if let Ok(iccp) = construct_iccp(&icc, opts.deflate) {
|
|
||||||
let cur_len = png.aux_chunks[iccp_idx].data.len();
|
|
||||||
let new_len = iccp.data.len();
|
|
||||||
if new_len < cur_len {
|
|
||||||
debug!(
|
|
||||||
"Recompressed iCCP chunk: {} ({} bytes decrease)",
|
|
||||||
new_len,
|
|
||||||
cur_len - new_len
|
|
||||||
);
|
|
||||||
png.aux_chunks[iccp_idx] = iccp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Ensure we don't try to recompress frames with a predefined filter
|
||||||
// If the depth/color type has changed, some chunks may be invalid and should be dropped
|
debug_assert!(!matches!(filter, FilterStrategy::Predefined { .. }));
|
||||||
// While these could potentially be converted, they have no known use case today and are
|
png.frames
|
||||||
// generally more trouble than they're worth
|
.par_iter_mut()
|
||||||
let ihdr = &png.raw.ihdr;
|
.with_max_len(1)
|
||||||
if orig_ihdr.bit_depth != ihdr.bit_depth || orig_ihdr.color_type != ihdr.color_type {
|
.enumerate()
|
||||||
png.aux_chunks.retain(|c| {
|
.try_for_each(|(i, frame)| {
|
||||||
let invalid = &c.name == b"bKGD" || &c.name == b"sBIT" || &c.name == b"hIST";
|
if deadline.passed() {
|
||||||
if invalid {
|
return Ok(());
|
||||||
warn!(
|
}
|
||||||
"Removing {} chunk as it no longer matches the image data",
|
let mut ihdr = png.raw.ihdr.clone();
|
||||||
std::str::from_utf8(&c.name).unwrap()
|
ihdr.width = frame.width;
|
||||||
|
ihdr.height = frame.height;
|
||||||
|
let image = PngImage::new(ihdr, &frame.data)?;
|
||||||
|
let (filtered, _) = image.filter_image(filter.clone(), opts.optimize_alpha);
|
||||||
|
let max_size = Some(frame.data.len() - 1);
|
||||||
|
if let Ok(data) = opts.deflater.deflate(&filtered, max_size) {
|
||||||
|
debug!(
|
||||||
|
"Recompressed fdAT #{:<2}: {} ({} bytes decrease)",
|
||||||
|
i,
|
||||||
|
data.len(),
|
||||||
|
frame.data.len() - data.len()
|
||||||
);
|
);
|
||||||
|
frame.data = data;
|
||||||
}
|
}
|
||||||
!invalid
|
Ok(())
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find fdAT chunks and attempt to recompress them
|
|
||||||
// Note if there are multiple fdATs per frame then decompression will fail and nothing will change
|
|
||||||
let mut fdat: Vec<_> = png
|
|
||||||
.aux_chunks
|
|
||||||
.iter_mut()
|
|
||||||
.filter(|c| &c.name == b"fdAT")
|
|
||||||
.collect();
|
|
||||||
if opts.idat_recoding && !fdat.is_empty() {
|
|
||||||
let buffer_size = orig_ihdr.raw_data_size();
|
|
||||||
fdat.par_iter_mut()
|
|
||||||
.with_max_len(1)
|
|
||||||
.enumerate()
|
|
||||||
.for_each(|(i, c)| {
|
|
||||||
if deadline.passed() || c.data.len() <= 4 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Ok(mut data) = deflate::inflate(&c.data[4..], buffer_size).and_then(|data| {
|
|
||||||
let max_size = AtomicMin::new(Some(c.data.len() - 5));
|
|
||||||
opts.deflate.deflate(&data, &max_size)
|
|
||||||
}) {
|
|
||||||
debug!(
|
|
||||||
"Recompressed fdAT #{:<2}: {} ({} bytes decrease)",
|
|
||||||
i,
|
|
||||||
c.data.len(),
|
|
||||||
c.data.len() - 4 - data.len()
|
|
||||||
);
|
|
||||||
c.data.truncate(4);
|
|
||||||
c.data.append(&mut data);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if an image was already optimized prior to oxipng's operations
|
|
||||||
fn is_fully_optimized(original_size: usize, optimized_size: usize, opts: &Options) -> bool {
|
|
||||||
original_size <= optimized_size && !opts.force
|
|
||||||
}
|
|
||||||
|
|
||||||
fn copy_permissions(metadata_input: &Metadata, out_file: &File) -> PngResult<()> {
|
|
||||||
out_file
|
|
||||||
.set_permissions(metadata_input.permissions())
|
|
||||||
.map_err(|err_io| {
|
|
||||||
PngError::new(&format!(
|
|
||||||
"unable to set permissions for output file: {}",
|
|
||||||
err_io
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "filetime"))]
|
/// Check if an image was already optimized prior to oxipng's operations
|
||||||
fn copy_times(_: &Metadata, _: &Path) -> PngResult<()> {
|
const fn is_fully_optimized(original_size: usize, optimized_size: usize, opts: &Options) -> bool {
|
||||||
Ok(())
|
original_size <= optimized_size && !opts.force
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "filetime")]
|
|
||||||
fn copy_times(input_path_meta: &Metadata, out_path: &Path) -> PngResult<()> {
|
|
||||||
let atime = filetime::FileTime::from_last_access_time(input_path_meta);
|
|
||||||
let mtime = filetime::FileTime::from_last_modification_time(input_path_meta);
|
|
||||||
trace!(
|
|
||||||
"attempting to set file times: atime: {:?}, mtime: {:?}",
|
|
||||||
atime,
|
|
||||||
mtime
|
|
||||||
);
|
|
||||||
filetime::set_file_times(out_path, atime, mtime).map_err(|err_io| {
|
|
||||||
PngError::new(&format!(
|
|
||||||
"unable to set file times on {:?}: {}",
|
|
||||||
out_path, err_io
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
468
src/main.rs
|
|
@ -1,99 +1,172 @@
|
||||||
#![warn(trivial_casts, trivial_numeric_casts, unused_import_braces)]
|
|
||||||
#![deny(missing_debug_implementations, missing_copy_implementations)]
|
|
||||||
#![warn(clippy::expl_impl_clone_on_copy)]
|
|
||||||
#![warn(clippy::float_cmp_const)]
|
|
||||||
#![warn(clippy::linkedlist)]
|
|
||||||
#![warn(clippy::map_flatten)]
|
|
||||||
#![warn(clippy::match_same_arms)]
|
|
||||||
#![warn(clippy::mem_forget)]
|
|
||||||
#![warn(clippy::mut_mut)]
|
|
||||||
#![warn(clippy::mutex_integer)]
|
|
||||||
#![warn(clippy::needless_continue)]
|
|
||||||
#![warn(clippy::path_buf_push_overwrite)]
|
|
||||||
#![warn(clippy::range_plus_one)]
|
|
||||||
#![allow(clippy::cognitive_complexity)]
|
|
||||||
|
|
||||||
#[cfg(not(feature = "parallel"))]
|
#[cfg(not(feature = "parallel"))]
|
||||||
mod rayon;
|
mod rayon;
|
||||||
|
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
use std::num::NonZeroU8;
|
use std::num::NonZeroU64;
|
||||||
use std::{ffi::OsString, fs::DirBuilder, io::Write, path::PathBuf, process::exit, time::Duration};
|
use std::{
|
||||||
|
ffi::{OsStr, OsString},
|
||||||
|
fs::DirBuilder,
|
||||||
|
io::{IsTerminal, Write, stdout},
|
||||||
|
path::PathBuf,
|
||||||
|
process::ExitCode,
|
||||||
|
sync::atomic::{AtomicUsize, Ordering::AcqRel},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use clap::ArgMatches;
|
use clap::ArgMatches;
|
||||||
mod cli;
|
mod cli;
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use log::{error, warn, Level, LevelFilter};
|
use log::{Level, LevelFilter, error, warn};
|
||||||
use oxipng::{Deflaters, InFile, Options, OutFile, RowFilter, StripChunks};
|
#[cfg(feature = "zopfli")]
|
||||||
|
use oxipng::ZopfliOptions;
|
||||||
|
use oxipng::{
|
||||||
|
Deflater, FilterStrategy, InFile, OptimizationResult, Options, OutFile, PngError, StripChunks,
|
||||||
|
};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
|
||||||
use crate::cli::DISPLAY_CHUNKS;
|
use crate::cli::DISPLAY_CHUNKS;
|
||||||
|
|
||||||
fn main() {
|
fn main() -> ExitCode {
|
||||||
let matches = cli::build_command()
|
let matches = cli::build_command()
|
||||||
// Set the value parser for filters which isn't appropriate to do in the build_command function
|
// Set the value parser for filters which isn't appropriate to do in the build_command function
|
||||||
.mut_arg("filters", |arg| {
|
.mut_arg("filters", |arg| {
|
||||||
arg.value_parser(|x: &str| {
|
arg.value_parser(|x: &str| {
|
||||||
parse_numeric_range_opts(x, 0, RowFilter::LAST)
|
parse_numeric_range_opts(x, 0, 9).map_err(|_| "Invalid option for filters")
|
||||||
.map_err(|_| "Invalid option for filters")
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.after_help("Run `oxipng --help` to see full details of all options")
|
.after_help("Run `oxipng --help` to see full details of all options")
|
||||||
.after_long_help("")
|
.after_long_help("")
|
||||||
.get_matches_from(std::env::args());
|
.get_matches_from(std::env::args());
|
||||||
|
|
||||||
if matches.get_flag("backup") {
|
let (mut out_file, out_dir, opts) = match parse_opts_into_struct(&matches) {
|
||||||
eprintln!("The --backup flag is no longer supported. Please use --out or --dir to preserve your existing files.");
|
|
||||||
exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
let (out_file, out_dir, opts) = match parse_opts_into_struct(&matches) {
|
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
Err(x) => {
|
Err(x) => {
|
||||||
error!("{}", x);
|
error!("{x}");
|
||||||
exit(1)
|
return ExitCode::FAILURE;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let files = collect_files(
|
// Determine input and output
|
||||||
#[cfg(windows)]
|
let file_args = matches.get_many::<PathBuf>("files").unwrap().cloned();
|
||||||
matches
|
#[cfg(windows)]
|
||||||
.get_many::<PathBuf>("files")
|
let inputs: Vec<_> = file_args.flat_map(apply_glob_pattern).collect();
|
||||||
.unwrap()
|
#[cfg(not(windows))]
|
||||||
.cloned()
|
let inputs: Vec<_> = file_args.collect();
|
||||||
.flat_map(apply_glob_pattern)
|
let using_stdin = inputs.len() == 1 && inputs[0].to_str() == Some("-");
|
||||||
.collect(),
|
if using_stdin && out_dir.is_some() {
|
||||||
#[cfg(not(windows))]
|
error!("Cannot use --dir when reading from stdin.");
|
||||||
matches
|
return ExitCode::FAILURE;
|
||||||
.get_many::<PathBuf>("files")
|
}
|
||||||
.unwrap()
|
if using_stdin && matches!(out_file, OutFile::Path { path: None, .. }) {
|
||||||
.cloned()
|
out_file = OutFile::StdOut;
|
||||||
.collect(),
|
}
|
||||||
&out_dir,
|
let using_stdout = matches!(out_file, OutFile::StdOut);
|
||||||
&out_file,
|
let json = matches.get_flag("json");
|
||||||
matches.get_flag("recursive"),
|
if using_stdout && json {
|
||||||
true,
|
error!("Cannot use --json when writing to stdout.");
|
||||||
);
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
let success = files.into_par_iter().filter(|(input, output)| {
|
let files = if using_stdin {
|
||||||
match oxipng::optimize(input, output, &opts) {
|
vec![(InFile::StdIn, out_file)]
|
||||||
// For optimizing single files, this will return the correct exit code always.
|
} else {
|
||||||
// For recursive optimization, the correct choice is a bit subjective.
|
collect_files(
|
||||||
// We're choosing to return a 0 exit code if ANY file in the set
|
inputs,
|
||||||
// runs correctly.
|
&out_dir,
|
||||||
// The reason for this is that recursion may pick up files that are not
|
&out_file,
|
||||||
// PNG files, and return an error for them.
|
matches.get_flag("recursive"),
|
||||||
// We don't really want to return an error code for those files.
|
true,
|
||||||
Ok(_) => true,
|
)
|
||||||
Err(e) => {
|
};
|
||||||
error!("{}: {}", input, e);
|
|
||||||
false
|
let is_verbose = matches.get_count("verbose") > 0;
|
||||||
}
|
let print_summary = !matches.get_flag("quiet") && !using_stdout;
|
||||||
|
let print_progress = print_summary && !is_verbose && stdout().is_terminal();
|
||||||
|
let total_files = files.len();
|
||||||
|
let num_processed = AtomicUsize::new(0);
|
||||||
|
if print_progress {
|
||||||
|
print!("Files processed: 0/{}...", total_files);
|
||||||
|
stdout().flush().ok();
|
||||||
|
}
|
||||||
|
let process = |(input, output): &(InFile, OutFile)| {
|
||||||
|
let result = process_file(input, output, &opts);
|
||||||
|
if print_progress && matches!(result, OptimizationResult::Ok(_)) {
|
||||||
|
let value = num_processed.fetch_add(1, AcqRel) + 1;
|
||||||
|
print!("\rFiles processed: {}/{}...", value, total_files);
|
||||||
|
stdout().flush().ok();
|
||||||
}
|
}
|
||||||
});
|
result
|
||||||
|
};
|
||||||
|
let results: Vec<OptimizationResult> = if matches.get_flag("parallel-files") {
|
||||||
|
files.par_iter().map(process).collect()
|
||||||
|
} else {
|
||||||
|
files.iter().map(process).collect()
|
||||||
|
};
|
||||||
|
|
||||||
if success.count() == 0 {
|
// Collect stats
|
||||||
exit(1);
|
let mut num_succeeded = 0;
|
||||||
|
let mut num_not_optimized = 0;
|
||||||
|
let mut num_failed = 0;
|
||||||
|
let mut total_in: i64 = 0;
|
||||||
|
let mut total_out: i64 = 0;
|
||||||
|
for result in &results {
|
||||||
|
match result {
|
||||||
|
Ok((insize, outsize)) => {
|
||||||
|
num_succeeded += 1;
|
||||||
|
total_in += *insize as i64;
|
||||||
|
total_out += *outsize as i64;
|
||||||
|
if !opts.force && insize == outsize {
|
||||||
|
num_not_optimized += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(PngError::C2PAMetadataPreventsChanges | PngError::InflatedDataTooLong(_)) => {}
|
||||||
|
Err(_) => num_failed += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print results
|
||||||
|
if json {
|
||||||
|
json_output(&files, &results);
|
||||||
|
} else if print_summary {
|
||||||
|
let in_bytes = format_bytes(total_in, true);
|
||||||
|
let out_bytes = format_bytes(total_out, true);
|
||||||
|
let saved = total_in - total_out;
|
||||||
|
let saved_bytes = format_bytes(saved, false);
|
||||||
|
let percent = if total_in > 0 {
|
||||||
|
saved as f64 / total_in as f64 * 100_f64
|
||||||
|
} else {
|
||||||
|
0_f64
|
||||||
|
};
|
||||||
|
if is_verbose {
|
||||||
|
println!("--------------------");
|
||||||
|
}
|
||||||
|
println!("\rFiles processed: {num_succeeded}/{total_files} ");
|
||||||
|
println!("Input size: {}", in_bytes);
|
||||||
|
println!("Output size: {}", out_bytes);
|
||||||
|
println!("Total saved: {} ({:.2}%)", saved_bytes, percent);
|
||||||
|
if num_not_optimized == 1 {
|
||||||
|
println!("({num_not_optimized} file could not be optimized further)");
|
||||||
|
} else if num_not_optimized > 0 {
|
||||||
|
println!("({num_not_optimized} files could not be optimized further)");
|
||||||
|
}
|
||||||
|
if matches.get_flag("dry-run") {
|
||||||
|
println!("Dry run, no changes saved");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For optimizing single files, this will return the correct exit code always.
|
||||||
|
// For recursive optimization, the correct choice is a bit subjective.
|
||||||
|
// We're choosing to return a 0 exit code if ANY file in the set
|
||||||
|
// runs correctly.
|
||||||
|
// The reason for this is that recursion may pick up files that are not
|
||||||
|
// PNG files, and return an error for them.
|
||||||
|
// We don't really want to return an error code for those files.
|
||||||
|
if num_succeeded > 0 {
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
} else if num_failed > 0 {
|
||||||
|
ExitCode::FAILURE
|
||||||
|
} else {
|
||||||
|
ExitCode::from(3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,10 +178,8 @@ fn collect_files(
|
||||||
top_level: bool, //explicitly specify files
|
top_level: bool, //explicitly specify files
|
||||||
) -> Vec<(InFile, OutFile)> {
|
) -> Vec<(InFile, OutFile)> {
|
||||||
let mut in_out_pairs = Vec::new();
|
let mut in_out_pairs = Vec::new();
|
||||||
let allow_stdin = top_level && files.len() == 1;
|
|
||||||
for input in files {
|
for input in files {
|
||||||
let using_stdin = allow_stdin && input.to_str().map_or(false, |p| p == "-");
|
if input.is_dir() {
|
||||||
if !using_stdin && input.is_dir() {
|
|
||||||
if recursive {
|
if recursive {
|
||||||
match input.read_dir() {
|
match input.read_dir() {
|
||||||
Ok(dir) => {
|
Ok(dir) => {
|
||||||
|
|
@ -124,7 +195,16 @@ fn collect_files(
|
||||||
warn!("{} is a directory, skipping", input.display());
|
warn!("{} is a directory, skipping", input.display());
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
// Skip non png files if not given on top level
|
||||||
|
if !top_level && {
|
||||||
|
let extension = input.extension().map(OsStr::to_ascii_lowercase);
|
||||||
|
extension != Some(OsString::from("png")) && extension != Some(OsString::from("apng"))
|
||||||
|
} {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let out_file =
|
let out_file =
|
||||||
if let (Some(out_dir), &OutFile::Path { preserve_attrs, .. }) = (out_dir, out_file) {
|
if let (Some(out_dir), &OutFile::Path { preserve_attrs, .. }) = (out_dir, out_file) {
|
||||||
let path = Some(out_dir.join(input.file_name().unwrap()));
|
let path = Some(out_dir.join(input.file_name().unwrap()));
|
||||||
|
|
@ -135,19 +215,7 @@ fn collect_files(
|
||||||
} else {
|
} else {
|
||||||
(*out_file).clone()
|
(*out_file).clone()
|
||||||
};
|
};
|
||||||
let in_file = if using_stdin {
|
let in_file = InFile::Path(input);
|
||||||
InFile::StdIn
|
|
||||||
} else {
|
|
||||||
// Skip non png files if not given on top level
|
|
||||||
if !top_level && {
|
|
||||||
let extension = input.extension().map(|f| f.to_ascii_lowercase());
|
|
||||||
extension != Some(OsString::from("png"))
|
|
||||||
&& extension != Some(OsString::from("apng"))
|
|
||||||
} {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
InFile::Path(input)
|
|
||||||
};
|
|
||||||
in_out_pairs.push((in_file, out_file));
|
in_out_pairs.push((in_file, out_file));
|
||||||
}
|
}
|
||||||
in_out_pairs
|
in_out_pairs
|
||||||
|
|
@ -157,7 +225,8 @@ fn collect_files(
|
||||||
fn apply_glob_pattern(path: PathBuf) -> Vec<PathBuf> {
|
fn apply_glob_pattern(path: PathBuf) -> Vec<PathBuf> {
|
||||||
let matches = path
|
let matches = path
|
||||||
.to_str()
|
.to_str()
|
||||||
.and_then(|pattern| glob::glob(pattern).ok())
|
// Use MatchOptions::default() to disable case-sensitivity
|
||||||
|
.and_then(|pattern| glob::glob_with(pattern, glob::MatchOptions::default()).ok())
|
||||||
.map(|paths| paths.flatten().collect::<Vec<_>>());
|
.map(|paths| paths.flatten().collect::<Vec<_>>());
|
||||||
|
|
||||||
match matches {
|
match matches {
|
||||||
|
|
@ -171,8 +240,9 @@ fn parse_opts_into_struct(
|
||||||
) -> Result<(OutFile, Option<PathBuf>, Options), String> {
|
) -> Result<(OutFile, Option<PathBuf>, Options), String> {
|
||||||
let log_level = match matches.get_count("verbose") {
|
let log_level = match matches.get_count("verbose") {
|
||||||
_ if matches.get_flag("quiet") => LevelFilter::Off,
|
_ if matches.get_flag("quiet") => LevelFilter::Off,
|
||||||
0 => LevelFilter::Info,
|
0 => LevelFilter::Warn,
|
||||||
1 => LevelFilter::Debug,
|
1 => LevelFilter::Info,
|
||||||
|
2 => LevelFilter::Debug,
|
||||||
_ => LevelFilter::Trace,
|
_ => LevelFilter::Trace,
|
||||||
};
|
};
|
||||||
env_logger::builder()
|
env_logger::builder()
|
||||||
|
|
@ -181,7 +251,8 @@ fn parse_opts_into_struct(
|
||||||
match record.level() {
|
match record.level() {
|
||||||
Level::Error | Level::Warn => {
|
Level::Error | Level::Warn => {
|
||||||
let style = buf.default_level_style(record.level());
|
let style = buf.default_level_style(record.level());
|
||||||
writeln!(buf, "{style}{}{style:#}", record.args())
|
// Prepend carriage return to clear progress line
|
||||||
|
writeln!(buf, "\r{style}{}{style:#}", record.args())
|
||||||
}
|
}
|
||||||
// Leave info, debug and trace unstyled
|
// Leave info, debug and trace unstyled
|
||||||
_ => writeln!(buf, "{}", record.args()),
|
_ => writeln!(buf, "{}", record.args()),
|
||||||
|
|
@ -195,11 +266,39 @@ fn parse_opts_into_struct(
|
||||||
Some(level) => Options::from_preset(level.parse::<u8>().unwrap()),
|
Some(level) => Options::from_preset(level.parse::<u8>().unwrap()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(x) = matches.get_one::<IndexSet<u8>>("filters") {
|
// Get custom brute settings and rebuild the filter set to apply them
|
||||||
opts.filter.clear();
|
let mut brute_lines = matches.get_one::<usize>("brute-lines").copied();
|
||||||
for &f in x {
|
let mut brute_level = matches.get_one::<i64>("brute-level").map(|x| *x as u8);
|
||||||
opts.filter.insert(f.try_into().unwrap());
|
let mut new_filters = IndexSet::new();
|
||||||
|
for mut f in opts.filters.drain(..) {
|
||||||
|
if let FilterStrategy::Brute { num_lines, level } = &mut f {
|
||||||
|
*num_lines = brute_lines.unwrap_or(*num_lines);
|
||||||
|
*level = brute_level.unwrap_or(*level);
|
||||||
|
// If custom settings were not given, we still need to retain the default values
|
||||||
|
// from the preset so we can re-apply them if the filters are overridden below
|
||||||
|
brute_lines = Some(*num_lines);
|
||||||
|
brute_level = Some(*level);
|
||||||
}
|
}
|
||||||
|
new_filters.insert(f);
|
||||||
|
}
|
||||||
|
opts.filters = new_filters;
|
||||||
|
|
||||||
|
if let Some(x) = matches.get_one::<IndexSet<u8>>("filters") {
|
||||||
|
opts.filters = x
|
||||||
|
.iter()
|
||||||
|
.map(|&f| match f {
|
||||||
|
0..=4 => FilterStrategy::Basic(f.try_into().unwrap()),
|
||||||
|
5 => FilterStrategy::MinSum,
|
||||||
|
6 => FilterStrategy::Entropy,
|
||||||
|
7 => FilterStrategy::Bigrams,
|
||||||
|
8 => FilterStrategy::BigEnt,
|
||||||
|
9 => FilterStrategy::Brute {
|
||||||
|
num_lines: brute_lines.unwrap_or(3),
|
||||||
|
level: brute_level.unwrap_or(1),
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(&num) = matches.get_one::<u64>("timeout") {
|
if let Some(&num) = matches.get_one::<u64>("timeout") {
|
||||||
|
|
@ -209,9 +308,9 @@ fn parse_opts_into_struct(
|
||||||
let out_dir = if let Some(path) = matches.get_one::<PathBuf>("output_dir") {
|
let out_dir = if let Some(path) = matches.get_one::<PathBuf>("output_dir") {
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
match DirBuilder::new().recursive(true).create(path) {
|
match DirBuilder::new().recursive(true).create(path) {
|
||||||
Ok(_) => (),
|
Ok(()) => (),
|
||||||
Err(x) => return Err(format!("Could not create output directory {}", x)),
|
Err(x) => return Err(format!("Could not create output directory {x}")),
|
||||||
};
|
}
|
||||||
} else if !path.is_dir() {
|
} else if !path.is_dir() {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"{} is an existing file (not a directory), cannot create directory",
|
"{} is an existing file (not a directory), cannot create directory",
|
||||||
|
|
@ -223,7 +322,7 @@ fn parse_opts_into_struct(
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let out_file = if matches.get_flag("pretend") {
|
let out_file = if matches.get_flag("dry-run") {
|
||||||
OutFile::None
|
OutFile::None
|
||||||
} else if matches.get_flag("stdout") {
|
} else if matches.get_flag("stdout") {
|
||||||
OutFile::StdOut
|
OutFile::StdOut
|
||||||
|
|
@ -247,6 +346,8 @@ fn parse_opts_into_struct(
|
||||||
|
|
||||||
opts.fix_errors = matches.get_flag("fix");
|
opts.fix_errors = matches.get_flag("fix");
|
||||||
|
|
||||||
|
opts.max_decompressed_size = matches.get_one::<u64>("max-size").map(|&x| x as usize);
|
||||||
|
|
||||||
opts.bit_depth_reduction = !matches.get_flag("no-bit-reduction");
|
opts.bit_depth_reduction = !matches.get_flag("no-bit-reduction");
|
||||||
|
|
||||||
opts.color_type_reduction = !matches.get_flag("no-color-reduction");
|
opts.color_type_reduction = !matches.get_flag("no-color-reduction");
|
||||||
|
|
@ -266,10 +367,10 @@ fn parse_opts_into_struct(
|
||||||
opts.idat_recoding = !matches.get_flag("no-recoding");
|
opts.idat_recoding = !matches.get_flag("no-recoding");
|
||||||
|
|
||||||
if let Some(x) = matches.get_one::<String>("interlace") {
|
if let Some(x) = matches.get_one::<String>("interlace") {
|
||||||
opts.interlace = if x == "keep" {
|
opts.interlace = match x.as_str() {
|
||||||
None
|
"off" | "0" => Some(false),
|
||||||
} else {
|
"on" | "1" => Some(true),
|
||||||
x.parse::<u8>().unwrap().try_into().ok()
|
_ => None, // keep
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -286,9 +387,9 @@ fn parse_opts_into_struct(
|
||||||
})
|
})
|
||||||
.collect::<Result<IndexSet<_>, _>>()?;
|
.collect::<Result<IndexSet<_>, _>>()?;
|
||||||
if keep_display {
|
if keep_display {
|
||||||
names.extend(DISPLAY_CHUNKS.iter().cloned());
|
names.extend(DISPLAY_CHUNKS.iter().copied());
|
||||||
}
|
}
|
||||||
opts.strip = StripChunks::Keep(names)
|
opts.strip = StripChunks::Keep(names);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(strip) = matches.get_one::<String>("strip") {
|
if let Some(strip) = matches.get_one::<String>("strip") {
|
||||||
|
|
@ -310,7 +411,7 @@ fn parse_opts_into_struct(
|
||||||
}
|
}
|
||||||
let name = parse_chunk_name(x)?;
|
let name = parse_chunk_name(x)?;
|
||||||
if FORBIDDEN_CHUNKS.contains(&name) {
|
if FORBIDDEN_CHUNKS.contains(&name) {
|
||||||
return Err(format!("{} chunk is not allowed to be stripped", x));
|
return Err(format!("{x} chunk is not allowed to be stripped"));
|
||||||
}
|
}
|
||||||
Ok(name)
|
Ok(name)
|
||||||
})
|
})
|
||||||
|
|
@ -323,15 +424,32 @@ fn parse_opts_into_struct(
|
||||||
opts.strip = StripChunks::Safe;
|
opts.strip = StripChunks::Safe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "zopfli")]
|
||||||
if matches.get_flag("zopfli") {
|
if matches.get_flag("zopfli") {
|
||||||
#[cfg(feature = "zopfli")]
|
let iteration_count = *matches.get_one::<NonZeroU64>("iterations").unwrap();
|
||||||
if let Some(iterations) = NonZeroU8::new(15) {
|
let iterations_without_improvement = *matches
|
||||||
opts.deflate = Deflaters::Zopfli { iterations };
|
.get_one::<NonZeroU64>("iterations-without-improvement")
|
||||||
}
|
.unwrap_or(&NonZeroU64::MAX);
|
||||||
} else if let Deflaters::Libdeflater { compression } = &mut opts.deflate {
|
|
||||||
if let Some(x) = matches.get_one::<i64>("compression") {
|
if iterations_without_improvement > iteration_count
|
||||||
*compression = *x as u8;
|
&& iterations_without_improvement != NonZeroU64::MAX
|
||||||
|
{
|
||||||
|
warn!(
|
||||||
|
"--ziwi ({}) is higher than --zi ({}) and will never be reached.",
|
||||||
|
iterations_without_improvement, iteration_count
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
opts.deflater = Deflater::Zopfli(ZopfliOptions {
|
||||||
|
iteration_count,
|
||||||
|
iterations_without_improvement,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let (Deflater::Libdeflater { compression }, Some(x)) =
|
||||||
|
(&mut opts.deflater, matches.get_one::<i64>("compression"))
|
||||||
|
{
|
||||||
|
*compression = *x as u8;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "parallel")]
|
#[cfg(feature = "parallel")]
|
||||||
|
|
@ -349,7 +467,7 @@ fn parse_chunk_name(name: &str) -> Result<[u8; 4], String> {
|
||||||
name.trim()
|
name.trim()
|
||||||
.as_bytes()
|
.as_bytes()
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|_| format!("Invalid chunk name {}", name))
|
.map_err(|_| format!("Invalid chunk name {name}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_numeric_range_opts(
|
fn parse_numeric_range_opts(
|
||||||
|
|
@ -404,3 +522,133 @@ fn parse_numeric_range_opts(
|
||||||
|
|
||||||
Err(ERROR_MESSAGE.to_owned())
|
Err(ERROR_MESSAGE.to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn process_file(input: &InFile, output: &OutFile, opts: &Options) -> OptimizationResult {
|
||||||
|
if let (Some(max_size), InFile::Path(path)) = (opts.max_decompressed_size, input) {
|
||||||
|
if path.metadata().is_ok_and(|m| m.len() > max_size as u64) {
|
||||||
|
warn!("{input}: Skipped: File exceeds the maximum size ({max_size} bytes)");
|
||||||
|
return Err(PngError::InflatedDataTooLong(max_size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = oxipng::optimize(input, output, opts);
|
||||||
|
match &result {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e @ PngError::C2PAMetadataPreventsChanges | e @ PngError::InflatedDataTooLong(_)) => {
|
||||||
|
warn!("{input}: Skipped: {e}");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("{input}: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write optimization results as json.
|
||||||
|
/// ```
|
||||||
|
/// {
|
||||||
|
/// "results": [
|
||||||
|
/// {
|
||||||
|
/// "input": string,
|
||||||
|
/// "status": "success",
|
||||||
|
/// "output": string|null,
|
||||||
|
/// "insize": number,
|
||||||
|
/// "outsize": number
|
||||||
|
/// },
|
||||||
|
/// {
|
||||||
|
/// "input": string,
|
||||||
|
/// "status": "error",
|
||||||
|
/// "error": string
|
||||||
|
/// }
|
||||||
|
/// ]
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
fn json_output(files: &[(InFile, OutFile)], results: &[OptimizationResult]) {
|
||||||
|
print!(r#"{{"results":["#);
|
||||||
|
let mut first = true;
|
||||||
|
results
|
||||||
|
.iter()
|
||||||
|
.zip(files)
|
||||||
|
.for_each(|(result, (input, output))| {
|
||||||
|
if !first {
|
||||||
|
print!(",");
|
||||||
|
}
|
||||||
|
print!(r#"{{"input":"{}","#, json_escape(&input.to_string()));
|
||||||
|
match result {
|
||||||
|
Ok((insize, outsize)) => {
|
||||||
|
let outpath = match output {
|
||||||
|
OutFile::None => "null".to_owned(),
|
||||||
|
OutFile::Path { path: None, .. } => {
|
||||||
|
format!(r#""{}""#, json_escape(&input.to_string()))
|
||||||
|
}
|
||||||
|
OutFile::Path { path: Some(p), .. } => {
|
||||||
|
format!(r#""{}""#, json_escape(&p.display().to_string()))
|
||||||
|
}
|
||||||
|
OutFile::StdOut => unreachable!(),
|
||||||
|
};
|
||||||
|
print!(
|
||||||
|
r#""status":"success","output":{},"insize":{},"outsize":{}}}"#,
|
||||||
|
outpath, insize, outsize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
print!(
|
||||||
|
r#""status":"error","error":"{}"}}"#,
|
||||||
|
json_escape(&e.to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
});
|
||||||
|
print!("]}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_escape(string: &str) -> String {
|
||||||
|
string
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("\"", "\\\"")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\t", "\\t")
|
||||||
|
.replace("\r", "\\r")
|
||||||
|
.replace("\x08", "\\b")
|
||||||
|
.replace("\x0c", "\\f")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format byte counts as IEC units to 3 significant figures.
|
||||||
|
fn format_bytes(count: i64, include_raw: bool) -> String {
|
||||||
|
const K: i64 = 1 << 10;
|
||||||
|
const M: i64 = 1 << 20;
|
||||||
|
const G: i64 = 1 << 30;
|
||||||
|
fn format_3sf(value: f64) -> String {
|
||||||
|
match value.abs() {
|
||||||
|
..9.995 => format!("{:.2}", value),
|
||||||
|
9.995..99.95 => format!("{:.1}", value),
|
||||||
|
_ => format!("{:.0}", value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let formatted = match count.abs() {
|
||||||
|
..K => format!("{} bytes", count),
|
||||||
|
K..M => format!("{} KiB", format_3sf(count as f64 / K as f64)),
|
||||||
|
M..G => format!("{} MiB", format_3sf(count as f64 / M as f64)),
|
||||||
|
_ => format!("{} GiB", format_3sf(count as f64 / G as f64)),
|
||||||
|
};
|
||||||
|
if include_raw && count.abs() >= K {
|
||||||
|
format!("{} ({} bytes)", formatted, count)
|
||||||
|
} else {
|
||||||
|
formatted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::format_bytes;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_bytes() {
|
||||||
|
assert_eq!(format_bytes(1023, false), "1023 bytes");
|
||||||
|
assert_eq!(format_bytes(800_000, false), "781 KiB");
|
||||||
|
assert_eq!(format_bytes(12_500_000, false), "11.9 MiB");
|
||||||
|
assert_eq!(format_bytes(2_000_000_000, false), "1.86 GiB");
|
||||||
|
assert_eq!(format_bytes(-1024, false), "-1.00 KiB");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
169
src/options.rs
|
|
@ -4,11 +4,13 @@ use std::{
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use indexmap::{indexset, IndexSet};
|
use indexmap::{IndexSet, indexset};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
|
|
||||||
use crate::{deflate::Deflaters, filters::RowFilter, headers::StripChunks, interlace::Interlacing};
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum OutFile {
|
pub enum OutFile {
|
||||||
/// Don't actually write any output, just calculate the best results.
|
/// Don't actually write any output, just calculate the best results.
|
||||||
|
|
@ -29,16 +31,18 @@ impl OutFile {
|
||||||
/// Construct a new `OutFile` with the given path.
|
/// Construct a new `OutFile` with the given path.
|
||||||
///
|
///
|
||||||
/// This is a convenience method for `OutFile::Path { path: Some(path), preserve_attrs: false }`.
|
/// This is a convenience method for `OutFile::Path { path: Some(path), preserve_attrs: false }`.
|
||||||
pub fn from_path(path: PathBuf) -> Self {
|
#[must_use]
|
||||||
OutFile::Path {
|
pub const fn from_path(path: PathBuf) -> Self {
|
||||||
|
Self::Path {
|
||||||
path: Some(path),
|
path: Some(path),
|
||||||
preserve_attrs: false,
|
preserve_attrs: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn path(&self) -> Option<&Path> {
|
pub fn path(&self) -> Option<&Path> {
|
||||||
match *self {
|
match *self {
|
||||||
OutFile::Path {
|
Self::Path {
|
||||||
path: Some(ref p), ..
|
path: Some(ref p), ..
|
||||||
} => Some(p.as_path()),
|
} => Some(p.as_path()),
|
||||||
_ => None,
|
_ => None,
|
||||||
|
|
@ -46,7 +50,8 @@ impl OutFile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where to read images from
|
/// 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)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum InFile {
|
pub enum InFile {
|
||||||
Path(PathBuf),
|
Path(PathBuf),
|
||||||
|
|
@ -54,10 +59,11 @@ pub enum InFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InFile {
|
impl InFile {
|
||||||
|
#[must_use]
|
||||||
pub fn path(&self) -> Option<&Path> {
|
pub fn path(&self) -> Option<&Path> {
|
||||||
match *self {
|
match *self {
|
||||||
InFile::Path(ref p) => Some(p.as_path()),
|
Self::Path(ref p) => Some(p.as_path()),
|
||||||
InFile::StdIn => None,
|
Self::StdIn => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -65,15 +71,15 @@ impl InFile {
|
||||||
impl fmt::Display for InFile {
|
impl fmt::Display for InFile {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match *self {
|
match *self {
|
||||||
InFile::Path(ref p) => write!(f, "{}", p.display()),
|
Self::Path(ref p) => write!(f, "{}", p.display()),
|
||||||
InFile::StdIn => f.write_str("stdin"),
|
Self::StdIn => f.write_str("stdin"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Into<PathBuf>> From<T> for InFile {
|
impl<T: Into<PathBuf>> From<T> for InFile {
|
||||||
fn from(s: T) -> Self {
|
fn from(s: T) -> Self {
|
||||||
InFile::Path(s.into())
|
Self::Path(s.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,19 +94,20 @@ pub struct Options {
|
||||||
///
|
///
|
||||||
/// Default: `false`
|
/// Default: `false`
|
||||||
pub force: bool,
|
pub force: bool,
|
||||||
/// Which RowFilters to try on the file
|
/// Which `FilterStrategy` to try on the file
|
||||||
///
|
///
|
||||||
/// Default: `None,Sub,Entropy,Bigrams`
|
/// Default: `None,Sub,Entropy,Bigrams`
|
||||||
pub filter: IndexSet<RowFilter>,
|
pub filters: IndexSet<FilterStrategy>,
|
||||||
/// Whether to change the interlacing type of the file.
|
/// Whether to change the interlacing of the file.
|
||||||
///
|
///
|
||||||
/// `None` will not change the current interlacing type.
|
/// - `None` will not change the current interlacing.
|
||||||
|
/// - `Some(x)` will turn interlacing on or off.
|
||||||
///
|
///
|
||||||
/// `Some(x)` will change the file to interlacing mode `x`.
|
/// Default: `Some(false)`
|
||||||
///
|
pub interlace: Option<bool>,
|
||||||
/// Default: `Some(Interlacing::None)`
|
|
||||||
pub interlace: Option<Interlacing>,
|
|
||||||
/// Whether to allow transparent pixels to be altered to improve compression.
|
/// Whether to allow transparent pixels to be altered to improve compression.
|
||||||
|
///
|
||||||
|
/// Default: `false`
|
||||||
pub optimize_alpha: bool,
|
pub optimize_alpha: bool,
|
||||||
/// Whether to attempt bit depth reduction
|
/// Whether to attempt bit depth reduction
|
||||||
///
|
///
|
||||||
|
|
@ -133,23 +140,31 @@ pub struct Options {
|
||||||
///
|
///
|
||||||
/// Default: `None`
|
/// Default: `None`
|
||||||
pub strip: StripChunks,
|
pub strip: StripChunks,
|
||||||
/// Which DEFLATE algorithm to use
|
/// Which DEFLATE (zlib) algorithm to use
|
||||||
|
#[cfg_attr(feature = "zopfli", doc = "(e.g. Zopfli)")]
|
||||||
///
|
///
|
||||||
/// Default: `Libdeflater`
|
/// Default: `Libdeflater`
|
||||||
pub deflate: Deflaters,
|
pub deflater: Deflater,
|
||||||
/// Whether to use fast evaluation to pick the best filter
|
/// Whether to use fast evaluation to pick the best filter
|
||||||
///
|
///
|
||||||
/// Default: `true`
|
/// Default: `true`
|
||||||
pub fast_evaluation: bool,
|
pub fast_evaluation: bool,
|
||||||
|
|
||||||
/// Maximum amount of time to spend on optimizations.
|
/// Maximum amount of time to spend on optimizations.
|
||||||
/// Further potential optimizations are skipped if the timeout is exceeded.
|
/// Further potential optimizations are skipped if the timeout is exceeded.
|
||||||
|
///
|
||||||
|
/// Default: `None`
|
||||||
pub timeout: Option<Duration>,
|
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 {
|
impl Options {
|
||||||
pub fn from_preset(level: u8) -> Options {
|
#[must_use]
|
||||||
let opts = Options::default();
|
pub fn from_preset(level: u8) -> Self {
|
||||||
|
let opts = Self::default();
|
||||||
match level {
|
match level {
|
||||||
0 => opts.apply_preset_0(),
|
0 => opts.apply_preset_0(),
|
||||||
1 => opts.apply_preset_1(),
|
1 => opts.apply_preset_1(),
|
||||||
|
|
@ -165,77 +180,112 @@ impl Options {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn max_compression() -> Options {
|
#[must_use]
|
||||||
Options::from_preset(6)
|
pub fn max_compression() -> Self {
|
||||||
|
Self::from_preset(6)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The following methods make assumptions that they are operating
|
// The following methods make assumptions that they are operating
|
||||||
// on an `Options` struct generated by the `default` method.
|
// on an `Options` struct generated by the `default` method.
|
||||||
fn apply_preset_0(mut self) -> Self {
|
fn apply_preset_0(mut self) -> Self {
|
||||||
self.filter.clear();
|
self.filters.clear();
|
||||||
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
|
self.deflater = Deflater::Libdeflater { compression: 5 };
|
||||||
*compression = 5;
|
|
||||||
}
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_preset_1(mut self) -> Self {
|
fn apply_preset_1(mut self) -> Self {
|
||||||
self.filter.clear();
|
self.filters.clear();
|
||||||
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
|
self.deflater = Deflater::Libdeflater { compression: 10 };
|
||||||
*compression = 10;
|
|
||||||
}
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_preset_2(self) -> Self {
|
const fn apply_preset_2(self) -> Self {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_preset_3(mut self) -> Self {
|
fn apply_preset_3(mut self) -> Self {
|
||||||
self.fast_evaluation = false;
|
self.fast_evaluation = false;
|
||||||
self.filter = indexset! {
|
self.filters = indexset! {
|
||||||
RowFilter::None,
|
FilterStrategy::NONE,
|
||||||
RowFilter::Bigrams,
|
FilterStrategy::Bigrams,
|
||||||
RowFilter::BigEnt,
|
FilterStrategy::BigEnt,
|
||||||
RowFilter::Brute
|
FilterStrategy::Brute {
|
||||||
|
num_lines: 3,
|
||||||
|
level: 1,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_preset_4(mut self) -> Self {
|
fn apply_preset_4(mut self) -> Self {
|
||||||
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
|
self.fast_evaluation = false;
|
||||||
*compression = 12;
|
self.filters = indexset! {
|
||||||
}
|
FilterStrategy::NONE,
|
||||||
self.apply_preset_3()
|
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 {
|
fn apply_preset_5(mut self) -> Self {
|
||||||
self.fast_evaluation = false;
|
self.fast_evaluation = false;
|
||||||
self.filter.insert(RowFilter::Up);
|
self.filters = indexset! {
|
||||||
self.filter.insert(RowFilter::MinSum);
|
FilterStrategy::NONE,
|
||||||
self.filter.insert(RowFilter::BigEnt);
|
FilterStrategy::SUB,
|
||||||
self.filter.insert(RowFilter::Brute);
|
FilterStrategy::UP,
|
||||||
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
|
FilterStrategy::MinSum,
|
||||||
*compression = 12;
|
FilterStrategy::Entropy,
|
||||||
}
|
FilterStrategy::Bigrams,
|
||||||
|
FilterStrategy::BigEnt,
|
||||||
|
FilterStrategy::Brute {
|
||||||
|
num_lines: 4,
|
||||||
|
level: 4,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
self.deflater = Deflater::Libdeflater { compression: 12 };
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_preset_6(mut self) -> Self {
|
fn apply_preset_6(mut self) -> Self {
|
||||||
self.filter.insert(RowFilter::Average);
|
self.fast_evaluation = false;
|
||||||
self.filter.insert(RowFilter::Paeth);
|
self.filters = indexset! {
|
||||||
self.apply_preset_5()
|
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 {
|
impl Default for Options {
|
||||||
fn default() -> Options {
|
fn default() -> Self {
|
||||||
// Default settings based on -o 2 from the CLI interface
|
// Default settings based on -o 2 from the CLI interface
|
||||||
Options {
|
Self {
|
||||||
fix_errors: false,
|
fix_errors: false,
|
||||||
force: false,
|
force: false,
|
||||||
filter: indexset! {RowFilter::None, RowFilter::Sub, RowFilter::Entropy, RowFilter::Bigrams},
|
filters: indexset! {
|
||||||
interlace: Some(Interlacing::None),
|
FilterStrategy::NONE,
|
||||||
|
FilterStrategy::SUB,
|
||||||
|
FilterStrategy::Entropy,
|
||||||
|
FilterStrategy::Bigrams
|
||||||
|
},
|
||||||
|
interlace: Some(false),
|
||||||
optimize_alpha: false,
|
optimize_alpha: false,
|
||||||
bit_depth_reduction: true,
|
bit_depth_reduction: true,
|
||||||
color_type_reduction: true,
|
color_type_reduction: true,
|
||||||
|
|
@ -244,9 +294,10 @@ impl Default for Options {
|
||||||
idat_recoding: true,
|
idat_recoding: true,
|
||||||
scale_16: false,
|
scale_16: false,
|
||||||
strip: StripChunks::None,
|
strip: StripChunks::None,
|
||||||
deflate: Deflaters::Libdeflater { compression: 11 },
|
deflater: Deflater::Libdeflater { compression: 11 },
|
||||||
fast_evaluation: true,
|
fast_evaluation: true,
|
||||||
timeout: None,
|
timeout: None,
|
||||||
|
max_decompressed_size: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
333
src/png/mod.rs
|
|
@ -1,35 +1,24 @@
|
||||||
use std::{
|
use std::{fs, path::Path, sync::Arc};
|
||||||
fs::File,
|
|
||||||
io::{BufReader, Read, Write},
|
|
||||||
path::Path,
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use bitvec::bitarr;
|
|
||||||
use libdeflater::{CompressionLvl, Compressor};
|
use libdeflater::{CompressionLvl, Compressor};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use rgb::ComponentSlice;
|
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
Options, PngResult,
|
||||||
|
apng::*,
|
||||||
colors::{BitDepth, ColorType},
|
colors::{BitDepth, ColorType},
|
||||||
deflate,
|
deflate,
|
||||||
error::PngError,
|
error::PngError,
|
||||||
filters::*,
|
filters::*,
|
||||||
headers::*,
|
headers::*,
|
||||||
interlace::{deinterlace_image, interlace_image, Interlacing},
|
interlace::{deinterlace_image, interlace_image},
|
||||||
Options,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) mod scan_lines;
|
pub(crate) mod scan_lines;
|
||||||
|
|
||||||
use self::scan_lines::ScanLines;
|
use self::scan_lines::ScanLines;
|
||||||
|
|
||||||
/// Compression level to use for the Brute filter strategy
|
|
||||||
const BRUTE_LEVEL: i32 = 1; // 1 is fastest, 2-4 are not useful, 5 is slower but more effective
|
|
||||||
/// Number of lines to compress with the Brute filter strategy
|
|
||||||
const BRUTE_LINES: usize = 4; // Values over 8 are generally not useful
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PngImage {
|
pub struct PngImage {
|
||||||
/// The headers stored in the IHDR chunk
|
/// The headers stored in the IHDR chunk
|
||||||
|
|
@ -47,44 +36,25 @@ pub struct PngData {
|
||||||
pub idat_data: Vec<u8>,
|
pub idat_data: Vec<u8>,
|
||||||
/// All non-critical chunks from the PNG are stored here
|
/// All non-critical chunks from the PNG are stored here
|
||||||
pub aux_chunks: Vec<Chunk>,
|
pub aux_chunks: Vec<Chunk>,
|
||||||
|
/// APNG frames
|
||||||
|
pub frames: Vec<Frame>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PngData {
|
impl PngData {
|
||||||
/// Create a new `PngData` struct by opening a file
|
/// Create a new `PngData` struct by opening a file
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn new(filepath: &Path, opts: &Options) -> Result<Self, PngError> {
|
pub fn new(filepath: &Path, opts: &Options) -> PngResult<Self> {
|
||||||
let byte_data = Self::read_file(filepath)?;
|
let byte_data = Self::read_file(filepath)?;
|
||||||
|
|
||||||
Self::from_slice(&byte_data, opts)
|
Self::from_slice(&byte_data, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_file(filepath: &Path) -> Result<Vec<u8>, PngError> {
|
pub fn read_file(filepath: &Path) -> PngResult<Vec<u8>> {
|
||||||
let file = match File::open(filepath) {
|
fs::read(filepath).map_err(|e| PngError::ReadFailed(filepath.display().to_string(), e))
|
||||||
Ok(f) => f,
|
|
||||||
Err(_) => return Err(PngError::new("Failed to open file for reading")),
|
|
||||||
};
|
|
||||||
let file_len = file.metadata().map(|m| m.len() as usize).unwrap_or(0);
|
|
||||||
let mut reader = BufReader::new(file);
|
|
||||||
// Check file for PNG header
|
|
||||||
let mut header = [0; 8];
|
|
||||||
if reader.read_exact(&mut header).is_err() {
|
|
||||||
return Err(PngError::new("Not a PNG file: too small"));
|
|
||||||
}
|
|
||||||
if !file_header_is_valid(&header) {
|
|
||||||
return Err(PngError::new("Invalid PNG header detected"));
|
|
||||||
}
|
|
||||||
// Read raw png data into memory
|
|
||||||
let mut byte_data: Vec<u8> = Vec::with_capacity(file_len);
|
|
||||||
byte_data.extend_from_slice(&header);
|
|
||||||
match reader.read_to_end(&mut byte_data) {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(_) => return Err(PngError::new("Failed to read from file")),
|
|
||||||
}
|
|
||||||
Ok(byte_data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new `PngData` struct by reading a slice
|
/// Create a new `PngData` struct by reading a slice
|
||||||
pub fn from_slice(byte_data: &[u8], opts: &Options) -> Result<Self, PngError> {
|
pub fn from_slice(byte_data: &[u8], opts: &Options) -> PngResult<Self> {
|
||||||
let mut byte_offset: usize = 0;
|
let mut byte_offset: usize = 0;
|
||||||
// Test that png header is valid
|
// Test that png header is valid
|
||||||
let header = byte_data.get(0..8).ok_or(PngError::TruncatedData)?;
|
let header = byte_data.get(0..8).ok_or(PngError::TruncatedData)?;
|
||||||
|
|
@ -97,6 +67,8 @@ impl PngData {
|
||||||
let mut idat_data: Vec<u8> = Vec::new();
|
let mut idat_data: Vec<u8> = Vec::new();
|
||||||
let mut key_chunks: FxHashMap<[u8; 4], Vec<u8>> = FxHashMap::default();
|
let mut key_chunks: FxHashMap<[u8; 4], Vec<u8>> = FxHashMap::default();
|
||||||
let mut aux_chunks: Vec<Chunk> = Vec::new();
|
let mut aux_chunks: Vec<Chunk> = Vec::new();
|
||||||
|
let mut frames: Vec<Frame> = Vec::new();
|
||||||
|
let mut sequence_number = 0;
|
||||||
while let Some(chunk) = parse_next_chunk(byte_data, &mut byte_offset, opts.fix_errors)? {
|
while let Some(chunk) = parse_next_chunk(byte_data, &mut byte_offset, opts.fix_errors)? {
|
||||||
match &chunk.name {
|
match &chunk.name {
|
||||||
b"IDAT" => {
|
b"IDAT" => {
|
||||||
|
|
@ -105,25 +77,53 @@ impl PngData {
|
||||||
aux_chunks.push(Chunk {
|
aux_chunks.push(Chunk {
|
||||||
name: chunk.name,
|
name: chunk.name,
|
||||||
data: Vec::new(),
|
data: Vec::new(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
idat_data.extend_from_slice(chunk.data);
|
idat_data.extend_from_slice(chunk.data);
|
||||||
}
|
}
|
||||||
b"IHDR" | b"PLTE" | b"tRNS" => {
|
b"IHDR" | b"PLTE" | b"tRNS" => {
|
||||||
key_chunks.insert(chunk.name, chunk.data.to_owned());
|
key_chunks.insert(chunk.name, chunk.data.to_owned());
|
||||||
}
|
}
|
||||||
_ => {
|
_ if opts.strip.keep(&chunk.name) => {
|
||||||
if opts.strip.keep(&chunk.name) {
|
if chunk.is_c2pa() {
|
||||||
aux_chunks.push(Chunk {
|
// StripChunks::None is the default value, so to keep optimizing by default,
|
||||||
name: chunk.name,
|
// interpret it as stripping the C2PA metadata.
|
||||||
data: chunk.data.to_owned(),
|
// The C2PA metadata is invalidated if the file changes, so it shouldn't be kept.
|
||||||
})
|
if opts.strip == StripChunks::None {
|
||||||
} else if chunk.name == *b"acTL" {
|
continue;
|
||||||
warn!(
|
}
|
||||||
"Stripping animation data from APNG - image will become standard PNG"
|
return Err(PngError::C2PAMetadataPreventsChanges);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
if chunk.name == *b"fcTL" || chunk.name == *b"fdAT" {
|
||||||
|
// Validate the sequence number
|
||||||
|
if read_be_u32(&chunk.data[0..4]) != sequence_number {
|
||||||
|
return Err(PngError::APNGOutOfOrder);
|
||||||
|
}
|
||||||
|
sequence_number += 1;
|
||||||
|
if chunk.name == *b"fcTL" && !idat_data.is_empty() {
|
||||||
|
// Only create a Frame if it's after the IDAT (else store it as an aux chunk)
|
||||||
|
frames.push(Frame::from_fctl_data(chunk.data)?);
|
||||||
|
continue;
|
||||||
|
} else if chunk.name == *b"fdAT" {
|
||||||
|
// Append the data to the last frame
|
||||||
|
frames
|
||||||
|
.last_mut()
|
||||||
|
.ok_or(PngError::APNGOutOfOrder)?
|
||||||
|
.data
|
||||||
|
.extend_from_slice(&chunk.data[4..]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Regular ancillary chunk
|
||||||
|
aux_chunks.push(Chunk {
|
||||||
|
name: chunk.name,
|
||||||
|
data: chunk.data.to_owned(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
b"acTL" => {
|
||||||
|
warn!("Stripping animation data from APNG - image will become standard PNG");
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,64 +131,57 @@ impl PngData {
|
||||||
if idat_data.is_empty() {
|
if idat_data.is_empty() {
|
||||||
return Err(PngError::ChunkMissing("IDAT"));
|
return Err(PngError::ChunkMissing("IDAT"));
|
||||||
}
|
}
|
||||||
let ihdr_chunk = match key_chunks.remove(b"IHDR") {
|
let Some(ihdr_chunk) = key_chunks.remove(b"IHDR") else {
|
||||||
Some(ihdr) => ihdr,
|
return Err(PngError::ChunkMissing("IHDR"));
|
||||||
None => return Err(PngError::ChunkMissing("IHDR")),
|
|
||||||
};
|
};
|
||||||
let ihdr = parse_ihdr_chunk(
|
let ihdr = parse_ihdr_chunk(
|
||||||
&ihdr_chunk,
|
&ihdr_chunk,
|
||||||
key_chunks.remove(b"PLTE"),
|
key_chunks.remove(b"PLTE"),
|
||||||
key_chunks.remove(b"tRNS"),
|
key_chunks.remove(b"tRNS"),
|
||||||
)?;
|
)?;
|
||||||
let raw_data = deflate::inflate(idat_data.as_ref(), ihdr.raw_data_size())?;
|
if let Some(max) = opts.max_decompressed_size {
|
||||||
|
if ihdr.raw_data_size() > max {
|
||||||
// Reject files with incorrect width/height or truncated data
|
return Err(PngError::InflatedDataTooLong(max));
|
||||||
if raw_data.len() != ihdr.raw_data_size() {
|
}
|
||||||
return Err(PngError::TruncatedData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut raw = PngImage {
|
let raw = PngImage::new(ihdr, &idat_data)?;
|
||||||
ihdr,
|
|
||||||
data: raw_data,
|
|
||||||
};
|
|
||||||
raw.data = raw.unfilter_image()?;
|
|
||||||
// Return the PngData
|
// Return the PngData
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
idat_data,
|
idat_data,
|
||||||
raw: Arc::new(raw),
|
raw: Arc::new(raw),
|
||||||
aux_chunks,
|
aux_chunks,
|
||||||
|
frames,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return an estimate of the output size which can help with evaluation of very small data
|
|
||||||
pub fn estimated_output_size(&self) -> usize {
|
|
||||||
self.idat_data.len() + self.raw.key_chunks_size()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format the `PngData` struct into a valid PNG bytestream
|
/// Format the `PngData` struct into a valid PNG bytestream
|
||||||
|
#[must_use]
|
||||||
pub fn output(&self) -> Vec<u8> {
|
pub fn output(&self) -> Vec<u8> {
|
||||||
// PNG header
|
// PNG header
|
||||||
let mut output = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
|
let mut output = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
|
||||||
// IHDR
|
// IHDR
|
||||||
let mut ihdr_data = Vec::with_capacity(13);
|
let mut ihdr_data = Vec::with_capacity(13);
|
||||||
ihdr_data.write_all(&self.raw.ihdr.width.to_be_bytes()).ok();
|
ihdr_data.extend_from_slice(&self.raw.ihdr.width.to_be_bytes());
|
||||||
ihdr_data
|
ihdr_data.extend_from_slice(&self.raw.ihdr.height.to_be_bytes());
|
||||||
.write_all(&self.raw.ihdr.height.to_be_bytes())
|
ihdr_data.extend_from_slice(&[self.raw.ihdr.bit_depth as u8]);
|
||||||
.ok();
|
ihdr_data.extend_from_slice(&[
|
||||||
ihdr_data.write_all(&[self.raw.ihdr.bit_depth as u8]).ok();
|
self.raw.ihdr.color_type.png_header_code(),
|
||||||
ihdr_data
|
0, // Compression -- deflate
|
||||||
.write_all(&[self.raw.ihdr.color_type.png_header_code()])
|
0, // Filter method -- 5-way adaptive filtering
|
||||||
.ok();
|
self.raw.ihdr.interlaced as u8,
|
||||||
ihdr_data.write_all(&[0]).ok(); // Compression -- deflate
|
]);
|
||||||
ihdr_data.write_all(&[0]).ok(); // Filter method -- 5-way adaptive filtering
|
|
||||||
ihdr_data.write_all(&[self.raw.ihdr.interlaced as u8]).ok();
|
|
||||||
write_png_block(b"IHDR", &ihdr_data, &mut output);
|
write_png_block(b"IHDR", &ihdr_data, &mut output);
|
||||||
// Ancillary chunks - split into those that come before IDAT and those that come after
|
// Ancillary chunks - split into those that come before IDAT and those that come after
|
||||||
let mut aux_split = self.aux_chunks.split(|c| &c.name == b"IDAT");
|
let mut aux_split = self.aux_chunks.split(|c| &c.name == b"IDAT");
|
||||||
let aux_pre = aux_split.next().unwrap();
|
let aux_pre = aux_split.next().unwrap();
|
||||||
|
// Many chunks need to be before PLTE, so write all except those that explicitly need to be after
|
||||||
|
// Note: the PNG spec does not say that fcTL needs to be after PLTE, but some decoders expect
|
||||||
|
// that (see issue #625)
|
||||||
for chunk in aux_pre
|
for chunk in aux_pre
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|c| !(&c.name == b"bKGD" || &c.name == b"hIST" || &c.name == b"tRNS"))
|
.filter(|c| !matches!(&c.name, b"bKGD" | b"hIST" | b"tRNS" | b"fcTL"))
|
||||||
{
|
{
|
||||||
write_png_block(&chunk.name, &chunk.data, &mut output);
|
write_png_block(&chunk.name, &chunk.data, &mut output);
|
||||||
}
|
}
|
||||||
|
|
@ -197,7 +190,7 @@ impl PngData {
|
||||||
ColorType::Indexed { palette } => {
|
ColorType::Indexed { palette } => {
|
||||||
let mut palette_data = Vec::with_capacity(palette.len() * 3);
|
let mut palette_data = Vec::with_capacity(palette.len() * 3);
|
||||||
for px in palette {
|
for px in palette {
|
||||||
palette_data.extend_from_slice(px.rgb().as_slice());
|
palette_data.extend_from_slice(px.rgb().as_ref());
|
||||||
}
|
}
|
||||||
write_png_block(b"PLTE", &palette_data, &mut output);
|
write_png_block(b"PLTE", &palette_data, &mut output);
|
||||||
if let Some(last_trns) = palette.iter().rposition(|px| px.a != 255) {
|
if let Some(last_trns) = palette.iter().rposition(|px| px.a != 255) {
|
||||||
|
|
@ -215,20 +208,30 @@ impl PngData {
|
||||||
transparent_color: Some(trns),
|
transparent_color: Some(trns),
|
||||||
} => {
|
} => {
|
||||||
// Transparency pixel - 6 byte RGB16
|
// Transparency pixel - 6 byte RGB16
|
||||||
let trns_data: Vec<_> = trns.iter().flat_map(|c| c.to_be_bytes()).collect();
|
let trns_data: Vec<_> = trns.iter().flat_map(u16::to_be_bytes).collect();
|
||||||
write_png_block(b"tRNS", &trns_data, &mut output);
|
write_png_block(b"tRNS", &trns_data, &mut output);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
// Special ancillary chunks that need to come after PLTE but before IDAT
|
// Special ancillary chunks that need to come after PLTE but before IDAT
|
||||||
|
let mut sequence_number = 0;
|
||||||
for chunk in aux_pre
|
for chunk in aux_pre
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|c| &c.name == b"bKGD" || &c.name == b"hIST" || &c.name == b"tRNS")
|
.filter(|c| matches!(&c.name, b"bKGD" | b"hIST" | b"tRNS" | b"fcTL"))
|
||||||
{
|
{
|
||||||
write_png_block(&chunk.name, &chunk.data, &mut output);
|
write_png_block(&chunk.name, &chunk.data, &mut output);
|
||||||
|
if &chunk.name == b"fcTL" {
|
||||||
|
sequence_number += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// IDAT data
|
// IDAT data
|
||||||
write_png_block(b"IDAT", &self.idat_data, &mut output);
|
write_png_block(b"IDAT", &self.idat_data, &mut output);
|
||||||
|
// APNG frames
|
||||||
|
for frame in self.frames.iter() {
|
||||||
|
write_png_block(b"fcTL", &frame.fctl_data(sequence_number), &mut output);
|
||||||
|
write_png_block(b"fdAT", &frame.fdat_data(sequence_number + 1), &mut output);
|
||||||
|
sequence_number += 2;
|
||||||
|
}
|
||||||
// Ancillary chunks that come after IDAT
|
// Ancillary chunks that come after IDAT
|
||||||
for aux_post in aux_split {
|
for aux_post in aux_split {
|
||||||
for chunk in aux_post {
|
for chunk in aux_post {
|
||||||
|
|
@ -243,18 +246,33 @@ impl PngData {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PngImage {
|
impl PngImage {
|
||||||
/// Convert the image to the specified interlacing type
|
pub fn new(ihdr: IhdrData, compressed_data: &[u8]) -> PngResult<Self> {
|
||||||
/// Returns true if the interlacing was changed, false otherwise
|
let raw_data = deflate::inflate(compressed_data, ihdr.raw_data_size())?;
|
||||||
/// The `interlace` parameter specifies the *new* interlacing mode
|
|
||||||
|
// Reject files with incorrect width/height or truncated data
|
||||||
|
if raw_data.len() != ihdr.raw_data_size() {
|
||||||
|
return Err(PngError::TruncatedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut image = Self {
|
||||||
|
ihdr,
|
||||||
|
data: raw_data,
|
||||||
|
};
|
||||||
|
image.data = image.unfilter_image()?;
|
||||||
|
Ok(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable or disable interlacing
|
||||||
|
/// Returns the new image if the interlacing was changed, None otherwise
|
||||||
/// Assumes that the data has already been de-filtered
|
/// Assumes that the data has already been de-filtered
|
||||||
#[inline]
|
#[inline]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn change_interlacing(&self, interlace: Interlacing) -> Option<PngImage> {
|
pub fn change_interlacing(&self, interlace: bool) -> Option<Self> {
|
||||||
if interlace == self.ihdr.interlaced {
|
if interlace == self.ihdr.interlaced {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(if interlace == Interlacing::Adam7 {
|
Some(if interlace {
|
||||||
// Convert progressive to interlaced data
|
// Convert progressive to interlaced data
|
||||||
interlace_image(self)
|
interlace_image(self)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -265,13 +283,15 @@ impl PngImage {
|
||||||
|
|
||||||
/// Return the number of channels in the image, based on color type
|
/// Return the number of channels in the image, based on color type
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn channels_per_pixel(&self) -> usize {
|
#[must_use]
|
||||||
|
pub const fn channels_per_pixel(&self) -> usize {
|
||||||
self.ihdr.color_type.channels_per_pixel() as usize
|
self.ihdr.color_type.channels_per_pixel() as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the number of bytes per channel in the image
|
/// Return the number of bytes per channel in the image
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn bytes_per_channel(&self) -> usize {
|
#[must_use]
|
||||||
|
pub const fn bytes_per_channel(&self) -> usize {
|
||||||
match self.ihdr.bit_depth {
|
match self.ihdr.bit_depth {
|
||||||
BitDepth::Sixteen => 2,
|
BitDepth::Sixteen => 2,
|
||||||
// Depths lower than 8 will round up to 1 byte
|
// Depths lower than 8 will round up to 1 byte
|
||||||
|
|
@ -280,15 +300,15 @@ impl PngImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate the size of the PLTE and tRNS chunks
|
/// Calculate the size of the PLTE and tRNS chunks
|
||||||
|
#[must_use]
|
||||||
pub fn key_chunks_size(&self) -> usize {
|
pub fn key_chunks_size(&self) -> usize {
|
||||||
match &self.ihdr.color_type {
|
match &self.ihdr.color_type {
|
||||||
ColorType::Indexed { palette } => {
|
ColorType::Indexed { palette } => {
|
||||||
let plte = 12 + palette.len() * 3;
|
let plte = 12 + palette.len() * 3;
|
||||||
if let Some(trns) = palette.iter().rposition(|p| p.a != 255) {
|
palette
|
||||||
plte + 12 + trns + 1
|
.iter()
|
||||||
} else {
|
.rposition(|p| p.a != 255)
|
||||||
plte
|
.map_or(plte, |trns| plte + 12 + trns + 1)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ColorType::Grayscale { transparent_shade } if transparent_shade.is_some() => 12 + 2,
|
ColorType::Grayscale { transparent_shade } if transparent_shade.is_some() => 12 + 2,
|
||||||
ColorType::RGB { transparent_color } if transparent_color.is_some() => 12 + 6,
|
ColorType::RGB { transparent_color } if transparent_color.is_some() => 12 + 6,
|
||||||
|
|
@ -296,14 +316,21 @@ impl PngImage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return an estimate of the output size which can help with evaluation of very small data
|
||||||
|
#[must_use]
|
||||||
|
pub fn estimated_output_size(&self, idat_data: &[u8]) -> usize {
|
||||||
|
idat_data.len() + self.key_chunks_size()
|
||||||
|
}
|
||||||
|
|
||||||
/// Return an iterator over the scanlines of the image
|
/// Return an iterator over the scanlines of the image
|
||||||
#[inline]
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub fn scan_lines(&self, has_filter: bool) -> ScanLines<'_> {
|
pub fn scan_lines(&self, has_filter: bool) -> ScanLines<'_> {
|
||||||
ScanLines::new(self, has_filter)
|
ScanLines::new(self, has_filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reverse all filters applied on the image, returning an unfiltered IDAT bytestream
|
/// Reverse all filters applied on the image, returning an unfiltered IDAT bytestream
|
||||||
fn unfilter_image(&self) -> Result<Vec<u8>, PngError> {
|
fn unfilter_image(&self) -> PngResult<Vec<u8>> {
|
||||||
let mut unfiltered = Vec::with_capacity(self.data.len());
|
let mut unfiltered = Vec::with_capacity(self.data.len());
|
||||||
let bpp = self.bytes_per_channel() * self.channels_per_pixel();
|
let bpp = self.bytes_per_channel() * self.channels_per_pixel();
|
||||||
let mut last_line: Vec<u8> = Vec::new();
|
let mut last_line: Vec<u8> = Vec::new();
|
||||||
|
|
@ -315,8 +342,8 @@ impl PngImage {
|
||||||
last_pass = line.pass;
|
last_pass = line.pass;
|
||||||
}
|
}
|
||||||
last_line.resize(line.data.len(), 0);
|
last_line.resize(line.data.len(), 0);
|
||||||
let filter = RowFilter::try_from(line.filter).map_err(|_| PngError::InvalidData)?;
|
let filter = RowFilter::try_from(line.filter).map_err(|()| PngError::InvalidData)?;
|
||||||
filter.unfilter_line(bpp, line.data, &last_line, &mut unfiltered_buf)?;
|
filter.unfilter_line(bpp, line.data, &last_line, &mut unfiltered_buf);
|
||||||
unfiltered.extend_from_slice(&unfiltered_buf);
|
unfiltered.extend_from_slice(&unfiltered_buf);
|
||||||
std::mem::swap(&mut last_line, &mut unfiltered_buf);
|
std::mem::swap(&mut last_line, &mut unfiltered_buf);
|
||||||
unfiltered_buf.clear();
|
unfiltered_buf.clear();
|
||||||
|
|
@ -325,7 +352,12 @@ impl PngImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply the specified filter type to all rows in the image
|
/// Apply the specified filter type to all rows in the image
|
||||||
pub fn filter_image(&self, filter: RowFilter, optimize_alpha: bool) -> Vec<u8> {
|
#[must_use]
|
||||||
|
pub fn filter_image(
|
||||||
|
&self,
|
||||||
|
strategy: FilterStrategy,
|
||||||
|
optimize_alpha: bool,
|
||||||
|
) -> (Vec<u8>, FilterStrategy) {
|
||||||
let mut filtered = Vec::with_capacity(self.data.len());
|
let mut filtered = Vec::with_capacity(self.data.len());
|
||||||
let bpp = self.bytes_per_channel() * self.channels_per_pixel();
|
let bpp = self.bytes_per_channel() * self.channels_per_pixel();
|
||||||
// If alpha optimization is enabled, determine how many bytes of alpha there are per pixel
|
// If alpha optimization is enabled, determine how many bytes of alpha there are per pixel
|
||||||
|
|
@ -338,35 +370,58 @@ impl PngImage {
|
||||||
let mut prev_line = Vec::new();
|
let mut prev_line = Vec::new();
|
||||||
let mut prev_pass: Option<u8> = None;
|
let mut prev_pass: Option<u8> = None;
|
||||||
let mut f_buf = Vec::new();
|
let mut f_buf = Vec::new();
|
||||||
for line in self.scan_lines(false) {
|
// For heuristic strategies, keep track of the actual filter used for each line
|
||||||
|
let mut filters_used = Vec::new();
|
||||||
|
// Pre-allocate buffers for the Bigrams strategy to avoid per-line allocations
|
||||||
|
let (mut bigram_seen, mut bigram_touched) = if matches!(strategy, FilterStrategy::Bigrams) {
|
||||||
|
(vec![false; 0x10000], Vec::<u16>::new())
|
||||||
|
} else {
|
||||||
|
(Vec::new(), Vec::new())
|
||||||
|
};
|
||||||
|
for (i, line) in self.scan_lines(false).enumerate() {
|
||||||
if prev_pass != line.pass || line.data.len() != prev_line.len() {
|
if prev_pass != line.pass || line.data.len() != prev_line.len() {
|
||||||
prev_line = vec![0; line.data.len()];
|
prev_line = vec![0; line.data.len()];
|
||||||
}
|
}
|
||||||
// Alpha optimisation may alter the line data, so we need a mutable copy of it
|
// Alpha optimisation may alter the line data, so we need a mutable copy of it
|
||||||
let mut line_data = line.data.to_vec();
|
let mut line_data = line.data.to_vec();
|
||||||
|
|
||||||
if filter <= RowFilter::Paeth {
|
if let FilterStrategy::Basic(mut filter) = strategy {
|
||||||
// Standard filters
|
// Standard filters
|
||||||
let filter = if prev_pass == line.pass || filter <= RowFilter::Sub {
|
if prev_pass != line.pass && filter > RowFilter::Sub {
|
||||||
filter
|
filter = RowFilter::None;
|
||||||
} else {
|
}
|
||||||
RowFilter::None
|
filter.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
|
||||||
};
|
filtered.extend_from_slice(&f_buf);
|
||||||
|
prev_line = line_data;
|
||||||
|
} else if let FilterStrategy::Predefined(lines) = &strategy {
|
||||||
|
// Predefined filter for each line
|
||||||
|
let filter = lines.get(i).unwrap_or(&RowFilter::None);
|
||||||
filter.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
|
filter.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
|
||||||
filtered.extend_from_slice(&f_buf);
|
filtered.extend_from_slice(&f_buf);
|
||||||
prev_line = line_data;
|
prev_line = line_data;
|
||||||
} else {
|
} else {
|
||||||
// Heuristic filter selection strategies
|
// Heuristic filter selection strategies
|
||||||
|
|
||||||
|
let mut best_filter = RowFilter::None;
|
||||||
|
if line_data.iter().all(|&x| x == 0) {
|
||||||
|
// Assume None if the line is all zeros
|
||||||
|
filtered.push(best_filter as u8);
|
||||||
|
filtered.extend_from_slice(&line_data);
|
||||||
|
prev_line = line_data;
|
||||||
|
filters_used.push(best_filter);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let mut best_line = Vec::new();
|
let mut best_line = Vec::new();
|
||||||
let mut best_line_raw = Vec::new();
|
let mut best_line_raw = Vec::new();
|
||||||
// Avoid vertical filtering on first line of each interlacing pass
|
// Avoid vertical filtering on first line of each interlacing pass
|
||||||
let try_filters = if prev_pass == line.pass {
|
let try_filters = if prev_pass == line.pass {
|
||||||
RowFilter::STANDARD.iter()
|
RowFilter::ALL.iter()
|
||||||
} else {
|
} else {
|
||||||
RowFilter::SINGLE_LINE.iter()
|
RowFilter::SINGLE_LINE.iter()
|
||||||
};
|
};
|
||||||
match filter {
|
match strategy {
|
||||||
RowFilter::MinSum => {
|
FilterStrategy::MinSum => {
|
||||||
// MSAD algorithm mentioned in libpng reference docs
|
// MSAD algorithm mentioned in libpng reference docs
|
||||||
// http://www.libpng.org/pub/png/book/chapter09.html
|
// http://www.libpng.org/pub/png/book/chapter09.html
|
||||||
let mut best_size = usize::MAX;
|
let mut best_size = usize::MAX;
|
||||||
|
|
@ -380,17 +435,18 @@ impl PngImage {
|
||||||
best_size = size;
|
best_size = size;
|
||||||
std::mem::swap(&mut best_line, &mut f_buf);
|
std::mem::swap(&mut best_line, &mut f_buf);
|
||||||
best_line_raw.clone_from(&line_data);
|
best_line_raw.clone_from(&line_data);
|
||||||
|
best_filter = *f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RowFilter::Entropy => {
|
FilterStrategy::Entropy => {
|
||||||
// Shannon entropy algorithm, from LodePNG
|
// Shannon entropy algorithm, from LodePNG
|
||||||
// https://github.com/lvandeve/lodepng
|
// https://github.com/lvandeve/lodepng
|
||||||
let mut best_size = i32::MIN;
|
let mut best_size = i32::MIN;
|
||||||
for f in try_filters {
|
for f in try_filters {
|
||||||
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
|
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
|
||||||
let mut counts = vec![0; 0x100];
|
let mut counts = vec![0; 0x100];
|
||||||
for &i in f_buf.iter() {
|
for &i in &f_buf {
|
||||||
counts[i as usize] += 1;
|
counts[i as usize] += 1;
|
||||||
}
|
}
|
||||||
let size = counts.into_iter().fold(0, |acc, x| {
|
let size = counts.into_iter().fold(0, |acc, x| {
|
||||||
|
|
@ -403,29 +459,42 @@ impl PngImage {
|
||||||
best_size = size;
|
best_size = size;
|
||||||
std::mem::swap(&mut best_line, &mut f_buf);
|
std::mem::swap(&mut best_line, &mut f_buf);
|
||||||
best_line_raw.clone_from(&line_data);
|
best_line_raw.clone_from(&line_data);
|
||||||
|
best_filter = *f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RowFilter::Bigrams => {
|
FilterStrategy::Bigrams => {
|
||||||
// Count distinct bigrams, from pngwolf
|
// Count distinct bigrams, from pngwolf
|
||||||
// https://bjoern.hoehrmann.de/pngwolf/
|
// https://bjoern.hoehrmann.de/pngwolf/
|
||||||
let mut best_size = usize::MAX;
|
let mut best_size = usize::MAX;
|
||||||
for f in try_filters {
|
for f in try_filters {
|
||||||
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
|
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
|
||||||
let mut set = bitarr![0; 0x10000];
|
let mut count = 0;
|
||||||
for pair in f_buf.windows(2) {
|
for pair in f_buf.windows(2) {
|
||||||
let bigram = (pair[0] as usize) << 8 | pair[1] as usize;
|
let bigram = ((pair[0] as usize) << 8) | pair[1] as usize;
|
||||||
set.set(bigram, true);
|
if !bigram_seen[bigram] {
|
||||||
|
count += 1;
|
||||||
|
if count >= best_size {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
bigram_seen[bigram] = true;
|
||||||
|
bigram_touched.push(bigram as u16);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let size = set.count_ones();
|
if count < best_size {
|
||||||
if size < best_size {
|
best_size = count;
|
||||||
best_size = size;
|
|
||||||
std::mem::swap(&mut best_line, &mut f_buf);
|
std::mem::swap(&mut best_line, &mut f_buf);
|
||||||
best_line_raw.clone_from(&line_data);
|
best_line_raw.clone_from(&line_data);
|
||||||
|
best_filter = *f;
|
||||||
}
|
}
|
||||||
|
// Clear only the entries that were touched
|
||||||
|
for &idx in &bigram_touched {
|
||||||
|
bigram_seen[idx as usize] = false;
|
||||||
|
}
|
||||||
|
bigram_touched.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RowFilter::BigEnt => {
|
FilterStrategy::BigEnt => {
|
||||||
// Bigram entropy, combined from Entropy and Bigrams filters
|
// Bigram entropy, combined from Entropy and Bigrams filters
|
||||||
let mut best_size = i32::MIN;
|
let mut best_size = i32::MIN;
|
||||||
// FxHasher is the fastest rust hasher currently available for this purpose
|
// FxHasher is the fastest rust hasher currently available for this purpose
|
||||||
|
|
@ -434,7 +503,7 @@ impl PngImage {
|
||||||
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
|
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
|
||||||
counts.clear();
|
counts.clear();
|
||||||
for pair in f_buf.windows(2) {
|
for pair in f_buf.windows(2) {
|
||||||
let bigram = (pair[0] as u16) << 8 | pair[1] as u16;
|
let bigram = (u16::from(pair[0]) << 8) | u16::from(pair[1]);
|
||||||
counts.entry(bigram).and_modify(|e| *e += 1).or_insert(1);
|
counts.entry(bigram).and_modify(|e| *e += 1).or_insert(1);
|
||||||
}
|
}
|
||||||
let size = counts.values().fold(0, |acc, &x| acc + ilog2i(x)) as i32;
|
let size = counts.values().fold(0, |acc, &x| acc + ilog2i(x)) as i32;
|
||||||
|
|
@ -442,31 +511,33 @@ impl PngImage {
|
||||||
best_size = size;
|
best_size = size;
|
||||||
std::mem::swap(&mut best_line, &mut f_buf);
|
std::mem::swap(&mut best_line, &mut f_buf);
|
||||||
best_line_raw.clone_from(&line_data);
|
best_line_raw.clone_from(&line_data);
|
||||||
|
best_filter = *f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RowFilter::Brute => {
|
FilterStrategy::Brute { num_lines, level } => {
|
||||||
// Brute force by compressing each filter attempt
|
// Brute force by compressing each filter attempt
|
||||||
// Similar to that of LodePNG but includes some previous lines for context
|
// Similar to that of LodePNG but includes some previous lines for context
|
||||||
let mut best_size = usize::MAX;
|
let mut best_size = usize::MAX;
|
||||||
let line_start = filtered.len();
|
let line_start = filtered.len();
|
||||||
filtered.resize(filtered.len() + line.data.len() + 1, 0);
|
filtered.resize(filtered.len() + line.data.len() + 1, 0);
|
||||||
let mut compressor =
|
let mut compressor =
|
||||||
Compressor::new(CompressionLvl::new(BRUTE_LEVEL).unwrap());
|
Compressor::new(CompressionLvl::new(level.into()).unwrap());
|
||||||
let limit = filtered.len().min((line.data.len() + 1) * BRUTE_LINES);
|
let limit = filtered.len().min((line.data.len() + 1) * num_lines);
|
||||||
let capacity = compressor.zlib_compress_bound(limit);
|
let capacity = compressor.deflate_compress_bound(limit);
|
||||||
let mut dest = vec![0; capacity];
|
let mut dest = vec![0; capacity];
|
||||||
|
|
||||||
for f in try_filters {
|
for f in try_filters {
|
||||||
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
|
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
|
||||||
filtered[line_start..].copy_from_slice(&f_buf);
|
filtered[line_start..].copy_from_slice(&f_buf);
|
||||||
let size = compressor
|
let size = compressor
|
||||||
.zlib_compress(&filtered[filtered.len() - limit..], &mut dest)
|
.deflate_compress(&filtered[filtered.len() - limit..], &mut dest)
|
||||||
.unwrap_or(usize::MAX);
|
.unwrap_or(usize::MAX);
|
||||||
if size < best_size {
|
if size < best_size {
|
||||||
best_size = size;
|
best_size = size;
|
||||||
std::mem::swap(&mut best_line, &mut f_buf);
|
std::mem::swap(&mut best_line, &mut f_buf);
|
||||||
best_line_raw.clone_from(&line_data);
|
best_line_raw.clone_from(&line_data);
|
||||||
|
best_filter = *f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
filtered.resize(line_start, 0);
|
filtered.resize(line_start, 0);
|
||||||
|
|
@ -475,11 +546,17 @@ impl PngImage {
|
||||||
}
|
}
|
||||||
filtered.extend_from_slice(&best_line);
|
filtered.extend_from_slice(&best_line);
|
||||||
prev_line = best_line_raw;
|
prev_line = best_line_raw;
|
||||||
|
filters_used.push(best_filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
prev_pass = line.pass;
|
prev_pass = line.pass;
|
||||||
}
|
}
|
||||||
filtered
|
|
||||||
|
if filters_used.is_empty() {
|
||||||
|
(filtered, strategy)
|
||||||
|
} else {
|
||||||
|
(filtered, FilterStrategy::Predefined(filters_used))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -495,7 +572,7 @@ fn write_png_block(key: &[u8], chunk: &[u8], output: &mut Vec<u8>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Integer approximation for i * log2(i) - much faster than float calculations
|
// Integer approximation for i * log2(i) - much faster than float calculations
|
||||||
fn ilog2i(i: u32) -> u32 {
|
const fn ilog2i(i: u32) -> u32 {
|
||||||
let log = 32 - i.leading_zeros() - 1;
|
let log = 32 - i.leading_zeros() - 1;
|
||||||
i * log + ((i - (1 << log)) << 1)
|
i * log + ((i - (1 << log)) << 1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{interlace::Interlacing, png::PngImage};
|
use crate::png::PngImage;
|
||||||
|
|
||||||
/// An iterator over the scan lines of a PNG image
|
/// An iterator over the scan lines of a PNG image
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -11,6 +11,7 @@ pub struct ScanLines<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ScanLines<'a> {
|
impl<'a> ScanLines<'a> {
|
||||||
|
#[must_use]
|
||||||
pub fn new(png: &'a PngImage, has_filter: bool) -> Self {
|
pub fn new(png: &'a PngImage, has_filter: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
iter: ScanLineRanges::new(png, has_filter),
|
iter: ScanLineRanges::new(png, has_filter),
|
||||||
|
|
@ -22,6 +23,7 @@ impl<'a> ScanLines<'a> {
|
||||||
|
|
||||||
impl<'a> Iterator for ScanLines<'a> {
|
impl<'a> Iterator for ScanLines<'a> {
|
||||||
type Item = ScanLine<'a>;
|
type Item = ScanLine<'a>;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
let (len, pass, num_pixels) = self.iter.next()?;
|
let (len, pass, num_pixels) = self.iter.next()?;
|
||||||
|
|
@ -67,7 +69,7 @@ impl ScanLineRanges {
|
||||||
width: png.ihdr.width,
|
width: png.ihdr.width,
|
||||||
height: png.ihdr.height,
|
height: png.ihdr.height,
|
||||||
left: png.data.len(),
|
left: png.data.len(),
|
||||||
pass: if png.ihdr.interlaced == Interlacing::Adam7 {
|
pass: if png.ihdr.interlaced {
|
||||||
Some((1, 0))
|
Some((1, 0))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -79,6 +81,7 @@ impl ScanLineRanges {
|
||||||
|
|
||||||
impl Iterator for ScanLineRanges {
|
impl Iterator for ScanLineRanges {
|
||||||
type Item = (usize, Option<u8>, usize);
|
type Item = (usize, Option<u8>, usize);
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
if self.left == 0 {
|
if self.left == 0 {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -135,7 +138,7 @@ impl Iterator for ScanLineRanges {
|
||||||
pixels_per_line += 1;
|
pixels_per_line += 1;
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
};
|
}
|
||||||
let current_pass = Some(pass.0);
|
let current_pass = Some(pass.0);
|
||||||
if pass.1 + y_steps >= self.height {
|
if pass.1 + y_steps >= self.height {
|
||||||
pass.0 += 1;
|
pass.0 += 1;
|
||||||
|
|
@ -154,7 +157,7 @@ impl Iterator for ScanLineRanges {
|
||||||
(self.width, None)
|
(self.width, None)
|
||||||
};
|
};
|
||||||
let bits_per_line = pixels_per_line as usize * self.bits_per_pixel;
|
let bits_per_line = pixels_per_line as usize * self.bits_per_pixel;
|
||||||
let mut len = (bits_per_line + 7) / 8;
|
let mut len = bits_per_line.div_ceil(8);
|
||||||
if self.has_filter {
|
if self.has_filter {
|
||||||
len += 1;
|
len += 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use super::*;
|
pub use super::*;
|
||||||
}
|
}
|
||||||
|
|
@ -70,12 +72,10 @@ where
|
||||||
|
|
||||||
impl<I: Iterator> ParallelIterator for I {}
|
impl<I: Iterator> ParallelIterator for I {}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn join<A, B>(a: impl FnOnce() -> A, b: impl FnOnce() -> B) -> (A, B) {
|
pub fn join<A, B>(a: impl FnOnce() -> A, b: impl FnOnce() -> B) -> (A, B) {
|
||||||
(a(), b())
|
(a(), b())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn spawn<A>(a: impl FnOnce() -> A) -> A {
|
pub fn spawn<A>(a: impl FnOnce() -> A) -> A {
|
||||||
a()
|
a()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Clean the alpha channel by setting the color of all fully transparent pixels to black
|
/// 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> {
|
pub fn cleaned_alpha_channel(png: &PngImage) -> Option<PngImage> {
|
||||||
if !png.ihdr.color_type.has_alpha() {
|
if !png.ihdr.color_type.has_alpha() {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -16,7 +17,7 @@ pub fn cleaned_alpha_channel(png: &PngImage) -> Option<PngImage> {
|
||||||
let colored_bytes = bpp - byte_depth;
|
let colored_bytes = bpp - byte_depth;
|
||||||
|
|
||||||
let mut reduced = Vec::with_capacity(png.data.len());
|
let mut reduced = Vec::with_capacity(png.data.len());
|
||||||
for pixel in png.data.chunks(bpp) {
|
for pixel in png.data.chunks_exact(bpp) {
|
||||||
if pixel.iter().skip(colored_bytes).all(|b| *b == 0) {
|
if pixel.iter().skip(colored_bytes).all(|b| *b == 0) {
|
||||||
reduced.resize(reduced.len() + bpp, 0);
|
reduced.resize(reduced.len() + bpp, 0);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -45,7 +46,7 @@ pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option<Png
|
||||||
let mut has_transparency = false;
|
let mut has_transparency = false;
|
||||||
let mut used_colors = vec![false; 256];
|
let mut used_colors = vec![false; 256];
|
||||||
|
|
||||||
for pixel in png.data.chunks(bpp) {
|
for pixel in png.data.chunks_exact(bpp) {
|
||||||
if optimize_alpha && pixel.iter().skip(colored_bytes).all(|b| *b == 0) {
|
if optimize_alpha && pixel.iter().skip(colored_bytes).all(|b| *b == 0) {
|
||||||
// Fully transparent, we may be able to reduce with tRNS
|
// Fully transparent, we may be able to reduce with tRNS
|
||||||
has_transparency = true;
|
has_transparency = true;
|
||||||
|
|
@ -74,19 +75,19 @@ pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option<Png
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut raw_data = Vec::with_capacity(png.data.len());
|
let mut raw_data = Vec::with_capacity(png.data.len());
|
||||||
for pixel in png.data.chunks(bpp) {
|
for pixel in png.data.chunks_exact(bpp) {
|
||||||
match transparency_pixel {
|
match transparency_pixel {
|
||||||
Some(trns) if pixel.iter().skip(colored_bytes).all(|b| *b == 0) => {
|
Some(trns) if pixel.iter().skip(colored_bytes).all(|b| *b == 0) => {
|
||||||
raw_data.resize(raw_data.len() + colored_bytes, trns);
|
raw_data.resize(raw_data.len() + colored_bytes, trns);
|
||||||
}
|
}
|
||||||
_ => raw_data.extend_from_slice(&pixel[0..colored_bytes]),
|
_ => raw_data.extend_from_slice(&pixel[0..colored_bytes]),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct the color type with appropriate transparency data
|
// Construct the color type with appropriate transparency data
|
||||||
let transparent = transparency_pixel.map(|trns| match png.ihdr.bit_depth {
|
let transparent = transparency_pixel.map(|trns| match png.ihdr.bit_depth {
|
||||||
BitDepth::Sixteen => (trns as u16) << 8 | trns as u16,
|
BitDepth::Sixteen => (u16::from(trns) << 8) | u16::from(trns),
|
||||||
_ => trns as u16,
|
_ => u16::from(trns),
|
||||||
});
|
});
|
||||||
let target_color_type = match png.ihdr.color_type {
|
let target_color_type = match png.ihdr.color_type {
|
||||||
ColorType::GrayscaleAlpha => ColorType::Grayscale {
|
ColorType::GrayscaleAlpha => ColorType::Grayscale {
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,13 @@ pub fn reduced_bit_depth_16_to_8(png: &PngImage, force_scale: bool) -> Option<Pn
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reduce from 16 to 8 bits per channel per pixel
|
// Reduce from 16 to 8 bits per channel per pixel
|
||||||
if png.data.chunks(2).any(|pair| pair[0] != pair[1]) {
|
if png.data.chunks_exact(2).any(|pair| pair[0] != pair[1]) {
|
||||||
// Can't reduce
|
// Can't reduce
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(PngImage {
|
Some(PngImage {
|
||||||
data: png.data.iter().step_by(2).cloned().collect(),
|
data: png.data.chunks_exact(2).map(|pair| pair[0]).collect(),
|
||||||
ihdr: IhdrData {
|
ihdr: IhdrData {
|
||||||
color_type: png.ihdr.color_type.clone(),
|
color_type: png.ihdr.color_type.clone(),
|
||||||
bit_depth: BitDepth::Eight,
|
bit_depth: BitDepth::Eight,
|
||||||
|
|
@ -41,15 +41,15 @@ pub fn scaled_bit_depth_16_to_8(png: &PngImage) -> Option<PngImage> {
|
||||||
// Reduce from 16 to 8 bits per channel per pixel by scaling when necessary
|
// Reduce from 16 to 8 bits per channel per pixel by scaling when necessary
|
||||||
let data = png
|
let data = png
|
||||||
.data
|
.data
|
||||||
.chunks(2)
|
.chunks_exact(2)
|
||||||
.map(|pair| {
|
.map(|pair| {
|
||||||
if pair[0] == pair[1] {
|
if pair[0] == pair[1] {
|
||||||
return pair[0];
|
return pair[0];
|
||||||
}
|
}
|
||||||
// See: http://www.libpng.org/pub/png/spec/1.2/PNG-Decoders.html#D.Sample-depth-rescaling
|
// 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
|
// This allows values such as 0x00FF to be rounded to 0x01 rather than truncated to 0x00
|
||||||
let val = u16::from_be_bytes([pair[0], pair[1]]) as f64;
|
let val = f32::from(u16::from_be_bytes([pair[0], pair[1]]));
|
||||||
(val * 255.0 / 65535.0).round() as u8
|
(val * (255.0 / 65535.0)).round() as u8
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
@ -70,17 +70,17 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage) -> Option<PngImage> {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut minimum_bits = 1;
|
let minimum_bits = if let ColorType::Indexed { palette } = &png.ihdr.color_type {
|
||||||
|
|
||||||
if let ColorType::Indexed { palette } = &png.ihdr.color_type {
|
|
||||||
// We can easily determine minimum depth by the palette size
|
// We can easily determine minimum depth by the palette size
|
||||||
minimum_bits = match palette.len() {
|
match palette.len() {
|
||||||
0..=2 => 1,
|
0..=2 => 1,
|
||||||
3..=4 => 2,
|
3..=4 => 2,
|
||||||
5..=16 => 4,
|
5..=16 => 4,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
}
|
||||||
} else {
|
} else {
|
||||||
|
let mut minimum_bits = 1;
|
||||||
|
|
||||||
// Finding minimum depth for grayscale is much more complicated
|
// Finding minimum depth for grayscale is much more complicated
|
||||||
let mut mask = 1;
|
let mut mask = 1;
|
||||||
let mut divisions = 1..8;
|
let mut divisions = 1..8;
|
||||||
|
|
@ -110,7 +110,9 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage) -> Option<PngImage> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
minimum_bits
|
||||||
|
};
|
||||||
|
|
||||||
let mut reduced = Vec::with_capacity(png.data.len());
|
let mut reduced = Vec::with_capacity(png.data.len());
|
||||||
let mask = (1 << minimum_bits) - 1;
|
let mask = (1 << minimum_bits) - 1;
|
||||||
|
|
@ -138,7 +140,7 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage) -> Option<PngImage> {
|
||||||
let mut check = reduced_trans;
|
let mut check = reduced_trans;
|
||||||
let mut bits = minimum_bits;
|
let mut bits = minimum_bits;
|
||||||
while bits < 8 {
|
while bits < 8 {
|
||||||
check = check << bits | check;
|
check = (check << bits) | check;
|
||||||
bits <<= 1;
|
bits <<= 1;
|
||||||
}
|
}
|
||||||
// If the transparency doesn't fit the new bit depth it is therefore unused - set it to None
|
// If the transparency doesn't fit the new bit depth it is therefore unused - set it to None
|
||||||
|
|
@ -188,7 +190,7 @@ pub fn expanded_bit_depth_to_8(png: &PngImage) -> Option<PngImage> {
|
||||||
// Expand gray by repeating the bits
|
// Expand gray by repeating the bits
|
||||||
let mut bits = bit_depth;
|
let mut bits = bit_depth;
|
||||||
while bits < 8 {
|
while bits < 8 {
|
||||||
val = val << bits | val;
|
val = (val << bits) | val;
|
||||||
bits <<= 1;
|
bits <<= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -207,7 +209,7 @@ pub fn expanded_bit_depth_to_8(png: &PngImage) -> Option<PngImage> {
|
||||||
{
|
{
|
||||||
let mut bits = bit_depth;
|
let mut bits = bit_depth;
|
||||||
while bits < 8 {
|
while bits < 8 {
|
||||||
trans = trans << bits | trans;
|
trans = (trans << bits) | trans;
|
||||||
bits <<= 1;
|
bits <<= 1;
|
||||||
}
|
}
|
||||||
ColorType::Grayscale {
|
ColorType::Grayscale {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::hash::{BuildHasherDefault, Hash};
|
use std::hash::{BuildHasherDefault, Hash};
|
||||||
|
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use rgb::{alt::Gray, ComponentMap, ComponentSlice, FromSlice, RGB, RGBA};
|
use rgb::{ComponentMap, FromSlice, RGB, RGBA, alt::Gray};
|
||||||
use rustc_hash::FxHasher;
|
use rustc_hash::FxHasher;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -49,39 +49,39 @@ pub fn reduced_to_indexed(png: &PngImage, allow_grayscale: bool) -> Option<PngIm
|
||||||
let mut raw_data = Vec::with_capacity(png.data.len() / png.channels_per_pixel());
|
let mut raw_data = Vec::with_capacity(png.data.len() / png.channels_per_pixel());
|
||||||
let palette: Vec<_> = match png.ihdr.color_type {
|
let palette: Vec<_> = match png.ihdr.color_type {
|
||||||
ColorType::Grayscale { transparent_shade } => {
|
ColorType::Grayscale { transparent_shade } => {
|
||||||
let pmap = build_palette(png.data.as_gray().iter().cloned(), &mut raw_data)?;
|
let pmap = build_palette(png.data.as_gray().iter().copied(), &mut raw_data)?;
|
||||||
// Convert the Gray16 transparency to Gray8
|
// Convert the Gray16 transparency to Gray8
|
||||||
let transparency_pixel = transparent_shade.map(|t| Gray::from(t as u8));
|
let transparency_pixel = transparent_shade.map(|t| Gray::from(t as u8));
|
||||||
pmap.into_iter()
|
pmap.into_iter()
|
||||||
.map(|px| {
|
.map(|px| {
|
||||||
RGB::from(px).alpha(if Some(px) != transparency_pixel {
|
RGB::from(px).with_alpha(if Some(px) == transparency_pixel {
|
||||||
255
|
|
||||||
} else {
|
|
||||||
0
|
0
|
||||||
|
} else {
|
||||||
|
255
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
ColorType::RGB { transparent_color } => {
|
ColorType::RGB { transparent_color } => {
|
||||||
let pmap = build_palette(png.data.as_rgb().iter().cloned(), &mut raw_data)?;
|
let pmap = build_palette(png.data.as_rgb().iter().copied(), &mut raw_data)?;
|
||||||
// Convert the RGB16 transparency to RGB8
|
// Convert the RGB16 transparency to RGB8
|
||||||
let transparency_pixel = transparent_color.map(|t| t.map(|c| c as u8));
|
let transparency_pixel = transparent_color.map(|t| t.map(|c| c as u8));
|
||||||
pmap.into_iter()
|
pmap.into_iter()
|
||||||
.map(|px| {
|
.map(|px| {
|
||||||
px.alpha(if Some(px) != transparency_pixel {
|
px.with_alpha(if Some(px) == transparency_pixel {
|
||||||
255
|
|
||||||
} else {
|
|
||||||
0
|
0
|
||||||
|
} else {
|
||||||
|
255
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
ColorType::GrayscaleAlpha => {
|
ColorType::GrayscaleAlpha => {
|
||||||
let pmap = build_palette(png.data.as_gray_alpha().iter().cloned(), &mut raw_data)?;
|
let pmap = build_palette(png.data.as_gray_alpha().iter().copied(), &mut raw_data)?;
|
||||||
pmap.into_iter().map(RGBA::from).collect()
|
pmap.into_iter().map(RGBA::from).collect()
|
||||||
}
|
}
|
||||||
ColorType::RGBA => {
|
ColorType::RGBA => {
|
||||||
let pmap = build_palette(png.data.as_rgba().iter().cloned(), &mut raw_data)?;
|
let pmap = build_palette(png.data.as_rgba().iter().copied(), &mut raw_data)?;
|
||||||
pmap.into_iter().collect()
|
pmap.into_iter().collect()
|
||||||
}
|
}
|
||||||
_ => return None,
|
_ => return None,
|
||||||
|
|
@ -106,7 +106,7 @@ pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
|
||||||
let byte_depth = png.bytes_per_channel();
|
let byte_depth = png.bytes_per_channel();
|
||||||
let bpp = png.channels_per_pixel() * byte_depth;
|
let bpp = png.channels_per_pixel() * byte_depth;
|
||||||
let last_color = 2 * byte_depth;
|
let last_color = 2 * byte_depth;
|
||||||
for pixel in png.data.chunks(bpp) {
|
for pixel in png.data.chunks_exact(bpp) {
|
||||||
if byte_depth == 1 {
|
if byte_depth == 1 {
|
||||||
if pixel[0] != pixel[1] || pixel[1] != pixel[2] {
|
if pixel[0] != pixel[1] || pixel[1] != pixel[2] {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -138,15 +138,30 @@ pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
|
||||||
|
|
||||||
/// Attempt to convert indexed to a different color type, returning the resulting image if successful
|
/// Attempt to convert indexed to a different color type, returning the resulting image if successful
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn indexed_to_channels(png: &PngImage, allow_grayscale: bool) -> Option<PngImage> {
|
pub fn indexed_to_channels(
|
||||||
|
png: &PngImage,
|
||||||
|
allow_grayscale: bool,
|
||||||
|
optimize_alpha: bool,
|
||||||
|
) -> Option<PngImage> {
|
||||||
if png.ihdr.bit_depth != BitDepth::Eight {
|
if png.ihdr.bit_depth != BitDepth::Eight {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let palette = match &png.ihdr.color_type {
|
let mut palette = match &png.ihdr.color_type {
|
||||||
ColorType::Indexed { palette } => palette,
|
ColorType::Indexed { palette } => palette.clone(),
|
||||||
_ => return None,
|
_ => 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
|
// Determine which channels are required
|
||||||
let is_gray = if allow_grayscale {
|
let is_gray = if allow_grayscale {
|
||||||
palette.iter().all(|c| c.r == c.g && c.g == c.b)
|
palette.iter().all(|c| c.r == c.g && c.g == c.b)
|
||||||
|
|
@ -178,7 +193,7 @@ pub fn indexed_to_channels(png: &PngImage, allow_grayscale: bool) -> Option<PngI
|
||||||
let mut data = Vec::with_capacity(out_size);
|
let mut data = Vec::with_capacity(out_size);
|
||||||
for b in &png.data {
|
for b in &png.data {
|
||||||
let color = palette.get(*b as usize).unwrap_or(&black);
|
let color = palette.get(*b as usize).unwrap_or(&black);
|
||||||
data.extend_from_slice(&color.as_slice()[ch_start..=ch_end]);
|
data.extend_from_slice(&color.as_ref()[ch_start..=ch_end]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(PngImage {
|
Some(PngImage {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{evaluate::Evaluator, png::PngImage, ColorType, Deadline, Deflaters, Options};
|
use crate::{ColorType, Deadline, Deflater, Options, evaluate::Evaluator, png::PngImage};
|
||||||
|
|
||||||
pub mod alpha;
|
pub mod alpha;
|
||||||
use crate::alpha::*;
|
use crate::alpha::*;
|
||||||
|
|
@ -21,8 +21,8 @@ pub(crate) fn perform_reductions(
|
||||||
|
|
||||||
// At low compression levels, skip some transformations which are less likely to be effective
|
// At low compression levels, skip some transformations which are less likely to be effective
|
||||||
// This currently affects optimization presets 0-2
|
// This currently affects optimization presets 0-2
|
||||||
let cheap = match opts.deflate {
|
let cheap = match opts.deflater {
|
||||||
Deflaters::Libdeflater { compression } => compression < 12 && opts.fast_evaluation,
|
Deflater::Libdeflater { compression } => compression < 12 && opts.fast_evaluation,
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -106,7 +106,9 @@ pub(crate) fn perform_reductions(
|
||||||
// Attempt to convert from indexed to channels
|
// Attempt to convert from indexed to channels
|
||||||
// This may give a better result due to dropping the PLTE chunk
|
// This may give a better result due to dropping the PLTE chunk
|
||||||
if !cheap && opts.color_type_reduction && !deadline.passed() {
|
if !cheap && opts.color_type_reduction && !deadline.passed() {
|
||||||
if let Some(reduced) = indexed_to_channels(&png, opts.grayscale_reduction) {
|
if let Some(reduced) =
|
||||||
|
indexed_to_channels(&png, opts.grayscale_reduction, opts.optimize_alpha)
|
||||||
|
{
|
||||||
// This result should not be passed on to subsequent reductions
|
// This result should not be passed on to subsequent reductions
|
||||||
eval.try_image(Arc::new(reduced));
|
eval.try_image(Arc::new(reduced));
|
||||||
evaluation_added = true;
|
evaluation_added = true;
|
||||||
|
|
@ -116,7 +118,7 @@ pub(crate) fn perform_reductions(
|
||||||
// Attempt to reduce to indexed
|
// Attempt to reduce to indexed
|
||||||
// Keep the existing `png` var in case it is grayscale - we can test both for depth reduction later
|
// Keep the existing `png` var in case it is grayscale - we can test both for depth reduction later
|
||||||
let mut indexed = None;
|
let mut indexed = None;
|
||||||
if opts.color_type_reduction && !deadline.passed() {
|
if opts.color_type_reduction && opts.palette_reduction && !deadline.passed() {
|
||||||
if let Some(reduced) = reduced_to_indexed(&png, opts.grayscale_reduction) {
|
if let Some(reduced) = reduced_to_indexed(&png, opts.grayscale_reduction) {
|
||||||
// Make sure the palette gets sorted (but don't bother evaluating both results)
|
// Make sure the palette gets sorted (but don't bother evaluating both results)
|
||||||
let new = Arc::new(sorted_palette(&reduced).unwrap_or(reduced));
|
let new = Arc::new(sorted_palette(&reduced).unwrap_or(reduced));
|
||||||
|
|
@ -181,7 +183,7 @@ pub(crate) fn perform_reductions(
|
||||||
if (!cheap || reduced.is_none()) && !deadline.passed() {
|
if (!cheap || reduced.is_none()) && !deadline.passed() {
|
||||||
if let Some(indexed) = indexed.and_then(|png| reduced_bit_depth_8_or_less(&png)) {
|
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)
|
// Only evaluate this if it's different from the first result (which must be grayscale if it exists)
|
||||||
if reduced.as_ref().map_or(true, |r| r.data != indexed.data) {
|
if reduced.as_ref().is_none_or(|r| r.data != indexed.data) {
|
||||||
eval.try_image(Arc::new(indexed));
|
eval.try_image(Arc::new(indexed));
|
||||||
evaluation_added = true;
|
evaluation_added = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@ use rgb::RGBA8;
|
||||||
use crate::{
|
use crate::{
|
||||||
colors::{BitDepth, ColorType},
|
colors::{BitDepth, ColorType},
|
||||||
headers::IhdrData,
|
headers::IhdrData,
|
||||||
png::{scan_lines::ScanLine, PngImage},
|
png::{PngImage, scan_lines::ScanLine},
|
||||||
Interlacing,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Attempt to reduce the number of colors in the palette, returning the reduced image if successful
|
/// Attempt to reduce the number of colors in the palette, returning the reduced image if successful
|
||||||
|
|
@ -87,7 +86,7 @@ pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
|
||||||
let mut enumerated: Vec<_> = palette.iter().enumerate().collect();
|
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
|
// 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 keep_first = most_popular_edge_color(palette.len(), png);
|
||||||
let first = enumerated.remove(keep_first);
|
let first = keep_first.map(|f| enumerated.remove(f));
|
||||||
|
|
||||||
// Sort the palette
|
// Sort the palette
|
||||||
enumerated.sort_by(|a, b| {
|
enumerated.sort_by(|a, b| {
|
||||||
|
|
@ -104,7 +103,9 @@ pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
|
||||||
};
|
};
|
||||||
color_val(a.1).cmp(&color_val(b.1))
|
color_val(a.1).cmp(&color_val(b.1))
|
||||||
});
|
});
|
||||||
enumerated.insert(0, first);
|
if let Some(first) = first {
|
||||||
|
enumerated.insert(0, first);
|
||||||
|
}
|
||||||
|
|
||||||
// Extract the new palette and determine if anything changed
|
// Extract the new palette and determine if anything changed
|
||||||
let (remapping, palette): (Vec<_>, Vec<RGBA8>) = enumerated.into_iter().unzip();
|
let (remapping, palette): (Vec<_>, Vec<RGBA8>) = enumerated.into_iter().unzip();
|
||||||
|
|
@ -132,7 +133,7 @@ pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn sorted_palette_mzeng(png: &PngImage) -> Option<PngImage> {
|
pub fn sorted_palette_mzeng(png: &PngImage) -> Option<PngImage> {
|
||||||
// Interlacing not currently supported
|
// Interlacing not currently supported
|
||||||
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced != Interlacing::None {
|
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let palette = match &png.ihdr.color_type {
|
let palette = match &png.ihdr.color_type {
|
||||||
|
|
@ -154,7 +155,7 @@ pub fn sorted_palette_mzeng(png: &PngImage) -> Option<PngImage> {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn sorted_palette_battiato(png: &PngImage) -> Option<PngImage> {
|
pub fn sorted_palette_battiato(png: &PngImage) -> Option<PngImage> {
|
||||||
// Interlacing not currently supported
|
// Interlacing not currently supported
|
||||||
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced != Interlacing::None {
|
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let palette = match &png.ihdr.color_type {
|
let palette = match &png.ihdr.color_type {
|
||||||
|
|
@ -204,27 +205,31 @@ fn apply_palette_reorder(png: &PngImage, remapping: &[usize]) -> Option<PngImage
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the most popular color on the image edges (the pixels neighboring the filter bytes)
|
// 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) -> usize {
|
fn most_popular_edge_color(num_colors: usize, png: &PngImage) -> Option<usize> {
|
||||||
let mut counts = [0u32; 256];
|
let mut counts = [0_u32; 256];
|
||||||
for line in png.scan_lines(false) {
|
for line in png.scan_lines(false) {
|
||||||
if let &[first, .., last] = line.data {
|
if let &[first, .., last] = line.data {
|
||||||
counts[first as usize] += 1;
|
counts[first as usize] += 1;
|
||||||
counts[last as usize] += 1;
|
counts[last as usize] += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
counts
|
let max = counts
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
|
||||||
.take(num_colors)
|
.take(num_colors)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.max_by_key(|&(_, v)| v)
|
.max_by_key(|&(_, v)| v)
|
||||||
.unwrap_or_default()
|
.unwrap();
|
||||||
.0
|
// 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
|
// Find the most popular color in the image, along with its count
|
||||||
fn most_popular_color(num_colors: usize, png: &PngImage) -> (usize, u32) {
|
fn most_popular_color(num_colors: usize, png: &PngImage) -> (usize, u32) {
|
||||||
let mut counts = [0u32; 256];
|
let mut counts = [0_u32; 256];
|
||||||
for &val in &png.data {
|
for &val in &png.data {
|
||||||
counts[val as usize] += 1;
|
counts[val as usize] += 1;
|
||||||
}
|
}
|
||||||
|
|
@ -256,7 +261,7 @@ fn apply_most_popular_color(png: &PngImage, remapping: &mut [usize]) {
|
||||||
|
|
||||||
// Calculate co-occurences matrix
|
// Calculate co-occurences matrix
|
||||||
fn co_occurrence_matrix(num_colors: usize, png: &PngImage) -> Vec<Vec<u32>> {
|
fn co_occurrence_matrix(num_colors: usize, png: &PngImage) -> Vec<Vec<u32>> {
|
||||||
let mut matrix = vec![vec![0u32; num_colors]; num_colors];
|
let mut matrix = vec![vec![0_u32; num_colors]; num_colors];
|
||||||
let mut prev: Option<ScanLine> = None;
|
let mut prev: Option<ScanLine> = None;
|
||||||
let mut prev_val = None;
|
let mut prev_val = None;
|
||||||
for line in png.scan_lines(false) {
|
for line in png.scan_lines(false) {
|
||||||
|
|
@ -278,7 +283,7 @@ fn co_occurrence_matrix(num_colors: usize, png: &PngImage) -> Vec<Vec<u32>> {
|
||||||
matrix[val][prev_val] += 1;
|
matrix[val][prev_val] += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prev = Some(line)
|
prev = Some(line);
|
||||||
}
|
}
|
||||||
matrix
|
matrix
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
use image::{codecs::png::PngDecoder, *};
|
use image::{codecs::png::PngDecoder, *};
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
use std::io::Cursor;
|
|
||||||
|
#[cfg(not(feature = "parallel"))]
|
||||||
|
use crate::rayon;
|
||||||
|
|
||||||
/// Validate that the output png data still matches the original image
|
/// Validate that the output png data still matches the original image
|
||||||
pub fn validate_output(output: &[u8], original_data: &[u8]) -> bool {
|
pub fn validate_output(output: &[u8], original_data: &[u8]) -> bool {
|
||||||
|
|
@ -40,7 +44,7 @@ fn load_png_image_from_memory(png_data: &[u8]) -> Result<Vec<RgbaImage>, image::
|
||||||
decoder
|
decoder
|
||||||
.apng()?
|
.apng()?
|
||||||
.into_frames()
|
.into_frames()
|
||||||
.map(|f| f.map(|f| f.into_buffer()))
|
.map(|f| f.map(image::Frame::into_buffer))
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
DynamicImage::from_decoder(decoder).map(|i| vec![i.into_rgba8()])
|
DynamicImage::from_decoder(decoder).map(|i| vec![i.into_rgba8()])
|
||||||
|
|
|
||||||
346
tests/alpha.rs
Normal file
|
|
@ -0,0 +1,346 @@
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
tests/files/c2pa-signed.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 328 KiB After Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 193 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 285 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 546 B |
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 328 KiB After Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 193 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 285 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 546 B |
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 328 KiB After Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 193 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 285 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 546 B |
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 328 KiB After Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 193 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 285 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 546 B |
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 328 KiB After Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 193 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 285 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 546 B |
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 206 KiB |