Compare commits
1 commit
master
...
79-APNG-Su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85b0e8ec0b |
|
|
@ -1,2 +0,0 @@
|
||||||
[alias]
|
|
||||||
xtask = "run --manifest-path xtask/Cargo.toml --"
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
.editorconfig
|
|
||||||
.gitignore
|
|
||||||
/.git/
|
|
||||||
/.github/
|
|
||||||
/.pre-commit-hooks.yaml
|
|
||||||
/scripts/
|
|
||||||
/target/
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
charset = utf-8
|
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
indent_style = space
|
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
|
||||||
|
|
||||||
[*.rs]
|
[*.rs]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
|
||||||
2
.gitattributes
vendored
|
|
@ -1,2 +0,0 @@
|
||||||
# Enforce Unix newlines
|
|
||||||
* text=auto eol=lf
|
|
||||||
24
.github/dependabot.yml
vendored
|
|
@ -1,24 +0,0 @@
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "cargo"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
open-pull-requests-limit: 5
|
|
||||||
commit-message:
|
|
||||||
prefix: "chore"
|
|
||||||
include: "scope"
|
|
||||||
labels:
|
|
||||||
- "dependencies"
|
|
||||||
- "rust"
|
|
||||||
groups:
|
|
||||||
rust-dependencies:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
- "patch"
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
138
.github/workflows/deploy.yml
vendored
|
|
@ -1,138 +0,0 @@
|
||||||
name: deploy
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- v*.*.*
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
# If we ever end up with two concurrent jobs for releasing the same tag, the former should be cancelled
|
|
||||||
group: release-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
name: Deploy release
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 30
|
|
||||||
|
|
||||||
# Prevent job from running on forks
|
|
||||||
if: ${{ !github.event.repository.fork }}
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
# Execute one job at a time to avoid potential race conditions in the
|
|
||||||
# GitHub release management APIs. See:
|
|
||||||
# https://github.com/softprops/action-gh-release/issues/445#issuecomment-2407940052
|
|
||||||
max-parallel: 1
|
|
||||||
matrix:
|
|
||||||
target:
|
|
||||||
- x86_64-unknown-linux-gnu
|
|
||||||
- x86_64-unknown-linux-musl
|
|
||||||
- aarch64-unknown-linux-gnu
|
|
||||||
- aarch64-unknown-linux-musl
|
|
||||||
- x86_64-pc-windows-msvc
|
|
||||||
- i686-pc-windows-msvc
|
|
||||||
- x86_64-apple-darwin
|
|
||||||
- aarch64-apple-darwin
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout source
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: nightly
|
|
||||||
cache-bin: false
|
|
||||||
cache-shared-key: cache
|
|
||||||
|
|
||||||
- name: Install cargo-deb
|
|
||||||
if: endsWith(matrix.target, '-linux-gnu')
|
|
||||||
uses: taiki-e/install-action@v2
|
|
||||||
with:
|
|
||||||
tool: cargo-deb
|
|
||||||
|
|
||||||
- name: Get the Oxipng version
|
|
||||||
id: oxipngMeta
|
|
||||||
run: >
|
|
||||||
echo "version=$(cargo metadata --format-version 1 --no-deps |
|
|
||||||
jq -r '.packages[] | select(.name == "oxipng").version')" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Retrieve ${{ matrix.target }} binary
|
|
||||||
uses: dawidd6/action-download-artifact@v21
|
|
||||||
with:
|
|
||||||
workflow: oxipng.yml
|
|
||||||
commit: ${{ github.sha }}
|
|
||||||
name: Oxipng binary (${{ matrix.target }})
|
|
||||||
path: target
|
|
||||||
|
|
||||||
- name: Generate up to date manual
|
|
||||||
run: scripts/manual.sh
|
|
||||||
|
|
||||||
- name: Build archives
|
|
||||||
working-directory: target
|
|
||||||
run: |
|
|
||||||
ARCHIVE_NAME="oxipng-${{ steps.oxipngMeta.outputs.version }}-${{ matrix.target }}"
|
|
||||||
|
|
||||||
mkdir "$ARCHIVE_NAME"
|
|
||||||
cp ../CHANGELOG.md ../README.md ../MANUAL.txt "$ARCHIVE_NAME"
|
|
||||||
|
|
||||||
case '${{ matrix.target }}' in
|
|
||||||
*-windows-*)
|
|
||||||
cp ../LICENSE "$ARCHIVE_NAME/LICENSE.txt"
|
|
||||||
cp oxipng.exe "$ARCHIVE_NAME"
|
|
||||||
zip "${ARCHIVE_NAME}.zip" "$ARCHIVE_NAME"/*;;
|
|
||||||
*)
|
|
||||||
cp ../LICENSE "$ARCHIVE_NAME"
|
|
||||||
cp oxipng "$ARCHIVE_NAME"
|
|
||||||
# Execute permissions are not stored in artifact files,
|
|
||||||
# so make the binary world-executable to meet user
|
|
||||||
# expectations set by preceding releases.
|
|
||||||
# Related issue:
|
|
||||||
# https://github.com/oxipng/oxipng/issues/575
|
|
||||||
chmod ugo+x "$ARCHIVE_NAME"/oxipng
|
|
||||||
tar -vczf "${ARCHIVE_NAME}.tar.gz" "$ARCHIVE_NAME"/*;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
- name: Install AArch64 libc components
|
|
||||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
|
||||||
run: |
|
|
||||||
sudo apt-get -yq update
|
|
||||||
# The shared libc AArch64 libraries are needed for cargo deb below
|
|
||||||
# to be able to infer package requirements with dpkg-shlibdeps
|
|
||||||
# properly
|
|
||||||
sudo apt-get -yq install libc6-arm64-cross libgcc-s1-arm64-cross
|
|
||||||
|
|
||||||
- name: Build Debian packages
|
|
||||||
if: endsWith(matrix.target, '-linux-gnu')
|
|
||||||
env:
|
|
||||||
# The *-arm64-cross packages above install AArch64 libraries in
|
|
||||||
# /usr/<arch>/lib instead of /usr/lib/<arch>, as expected by
|
|
||||||
# cargo-deb and dpkg-shlibdeps to find such shared libraries.
|
|
||||||
# Make both of them visible to such commands by adding that directory
|
|
||||||
# to the dynamic linker's library search path. See:
|
|
||||||
# - <https://man7.org/linux/man-pages/man1/dpkg-shlibdeps.1.html> ("Errors" section)
|
|
||||||
# - <https://github.com/kornelski/cargo-deb/issues/21>
|
|
||||||
LD_LIBRARY_PATH: /usr/aarch64-linux-gnu/lib
|
|
||||||
run: |
|
|
||||||
mkdir -p "target/${{ matrix.target }}/release"
|
|
||||||
mv target/oxipng "target/${{ matrix.target }}/release"
|
|
||||||
cargo deb --target "${{ matrix.target }}" --no-build --no-strip
|
|
||||||
|
|
||||||
- name: Create release notes
|
|
||||||
run: tail -n +3 CHANGELOG.md | sed -e '/^$/,$d' > RELEASE_NOTES.txt
|
|
||||||
|
|
||||||
- name: Create release
|
|
||||||
uses: softprops/action-gh-release@v3
|
|
||||||
with:
|
|
||||||
name: v${{ steps.oxipngMeta.outputs.version }}
|
|
||||||
body_path: RELEASE_NOTES.txt
|
|
||||||
files: |
|
|
||||||
target/*.zip
|
|
||||||
target/*.tar.gz
|
|
||||||
target/${{ matrix.target }}/debian/*.deb
|
|
||||||
60
.github/workflows/docker.yml
vendored
|
|
@ -1,60 +0,0 @@
|
||||||
name: docker
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
tags:
|
|
||||||
- v*.*.*
|
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- synchronize
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
use_cache:
|
|
||||||
description: "Use build cache"
|
|
||||||
required: true
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
# If we ever end up with two concurrent Docker build jobs for the same commit
|
|
||||||
# and same versioning context, the former should be cancelled
|
|
||||||
group: docker-${{ github.sha }}-${{ github.ref_type }}-${{ github.head_ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
uses: docker/github-builder/.github/workflows/build.yml@v1
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
id-token: write
|
|
||||||
attestations: write
|
|
||||||
with:
|
|
||||||
output: image
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
cache: ${{ github.event_name != 'workflow_dispatch' || inputs.use_cache }}
|
|
||||||
# We don't sign the image because ghcr.io doesn't support OCI Referrers API yet, and the fallback relies on pushing new tags on each build
|
|
||||||
# https://github.com/docker/github-builder/issues/109#issuecomment-3885082486
|
|
||||||
# https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#backwards-compatibility
|
|
||||||
sign: false
|
|
||||||
sbom: true
|
|
||||||
meta-images: ghcr.io/${{ github.repository }}
|
|
||||||
meta-tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=pr
|
|
||||||
type=semver,pattern={{raw}}
|
|
||||||
set-meta-annotations: true
|
|
||||||
set-meta-labels: true
|
|
||||||
labels: |
|
|
||||||
org.opencontainers.image.title=Oxipng
|
|
||||||
annotations: |
|
|
||||||
org.opencontainers.image.title=Oxipng
|
|
||||||
|
|
||||||
secrets:
|
|
||||||
registry-auths: |
|
|
||||||
- registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
180
.github/workflows/oxipng.yml
vendored
|
|
@ -1,180 +0,0 @@
|
||||||
name: oxipng
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- synchronize
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
# If we ever end up with two concurrent CI jobs for the same commit, the former should be cancelled
|
|
||||||
group: ci-${{ github.sha }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
ci:
|
|
||||||
name: CI
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
timeout-minutes: 60
|
|
||||||
|
|
||||||
# Prevent tags and in-repo PRs from triggering this workflow more than once for a commit
|
|
||||||
if: github.ref_type != 'tag' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork)
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
target:
|
|
||||||
- x86_64-unknown-linux-gnu
|
|
||||||
- x86_64-unknown-linux-musl
|
|
||||||
- aarch64-unknown-linux-gnu
|
|
||||||
- aarch64-unknown-linux-musl
|
|
||||||
- x86_64-pc-windows-msvc
|
|
||||||
- i686-pc-windows-msvc
|
|
||||||
- x86_64-apple-darwin
|
|
||||||
- aarch64-apple-darwin
|
|
||||||
|
|
||||||
include:
|
|
||||||
- target: x86_64-unknown-linux-gnu
|
|
||||||
os: ubuntu-24.04
|
|
||||||
target-apt-arch: amd64
|
|
||||||
- target: x86_64-unknown-linux-musl
|
|
||||||
os: ubuntu-24.04
|
|
||||||
target-apt-arch: amd64
|
|
||||||
- target: aarch64-unknown-linux-gnu
|
|
||||||
os: ubuntu-24.04-arm
|
|
||||||
target-apt-arch: arm64
|
|
||||||
- target: aarch64-unknown-linux-musl
|
|
||||||
os: ubuntu-24.04-arm
|
|
||||||
target-apt-arch: arm64
|
|
||||||
- target: x86_64-pc-windows-msvc
|
|
||||||
os: windows-latest
|
|
||||||
- target: i686-pc-windows-msvc
|
|
||||||
os: windows-latest
|
|
||||||
- target: x86_64-apple-darwin
|
|
||||||
os: macos-15
|
|
||||||
- target: aarch64-apple-darwin
|
|
||||||
os: macos-15
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_BUILD_TARGET: ${{ matrix.target }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout source
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install musl development files
|
|
||||||
if: endsWith(matrix.target, '-musl')
|
|
||||||
run: |
|
|
||||||
sudo apt-get -yq update
|
|
||||||
sudo apt-get -yq install musl-tools musl-dev:${{ matrix.target-apt-arch }}
|
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: nightly
|
|
||||||
target: ${{ env.CARGO_BUILD_TARGET }}
|
|
||||||
components: clippy, rustfmt, rust-src
|
|
||||||
cache-bin: false
|
|
||||||
cache-shared-key: cache
|
|
||||||
|
|
||||||
- name: Install nextest
|
|
||||||
uses: taiki-e/install-action@nextest
|
|
||||||
|
|
||||||
- name: Install cargo-hack
|
|
||||||
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
|
||||||
uses: taiki-e/install-action@cargo-hack
|
|
||||||
|
|
||||||
- name: Install clippy-sarif
|
|
||||||
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
|
||||||
uses: taiki-e/install-action@v2
|
|
||||||
with:
|
|
||||||
tool: clippy-sarif
|
|
||||||
|
|
||||||
- name: Install sarif-fmt
|
|
||||||
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
|
||||||
uses: taiki-e/install-action@v2
|
|
||||||
with:
|
|
||||||
tool: sarif-fmt
|
|
||||||
|
|
||||||
- name: Run rustfmt
|
|
||||||
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
|
||||||
run: cargo fmt --check
|
|
||||||
|
|
||||||
- name: Run Clippy for all feature combinations
|
|
||||||
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
|
||||||
run: >
|
|
||||||
set -o pipefail;
|
|
||||||
cargo hack clippy --no-deps --all-targets --feature-powerset \
|
|
||||||
--exclude-features sanity-checks,system-libdeflate \
|
|
||||||
--message-format=json -- -D warnings \
|
|
||||||
| clippy-sarif
|
|
||||||
| tee clippy-results.sarif
|
|
||||||
| sarif-fmt
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
cargo nextest run --release --features sanity-checks
|
|
||||||
cargo test --doc --release --features sanity-checks
|
|
||||||
|
|
||||||
- name: Build benchmarks
|
|
||||||
run: cargo bench --no-run
|
|
||||||
|
|
||||||
- name: Build docs
|
|
||||||
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
|
||||||
run: cargo doc --release --no-deps
|
|
||||||
|
|
||||||
- name: Build CLI binary
|
|
||||||
run: cargo build --release -Zbuild-std
|
|
||||||
env:
|
|
||||||
RUSTFLAGS: "-Zlocation-detail=none -Zunstable-options -Cpanic=immediate-abort"
|
|
||||||
|
|
||||||
- name: Upload CLI binary as artifact
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: Oxipng binary (${{ matrix.target }})
|
|
||||||
path: |
|
|
||||||
target/${{ env.CARGO_BUILD_TARGET }}/release/oxipng
|
|
||||||
target/${{ env.CARGO_BUILD_TARGET }}/release/oxipng.exe
|
|
||||||
|
|
||||||
- name: Upload analysis results to GitHub
|
|
||||||
uses: github/codeql-action/upload-sarif@v4
|
|
||||||
if: always() && matrix.target == 'x86_64-unknown-linux-gnu'
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
sarif_file: clippy-results.sarif
|
|
||||||
category: clippy
|
|
||||||
|
|
||||||
msrv-check:
|
|
||||||
name: MSRV check
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 30
|
|
||||||
|
|
||||||
# Prevent tags and in-repo PRs from triggering this workflow more than once for a commit
|
|
||||||
if: github.ref_type != 'tag' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork)
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout source
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install MSRV Rust toolchain
|
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: 1.85.1
|
|
||||||
cache-bin: false
|
|
||||||
cache-shared-key: cache
|
|
||||||
|
|
||||||
- name: Install nextest
|
|
||||||
uses: taiki-e/install-action@nextest
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
cargo nextest run --release --features sanity-checks
|
|
||||||
cargo test --doc --release --features sanity-checks
|
|
||||||
9
.gitignore
vendored
|
|
@ -1,6 +1,7 @@
|
||||||
.DS_Store
|
target
|
||||||
*.bk
|
*.bk
|
||||||
|
.DS_Store
|
||||||
*.out.png
|
*.out.png
|
||||||
/.idea/
|
/.idea
|
||||||
/node_modules/
|
/.vscode
|
||||||
/target/
|
/node_modules
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
- id: oxipng
|
|
||||||
name: oxipng
|
|
||||||
description: 'Multithreaded PNG optimizer written in Rust.'
|
|
||||||
entry: oxipng
|
|
||||||
language: rust
|
|
||||||
types: [png]
|
|
||||||
require_serial: true
|
|
||||||
49
.travis.yml
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
language: rust
|
||||||
|
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- PROJECT_NAME=oxipng
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: osx
|
||||||
|
rust: stable
|
||||||
|
env: TARGET=x86_64-apple-darwin
|
||||||
|
cache: cargo
|
||||||
|
- os: linux
|
||||||
|
rust: 1.27.0
|
||||||
|
env: TARGET=x86_64-unknown-linux-gnu
|
||||||
|
cache: cargo
|
||||||
|
- os: linux
|
||||||
|
rust: stable
|
||||||
|
env: TARGET=x86_64-unknown-linux-gnu
|
||||||
|
cache: cargo
|
||||||
|
- os: linux
|
||||||
|
rust: beta
|
||||||
|
env: TARGET=x86_64-unknown-linux-gnu
|
||||||
|
cache: cargo
|
||||||
|
- os: linux
|
||||||
|
rust: nightly
|
||||||
|
env: TARGET=x86_64-unknown-linux-gnu
|
||||||
|
cache: cargo
|
||||||
|
allow_failures:
|
||||||
|
- rust: beta
|
||||||
|
- rust: nightly
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
provider: releases
|
||||||
|
api_key:
|
||||||
|
secure: FarU1VnPW15WXkNP76rQH0VdB+dnp6Op7t2/bl5oCjn7OQ8R3vB32vxCP7DXtBvtE2U9z3CeRmE4pKIRN1rObb/GUyNKxPH0UJiTtDgGtxtTYZHdBjED2nEqdcA2+G1ElCub5O3NhtzsRLM1KPPukJcts6gCFgK/tgcDyKLMX/iGsXOuxOwqu6algZOTcHIaT4M8/TjH7g/hzOz8UKoaqZgSLuKLJ+J30LCJKK4umW57Nev66ATavBWQ3MIFCTyBc9YSsMSQcsjtlA7jcHNhelsCxQHzbxXWQK1jF439QXyNHog3tS41qmfMal+TUhW/NiSkqwJ1//PowD+oYI7si/biBawf6PU2oz5AfWknzPr4k6lAX2WYDcRLIkwEwKi5fHrvMYL0TuNWGb8gF+7DD+7DRaZrRLAbrA6SOFGydSPNzO7HvEYJjs39/2LfqXOQ0aie4ez5fGzsE655D+U1ulmVOx3Y6C1rxKi/oBVJZNuvdMPqtRpgbFYtAsk//wM+nt+YH8TndFR+x6AjZ9OTSDbbJwMLm1SckDLAE/sAC/vU+A6io82uMuDvKmPrtiOKsZp4kmwo5N1CWvIBtvd8evIMPAOFW51AncxnCvCiqYGtfB/BkKAr7mcxiwuWVwXMkOpSr7JzfQd8C5gOEMVczA9qnB6fRCY+dpxGjY0LSY0=
|
||||||
|
file_glob: true
|
||||||
|
file: ${PROJECT_NAME}-${TRAVIS_TAG}-${TARGET}.*
|
||||||
|
# don't delete the artifacts from previous phases
|
||||||
|
skip_cleanup: true
|
||||||
|
# deploy when a new tag is pushed
|
||||||
|
on:
|
||||||
|
# channel to use to produce the release artifacts
|
||||||
|
condition: $TRAVIS_RUST_VERSION = stable
|
||||||
|
tags: true
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
email:
|
||||||
|
on_success: never
|
||||||
642
CHANGELOG.md
|
|
@ -1,587 +1,265 @@
|
||||||
## Version 10.1.1
|
|
||||||
|
|
||||||
- [Performance] Improve Bigrams performance, giving notably faster results at lower levels.
|
|
||||||
- [Improvement] Change `--np` flag to also prevent conversion to indexed from other color types.
|
|
||||||
- [Improvement] Improve support for recompressing ICC profiles with high compression ratios.
|
|
||||||
- [Misc] Add warning when `--ziwi` exceeds `--zi`.
|
|
||||||
- [Build] Further reduce size of binaries.
|
|
||||||
|
|
||||||
## Version 10.1.0
|
|
||||||
|
|
||||||
- [Feature] Add `--json` option for machine-readable output.
|
|
||||||
- [Improvement] New default output with file counter and summary. (Use an extra `-v` flag to get the same output as before.)
|
|
||||||
- [Bugfix] Fix fast mode sometimes giving suboptimal results for small, indexed images.
|
|
||||||
|
|
||||||
## Version 10.0.0
|
|
||||||
|
|
||||||
- [Breaking] CLI: Change short zopfli flag from `-Z` to `-z`.
|
|
||||||
- [Breaking] CLI: Change `--pretend`/`-P` to `--dry-run`/`-d`.
|
|
||||||
- [Breaking] CLI: Change `--interlace` options from `1`/`0` to `on`/`off`.
|
|
||||||
- [Breaking] API: Change `Options.interlace: Option<Interlacing>` to `Options.interlace: Option<bool>`.
|
|
||||||
- [Breaking] API: Change `Options.filter: IndexSet<RowFilter>` to `Options.filters: IndexSet<FilterStrategy>`.
|
|
||||||
- [Breaking] API: Change `Options.deflate: Deflaters` to `Options.deflater: Deflater`.
|
|
||||||
- [Breaking] API: Change `Deflaters::Zopfli { .. }` to `Deflater::Zopfli(ZopfliOptions { .. })`.
|
|
||||||
- [Breaking] API: Change `optimize()` to return `(usize, usize)`, with original and optimized sizes.
|
|
||||||
- [Feature] Allow zopfli iterations higher than 255.
|
|
||||||
- [Feature] Add `--ziwi` option for zopfli iterations without improvement.
|
|
||||||
- [Feature] Add `--max-raw-size` option to skip images that are too large.
|
|
||||||
- [Feature] Add `--brute-level` and `--brute-lines` options for advanced control of Brute filter.
|
|
||||||
- [Improvement] Reduce memory usage for `fast` mode.
|
|
||||||
- [Improvement] Slightly improve compression with Brute filter at levels 5 and 6.
|
|
||||||
- [Misc] Change `--preserve` option to no longer preserve last access time.
|
|
||||||
- [Misc] Make help output colored.
|
|
||||||
|
|
||||||
## Version 9.1.5
|
|
||||||
|
|
||||||
- [Feature] Add `--sequential` option to process files sequentially rather than in parallel.
|
|
||||||
- [Performance] Update to latest Zopfli with greatly improved performance.
|
|
||||||
- [Improvement] Reduce memory usage.
|
|
||||||
- [Bugfix] Correct handling of grayscale conversion when ICC profile is present.
|
|
||||||
|
|
||||||
## Version 9.1.4
|
|
||||||
|
|
||||||
- [Improvement] Improve optimization of APNG files (reductions still not supported yet).
|
|
||||||
- [Improvement] Improve reductions for small images and ensure consistent results for repeat runs.
|
|
||||||
- [Build] Add feature `system-libdeflate` to use the system-installed version of libdeflate.
|
|
||||||
- [Misc] Strip C2PA metadata by default.
|
|
||||||
|
|
||||||
## Version 9.1.3
|
|
||||||
|
|
||||||
- [Feature] Add `--zi` option to control the number of Zopfli iterations.
|
|
||||||
- [Improvement] Allow setting compression level to 0.
|
|
||||||
- [Performance] Improve filtering performance for some images.
|
|
||||||
- [Build] Move man page generation to an xtask.
|
|
||||||
|
|
||||||
## Version 9.1.2
|
|
||||||
|
|
||||||
- [Bugfix] Fix `--nx` still applying deinterlacing by default.
|
|
||||||
- [Bugfix] Fix wildcard matching being case-sensitive on Windows.
|
|
||||||
- [Bugfix] Fix optimized APNGs not being compatible with some programs.
|
|
||||||
- [Build] Fix feature `sanity-checks` not working without `parallel`.
|
|
||||||
- [Misc] Resolve ambiguity between optional dependencies and crate features.
|
|
||||||
|
|
||||||
## Version 9.1.1
|
|
||||||
|
|
||||||
- [Build] Change man page generation path to resolve issue with cargo publish.
|
|
||||||
|
|
||||||
## Version 9.1.0
|
|
||||||
|
|
||||||
- [Improvement] Add `--keep display` equivalent to `--strip safe`.
|
|
||||||
- [Improvement] Add modified zeng palette sorting method, improving optimization of indexed images.
|
|
||||||
- [Improvement] If only one filter is specified, guarantee to only use this one.
|
|
||||||
- [Improvement] Evaluate low-depth indexed even if low-depth grayscale was already achieved.
|
|
||||||
- [Bugfix] Fix battiato palette sorting method not being used if the input was not already indexed.
|
|
||||||
- [Bugfix] Fix rare crash caused by a truncated palette.
|
|
||||||
- [Build] Reduce size of binaries.
|
|
||||||
- [Build] Add man page generation.
|
|
||||||
- [Build] Publish deb archives for Linux.
|
|
||||||
- [Misc] Bump minimum Rust version to 1.74.0.
|
|
||||||
|
|
||||||
## Version 9.0.0
|
|
||||||
|
|
||||||
- [Breaking] Remove `--backup` option. Use `--out` or `--dir` to preserve existing files.
|
|
||||||
- [Breaking] Remove `--check` option. Use `--nx --nz` to perform a non-optimizing run.
|
|
||||||
- [Breaking] API: Replace `pretend` option with `OutFile::None`.
|
|
||||||
- [Breaking] API: Move `preserve_attrs` into `OutFile::Path`.
|
|
||||||
- [Breaking] Default to removing interlacing. Use `-i keep` to retain existing interlacing.
|
|
||||||
- [Feature] Add Raw API for creating optimised PNGs from raw image data.
|
|
||||||
- [Feature] Add basic support for APNG files.
|
|
||||||
- [Feature] Add `--scale16` option to forcibly reduce 16-bit images to 8-bit.
|
|
||||||
- [Improvement] Process multiple files in parallel.
|
|
||||||
- [Improvement] Improve reductions, particularly for indexed or very small images.
|
|
||||||
- [Improvement] Improve compression with latest version of libdeflate.
|
|
||||||
- [Improvement] Recompress iCCP chunks.
|
|
||||||
- [Improvement] Change recursive mode to only process .png/.apng files.
|
|
||||||
- [Improvement] Add support for glob patterns in quotes on Windows.
|
|
||||||
- [Improvement] Quieter default output logging, with multiple levels of verbosity.
|
|
||||||
- [Bugfix] Fix deadlock when using oxipng within an existing Rayon thread pool.
|
|
||||||
- [Bugfix] Fix early abort in recursive mode when a read error occurred.
|
|
||||||
- [Bugfix] Fix losing aux chunks when there's more than one of the same type.
|
|
||||||
- [Bugfix] Fix sometimes writing output even when it was larger.
|
|
||||||
- [Misc] Revamp CI workflow to upload artifacts and generate binaries for additional architectures.
|
|
||||||
- [Misc] Bump minimum Rust version to 1.66.0.
|
|
||||||
|
|
||||||
## Version 8.0.0
|
|
||||||
|
|
||||||
- [Breaking] Revamp alpha optimization
|
|
||||||
- [Bugfix] Fix grayscale depth reduction with tRNS pixel
|
|
||||||
- [Bugfix] Fix fast mode with zopfli
|
|
||||||
- [Improvement] Tweaks to interlacing and format display
|
|
||||||
- [Improvement] Ability to reduce alpha channel to tRNS pixel
|
|
||||||
- [Improvement] Performance improvements to colorspace reduction
|
|
||||||
- [Misc] Disable image validation in release mode, as it is deemed no longer necessary
|
|
||||||
|
|
||||||
## Version 7.0.0
|
|
||||||
|
|
||||||
- [Breaking] Switch to libdeflater as the default
|
|
||||||
- [Breaking] Rebalance presets
|
|
||||||
- [Breaking] Update safe headers to remove
|
|
||||||
- [Breaking] Bump minimum Rust version to 1.61.0
|
|
||||||
- [Feature] Introduce new filter strategies
|
|
||||||
- [Feature] Allow setting libdeflate compression level
|
|
||||||
- [Feature] Add optional dockerization
|
|
||||||
- [Improvement] Improve alpha reductions
|
|
||||||
- [Improvement] Fast deinterlace for 8-bit
|
|
||||||
- [Improvement] Refactor heuristics for fast filter evaluation
|
|
||||||
- [Improvement] Improve file size reduction further
|
|
||||||
- [Bugfix] Avoid printing output bytes on decompress error
|
|
||||||
- [Bugfix] Fix compression level 0
|
|
||||||
- [Bugfix] Fix directional alpha reductions
|
|
||||||
- [Bugfix] Fix edge cases in palette reduction
|
|
||||||
|
|
||||||
### Version 6.0.1
|
|
||||||
|
|
||||||
- [Bugfix] Fix an issue where Zopfli mode could generate corrupt images
|
|
||||||
|
|
||||||
### Version 6.0.0
|
|
||||||
|
|
||||||
- [Breaking] Bump minimum Rust version to 1.57.0
|
|
||||||
- [Feature] Add `--check`/`-c` CLI option
|
|
||||||
- [Security] Update stderrlog to 0.5.2 (Fixes RUSTSEC-2022-0006)
|
|
||||||
- [Security] Remove chrono as a transitive dependency (Fixes RUSTSEC-2020-0159)
|
|
||||||
- [Misc] Bump `clap` to 3.2
|
|
||||||
- [Misc] Bump `zopfli` to 0.7
|
|
||||||
- [Misc] Bump `libdeflater` to 0.10
|
|
||||||
- [Misc] Remove `byteorder` dependency in favor of stdlib functions
|
|
||||||
- [Misc] Bump `image` to 0.24
|
|
||||||
- [Misc] Bump `crc` to 3.0
|
|
||||||
- [Misc] Bump `miniz_oxide` to 0.6
|
|
||||||
- [Misc] Update to Rust edition 2021
|
|
||||||
- [Misc] Various internal improvements
|
|
||||||
|
|
||||||
### Version 5.0.1
|
|
||||||
|
|
||||||
- [Bugfix] Fix an issue where -o5 and -o6 were not testing all options
|
|
||||||
- [Misc] Bump miniz_oxide to 0.5
|
|
||||||
|
|
||||||
### Version 5.0.0
|
|
||||||
|
|
||||||
- [Breaking] Bump minimum Rust version to 1.46.0
|
|
||||||
- [Bugfix] Avoid many panics that may occur on broken files
|
|
||||||
- [Bugfix] Fix race condition that may cause tests to fail
|
|
||||||
- [Performance] Optimizations to unfiltering and copying
|
|
||||||
- [Feature] Add `--ng` option to skip grayscale reduction
|
|
||||||
- [Feature] Also preserve timestamps when using `--preserve`
|
|
||||||
- [Misc] Bump `crc` to 2.x
|
|
||||||
|
|
||||||
### Version 4.0.3
|
|
||||||
|
|
||||||
- Bump itertools to 0.10.x
|
|
||||||
- Temporarily disable i686 releases, which were failing due to an odd linker issue,
|
|
||||||
so that at least amd64 builds will publish successfully
|
|
||||||
- This only relates to the releases published on Github. You can still manually compile oxipng for any platform.
|
|
||||||
|
|
||||||
### Version 4.0.2
|
|
||||||
|
|
||||||
- Includes another update to libdeflater that improves support for targets
|
|
||||||
without a C stdlib, like wasm32.
|
|
||||||
|
|
||||||
### Version 4.0.1
|
|
||||||
|
|
||||||
- Includes an update to libdeflater that improves support for targets
|
|
||||||
without a C stdlib, like wasm32.
|
|
||||||
|
|
||||||
### Version 4.0.0
|
|
||||||
|
|
||||||
- [Breaking] Bump minimum Rust version to 1.45.0
|
|
||||||
- [Feature] Make `libdeflater` and `zopfli` optional for API users
|
|
||||||
- [Bugfix] Fix cloudflare-zlib on aarch64 CPUs
|
|
||||||
- [Bugfix] Don't exit on finding a non-PNG file when crawling recursively
|
|
||||||
- [Bugfix] Make `rayon` truly optional
|
|
||||||
- Various internal improvements
|
|
||||||
|
|
||||||
### Version 3.0.1
|
|
||||||
|
|
||||||
- [Bugfix] Re-add `--force` flag to CLI
|
|
||||||
- This was accidentally removed somehow
|
|
||||||
- Many non-breaking dependency version bumps
|
|
||||||
|
|
||||||
### Version 3.0.0
|
|
||||||
|
|
||||||
- [Breaking] Bump minimum Rust version to 1.41.0
|
|
||||||
- [Breaking] Use IndexMap/IndexSet to provide more consistent performance ([#202](https://github.com/oxipng/oxipng/pull/202))
|
|
||||||
- This changes some public-facing types.
|
|
||||||
`IndexMap` and `IndexSet` are reexported
|
|
||||||
at the crate root to aid migration.
|
|
||||||
- [Breaking] Remove fields from the `Options` struct which were never used ([#211](https://github.com/oxipng/oxipng/pull/211/files#diff-b4aea3e418ccdb71239b96952d9cddb6L217), [#212](https://github.com/oxipng/oxipng/pull/212/files#diff-b4aea3e418ccdb71239b96952d9cddb6L134))
|
|
||||||
- [Breaking] Refactor zlib-specific options in the `Options` struct ([#210](https://github.com/oxipng/oxipng/pull/210/files))
|
|
||||||
- [Feature] Add libdeflater as an option ([#203](https://github.com/oxipng/oxipng/pull/203))
|
|
||||||
- [Feature] Use standard `log` library ([#218](https://github.com/oxipng/oxipng/pull/218))
|
|
||||||
- [Feature] Add `-o max` setting which will always reference the highest compression preset ([#224](https://github.com/oxipng/oxipng/pull/224))
|
|
||||||
- [Deprecated] `-o 4` was found to be equivalent to `-o 3` and is deprecated.
|
|
||||||
It will likely be removed in a future release.
|
|
||||||
For now it remains equivalent to `-o 3`. ([#224](https://github.com/oxipng/oxipng/pull/224))
|
|
||||||
- [Bugfix] Ensure output is deterministic ([#199](https://github.com/oxipng/oxipng/pull/199))
|
|
||||||
- Update `image` crate to 0.23
|
|
||||||
- Update `itertools` crate to 0.9
|
|
||||||
- Various performance and internal improvements
|
|
||||||
|
|
||||||
### Version 2.3.0
|
|
||||||
|
|
||||||
- Allow disabling all alpha optimizations ([#181](https://github.com/oxipng/oxipng/pull/181))
|
|
||||||
- Fix interlacing issues on tiny images ([#182](https://github.com/oxipng/oxipng/pull/182))
|
|
||||||
- Reduce memory usage in filtering ([#191](https://github.com/oxipng/oxipng/pull/191))
|
|
||||||
- Implement palette sorting to improve compression ([#193](https://github.com/oxipng/oxipng/pull/193))
|
|
||||||
- Disable alpha optimizations by default ([#187](https://github.com/oxipng/oxipng/pull/187))
|
|
||||||
- Add support for WASM ([#194](https://github.com/oxipng/oxipng/pull/194))
|
|
||||||
|
|
||||||
### Version 2.2.2
|
|
||||||
|
|
||||||
- Fix grayscale bit-depth reduction ([#171](https://github.com/oxipng/oxipng/pull/171))
|
|
||||||
- Fix typos and incorrect log message ([#172](https://github.com/oxipng/oxipng/pull/172))
|
|
||||||
- Make metadata order deterministic ([#174](https://github.com/oxipng/oxipng/pull/174))
|
|
||||||
- Fix 32-bit builds ([#176](https://github.com/oxipng/oxipng/pull/176))
|
|
||||||
- Enable LTO in release builds ([#177](https://github.com/oxipng/oxipng/pull/177))
|
|
||||||
- Use deterministic compression strategy ([#179](https://github.com/oxipng/oxipng/pull/179))
|
|
||||||
- Fix decoding interlaced images with height or width <= 2 ([#175](https://github.com/oxipng/oxipng/pull/175))
|
|
||||||
- Preallocate memory in reduced_alpha_to_up ([#180](https://github.com/oxipng/oxipng/pull/180))
|
|
||||||
- Update `bit-vec` crate to 0.6
|
|
||||||
|
|
||||||
### Version 2.2.1
|
|
||||||
|
|
||||||
- Fix compression of very large files ([#167](https://github.com/oxipng/oxipng/pull/167)) ([#168](https://github.com/oxipng/oxipng/pull/168))
|
|
||||||
|
|
||||||
### Version 2.2.0
|
|
||||||
|
|
||||||
- Various internal improvements ([#154](https://github.com/oxipng/oxipng/pull/154)) ([#158](https://github.com/oxipng/oxipng/pull/158)) ([#160](https://github.com/oxipng/oxipng/pull/160)) ([#161](https://github.com/oxipng/oxipng/pull/161)) ([#162](https://github.com/oxipng/oxipng/pull/162)) ([#163](https://github.com/oxipng/oxipng/pull/163))
|
|
||||||
- Update `image` crate to 0.21.0
|
|
||||||
- Update `itertools` crate to 0.8.0
|
|
||||||
- Update `zopfli` crate to 0.4.0
|
|
||||||
- Use Rust edition 2018
|
|
||||||
- Bump minimum required Rust version to 1.31.0
|
|
||||||
|
|
||||||
### Version 2.1.8
|
|
||||||
|
|
||||||
- Fix non-standard sBIT headers in other code locations ([#153](https://github.com/oxipng/oxipng/issues/153))
|
|
||||||
|
|
||||||
### Version 2.1.7
|
|
||||||
|
|
||||||
- 80x faster palette reduction ([#150](https://github.com/oxipng/oxipng/pull/150))
|
|
||||||
- Optimize RGB to palette conversion ([#148](https://github.com/oxipng/oxipng/pull/148))
|
|
||||||
- Various microoptimizations ([#146](https://github.com/oxipng/oxipng/pull/146))
|
|
||||||
- Introduce third-party safe wrapper around cloudflare-zlib ([#149](https://github.com/oxipng/oxipng/pull/149))
|
|
||||||
|
|
||||||
### Version 2.1.6
|
|
||||||
|
|
||||||
- Identify and drop useless sRGB profiles ([#143](https://github.com/oxipng/oxipng/pull/143))
|
|
||||||
- Alpha heuristic improvements ([#144](https://github.com/oxipng/oxipng/pull/144))
|
|
||||||
- Bump `miniz_oxide` and `cloudflare-zlib-sys` to 0.2.0
|
|
||||||
|
|
||||||
### Version 2.1.5
|
### 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
|
||||||
|
- Bump `image` crate to 0.20.0
|
||||||
- Bump `image` crate to 0.20.0
|
|
||||||
|
|
||||||
### Version 2.1.3
|
### Version 2.1.3
|
||||||
|
- Fix i686 builds
|
||||||
- Fix i686 builds
|
- Performance improvements
|
||||||
- Performance improvements
|
|
||||||
|
|
||||||
### 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
|
||||||
|
- Revert making Cloudflare zlib the default, as it introduced a major memory leak. It will be put back behind a feature flag, and reenabled when the issue is fixed.
|
||||||
- Revert making Cloudflare zlib the default, as it introduced a major memory leak. It will be put back behind a feature flag, and reenabled when the issue is fixed.
|
- Revert minimum Rust version to 1.24.0
|
||||||
- Revert minimum Rust version to 1.24.0
|
|
||||||
|
|
||||||
### Version 2.0.0
|
### Version 2.0.0
|
||||||
|
- [SEMVER_MAJOR] Make PngError a proper Error enum
|
||||||
- [SEMVER_MAJOR] Make PngError a proper Error enum
|
- [SEMVER_MINOR] Bump minimum Rust version to 1.27.0
|
||||||
- [SEMVER_MINOR] Bump minimum Rust version to 1.27.0
|
- [SEMVER_MINOR] Make Rayon an optional dependency (enabled by default)
|
||||||
- [SEMVER_MINOR] Make Rayon an optional dependency (enabled by default)
|
- [SEMVER_MINOR] Option to limit wall clock time spent in optimization trials
|
||||||
- [SEMVER_MINOR] Option to limit wall clock time spent in optimization trials
|
- [SEMVER_MINOR] `--keep` option (works similar to `--strip`, but takes a comma-separated list of headers to keep, and removes all other non-critical headers)
|
||||||
- [SEMVER_MINOR] `--keep` option (works similar to `--strip`, but takes a comma-separated list of headers to keep, and removes all other non-critical headers)
|
- Use faster Cloudflare zlib compression on platforms that support it
|
||||||
- Use faster Cloudflare zlib compression on platforms that support it
|
- Allow specifying more than 2 filter types via the CLI
|
||||||
- Allow specifying more than 2 filter types via the CLI
|
- Avoid double glob processing on unix
|
||||||
- Avoid double glob processing on unix
|
- Fix reading from stdin
|
||||||
- Fix reading from stdin
|
- Cleanup help text
|
||||||
- Cleanup help text
|
- Various performance improvements
|
||||||
- Various performance improvements
|
|
||||||
|
|
||||||
### Version 1.0.4
|
### Version 1.0.4
|
||||||
|
- Bump `image` to 0.19.0
|
||||||
- Bump `image` to 0.19.0
|
- Bump `bit-vec` to 0.5.0
|
||||||
- Bump `bit-vec` to 0.5.0
|
- Bump `regex` to 1.0.0
|
||||||
- Bump `regex` to 1.0.0
|
|
||||||
|
|
||||||
### Version 1.0.3
|
### Version 1.0.3
|
||||||
|
- Bump dependencies
|
||||||
- Bump dependencies
|
|
||||||
|
|
||||||
### Version 1.0.2
|
### Version 1.0.2
|
||||||
|
- Fix a change that breaks Itertools::flatten with recent Rust nightlies
|
||||||
- Fix a change that breaks Itertools::flatten with recent Rust nightlies
|
|
||||||
|
|
||||||
### Version 1.0.1
|
### 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.
|
- [SEMVER_MAJOR] Adjust the presets, now that `-zm` is no longer an option.
|
||||||
- [SEMVER_MAJOR] Adjust the presets, now that `-zm` is no longer an option.
|
- `-o3` now tests all filter types. This will result in 50% more trials than before, but may give up to 10% more compression gain.
|
||||||
- `-o3` now tests all filter types. This will result in 50% more trials than before, but may give up to 10% more compression gain.
|
- `-o4` and higher now test all alpha optimization types. This adds 5 trials specific to the alpha channel. Only transparent images are affected.
|
||||||
- `-o4` and higher now test all alpha optimization types. This adds 5 trials specific to the alpha channel. Only transparent images are affected.
|
- Implement unix-specific permissions copying for `-p` option
|
||||||
- Implement unix-specific permissions copying for `-p` option
|
- Performance optimizations
|
||||||
- Performance optimizations
|
- Refactor of internal code
|
||||||
- Refactor of internal code
|
|
||||||
|
|
||||||
### Version 0.19.0
|
### Version 0.19.0
|
||||||
|
- [SEMVER_MAJOR] Default to overwriting the input file if `out_file` is not set.
|
||||||
- [SEMVER_MAJOR] Default to overwriting the input file if `out_file` is not set.
|
This does not affect the CLI, but with the library, it was easy to forget to set the `out_file`,
|
||||||
This does not affect the CLI, but with the library, it was easy to forget to set the `out_file`,
|
and there was no warning that no output file would be written.
|
||||||
and there was no warning that no output file would be written.
|
- Bump dependencies, reduces binary size by a considerable amount
|
||||||
- Bump dependencies, reduces binary size by a considerable amount
|
- Hide all modules from documentation, and only export the specific structures that should be public.
|
||||||
- Hide all modules from documentation, and only export the specific structures that should be public.
|
Previously there were too many implementation details made public. The modules are still public for the purposes of our integration tests,
|
||||||
Previously there were too many implementation details made public. The modules are still public for the purposes of our integration tests,
|
but we strongly advise against using undocumented modules. These may become private in the future.
|
||||||
but we strongly advise against using undocumented modules. These may become private in the future.
|
- Internal refactoring and code cleanup
|
||||||
- Internal refactoring and code cleanup
|
- Fix an error message that was displaying the wrong file path
|
||||||
- Fix an error message that was displaying the wrong file path
|
- Fix an issue where the output file would not be written if the input was already optimized,
|
||||||
- Fix an issue where the output file would not be written if the input was already optimized,
|
even if the output path was different from the input path
|
||||||
even if the output path was different from the input path
|
|
||||||
|
|
||||||
### 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
|
||||||
|
- Remove VC++ download requirement for Windows users
|
||||||
- Remove VC++ download requirement for Windows users
|
|
||||||
|
|
||||||
### Version 0.17.0
|
### Version 0.17.0
|
||||||
|
- [SEMVER_MAJOR] Bump minimum required rustc version to 1.19.0
|
||||||
- [SEMVER_MAJOR] Bump minimum required rustc version to 1.19.0
|
- [SEMVER_MINOR] Oxipng will now, by default, attempt to change all transparent pixels to `rgba(0, 0, 0, 0)` to improve compression.
|
||||||
- [SEMVER_MINOR] Oxipng will now, by default, attempt to change all transparent pixels to `rgba(0, 0, 0, 0)` to improve compression.
|
It does fast trials with filters 0 and 5 to see if this is an improvement over
|
||||||
It does fast trials with filters 0 and 5 to see if this is an improvement over
|
the existing alpha channel.
|
||||||
the existing alpha channel.
|
- [SEMVER_MINOR] Add a `-a` option to the command line (`alphas` in the struct) which enables 6 different
|
||||||
- [SEMVER_MINOR] Add a `-a` option to the command line (`alphas` in the struct) which enables 6 different
|
trials for optimizing the alpha channel, using the previously mentioned fast heuristic.
|
||||||
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
|
||||||
|
- Publicly export `error` module
|
||||||
- Publicly export `error` module
|
|
||||||
|
|
||||||
### Version 0.16.1
|
### Version 0.16.1
|
||||||
|
- Fix rayon's breaking changes that they made in a point release
|
||||||
- Fix rayon's breaking changes that they made in a point release
|
|
||||||
|
|
||||||
### Version 0.16.0
|
### Version 0.16.0
|
||||||
|
- [SEMVER_MAJOR] Bump minimum rustc version to 1.17.0
|
||||||
- [SEMVER_MAJOR] Bump minimum rustc version to 1.17.0
|
- Bump `image` to 0.14
|
||||||
- Bump `image` to 0.14
|
- Bump `rayon` to 0.8
|
||||||
- Bump `rayon` to 0.8
|
|
||||||
|
|
||||||
### 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
|
||||||
|
- Fix bug when reducing RGBA to Indexed if image has 256 colors plus a background color
|
||||||
- Fix bug when reducing RGBA to Indexed if image has 256 colors plus a background color
|
|
||||||
|
|
||||||
### Version 0.14.3
|
### Version 0.14.3
|
||||||
|
- Fix multiple bugs when reducing transparency palettes
|
||||||
- Fix multiple bugs when reducing transparency palettes
|
|
||||||
|
|
||||||
### Version 0.14.2
|
### Version 0.14.2
|
||||||
|
- Fix a bug when reducing palette in images with bit depth less than 8
|
||||||
- Fix a bug when reducing palette in images with bit depth less than 8
|
- Fix a bug when reducing palette in images with transparency
|
||||||
- Fix a bug when reducing palette in images with transparency
|
|
||||||
|
|
||||||
### Version 0.14.1
|
### Version 0.14.1
|
||||||
|
- Remove zlib dependency and switch entirely to miniz, since zlib 1.2.11 was not working with oxipng. This costs some performance, but is better than having a broken application.
|
||||||
- Remove zlib dependency and switch entirely to miniz, since zlib 1.2.11 was not working with oxipng. This costs some performance, but is better than having a broken application.
|
|
||||||
|
|
||||||
### Version 0.14.0
|
### Version 0.14.0
|
||||||
|
- Performance optimizations
|
||||||
- Performance optimizations
|
- [SEMVER_MAJOR] Bump minimum rustc version to 1.13.0
|
||||||
- [SEMVER_MAJOR] Bump minimum rustc version to 1.13.0
|
- Add categories on crates.io
|
||||||
- Add categories on crates.io
|
|
||||||
|
|
||||||
### Version 0.13.1
|
### Version 0.13.1
|
||||||
|
- Bump regex dependency to 0.2
|
||||||
- Bump regex dependency to 0.2
|
- Bump byteorder dependency to 1.0
|
||||||
- Bump byteorder dependency to 1.0
|
- Bump rayon dependency to 0.6
|
||||||
- Bump rayon dependency to 0.6
|
|
||||||
|
|
||||||
### 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
|
||||||
|
- [SEMVER_MAJOR] Bump minimum rustc version to 1.9.0, required by dependencies
|
||||||
- [SEMVER_MAJOR] Bump minimum rustc version to 1.9.0, required by dependencies
|
- [SEMVER_MINOR] Allow calling optimization presets via crate using `Options::from_preset`
|
||||||
- [SEMVER_MINOR] Allow calling optimization presets via crate using `Options::from_preset`
|
- [SEMVER_MAJOR] Return proper `PngError` type which implements `std::error::Error` from `Result`s
|
||||||
- [SEMVER_MAJOR] Return proper `PngError` type which implements `std::error::Error` from `Result`s
|
- [SEMVER_MAJOR] Rename module `deflate::deflate` to `deflate`
|
||||||
- [SEMVER_MAJOR] Rename module `deflate::deflate` to `deflate`
|
- Performance optimizations
|
||||||
- Performance optimizations
|
|
||||||
|
|
||||||
### Version 0.10.0
|
### Version 0.10.0
|
||||||
|
- [SEMVER_MINOR] Make clap and regex dependencies optional
|
||||||
- [SEMVER_MINOR] Make clap and regex dependencies optional
|
- Enabled by default, needed for executable build; can be disabled for use in crates
|
||||||
- Enabled by default, needed for executable build; can be disabled for use in crates
|
- Remove reduction from palette to grayscale, which was not working and provided minimal benefit
|
||||||
- Remove reduction from palette to grayscale, which was not working and provided minimal benefit
|
|
||||||
|
|
||||||
### Version 0.9.0
|
### Version 0.9.0
|
||||||
|
- [SEMVER_MAJOR] Significant refactoring of modules
|
||||||
- [SEMVER_MAJOR] Significant refactoring of modules
|
- Use `itertools` to cleanup areas of code
|
||||||
- Use `itertools` to cleanup areas of code
|
- Use multiple threads for filtering trials
|
||||||
- Use multiple threads for filtering trials
|
|
||||||
|
|
||||||
### 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
|
||||||
|
- Minor optimizations
|
||||||
- Minor optimizations
|
- Fix issue where interlaced images with certain widths would fail to optimize
|
||||||
- Fix issue where interlaced images with certain widths would fail to optimize
|
|
||||||
|
|
||||||
### Version 0.8.0
|
### Version 0.8.0
|
||||||
|
- [SEMVER_MINOR] Add support for optimizing PNGs already loaded into memory via library function
|
||||||
- [SEMVER_MINOR] Add support for optimizing PNGs already loaded into memory via library function
|
|
||||||
|
|
||||||
### Version 0.7.0
|
### Version 0.7.0
|
||||||
|
- 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
|
||||||
|
- Fix issue where output directory would not be created if it did not exist
|
||||||
- Fix issue where output directory would not be created if it did not exist
|
- Use miniz for compression strategies where it outperforms zlib
|
||||||
- Use miniz for compression strategies where it outperforms zlib
|
- [SEMVER_MINOR] Partially implement -p / --preserve, as far as stable Rust will allow for now
|
||||||
- [SEMVER_MINOR] Partially implement -p / --preserve, as far as stable Rust will allow for now
|
- [SEMVER_MINOR] Implement --fix to ignore CRC errors and recalculate correct CRC in output
|
||||||
- [SEMVER_MINOR] Implement --fix to ignore CRC errors and recalculate correct CRC in output
|
|
||||||
|
|
||||||
### 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
|
||||||
|
- Initial beta release
|
||||||
- Initial beta release
|
- Reduce color type and bit depth
|
||||||
- Reduce color type and bit depth
|
- Recompress with zlib
|
||||||
- Recompress with zlib
|
- Multithreading
|
||||||
- Multithreading
|
- Strip headers option
|
||||||
- Strip headers option
|
- Backup file before writing option
|
||||||
- Backup file before writing option
|
- Write to stdout option
|
||||||
- Write to stdout option
|
|
||||||
|
|
|
||||||
708
Cargo.lock
generated
|
|
@ -1,649 +1,459 @@
|
||||||
# This file is automatically @generated by Cargo.
|
[[package]]
|
||||||
# It is not intended for manual editing.
|
name = "adler32"
|
||||||
version = 4
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "adler2"
|
name = "adler32"
|
||||||
version = "2.0.1"
|
version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "ansi_term"
|
||||||
version = "1.0.0"
|
version = "0.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"anstyle-parse",
|
|
||||||
"anstyle-query",
|
|
||||||
"anstyle-wincon",
|
|
||||||
"colorchoice",
|
|
||||||
"is_terminal_polyfill",
|
|
||||||
"utf8parse",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle"
|
name = "arrayvec"
|
||||||
version = "1.0.14"
|
version = "0.4.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle-parse"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"utf8parse",
|
"nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-query"
|
name = "atty"
|
||||||
version = "1.1.5"
|
version = "0.2.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys",
|
"libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-wincon"
|
name = "bit-vec"
|
||||||
version = "3.0.11"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
|
||||||
dependencies = [
|
|
||||||
"anstyle",
|
|
||||||
"once_cell_polyfill",
|
|
||||||
"windows-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "autocfg"
|
|
||||||
version = "1.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.11.1"
|
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 = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitvec"
|
name = "build_const"
|
||||||
version = "1.0.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 = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
|
|
||||||
dependencies = [
|
|
||||||
"funty",
|
|
||||||
"radium",
|
|
||||||
"tap",
|
|
||||||
"wyz",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "byteorder"
|
||||||
version = "3.20.2"
|
version = "0.5.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "byteorder"
|
||||||
version = "1.25.0"
|
version = "1.2.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "byteorder-lite"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.60"
|
version = "1.0.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
|
|
||||||
dependencies = [
|
|
||||||
"find-msvc-tools",
|
|
||||||
"shlex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.1"
|
version = "2.32.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "cloudflare-zlib-sys"
|
||||||
version = "4.6.0"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"cc 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"anstyle",
|
|
||||||
"clap_lex",
|
|
||||||
"strsim",
|
|
||||||
"terminal_size",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_lex"
|
name = "crc"
|
||||||
version = "1.1.0"
|
version = "1.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "colorchoice"
|
|
||||||
version = "1.0.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crc32fast"
|
|
||||||
version = "1.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-deque"
|
name = "crossbeam-deque"
|
||||||
version = "0.8.6"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-epoch",
|
"crossbeam-epoch 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-epoch"
|
name = "crossbeam-epoch"
|
||||||
version = "0.9.18"
|
version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-utils",
|
"arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"cfg-if 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.21"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
dependencies = [
|
||||||
|
"cfg-if 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deflate"
|
||||||
|
version = "0.7.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.15.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 = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "env_filter"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
|
|
||||||
dependencies = [
|
|
||||||
"log",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "env_logger"
|
|
||||||
version = "0.11.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
|
|
||||||
dependencies = [
|
|
||||||
"anstream",
|
|
||||||
"anstyle",
|
|
||||||
"env_filter",
|
|
||||||
"log",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "equivalent"
|
|
||||||
version = "1.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "errno"
|
|
||||||
version = "0.3.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"windows-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fdeflate"
|
|
||||||
version = "0.3.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
|
||||||
dependencies = [
|
|
||||||
"simd-adler32",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "find-msvc-tools"
|
|
||||||
version = "0.1.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "flate2"
|
|
||||||
version = "1.1.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
|
||||||
dependencies = [
|
|
||||||
"crc32fast",
|
|
||||||
"miniz_oxide",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "funty"
|
|
||||||
version = "2.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glob"
|
name = "glob"
|
||||||
version = "0.3.3"
|
version = "0.2.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.17.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
version = "0.25.9"
|
version = "0.20.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"byteorder-lite",
|
"lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"moxcms",
|
"num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"num-traits",
|
"num-rational 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"png",
|
"num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"png 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "inflate"
|
||||||
version = "2.14.0"
|
version = "0.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"hashbrown",
|
|
||||||
"rayon",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "itertools"
|
||||||
version = "1.70.2"
|
version = "0.7.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
dependencies = [
|
||||||
|
"either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "lazy_static"
|
||||||
version = "1.0.18"
|
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 = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
dependencies = [
|
||||||
|
"version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.185"
|
version = "0.2.43"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libdeflate-sys"
|
name = "lzw"
|
||||||
version = "1.25.2"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72753e0008ea87963d2f0770042d0df7abe51fafbb8dcaf618ac440f2f1fec0a"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"pkg-config",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libdeflater"
|
name = "memoffset"
|
||||||
version = "1.25.2"
|
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 = "d1ee41cf6fb1bb6030dfb59ffb7bc01ab26aade44142084c87f0fc7a1658fe71"
|
|
||||||
dependencies = [
|
|
||||||
"libdeflate-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "linux-raw-sys"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "log"
|
|
||||||
version = "0.4.30"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "memchr"
|
|
||||||
version = "2.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adler2",
|
"adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"simd-adler32",
|
"libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "moxcms"
|
name = "nodrop"
|
||||||
version = "0.7.11"
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-integer"
|
||||||
|
version = "0.1.39"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-traits",
|
"num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"pxfm",
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-iter"
|
||||||
|
version = "0.1.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-rational"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell_polyfill"
|
name = "num_cpus"
|
||||||
version = "1.70.2"
|
version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
dependencies = [
|
||||||
|
"libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxipng"
|
name = "oxipng"
|
||||||
version = "10.1.1"
|
version = "2.1.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitvec",
|
"bit-vec 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"clap",
|
"byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"env_logger",
|
"clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"glob",
|
"cloudflare-zlib-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"image",
|
"crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"indexmap",
|
"image 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"libdeflater",
|
"itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"log",
|
"miniz_oxide 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"parse-size",
|
"num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rayon",
|
"rayon 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rgb",
|
"wild 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rustc-hash",
|
"zopfli 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde_json",
|
|
||||||
"zopfli",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parse-size"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pkg-config"
|
|
||||||
version = "0.3.33"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "png"
|
name = "png"
|
||||||
version = "0.18.1"
|
version = "0.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"crc32fast",
|
"deflate 0.7.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"fdeflate",
|
"inflate 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"flate2",
|
"num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"miniz_oxide",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro2"
|
|
||||||
version = "1.0.106"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pxfm"
|
|
||||||
version = "0.1.29"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quote"
|
|
||||||
version = "1.0.45"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "radium"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon"
|
name = "rayon"
|
||||||
version = "1.12.0"
|
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 = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"either",
|
"crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rayon-core",
|
"either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"rayon-core 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon-core"
|
name = "rayon-core"
|
||||||
version = "1.13.0"
|
version = "1.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-deque",
|
"crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"crossbeam-utils",
|
"lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rgb"
|
name = "redox_syscall"
|
||||||
version = "0.8.53"
|
version = "0.1.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_termios"
|
||||||
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "scopeguard"
|
||||||
version = "2.1.2"
|
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 = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustix"
|
|
||||||
version = "1.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"errno",
|
|
||||||
"libc",
|
|
||||||
"linux-raw-sys",
|
|
||||||
"windows-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde"
|
|
||||||
version = "1.0.228"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
|
||||||
dependencies = [
|
|
||||||
"serde_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_core"
|
|
||||||
version = "1.0.228"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
|
||||||
dependencies = [
|
|
||||||
"serde_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_derive"
|
|
||||||
version = "1.0.228"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_json"
|
|
||||||
version = "1.0.150"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
|
||||||
dependencies = [
|
|
||||||
"itoa",
|
|
||||||
"memchr",
|
|
||||||
"serde",
|
|
||||||
"serde_core",
|
|
||||||
"zmij",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "shlex"
|
|
||||||
version = "1.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "simd-adler32"
|
|
||||||
version = "0.3.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "termion"
|
||||||
version = "2.0.117"
|
version = "1.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"quote",
|
"redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"unicode-ident",
|
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tap"
|
name = "textwrap"
|
||||||
version = "1.0.1"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "terminal_size"
|
|
||||||
version = "0.4.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustix",
|
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"windows-sys",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "typed-arena"
|
||||||
version = "1.0.24"
|
version = "1.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "unicode-width"
|
||||||
version = "0.2.2"
|
version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "vec_map"
|
||||||
version = "0.2.1"
|
version = "0.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "version_check"
|
||||||
version = "0.61.2"
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wild"
|
||||||
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wyz"
|
name = "winapi"
|
||||||
version = "0.5.1"
|
version = "0.3.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tap",
|
"winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
version = "1.0.21"
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zopfli"
|
name = "zopfli"
|
||||||
version = "0.8.3"
|
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 = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"adler32 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"crc32fast",
|
"byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"log",
|
"crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"simd-adler32",
|
"typed-arena 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
"checksum adler32 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e928aa58f6dbd754bda26eca562a242549cb606e27a2240fc305fc75a7f12af9"
|
||||||
|
"checksum adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7e522997b529f05601e05166c07ed17789691f562762c7f3b987263d2dedee5c"
|
||||||
|
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
|
||||||
|
"checksum arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a1e964f9e24d588183fcb43503abda40d288c8657dfc27311516ce2f05675aef"
|
||||||
|
"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
|
||||||
|
"checksum bit-vec 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4440d5cb623bb7390ae27fec0bb6c61111969860f8e3ae198bfa0663645e67cf"
|
||||||
|
"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12"
|
||||||
|
"checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39"
|
||||||
|
"checksum byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0fc10e8cc6b2580fda3f36eb6dc5316657f812a3df879a44a66fc9f0fdbc4855"
|
||||||
|
"checksum byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "90492c5858dd7d2e78691cfb89f90d273a2800fc11d98f60786e5d87e2f83781"
|
||||||
|
"checksum cc 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "f159dfd43363c4d08055a07703eb7a3406b0dac4d0584d96965a3262db3c9d16"
|
||||||
|
"checksum cfg-if 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0c4e7bb64a8ebb0d856483e1e682ea3422f883c5f5615a90d51a2c82fe87fdd3"
|
||||||
|
"checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e"
|
||||||
|
"checksum cloudflare-zlib-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1a27574ebbf993003163c445f5f70d3fa9017f09013e5007889991f46aa85ea2"
|
||||||
|
"checksum crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb"
|
||||||
|
"checksum crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f739f8c5363aca78cfb059edf753d8f0d36908c348f3d8d1503f03d8b75d9cf3"
|
||||||
|
"checksum crossbeam-epoch 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "927121f5407de9956180ff5e936fe3cf4324279280001cd56b669d28ee7e9150"
|
||||||
|
"checksum crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2760899e32a1d58d5abb31129f8fae5de75220bc2176e77ff7c627ae45c918d9"
|
||||||
|
"checksum deflate 0.7.19 (registry+https://github.com/rust-lang/crates.io-index)" = "8a6abb26e16e8d419b5c78662aa9f82857c2386a073da266840e474d5055ec86"
|
||||||
|
"checksum either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0"
|
||||||
|
"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
|
||||||
|
"checksum image 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60710fd3cb40c2434451d8d5147bcf39bbb68aae0741041133e09439cb2401e3"
|
||||||
|
"checksum inflate 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6f53b811ee8e2057ccf9643ca6b4277de90efaf5e61e55fd5254576926bb4245"
|
||||||
|
"checksum itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)" = "f58856976b776fedd95533137617a02fb25719f40e7d9b01c7043cd65474f450"
|
||||||
|
"checksum lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca488b89a5657b0a2ecd45b95609b3e848cf1755da332a0da46e2b2b1cb371a7"
|
||||||
|
"checksum libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)" = "76e3a3ef172f1a0b9a9ff0dd1491ae5e6c948b94479a3021819ba7d860c8645d"
|
||||||
|
"checksum lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084"
|
||||||
|
"checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3"
|
||||||
|
"checksum miniz_oxide 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9ba430291c9d6cedae28bcd2d49d1c32fc57d60cd49086646c5dd5673a870eb5"
|
||||||
|
"checksum nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2228dca57108069a5262f2ed8bd2e82496d2e074a06d1ccc7ce1687b6ae0a2"
|
||||||
|
"checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea"
|
||||||
|
"checksum num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "af3fdbbc3291a5464dc57b03860ec37ca6bf915ed6ee385e7c6c052c422b2124"
|
||||||
|
"checksum num-rational 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4e96f040177bb3da242b5b1ecf3f54b5d5af3efbbfb18608977a5d2767b22f10"
|
||||||
|
"checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1"
|
||||||
|
"checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30"
|
||||||
|
"checksum png 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f54b9600d584d3b8a739e1662a595fab051329eff43f20e7d8cc22872962145b"
|
||||||
|
"checksum rayon 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "df7a791f788cb4c516f0e091301a29c2b71ef680db5e644a7d68835c8ae6dbfa"
|
||||||
|
"checksum rayon-core 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b055d1e92aba6877574d8fe604a63c8b5df60f60e5982bf7ccbb1338ea527356"
|
||||||
|
"checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1"
|
||||||
|
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
|
||||||
|
"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
|
||||||
|
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
|
||||||
|
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
||||||
|
"checksum textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "307686869c93e71f94da64286f9a9524c0f308a9e1c87a583de8e9c9039ad3f6"
|
||||||
|
"checksum typed-arena 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c6c06a92aef38bb4dc5b0df00d68496fc31307c5344c867bb61678c6e1671ec5"
|
||||||
|
"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526"
|
||||||
|
"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
|
||||||
|
"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
|
||||||
|
"checksum wild 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "690e5dbd46cfaf2f3bd09875ad94e92cc56459fce505807d6ce5332671aa93ae"
|
||||||
|
"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0"
|
||||||
|
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
"checksum zopfli 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "9fd2fb70c78a87410032abe2a9eb2bec4984a2d9bc26adb1f8f768e02673e84e"
|
||||||
|
|
|
||||||
147
Cargo.toml
|
|
@ -1,30 +1,23 @@
|
||||||
[package]
|
[package]
|
||||||
authors = ["Joshua Holmer <jholmer.in@gmail.com>"]
|
authors = ["Joshua Holmer <jholmer.in@gmail.com>"]
|
||||||
categories = ["command-line-utilities", "compression"]
|
categories = [
|
||||||
description = "A lossless PNG compression optimizer"
|
"command-line-utilities",
|
||||||
keywords = ["png", "image-compression", "optimization", "multi-threading", "lossless"]
|
"compression",
|
||||||
documentation = "https://docs.rs/oxipng"
|
|
||||||
edition = "2024"
|
|
||||||
exclude = [
|
|
||||||
".editorconfig",
|
|
||||||
".gitattributes",
|
|
||||||
".github/*",
|
|
||||||
".gitignore",
|
|
||||||
".pre-commit-hooks.yaml",
|
|
||||||
"Dockerfile",
|
|
||||||
"scripts/*",
|
|
||||||
"tests/*",
|
|
||||||
"xtask/*",
|
|
||||||
]
|
]
|
||||||
homepage = "https://github.com/oxipng/oxipng"
|
description = "A lossless PNG compression optimizer"
|
||||||
|
documentation = "https://docs.rs/oxipng"
|
||||||
|
exclude = [
|
||||||
|
"tests/*",
|
||||||
|
"bench/*",
|
||||||
|
]
|
||||||
|
homepage = "https://github.com/shssoichiro/oxipng"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "oxipng"
|
name = "oxipng"
|
||||||
repository = "https://github.com/oxipng/oxipng"
|
repository = "https://github.com/shssoichiro/oxipng"
|
||||||
version = "10.1.1"
|
version = "2.1.5"
|
||||||
rust-version = "1.85.1"
|
|
||||||
|
|
||||||
[badges]
|
[badges]
|
||||||
travis-ci = { repository = "oxipng/oxipng", branch = "master" }
|
travis-ci = { repository = "shssoichiro/oxipng", branch = "master" }
|
||||||
maintenance = { status = "actively-developed" }
|
maintenance = { status = "actively-developed" }
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|
@ -33,89 +26,53 @@ name = "oxipng"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
required-features = ["binary"]
|
required-features = ["binary"]
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "zopfli"
|
|
||||||
required-features = ["zopfli"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bitvec = "1.0.1"
|
bit-vec = "^0.5.0"
|
||||||
clap = { version = "4.6.0", optional = true, features = ["wrap_help"] }
|
byteorder = "^1.0.0"
|
||||||
env_logger = { version = "0.11.10", optional = true, default-features = false, features = ["auto-color"] }
|
crc = "^1.2.0"
|
||||||
image = { version = "0.25.9", optional = true, default-features = false, features = ["png"] }
|
itertools = "^0.7.7"
|
||||||
indexmap = "2.14.0"
|
num_cpus = "^1.0.0"
|
||||||
libdeflater = "1.25.2"
|
zopfli = "^0.3.4"
|
||||||
log = "0.4.30"
|
miniz_oxide = "0.1.2"
|
||||||
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"] }
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[dependencies.rayon]
|
||||||
glob = { version = "0.3.3", optional = true }
|
optional = true
|
||||||
|
version = "^1.0.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dependencies.clap]
|
||||||
serde_json = "1.0.150"
|
optional = true
|
||||||
|
version = "^2.10.0"
|
||||||
|
|
||||||
|
[dependencies.wild]
|
||||||
|
optional = true
|
||||||
|
version = "2.0.0"
|
||||||
|
|
||||||
|
[target.'cfg(any(target_arch = "x86_64", target_arch = "aarch64"))'.dependencies]
|
||||||
|
cloudflare-zlib-sys = "^0.1.2"
|
||||||
|
|
||||||
|
[dependencies.image]
|
||||||
|
default-features = false
|
||||||
|
features = ["png_codec"]
|
||||||
|
version = "^0.20.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
binary = ["dep:clap", "dep:glob", "dep:env_logger", "dep:parse-size"]
|
binary = [
|
||||||
default = ["binary", "parallel", "zopfli"]
|
"clap",
|
||||||
parallel = ["dep:rayon", "indexmap/rayon"]
|
"wild",
|
||||||
freestanding = ["libdeflater/freestanding"]
|
]
|
||||||
sanity-checks = ["dep:image"]
|
default = ["binary", "parallel"]
|
||||||
zopfli = ["dep:zopfli"]
|
parallel = ["rayon"]
|
||||||
system-libdeflate = ["libdeflater/dynamic"]
|
dev = [
|
||||||
|
"nightly-binary",
|
||||||
|
]
|
||||||
|
nightly = []
|
||||||
|
nightly-binary = [
|
||||||
|
"binary",
|
||||||
|
"nightly",
|
||||||
|
]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "oxipng"
|
name = "oxipng"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
opt-level = 2
|
opt-level = 2
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
lto = "fat"
|
|
||||||
strip = "symbols"
|
|
||||||
panic = "abort"
|
|
||||||
|
|
||||||
[package.metadata.deb]
|
|
||||||
assets = [
|
|
||||||
["target/release/oxipng", "usr/bin/", "755"],
|
|
||||||
["target/xtask/mangen/manpages/oxipng.1", "usr/share/man/man1/", "644"],
|
|
||||||
["README.md", "usr/share/doc/oxipng/", "644"],
|
|
||||||
["CHANGELOG.md", "usr/share/doc/oxipng/", "644"],
|
|
||||||
]
|
|
||||||
|
|
||||||
[lints.rust]
|
|
||||||
missing_copy_implementations = "deny"
|
|
||||||
missing_debug_implementations = "deny"
|
|
||||||
trivial_casts = "warn"
|
|
||||||
trivial_numeric_casts = "warn"
|
|
||||||
unused_import_braces = "warn"
|
|
||||||
|
|
||||||
[lints.clippy]
|
|
||||||
cloned_instead_of_copied = "warn"
|
|
||||||
expl_impl_clone_on_copy = "warn"
|
|
||||||
float_cmp_const = "warn"
|
|
||||||
if_not_else = "warn"
|
|
||||||
ignored_unit_patterns = "warn"
|
|
||||||
linkedlist = "warn"
|
|
||||||
manual_let_else = "warn"
|
|
||||||
map_flatten = "warn"
|
|
||||||
map_unwrap_or = "warn"
|
|
||||||
match_same_arms = "warn"
|
|
||||||
mem_forget = "warn"
|
|
||||||
missing_const_for_fn = "warn"
|
|
||||||
mut_mut = "warn"
|
|
||||||
mutex_integer = "warn"
|
|
||||||
needless_continue = "warn"
|
|
||||||
option_if_let_else = "warn"
|
|
||||||
path_buf_push_overwrite = "warn"
|
|
||||||
range_plus_one = "warn"
|
|
||||||
redundant_clone = "warn"
|
|
||||||
redundant_closure_for_method_calls = "warn"
|
|
||||||
semicolon_if_nothing_returned = "warn"
|
|
||||||
unnecessary_semicolon = "warn"
|
|
||||||
unseparated_literal_suffix = "warn"
|
|
||||||
use_self = "warn"
|
|
||||||
useless_let_if_seq = "warn"
|
|
||||||
|
|
|
||||||
46
Dockerfile
|
|
@ -1,46 +0,0 @@
|
||||||
# syntax=docker/dockerfile:1
|
|
||||||
# check=error=true
|
|
||||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
|
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM rust:1.85.1-alpine AS base
|
|
||||||
|
|
||||||
RUN apk update && \
|
|
||||||
apk add \
|
|
||||||
gcc \
|
|
||||||
g++ \
|
|
||||||
clang
|
|
||||||
|
|
||||||
COPY --from=xx / /
|
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
|
||||||
RUN xx-info env
|
|
||||||
|
|
||||||
RUN xx-apk add \
|
|
||||||
gcc \
|
|
||||||
musl-dev \
|
|
||||||
libdeflate
|
|
||||||
|
|
||||||
WORKDIR /src
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.cargo/git/db \
|
|
||||||
--mount=type=cache,target=/root/.cargo/registry/cache \
|
|
||||||
--mount=type=cache,target=/root/.cargo/registry/index \
|
|
||||||
xx-cargo build --release && \
|
|
||||||
xx-verify /src/target/$(xx-cargo --print-target-triple)/release/oxipng && \
|
|
||||||
cp /src/target/$(xx-cargo --print-target-triple)/release/oxipng /src/target/oxipng
|
|
||||||
|
|
||||||
FROM scratch AS tool
|
|
||||||
|
|
||||||
LABEL org.opencontainers.image.title="Oxipng"
|
|
||||||
LABEL org.opencontainers.image.description="Multithreaded PNG optimizer written in Rust"
|
|
||||||
LABEL org.opencontainers.image.authors="Joshua Holmer <jholmer.in@gmail.com>"
|
|
||||||
LABEL org.opencontainers.image.licenses="MIT"
|
|
||||||
LABEL org.opencontainers.image.source="https://github.com/oxipng/oxipng"
|
|
||||||
|
|
||||||
COPY --from=base /src/target/oxipng /usr/local/bin/oxipng
|
|
||||||
|
|
||||||
WORKDIR /work
|
|
||||||
ENTRYPOINT [ "oxipng" ]
|
|
||||||
CMD [ "--help" ]
|
|
||||||
227
MANUAL.txt
|
|
@ -1,227 +0,0 @@
|
||||||
oxipng 10.1.0
|
|
||||||
Losslessly improve compression of PNG files
|
|
||||||
|
|
||||||
Usage: oxipng [OPTIONS] <files>...
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
<files>...
|
|
||||||
File(s) to compress (use '-' for stdin)
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-o, --opt <level>
|
|
||||||
Set the optimization level preset. The default level 2 is quite fast and provides good
|
|
||||||
compression. Lower levels are faster, higher levels provide better compression, though
|
|
||||||
with increasingly diminishing returns.
|
|
||||||
|
|
||||||
0 => --zc 5 --fast (filter chosen heuristically)
|
|
||||||
1 => --zc 10 --fast (filter chosen heuristically)
|
|
||||||
2 => --zc 11 -f 0,1,6,7 --fast
|
|
||||||
3 => --zc 11 -f 0,7,8,9 --brute-level 1 --brute-lines 3
|
|
||||||
4 => --zc 12 -f 0,7,8,9 --brute-level 1 --brute-lines 4
|
|
||||||
5 => --zc 12 -f 0,1,2,5,6,7,8,9 --brute-level 4 --brute-lines 4
|
|
||||||
6 => --zc 12 -f 0-9 --brute-level 5 --brute-lines 8
|
|
||||||
max => (stable alias for the maximum level)
|
|
||||||
|
|
||||||
Manually specifying a compression option (zc, f, etc.) will override the optimization
|
|
||||||
preset, regardless of the order you write the arguments.
|
|
||||||
|
|
||||||
[default: 2]
|
|
||||||
|
|
||||||
-r, --recursive
|
|
||||||
When directories are given as input, traverse the directory trees and optimize all PNG
|
|
||||||
files found (files with “.png” or “.apng” extension).
|
|
||||||
|
|
||||||
--dir <directory>
|
|
||||||
Write output file(s) to <directory>. If the directory does not exist, it will be created.
|
|
||||||
Note that this will not preserve the directory structure of the input files when used with
|
|
||||||
'--recursive'.
|
|
||||||
|
|
||||||
--out <file>
|
|
||||||
Write output file to <file>
|
|
||||||
|
|
||||||
--stdout
|
|
||||||
Write output to stdout
|
|
||||||
|
|
||||||
-p, --preserve
|
|
||||||
Preserve file permissions and timestamps if possible
|
|
||||||
|
|
||||||
-d, --dry-run
|
|
||||||
Do not write any files, only show compression results
|
|
||||||
|
|
||||||
-s
|
|
||||||
Strip safely-removable chunks, same as '--strip safe'
|
|
||||||
|
|
||||||
--strip <mode>
|
|
||||||
Strip metadata chunks, where <mode> is one of:
|
|
||||||
|
|
||||||
safe => Strip all non-critical chunks, except for the following:
|
|
||||||
cICP, iCCP, sRGB, pHYs, acTL, fcTL, fdAT
|
|
||||||
all => Strip all non-critical chunks
|
|
||||||
<list> => Strip chunks in the comma-separated list, e.g. 'bKGD,cHRM'
|
|
||||||
|
|
||||||
CAUTION: 'all' will convert APNGs to standard PNGs.
|
|
||||||
|
|
||||||
Please note that regardless of any options set, some chunks will necessarily be stripped
|
|
||||||
when invalidated by the optimization:
|
|
||||||
bKGD, sBIT, hIST: Stripped if the color type or bit depth changes.
|
|
||||||
iDOT: Always stripped.
|
|
||||||
caBX: Stripped if it contains C2PA metadata. If explicitly retained by `--keep`,
|
|
||||||
optimization will be aborted.
|
|
||||||
|
|
||||||
The default when --strip is not passed is to keep all chunks that remain valid.
|
|
||||||
|
|
||||||
--keep <list>
|
|
||||||
Strip all metadata chunks except those in the comma-separated list. The special value
|
|
||||||
'display' includes chunks that affect the image appearance, equivalent to '--strip safe'.
|
|
||||||
|
|
||||||
E.g. '--keep eXIf,display' will strip chunks, keeping only eXIf and those that affect the
|
|
||||||
image appearance.
|
|
||||||
|
|
||||||
-a, --alpha
|
|
||||||
Perform additional optimization on images with an alpha channel, by altering the color
|
|
||||||
values of fully transparent pixels. This is generally recommended for better compression,
|
|
||||||
but take care as while this is “visually lossless”, it is technically a lossy
|
|
||||||
transformation and may be unsuitable for some applications.
|
|
||||||
|
|
||||||
-i, --interlace <mode>
|
|
||||||
Set the PNG interlacing mode, where <mode> is one of:
|
|
||||||
|
|
||||||
off => Remove interlacing from all images that are processed
|
|
||||||
on => Apply Adam7 interlacing on all images that are processed
|
|
||||||
keep => Keep the existing interlacing mode of each image
|
|
||||||
|
|
||||||
Note that interlacing can add 25-50% to the size of an optimized image. Only use it if you
|
|
||||||
believe the benefits outweigh the costs for your use case.
|
|
||||||
|
|
||||||
[default: off]
|
|
||||||
|
|
||||||
--scale16
|
|
||||||
Forcibly reduce images with 16 bits per channel to 8 bits per channel. This is a lossy
|
|
||||||
operation but can provide significant savings when you have no need for higher depth.
|
|
||||||
Reduction is performed by scaling the values such that, e.g. 0x00FF is reduced to 0x01
|
|
||||||
rather than 0x00.
|
|
||||||
|
|
||||||
Without this flag, 16-bit images will only be reduced in depth if it can be done
|
|
||||||
losslessly.
|
|
||||||
|
|
||||||
-v, --verbose...
|
|
||||||
Show per-file info (use multiple times for more detail)
|
|
||||||
|
|
||||||
-q, --quiet
|
|
||||||
Suppress all output messages
|
|
||||||
|
|
||||||
-j, --json
|
|
||||||
Print results as JSON
|
|
||||||
|
|
||||||
-f, --filters <list>
|
|
||||||
Perform compression trials with each of the given filter types. You can specify a
|
|
||||||
comma-separated list, or a range of values. E.g. '-f 0-3' is the same as '-f 0,1,2,3'.
|
|
||||||
|
|
||||||
PNG delta filters (apply the same filter to every line)
|
|
||||||
0 => None (recommended to always include this filter)
|
|
||||||
1 => Sub
|
|
||||||
2 => Up
|
|
||||||
3 => Average
|
|
||||||
4 => Paeth
|
|
||||||
|
|
||||||
Heuristic strategies (try to find the best delta filter for each line)
|
|
||||||
5 => MinSum Minimum sum of absolute differences
|
|
||||||
6 => Entropy Smallest Shannon entropy
|
|
||||||
7 => Bigrams Lowest count of distinct bigrams
|
|
||||||
8 => BigEnt Smallest Shannon entropy of bigrams
|
|
||||||
9 => Brute Smallest compressed size (slow)
|
|
||||||
|
|
||||||
The default value depends on the optimization level preset.
|
|
||||||
|
|
||||||
--fast
|
|
||||||
Perform a fast compression evaluation of each enabled filter, followed by a single main
|
|
||||||
compression trial of the best result. Recommended if you have more filters enabled than
|
|
||||||
CPU cores.
|
|
||||||
|
|
||||||
--zc <level>
|
|
||||||
Deflate compression level (0-12) for main compression trials. The levels here are defined
|
|
||||||
by the libdeflate compression library.
|
|
||||||
|
|
||||||
The default value depends on the optimization level preset.
|
|
||||||
|
|
||||||
--nb
|
|
||||||
Do not change bit depth
|
|
||||||
|
|
||||||
--nc
|
|
||||||
Do not change color type
|
|
||||||
|
|
||||||
--np
|
|
||||||
Do not change color palette
|
|
||||||
|
|
||||||
--ng
|
|
||||||
Do not change to or from grayscale
|
|
||||||
|
|
||||||
--nx
|
|
||||||
Do not perform any transformations and do not deinterlace by default.
|
|
||||||
|
|
||||||
--nz
|
|
||||||
Do not recompress IDAT unless required due to transformations. Recompression of other
|
|
||||||
compressed chunks (such as iCCP) will also be disabled. Note that the combination of
|
|
||||||
'--nx' and '--nz' will fully disable all optimization.
|
|
||||||
|
|
||||||
--fix
|
|
||||||
Do not perform checksum validation of PNG chunks. This may allow some files with errors to
|
|
||||||
be processed successfully. The output will always have correct checksums.
|
|
||||||
|
|
||||||
--force
|
|
||||||
Write the output even if it is larger than the input
|
|
||||||
|
|
||||||
-z, --zopfli
|
|
||||||
Use the much slower but stronger Zopfli compressor for main compression trials.
|
|
||||||
Recommended use is with '-o max' and '--fast'.
|
|
||||||
|
|
||||||
--zi <iterations>
|
|
||||||
Set the number of iterations to use for Zopfli compression. Using fewer iterations may
|
|
||||||
speed up compression for large files. This option requires '--zopfli' to be set.
|
|
||||||
|
|
||||||
[default: 15]
|
|
||||||
|
|
||||||
--ziwi <iterations>
|
|
||||||
Stop Zopfli compression after this number of iterations without improvement. Use this in
|
|
||||||
conjunction with a high value for '--zi' to achieve better compression in reasonable time.
|
|
||||||
|
|
||||||
--brute-level <level>
|
|
||||||
Set the libdeflate compression level to use with the Brute filter strategy. Sane values
|
|
||||||
are 1-5. Higher values are not necessarily better.
|
|
||||||
|
|
||||||
--brute-lines <lines>
|
|
||||||
Set the number of lines to compress at once with the Brute filter strategy. Sane values
|
|
||||||
are 2-16. Higher values are not necessarily better.
|
|
||||||
|
|
||||||
--timeout <secs>
|
|
||||||
Maximum amount of time, in seconds, to spend on optimizations. Oxipng will check the
|
|
||||||
timeout before each transformation or compression trial, and will stop trying to optimize
|
|
||||||
the file if the timeout is exceeded. Note that this does not cut short any operations that
|
|
||||||
are already in progress, so it is currently of limited effectiveness for large files with
|
|
||||||
high compression levels.
|
|
||||||
|
|
||||||
--max-raw-size <bytes>
|
|
||||||
Maximum size to allow for the input image. If the raw, decompressed image data (or the
|
|
||||||
file size) of the image exceeds this size, it will be skipped. This is useful for limiting
|
|
||||||
memory usage or avoiding long processing times on large images. The value may be specified
|
|
||||||
with a unit suffix such as k, KB, m, MB, etc.
|
|
||||||
|
|
||||||
The decompressed size of an image is roughly equal to width * height * bit-depth / 8. E.g.
|
|
||||||
a 1920x1080 image with 24-bit color depth would be roughly 6MB.
|
|
||||||
|
|
||||||
-t, --threads <num>
|
|
||||||
Set the maximum number of threads to use. Oxipng uses multithreading to evaluate multiple
|
|
||||||
optimizations on the same file in parallel as well as process multiple files in parallel.
|
|
||||||
You can set this to a lower value if you need to limit memory or CPU usage.
|
|
||||||
|
|
||||||
[default: num logical CPUs]
|
|
||||||
|
|
||||||
--sequential
|
|
||||||
Process multiple files sequentially rather than in parallel. Use this if you need
|
|
||||||
determinism in the processing order. Note this is not necessary if using '--threads 1'.
|
|
||||||
|
|
||||||
-h, --help
|
|
||||||
Print help (see a summary with '-h')
|
|
||||||
|
|
||||||
-V, --version
|
|
||||||
Print version
|
|
||||||
196
README.md
|
|
@ -1,150 +1,84 @@
|
||||||
# Oxipng
|
# Oxipng
|
||||||
|
|
||||||
[](https://github.com/oxipng/oxipng/actions?query=branch%3Amaster)
|
[](https://travis-ci.org/shssoichiro/oxipng)
|
||||||
[](https://crates.io/crates/oxipng)
|
[](https://crates.io/crates/oxipng)
|
||||||
[](https://github.com/oxipng/oxipng/blob/master/LICENSE)
|
[](https://github.com/shssoichiro/oxipng/blob/master/LICENSE)
|
||||||
[](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 compression optimizer. It can be used via a command-line
|
||||||
interface or as a library in other Rust programs. It is fast and highly effective.
|
interface or as a library in other Rust programs.
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
|
|
||||||
Oxipng for Windows can be downloaded via the
|
Oxipng can be downloaded from the [Releases](https://github.com/shssoichiro/oxipng/releases) link on the GitHub page.
|
||||||
[Releases](https://github.com/oxipng/oxipng/releases) section on its GitHub page. Recently,
|
|
||||||
however, Oxipng has also been made available through package managers. Check the list below for
|
|
||||||
up-to-date options.
|
|
||||||
|
|
||||||
For MacOS or Linux, it is recommended to install from your distro's package repository, provided
|
|
||||||
Oxipng is available there in a not too outdated version for your use case.
|
|
||||||
|
|
||||||
Oxipng is known to be packaged for the environments listed below.
|
|
||||||
|
|
||||||
[](https://repology.org/project/oxipng/versions)
|
|
||||||
|
|
||||||
Alternatively, Oxipng can be installed from Cargo, via the following command:
|
|
||||||
|
|
||||||
|
Oxipng can also be installed from Cargo, via the following command:
|
||||||
```
|
```
|
||||||
cargo install oxipng
|
cargo install oxipng
|
||||||
```
|
```
|
||||||
|
|
||||||
Oxipng can also be built from source using the latest stable or nightly Rust.
|
Alternatively, oxipng can be built from source using the latest stable or nightly Rust:
|
||||||
This is primarily useful for developing on Oxipng.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/oxipng/oxipng.git
|
git clone https://github.com/shssoichiro/oxipng.git
|
||||||
cd oxipng
|
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.85.1**.
|
The current minimum supported Rust version is **1.27.0**. Oxipng may compile on earlier versions of Rust,
|
||||||
|
but there is no guarantee.
|
||||||
|
|
||||||
Oxipng follows Semantic Versioning.
|
Oxipng follows Semantic Versioning.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Oxipng is a command-line utility. An example usage, suitable for web, may be the following:
|
Oxipng is a command-line utility. Basic usage looks similar to the following:
|
||||||
|
|
||||||
```
|
```
|
||||||
oxipng -o 4 --strip safe --alpha *.png
|
oxipng -o 4 -i 1 --strip safe *.png
|
||||||
```
|
```
|
||||||
|
|
||||||
The most commonly used options are as follows:
|
The most commonly used options are as follows:
|
||||||
|
* Optimization: `-o 1` through `-o 6`, lower is faster, higher is better compression.
|
||||||
|
The default (`-o 2`) is sufficiently fast on a modern CPU and provides 30-50% compression
|
||||||
|
gains over an unoptimized PNG. `-o 4` is 6 times slower than `-o 2` but can provide 5-10%
|
||||||
|
extra compression over `-o 2`. Using any setting higher than `-o 4` is unlikely
|
||||||
|
to give any extra compression gains and is not recommended.
|
||||||
|
* Interlacing: `-i 1` will enable [Adam7](https://en.wikipedia.org/wiki/Adam7_algorithm)
|
||||||
|
PNG interlacing on any images that are processed. `-i 0` will remove interlacing from all
|
||||||
|
processed images. Not specifying either will keep the same interlacing state as the
|
||||||
|
input image. Note: Interlacing can add 25-50% to the size of an optimized image. Only use
|
||||||
|
it if you believe the benefits outweight the costs for your use case.
|
||||||
|
* Strip: Used to remove metadata info from processed images. Used via `--strip [safe,all]`.
|
||||||
|
Can save a few kilobytes if you don't need the metadata. "Safe" removes only metadata that
|
||||||
|
will never affect rendering of the image. "All" removes all metadata that is not critical
|
||||||
|
to the image. You can also pass a comma-separated list of specific metadata chunks to remove.
|
||||||
|
`-s` can be used as a shorthand for `--strip safe`.
|
||||||
|
|
||||||
- Optimization: `-o 0` through `-o 6` (or `-o max`), lower is faster, higher is better compression.
|
More advanced options can be found by running `oxipng -h`.
|
||||||
The default (`-o 2`) is quite fast and provides good compression. Higher levels can be notably
|
|
||||||
better* but generally have increasingly diminishing returns.
|
|
||||||
- Strip: Used to remove metadata info from processed images. Used via `--strip [safe,all]`.
|
|
||||||
Can save a few kilobytes if you don't need the metadata. "Safe" removes only metadata that
|
|
||||||
will never affect rendering of the image. "All" removes all metadata that is not critical
|
|
||||||
to the image. You can also pass a comma-separated list of specific metadata chunks to remove.
|
|
||||||
`-s` can be used as a shorthand for `--strip safe`.
|
|
||||||
- Alpha: `--alpha` can improve compression of images with transparency, by altering the color
|
|
||||||
values of fully transparent pixels. This is generally recommended, but take care as this is
|
|
||||||
technically a lossy transformation and may be unsuitable for some specific applications.
|
|
||||||
|
|
||||||
More advanced options can be found by running `oxipng --help`, or viewed [here](MANUAL.txt).
|
|
||||||
|
|
||||||
Some options have both short (`-a`) and long (`--alpha`) forms. Which form you use is just a
|
|
||||||
matter of preference. Multiple short options can be combined together, e.g.:
|
|
||||||
`-savvo6` is equivalent to to `--strip safe --alpha --verbose --verbose --opt 6`.
|
|
||||||
All options are case-sensitive.
|
|
||||||
|
|
||||||
\* Note that oxipng is not a brute-force optimizer. This means that while higher optimization levels
|
|
||||||
are almost always better or equal to lower levels, this is not guaranteed and it is possible in
|
|
||||||
rare circumstances that a lower level may give a marginally smaller output. Similarly, using Zopfli
|
|
||||||
compression (`-z`) is not guaranteed to always be better than default compression.
|
|
||||||
|
|
||||||
## APNG support
|
|
||||||
|
|
||||||
Oxipng currently only supports limited optimization of animated PNGs (APNGs). It can perform
|
|
||||||
alpha-optimization, refiltering and recompression of all frames, but all transformations will be
|
|
||||||
disabled. For best results, it is recommended to use another tool such as
|
|
||||||
[apngopt](https://sourceforge.net/projects/apng/files/APNG_Optimizer/) before running Oxipng.
|
|
||||||
|
|
||||||
## Git integration via [pre-commit]
|
|
||||||
|
|
||||||
Create a `.pre-commit-config.yaml` file like this, or add the lines after the `repos` map
|
|
||||||
preamble to an already existing one:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/oxipng/oxipng
|
|
||||||
rev: v10.0.0
|
|
||||||
hooks:
|
|
||||||
- id: oxipng
|
|
||||||
args: ["-o", "4", "--strip", "safe", "--alpha"]
|
|
||||||
```
|
|
||||||
[pre-commit]: https://pre-commit.com/
|
|
||||||
|
|
||||||
## Docker
|
|
||||||
|
|
||||||
A Docker image is availlable at [`ghcr.io/oxipng/oxipng`](https://github.com/oxipng/oxipng/pkgs/container/oxipng) for `linux/amd64` and `linux/arm64`.
|
|
||||||
|
|
||||||
You can use it the following way:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run --rm -v $(pwd):/work ghcr.io/oxipng/oxipng -o 4 /work/file.png
|
|
||||||
```
|
|
||||||
|
|
||||||
Some older images are also available at [`ghcr.io/shssoichiro/oxipng`](https://github.com/users/shssoichiro/packages/container/package/oxipng).
|
|
||||||
|
|
||||||
## 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. You should then
|
other Rust projects. To do so, simply add oxipng as a dependency in your Cargo.toml,
|
||||||
have access to all of the library functions [documented here](https://docs.rs/oxipng). The simplest
|
then `extern crate oxipng` in your project. You should then have access to all of the library
|
||||||
method of usage involves creating an [Options
|
functions [documented here](https://docs.rs/oxipng). The simplest
|
||||||
struct](https://docs.rs/oxipng/latest/oxipng/struct.Options.html) and passing it, along with an
|
method of usage involves creating an
|
||||||
input filename, into the [optimize function](https://docs.rs/oxipng/latest/oxipng/fn.optimize.html).
|
[Options struct](https://docs.rs/oxipng/0.13.0/oxipng/struct.Options.html) and
|
||||||
|
passing it, along with an input filename, into the
|
||||||
It is recommended to disable the "binary" feature when including Oxipng as a library. Currently, there is
|
[optimize function](https://docs.rs/oxipng/0.13.0/oxipng/fn.optimize.html).
|
||||||
no simple way to just disable one feature in Cargo, it has to be done by disabling default features
|
|
||||||
and specifying the desired ones, for example:
|
|
||||||
`oxipng = { version = "10.0", features = ["parallel", "zopfli", "filetime"], default-features = false }`
|
|
||||||
|
|
||||||
## Software using Oxipng
|
|
||||||
|
|
||||||
- [ImageOptim](https://imageoptim.com): Mac app and web service for optimizing images
|
|
||||||
- [Squoosh](https://squoosh.app): Web app for optimizing images
|
|
||||||
- [FileOptimizer](https://nikkhokkho.sourceforge.io/?page=FileOptimizer): Windows app for optimizing files
|
|
||||||
- [Curtail](https://github.com/Huluti/Curtail): Linux app for optimizing images
|
|
||||||
- [pyoxipng](https://pypi.org/project/pyoxipng/): Python wrapper for Oxipng
|
|
||||||
- [jSquash](https://github.com/jamsinclair/jSquash): Collection of WebAssembly image codecs
|
|
||||||
- [Trunk](https://trunk.io): Developer experience toolkit for managing code
|
|
||||||
|
|
||||||
## History
|
## History
|
||||||
|
|
||||||
Oxipng began in 2015 as a rewrite of the OptiPNG project. The core goal was to implement
|
Oxipng began as a complete rewrite of the OptiPNG project,
|
||||||
multithreading, which would have been very difficult to do within the existing C codebase of OptiPNG.
|
which was assumed to be dead as no commit had been made to it since March 2014.
|
||||||
This also served as an opportunity to choose a more modern, safer language (Rust).
|
(OptiPNG has since released a new version, after Oxipng was first released.)
|
||||||
|
The name has been changed to avoid confusion and potential legal issues.
|
||||||
|
|
||||||
However, Oxipng has evolved considerably since then. While some of the options remain similar to
|
The core goal of rewriting OptiPNG was to implement multithreading,
|
||||||
OptiPNG, the architecture and capabilities are now quite different. It is not a drop-in
|
which would be very difficult to do within the existing C codebase of OptiPNG.
|
||||||
replacement - if you are migrating from OptiPNG, please check the [help](MANUAL.txt) before use.
|
This also served as an opportunity to choose a more modern, safer language (Rust).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
@ -154,11 +88,45 @@ to submit a fix with the bug report, it is preferred that you do so via pull req
|
||||||
however you do not need to be a Rust developer to contribute.
|
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 2.1.4 (compiled on rustc 1.29.1 (b801ae664 2018-09-20)) against OptiPNG version 0.7.7 on Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz with 8 logical cores
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Benchmark #1: ./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png
|
||||||
|
|
||||||
|
Time (mean ± σ): 76.1 ms ± 2.2 ms [User: 165.1 ms, System: 26.9 ms]
|
||||||
|
|
||||||
|
Range (min … max): 71.1 ms … 80.4 ms
|
||||||
|
|
||||||
|
Benchmark #2: optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
|
||||||
|
|
||||||
|
Time (mean ± σ): 274.9 ms ± 2.3 ms [User: 272.8 ms, System: 2.0 ms]
|
||||||
|
|
||||||
|
Range (min … max): 271.8 ms … 278.1 ms
|
||||||
|
|
||||||
|
Summary
|
||||||
|
|
||||||
|
'./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png' ran
|
||||||
|
3.61x faster than 'optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png'
|
||||||
|
Benchmark #1: ./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png
|
||||||
|
|
||||||
|
Time (mean ± σ): 103.1 ms ± 3.1 ms [User: 444.9 ms, System: 25.2 ms]
|
||||||
|
|
||||||
|
Range (min … max): 98.6 ms … 111.4 ms
|
||||||
|
|
||||||
|
Benchmark #2: optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
|
||||||
|
|
||||||
|
Time (mean ± σ): 931.3 ms ± 8.7 ms [User: 929.6 ms, System: 1.1 ms]
|
||||||
|
|
||||||
|
Range (min … max): 920.4 ms … 943.3 ms
|
||||||
|
|
||||||
|
Summary
|
||||||
|
|
||||||
|
'./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png' ran
|
||||||
|
9.03x faster than 'optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png'
|
||||||
|
|
|
||||||
96
README.template.md
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
# Oxipng
|
||||||
|
|
||||||
|
[](https://travis-ci.org/shssoichiro/oxipng)
|
||||||
|
[](https://crates.io/crates/oxipng)
|
||||||
|
[](https://github.com/shssoichiro/oxipng/blob/master/LICENSE)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Oxipng is a multithreaded lossless PNG compression optimizer. It can be used via a command-line
|
||||||
|
interface or as a library in other Rust programs.
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
|
||||||
|
Oxipng can be downloaded from the [Releases](https://github.com/shssoichiro/oxipng/releases) link on the GitHub page.
|
||||||
|
|
||||||
|
Oxipng can also be installed from Cargo, via the following command:
|
||||||
|
```
|
||||||
|
cargo install oxipng
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, oxipng can be built from source using the latest stable or nightly Rust:
|
||||||
|
```
|
||||||
|
git clone https://github.com/shssoichiro/oxipng.git
|
||||||
|
cd oxipng
|
||||||
|
cargo build --release
|
||||||
|
cp target/release/oxipng /usr/local/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
The current minimum supported Rust version is **1.27.0**. Oxipng may compile on earlier versions of Rust,
|
||||||
|
but there is no guarantee.
|
||||||
|
|
||||||
|
Oxipng follows Semantic Versioning.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Oxipng is a command-line utility. Basic usage looks similar to the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
oxipng -o 4 -i 1 --strip safe *.png
|
||||||
|
```
|
||||||
|
|
||||||
|
The most commonly used options are as follows:
|
||||||
|
* Optimization: `-o 1` through `-o 6`, lower is faster, higher is better compression.
|
||||||
|
The default (`-o 2`) is sufficiently fast on a modern CPU and provides 30-50% compression
|
||||||
|
gains over an unoptimized PNG. `-o 4` is 6 times slower than `-o 2` but can provide 5-10%
|
||||||
|
extra compression over `-o 2`. Using any setting higher than `-o 4` is unlikely
|
||||||
|
to give any extra compression gains and is not recommended.
|
||||||
|
* Interlacing: `-i 1` will enable [Adam7](https://en.wikipedia.org/wiki/Adam7_algorithm)
|
||||||
|
PNG interlacing on any images that are processed. `-i 0` will remove interlacing from all
|
||||||
|
processed images. Not specifying either will keep the same interlacing state as the
|
||||||
|
input image. Note: Interlacing can add 25-50% to the size of an optimized image. Only use
|
||||||
|
it if you believe the benefits outweight the costs for your use case.
|
||||||
|
* Strip: Used to remove metadata info from processed images. Used via `--strip [safe,all]`.
|
||||||
|
Can save a few kilobytes if you don't need the metadata. "Safe" removes only metadata that
|
||||||
|
will never affect rendering of the image. "All" removes all metadata that is not critical
|
||||||
|
to the image. You can also pass a comma-separated list of specific metadata chunks to remove.
|
||||||
|
`-s` can be used as a shorthand for `--strip safe`.
|
||||||
|
|
||||||
|
More advanced options can be found by running `oxipng -h`.
|
||||||
|
|
||||||
|
## Library Usage
|
||||||
|
|
||||||
|
Although originally intended to be used as an executable, oxipng can also be used as a library in
|
||||||
|
other Rust projects. To do so, simply add oxipng as a dependency in your Cargo.toml,
|
||||||
|
then `extern crate oxipng` in your project. You should then have access to all of the library
|
||||||
|
functions [documented here](https://docs.rs/oxipng). The simplest
|
||||||
|
method of usage involves creating an
|
||||||
|
[Options struct](https://docs.rs/oxipng/0.13.0/oxipng/struct.Options.html) and
|
||||||
|
passing it, along with an input filename, into the
|
||||||
|
[optimize function](https://docs.rs/oxipng/0.13.0/oxipng/fn.optimize.html).
|
||||||
|
|
||||||
|
## History
|
||||||
|
|
||||||
|
Oxipng began as a complete rewrite of the OptiPNG project,
|
||||||
|
which was assumed to be dead as no commit had been made to it since March 2014.
|
||||||
|
(OptiPNG has since released a new version, after Oxipng was first released.)
|
||||||
|
The name has been changed to avoid confusion and potential legal issues.
|
||||||
|
|
||||||
|
The core goal of rewriting OptiPNG was to implement multithreading,
|
||||||
|
which would be very difficult to do within the existing C codebase of OptiPNG.
|
||||||
|
This also served as an opportunity to choose a more modern, safer language (Rust).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Any contributions are welcome and will be accepted via pull request on GitHub. Bug reports can be
|
||||||
|
filed via GitHub issues. Please include as many details as possible. If you have the capability
|
||||||
|
to submit a fix with the bug report, it is preferred that you do so via pull request,
|
||||||
|
however you do not need to be a Rust developer to contribute.
|
||||||
|
Other contributions (such as improving documentation or translations) are also welcome via GitHub.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Oxipng is open-source software, distributed under the MIT license.
|
||||||
|
|
||||||
|
## Benchmarks
|
||||||
|
|
||||||
13
SECURITY.md
|
|
@ -1,13 +0,0 @@
|
||||||
# Security Policy
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
Only the latest version will be supported with security updates. We will not maintain old branches.
|
|
||||||
For this reason, you should attempt to always use the latest released version.
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
To privately report a vulnerability in oxipng, please visit [our advisories page](https://github.com/oxipng/oxipng/security/advisories) and use the button to report a vulnerability.
|
|
||||||
We will review it as soon as possible.
|
|
||||||
|
|
||||||
For vulnerabilities in dependencies, please open a pull request updating the dependency to a patched version.
|
|
||||||
64
appveyor.yml
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
environment:
|
||||||
|
global:
|
||||||
|
# This will be used as part of the zipfile name
|
||||||
|
PROJECT_NAME: oxipng
|
||||||
|
matrix:
|
||||||
|
# Stable channel
|
||||||
|
- TARGET: i686-pc-windows-msvc
|
||||||
|
CHANNEL: stable
|
||||||
|
- TARGET: x86_64-pc-windows-msvc
|
||||||
|
CHANNEL: stable
|
||||||
|
- TARGET: x86_64-pc-windows-msvc
|
||||||
|
CHANNEL: nightly
|
||||||
|
|
||||||
|
# Install Rust and Cargo
|
||||||
|
# (Based on from https://github.com/rust-lang/libc/blob/master/appveyor.yml)
|
||||||
|
install:
|
||||||
|
- curl -sSf -o rustup-init.exe https://win.rustup.rs
|
||||||
|
- rustup-init.exe --default-host %TARGET% --default-toolchain %CHANNEL% -y
|
||||||
|
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
|
||||||
|
- rustc -Vv
|
||||||
|
- cargo -V
|
||||||
|
|
||||||
|
# 'cargo test' takes care of building for us, so disable Appveyor's build stage. This prevents
|
||||||
|
# the "directory does not contain a project or solution file" error.
|
||||||
|
# source: https://github.com/starkat99/appveyor-rust/blob/master/appveyor.yml#L113
|
||||||
|
build: false
|
||||||
|
|
||||||
|
# Equivalent to Travis' `script` phase
|
||||||
|
test_script:
|
||||||
|
- cargo build
|
||||||
|
- cargo test %TESTFLAGS%
|
||||||
|
|
||||||
|
before_deploy:
|
||||||
|
# Generate artifacts for release
|
||||||
|
- SET RUSTFLAGS=-C target-feature=+crt-static
|
||||||
|
- cargo build --release
|
||||||
|
- mkdir staging
|
||||||
|
- copy target\release\oxipng.exe staging
|
||||||
|
- cd staging
|
||||||
|
# release zipfile will look like 'rust-everywhere-v1.2.3-x86_64-pc-windows-msvc'
|
||||||
|
- 7z a ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip *
|
||||||
|
- appveyor PushArtifact ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
description: 'Windows release'
|
||||||
|
# All the zipped artifacts will be deployed
|
||||||
|
artifact: /.*\.zip/
|
||||||
|
# - Go to 'https://github.com/settings/tokens/new' and generate a Token with only the
|
||||||
|
# `public_repo` scope enabled
|
||||||
|
# - Then go to 'https://ci.appveyor.com/tools/encrypt' and enter the newly generated token.
|
||||||
|
# - Enter the "encrypted value" below
|
||||||
|
auth_token:
|
||||||
|
secure: JT77kw2p+U5Z9yM/77QjEWRFgvyaGOO2SquE5OBgUNpqSt4VDg/Mdicy8pkSvSEu
|
||||||
|
provider: GitHub
|
||||||
|
# deploy when a new tag is pushed and only on the stable channel
|
||||||
|
on:
|
||||||
|
# channel to use to produce the release artifacts
|
||||||
|
# NOTE make sure you only release *once* per target
|
||||||
|
CHANNEL: stable
|
||||||
|
appveyor_repo_tag: true
|
||||||
|
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
|
@ -3,61 +3,258 @@
|
||||||
extern crate oxipng;
|
extern crate oxipng;
|
||||||
extern crate test;
|
extern crate test;
|
||||||
|
|
||||||
|
use oxipng::internal_tests::*;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use oxipng::{internal_tests::*, *};
|
|
||||||
use test::Bencher;
|
use test::Bencher;
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn deflate_16_bits(b: &mut Bencher) {
|
fn deflate_16_bits_strategy_0(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 0, 15, &min)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn deflate_8_bits(b: &mut Bencher) {
|
fn deflate_8_bits_strategy_0(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 0, 15, &min)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn deflate_4_bits(b: &mut Bencher) {
|
fn deflate_4_bits_strategy_0(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_4_should_be_palette_4.png",
|
"tests/files/palette_4_should_be_palette_4.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 0, 15, &min)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn deflate_2_bits(b: &mut Bencher) {
|
fn deflate_2_bits_strategy_0(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_2_should_be_palette_2.png",
|
"tests/files/palette_2_should_be_palette_2.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 0, 15, &min)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn deflate_1_bits(b: &mut Bencher) {
|
fn deflate_1_bits_strategy_0(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_1_should_be_palette_1.png",
|
"tests/files/palette_1_should_be_palette_1.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 0, 15, &min)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn deflate_16_bits_strategy_1(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 1, 15, &min)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn deflate_8_bits_strategy_1(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 1, 15, &min)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn deflate_4_bits_strategy_1(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from(
|
||||||
|
"tests/files/palette_4_should_be_palette_4.png",
|
||||||
|
));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 1, 15, &min)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn deflate_2_bits_strategy_1(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from(
|
||||||
|
"tests/files/palette_2_should_be_palette_2.png",
|
||||||
|
));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 1, 15, &min)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn deflate_1_bits_strategy_1(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from(
|
||||||
|
"tests/files/palette_1_should_be_palette_1.png",
|
||||||
|
));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 1, 15, &min)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn deflate_16_bits_strategy_2(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 2, 15, &min)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn deflate_8_bits_strategy_2(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 2, 15, &min)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn deflate_4_bits_strategy_2(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from(
|
||||||
|
"tests/files/palette_4_should_be_palette_4.png",
|
||||||
|
));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 2, 15, &min)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn deflate_2_bits_strategy_2(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from(
|
||||||
|
"tests/files/palette_2_should_be_palette_2.png",
|
||||||
|
));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 2, 15, &min)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn deflate_1_bits_strategy_2(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from(
|
||||||
|
"tests/files/palette_1_should_be_palette_1.png",
|
||||||
|
));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 2, 15, &min)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn deflate_16_bits_strategy_3(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 3, 15, &min)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn deflate_8_bits_strategy_3(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 3, 15, &min)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn deflate_4_bits_strategy_3(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from(
|
||||||
|
"tests/files/palette_4_should_be_palette_4.png",
|
||||||
|
));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 3, 15, &min)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn deflate_2_bits_strategy_3(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from(
|
||||||
|
"tests/files/palette_2_should_be_palette_2.png",
|
||||||
|
));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 3, 15, &min)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn deflate_1_bits_strategy_3(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from(
|
||||||
|
"tests/files/palette_1_should_be_palette_1.png",
|
||||||
|
));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let min = AtomicMin::new(None);
|
||||||
|
deflate(png.raw_data.as_ref(), 9, 3, 15, &min)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn inflate_generic(b: &mut Bencher) {
|
fn inflate_generic(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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| inflate(png.idat_data.as_ref(), png.raw.ihdr.raw_data_size()));
|
b.iter(|| inflate(png.idat_data.as_ref()));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,28 @@
|
||||||
extern crate oxipng;
|
extern crate oxipng;
|
||||||
extern crate test;
|
extern crate test;
|
||||||
|
|
||||||
|
use oxipng::internal_tests::*;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use oxipng::{internal_tests::*, *};
|
|
||||||
use test::Bencher;
|
use test::Bencher;
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn filters_16_bits_filter_0(b: &mut Bencher) {
|
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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn filters_8_bits_filter_0(b: &mut Bencher) {
|
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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -29,9 +32,11 @@ fn filters_4_bits_filter_0(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_4_should_be_palette_4.png",
|
"tests/files/palette_4_should_be_palette_4.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -39,9 +44,11 @@ fn filters_2_bits_filter_0(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_2_should_be_palette_2.png",
|
"tests/files/palette_2_should_be_palette_2.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -49,25 +56,31 @@ fn filters_1_bits_filter_0(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_1_should_be_palette_1.png",
|
"tests/files/palette_1_should_be_palette_1.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn filters_16_bits_filter_1(b: &mut Bencher) {
|
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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn filters_8_bits_filter_1(b: &mut Bencher) {
|
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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -75,9 +88,11 @@ fn filters_4_bits_filter_1(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_4_should_be_palette_4.png",
|
"tests/files/palette_4_should_be_palette_4.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -85,9 +100,11 @@ fn filters_2_bits_filter_1(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_2_should_be_palette_2.png",
|
"tests/files/palette_2_should_be_palette_2.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -95,25 +112,31 @@ fn filters_1_bits_filter_1(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_1_should_be_palette_1.png",
|
"tests/files/palette_1_should_be_palette_1.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn filters_16_bits_filter_2(b: &mut Bencher) {
|
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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(2);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn filters_8_bits_filter_2(b: &mut Bencher) {
|
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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(2);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -121,9 +144,11 @@ fn filters_4_bits_filter_2(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_4_should_be_palette_4.png",
|
"tests/files/palette_4_should_be_palette_4.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(2);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -131,9 +156,11 @@ fn filters_2_bits_filter_2(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_2_should_be_palette_2.png",
|
"tests/files/palette_2_should_be_palette_2.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(2);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -141,25 +168,31 @@ fn filters_1_bits_filter_2(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_1_should_be_palette_1.png",
|
"tests/files/palette_1_should_be_palette_1.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(2);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn filters_16_bits_filter_3(b: &mut Bencher) {
|
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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(3);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn filters_8_bits_filter_3(b: &mut Bencher) {
|
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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(3);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -167,9 +200,11 @@ fn filters_4_bits_filter_3(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_4_should_be_palette_4.png",
|
"tests/files/palette_4_should_be_palette_4.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(3);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -177,9 +212,11 @@ fn filters_2_bits_filter_3(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_2_should_be_palette_2.png",
|
"tests/files/palette_2_should_be_palette_2.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(3);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -187,25 +224,31 @@ fn filters_1_bits_filter_3(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_1_should_be_palette_1.png",
|
"tests/files/palette_1_should_be_palette_1.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(3);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn filters_16_bits_filter_4(b: &mut Bencher) {
|
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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(4);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn filters_8_bits_filter_4(b: &mut Bencher) {
|
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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(4);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -213,9 +256,11 @@ fn filters_4_bits_filter_4(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_4_should_be_palette_4.png",
|
"tests/files/palette_4_should_be_palette_4.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(4);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -223,9 +268,11 @@ fn filters_2_bits_filter_4(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_2_should_be_palette_2.png",
|
"tests/files/palette_2_should_be_palette_2.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(4);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -233,25 +280,31 @@ fn filters_1_bits_filter_4(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_1_should_be_palette_1.png",
|
"tests/files/palette_1_should_be_palette_1.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(4);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn filters_16_bits_filter_5(b: &mut Bencher) {
|
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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(5);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn filters_8_bits_filter_5(b: &mut Bencher) {
|
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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(5);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -259,9 +312,11 @@ fn filters_4_bits_filter_5(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_4_should_be_palette_4.png",
|
"tests/files/palette_4_should_be_palette_4.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(5);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -269,9 +324,11 @@ fn filters_2_bits_filter_5(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_2_should_be_palette_2.png",
|
"tests/files/palette_2_should_be_palette_2.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(5);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -279,7 +336,9 @@ fn filters_1_bits_filter_5(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_1_should_be_palette_1.png",
|
"tests/files/palette_1_should_be_palette_1.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
|
b.iter(|| {
|
||||||
|
png.filter_image(5);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,30 @@
|
||||||
extern crate oxipng;
|
extern crate oxipng;
|
||||||
extern crate test;
|
extern crate test;
|
||||||
|
|
||||||
|
use oxipng::internal_tests::*;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use oxipng::{internal_tests::*, *};
|
|
||||||
use test::Bencher;
|
use test::Bencher;
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn interlacing_16_bits(b: &mut Bencher) {
|
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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(true));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.change_interlacing(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn interlacing_8_bits(b: &mut Bencher) {
|
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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(true));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.change_interlacing(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -29,9 +34,12 @@ fn interlacing_4_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_4_should_be_palette_4.png",
|
"tests/files/palette_4_should_be_palette_4.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(true));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.change_interlacing(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -39,9 +47,12 @@ fn interlacing_2_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_2_should_be_palette_2.png",
|
"tests/files/palette_2_should_be_palette_2.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(true));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.change_interlacing(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -49,9 +60,12 @@ fn interlacing_1_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_1_should_be_palette_1.png",
|
"tests/files/palette_1_should_be_palette_1.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(true));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.change_interlacing(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -59,9 +73,12 @@ fn deinterlacing_16_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/interlaced_rgb_16_should_be_rgb_16.png",
|
"tests/files/interlaced_rgb_16_should_be_rgb_16.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(false));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.change_interlacing(0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -69,9 +86,12 @@ fn deinterlacing_8_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/interlaced_rgb_8_should_be_rgb_8.png",
|
"tests/files/interlaced_rgb_8_should_be_rgb_8.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(false));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.change_interlacing(0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -79,9 +99,12 @@ fn deinterlacing_4_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/interlaced_palette_4_should_be_palette_4.png",
|
"tests/files/interlaced_palette_4_should_be_palette_4.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(false));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.change_interlacing(0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -89,9 +112,12 @@ fn deinterlacing_2_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/interlaced_palette_2_should_be_palette_2.png",
|
"tests/files/interlaced_palette_2_should_be_palette_2.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(false));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.change_interlacing(0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -99,7 +125,10 @@ fn deinterlacing_1_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/interlaced_palette_1_should_be_palette_1.png",
|
"tests/files/interlaced_palette_1_should_be_palette_1.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(false));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.change_interlacing(0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,19 @@
|
||||||
extern crate oxipng;
|
extern crate oxipng;
|
||||||
extern crate test;
|
extern crate test;
|
||||||
|
|
||||||
|
use oxipng::internal_tests::*;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use oxipng::{internal_tests::*, *};
|
|
||||||
use test::Bencher;
|
use test::Bencher;
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn reductions_16_to_8_bits(b: &mut Bencher) {
|
fn reductions_16_to_8_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_8.png"));
|
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_8.png"));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| bit_depth::reduced_bit_depth_16_to_8(&png.raw, false));
|
b.iter(|| {
|
||||||
}
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_bit_depth();
|
||||||
#[bench]
|
});
|
||||||
fn reductions_16_to_8_bits_scaled(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| bit_depth::reduced_bit_depth_16_to_8(&png.raw, true));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -29,9 +23,12 @@ fn reductions_8_to_4_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_8_should_be_palette_4.png",
|
"tests/files/palette_8_should_be_palette_4.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_bit_depth();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -39,9 +36,12 @@ fn reductions_8_to_2_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_8_should_be_palette_2.png",
|
"tests/files/palette_8_should_be_palette_2.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_bit_depth();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -49,63 +49,73 @@ fn reductions_8_to_1_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_8_should_be_palette_1.png",
|
"tests/files/palette_8_should_be_palette_1.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_bit_depth();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn reductions_grayscale_8_to_4_bits(b: &mut Bencher) {
|
fn reductions_4_to_2_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/grayscale_8_should_be_grayscale_4.png",
|
"tests/files/palette_4_should_be_palette_2.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_bit_depth();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn reductions_grayscale_8_to_2_bits(b: &mut Bencher) {
|
fn reductions_4_to_1_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/grayscale_8_should_be_grayscale_2.png",
|
"tests/files/palette_4_should_be_palette_1.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_bit_depth();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn reductions_grayscale_8_to_1_bits(b: &mut Bencher) {
|
fn reductions_2_to_1_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/grayscale_8_should_be_grayscale_1.png",
|
"tests/files/palette_2_should_be_palette_1.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_bit_depth();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_color_type();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn reductions_rgba_to_rgb_8(b: &mut Bencher) {
|
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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
|
b.iter(|| {
|
||||||
}
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_color_type();
|
||||||
#[bench]
|
});
|
||||||
fn reductions_rgba_to_rgb_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]
|
||||||
|
|
@ -113,9 +123,12 @@ fn reductions_rgba_to_grayscale_alpha_16(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/rgba_16_should_be_grayscale_alpha_16.png",
|
"tests/files/rgba_16_should_be_grayscale_alpha_16.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_color_type();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -123,9 +136,38 @@ fn reductions_rgba_to_grayscale_alpha_8(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/rgba_8_should_be_grayscale_alpha_8.png",
|
"tests/files/rgba_8_should_be_grayscale_alpha_8.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_color_type();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn reductions_rgba_to_grayscale_16(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from(
|
||||||
|
"tests/files/rgba_16_should_be_grayscale_16.png",
|
||||||
|
));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_color_type();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn reductions_rgba_to_grayscale_8(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from(
|
||||||
|
"tests/files/rgba_8_should_be_grayscale_8.png",
|
||||||
|
));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_color_type();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -133,83 +175,45 @@ fn reductions_rgb_to_grayscale_16(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/rgb_16_should_be_grayscale_16.png",
|
"tests/files/rgb_16_should_be_grayscale_16.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_color_type();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn reductions_rgb_to_grayscale_8(b: &mut Bencher) {
|
fn reductions_rgb_to_grayscale_8(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_grayscale_8.png"));
|
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_grayscale_8.png"));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_color_type();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn reductions_rgba_to_palette_8(b: &mut Bencher) {
|
fn reductions_rgba_to_palette_8(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_palette_8.png"));
|
let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_palette_8.png"));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| color::reduced_to_indexed(&png.raw, true));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_color_type();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn reductions_rgb_to_palette_8(b: &mut Bencher) {
|
fn reductions_rgb_to_palette_8(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_palette_8.png"));
|
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_palette_8.png"));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| color::reduced_to_indexed(&png.raw, true));
|
b.iter(|| {
|
||||||
}
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_color_type();
|
||||||
#[bench]
|
});
|
||||||
fn reductions_grayscale_alpha_to_grayscale_16(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from(
|
|
||||||
"tests/files/grayscale_alpha_16_should_be_grayscale_16.png",
|
|
||||||
));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn reductions_grayscale_alpha_to_grayscale_8(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from(
|
|
||||||
"tests/files/grayscale_alpha_8_should_be_grayscale_8.png",
|
|
||||||
));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn reductions_grayscale_alpha_to_grayscale_trns_8(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from(
|
|
||||||
"tests/files/grayscale_alpha_8_should_be_grayscale_trns_8.png",
|
|
||||||
));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn reductions_grayscale_8_to_palette_8(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from(
|
|
||||||
"tests/files/grayscale_8_should_be_palette_8.png",
|
|
||||||
));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| color::reduced_to_indexed(&png.raw, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn reductions_palette_8_to_grayscale_8(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from(
|
|
||||||
"tests/files/palette_8_should_be_grayscale_8.png",
|
|
||||||
));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| color::indexed_to_channels(&png.raw, true, false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -217,9 +221,12 @@ fn reductions_palette_duplicate_reduction(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_should_be_reduced_with_dupes.png",
|
"tests/files/palette_should_be_reduced_with_dupes.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| palette::reduced_palette(&png.raw, false));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_palette();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -227,9 +234,12 @@ fn reductions_palette_unused_reduction(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_should_be_reduced_with_unused.png",
|
"tests/files/palette_should_be_reduced_with_unused.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| palette::reduced_palette(&png.raw, false));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_palette();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -237,45 +247,76 @@ fn reductions_palette_full_reduction(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_should_be_reduced_with_both.png",
|
"tests/files/palette_should_be_reduced_with_both.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| palette::reduced_palette(&png.raw, false));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_palette();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn reductions_palette_sort(b: &mut Bencher) {
|
fn reductions_alpha_black(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_black.png"));
|
||||||
"tests/files/palette_8_should_be_palette_8.png",
|
let png = PngData::new(&input, false).unwrap();
|
||||||
));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| palette::sorted_palette(&png.raw));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_alpha_channel(AlphaOptim::Black);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn reductions_palette_sort_mzeng(b: &mut Bencher) {
|
fn reductions_alpha_white(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_white.png"));
|
||||||
"tests/files/palette_8_should_be_palette_8.png",
|
let png = PngData::new(&input, false).unwrap();
|
||||||
));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| palette::sorted_palette_mzeng(&png.raw));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_alpha_channel(AlphaOptim::White);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn reductions_palette_sort_battiato(b: &mut Bencher) {
|
fn reductions_alpha_left(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_left.png"));
|
||||||
"tests/files/palette_8_should_be_palette_8.png",
|
let png = PngData::new(&input, false).unwrap();
|
||||||
));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| palette::sorted_palette_battiato(&png.raw));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_alpha_channel(AlphaOptim::Left);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn reductions_alpha(b: &mut Bencher) {
|
fn reductions_alpha_right(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha.png"));
|
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_right.png"));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| alpha::cleaned_alpha_channel(&png.raw));
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_alpha_channel(AlphaOptim::Right);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn reductions_alpha_up(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_up.png"));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_alpha_channel(AlphaOptim::Up);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn reductions_alpha_down(b: &mut Bencher) {
|
||||||
|
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_down.png"));
|
||||||
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let mut safe_png = png.clone();
|
||||||
|
safe_png.reduce_alpha_channel(AlphaOptim::Down);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
#![feature(test)]
|
|
||||||
|
|
||||||
extern crate oxipng;
|
|
||||||
extern crate test;
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use oxipng::{internal_tests::*, *};
|
|
||||||
use test::Bencher;
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn filters_minsum(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn filters_entropy(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::Entropy, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn filters_bigrams(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::Bigrams, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn filters_bigent(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| png.raw.filter_image(FilterStrategy::BigEnt, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[bench]
|
|
||||||
fn filters_brute(b: &mut Bencher) {
|
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
|
||||||
|
|
||||||
b.iter(|| {
|
|
||||||
png.raw.filter_image(
|
|
||||||
FilterStrategy::Brute {
|
|
||||||
num_lines: 4,
|
|
||||||
level: 1,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -3,28 +3,27 @@
|
||||||
extern crate oxipng;
|
extern crate oxipng;
|
||||||
extern crate test;
|
extern crate test;
|
||||||
|
|
||||||
|
use oxipng::internal_tests::*;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use oxipng::{internal_tests::*, *};
|
|
||||||
use test::Bencher;
|
use test::Bencher;
|
||||||
|
|
||||||
#[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, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
|
zopfli_deflate(png.raw_data.as_ref()).ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn zopfli_8_bits_strategy_0(b: &mut Bencher) {
|
fn zopfli_8_bits_strategy_0(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
|
zopfli_deflate(png.raw_data.as_ref()).ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,10 +32,10 @@ fn zopfli_4_bits_strategy_0(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_4_should_be_palette_4.png",
|
"tests/files/palette_4_should_be_palette_4.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
|
zopfli_deflate(png.raw_data.as_ref()).ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,10 +44,10 @@ fn zopfli_2_bits_strategy_0(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_2_should_be_palette_2.png",
|
"tests/files/palette_2_should_be_palette_2.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
|
zopfli_deflate(png.raw_data.as_ref()).ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,9 +56,9 @@ fn zopfli_1_bits_strategy_0(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from(
|
let input = test::black_box(PathBuf::from(
|
||||||
"tests/files/palette_1_should_be_palette_1.png",
|
"tests/files/palette_1_should_be_palette_1.png",
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, false).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
|
zopfli_deflate(png.raw_data.as_ref()).ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<script>(function() {window.location.assign(window.location.href + "doc/oxipng/");})();</script>
|
||||||
386
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,386 @@
|
||||||
|
{
|
||||||
|
"name": "oxipng",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"requires": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
|
||||||
|
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"array-find-index": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"builtin-modules": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
|
||||||
|
"integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"camelcase": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
|
||||||
|
"integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"camelcase-keys": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
|
||||||
|
"integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"camelcase": "^2.0.0",
|
||||||
|
"map-obj": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"currently-unhandled": {
|
||||||
|
"version": "0.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
|
||||||
|
"integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"array-find-index": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"decamelize": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||||
|
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"error-ex": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz",
|
||||||
|
"integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"is-arrayish": "^0.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"find-up": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
|
||||||
|
"integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"path-exists": "^2.0.0",
|
||||||
|
"pinkie-promise": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"get-stdin": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
|
||||||
|
"integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"graceful-fs": {
|
||||||
|
"version": "4.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
|
||||||
|
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"hosted-git-info": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"indent-string": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
|
||||||
|
"integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"repeating": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-arrayish": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||||
|
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"is-builtin-module": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"builtin-modules": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-finite": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"number-is-nan": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-utf8": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
|
||||||
|
"integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"load-json-file": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"graceful-fs": "^4.1.2",
|
||||||
|
"parse-json": "^2.2.0",
|
||||||
|
"pify": "^2.0.0",
|
||||||
|
"pinkie-promise": "^2.0.0",
|
||||||
|
"strip-bom": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loud-rejection": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
|
||||||
|
"integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"currently-unhandled": "^0.4.1",
|
||||||
|
"signal-exit": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"map-obj": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"meow": {
|
||||||
|
"version": "3.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
|
||||||
|
"integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"camelcase-keys": "^2.0.0",
|
||||||
|
"decamelize": "^1.1.2",
|
||||||
|
"loud-rejection": "^1.0.0",
|
||||||
|
"map-obj": "^1.0.1",
|
||||||
|
"minimist": "^1.1.3",
|
||||||
|
"normalize-package-data": "^2.3.4",
|
||||||
|
"object-assign": "^4.0.1",
|
||||||
|
"read-pkg-up": "^1.0.1",
|
||||||
|
"redent": "^1.0.0",
|
||||||
|
"trim-newlines": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimist": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||||
|
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"normalize-package-data": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"hosted-git-info": "^2.1.4",
|
||||||
|
"is-builtin-module": "^1.0.0",
|
||||||
|
"semver": "2 || 3 || 4 || 5",
|
||||||
|
"validate-npm-package-license": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"number-is-nan": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"parse-json": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
|
||||||
|
"integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"error-ex": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"path-exists": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
|
||||||
|
"integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"pinkie-promise": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"path-type": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"graceful-fs": "^4.1.2",
|
||||||
|
"pify": "^2.0.0",
|
||||||
|
"pinkie-promise": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pify": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||||
|
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"pinkie": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
|
||||||
|
"integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"pinkie-promise": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"pinkie": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"read-pkg": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"load-json-file": "^1.0.0",
|
||||||
|
"normalize-package-data": "^2.3.2",
|
||||||
|
"path-type": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"read-pkg-up": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"find-up": "^1.0.0",
|
||||||
|
"read-pkg": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"redent": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"indent-string": "^2.1.0",
|
||||||
|
"strip-indent": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repeating": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"is-finite": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"semver": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"signal-exit": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||||
|
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"spdx-correct": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"spdx-license-ids": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"spdx-expression-parse": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz",
|
||||||
|
"integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"spdx-license-ids": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz",
|
||||||
|
"integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"strip-ansi": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
|
||||||
|
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ansi-regex": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strip-ansi-cli": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi-cli/-/strip-ansi-cli-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-D91Cq86DJTm/cv+ROSpxqzVGIIw=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"meow": "^3.7.0",
|
||||||
|
"strip-ansi": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strip-bom": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"is-utf8": "^0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strip-indent": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"get-stdin": "^4.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trim-newlines": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"validate-npm-package-license": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz",
|
||||||
|
"integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"spdx-correct": "~1.0.0",
|
||||||
|
"spdx-expression-parse": "~1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "oxipng",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "[](https://travis-ci.org/shssoichiro/oxipng) [](https://crates.io/crates/oxipng) [](https://github.com/shssoichiro/oxipng/blob/master/LICENSE)",
|
||||||
|
"main": "index.js",
|
||||||
|
"private": true,
|
||||||
|
"directories": {
|
||||||
|
"test": "tests"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/shssoichiro/oxipng.git"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/shssoichiro/oxipng/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/shssoichiro/oxipng#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"strip-ansi-cli": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
rustfmt.toml
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
format_strings = false
|
||||||
|
|
@ -1,19 +1,14 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
cargo build --release
|
cargo build --release
|
||||||
sed -i.bak '/## Benchmarks/,$d' README.md
|
cp README.template.md README.md
|
||||||
rm README.md.bak
|
|
||||||
|
|
||||||
CORES=$(sysctl -n hw.ncpu 2>/dev/null || grep -c ^processor /proc/cpuinfo)
|
CORES=$(sysctl -n hw.ncpu 2>/dev/null || grep -c ^processor /proc/cpuinfo)
|
||||||
CPU=$(sysctl -n machdep.cpu.brand_string 2>/dev/null || grep '^model name' /proc/cpuinfo | sed 's/model name.\+: //g' | head -n 1)
|
CPU=$(sysctl -n machdep.cpu.brand_string 2>/dev/null || grep '^model name' /proc/cpuinfo | sed 's/model name.\+: //g' | head -n 1)
|
||||||
OXIPNG_VERSION=$(./target/release/oxipng -V)
|
OXIPNG_VERSION=$(./target/release/oxipng -V)
|
||||||
OPTIPNG_VERSION=$(optipng -v | head -n 1)
|
OPTIPNG_VERSION=$(optipng -v | head -n 1)
|
||||||
RUST_VERSION=$(rustc -V)
|
RUST_VERSION=$(rustc -V)
|
||||||
echo -e '## Benchmarks\n' >> README.md
|
|
||||||
echo "Tested $OXIPNG_VERSION (compiled on $RUST_VERSION) against $OPTIPNG_VERSION on $CPU with $CORES logical cores" >> README.md
|
echo "Tested $OXIPNG_VERSION (compiled on $RUST_VERSION) against $OPTIPNG_VERSION on $CPU with $CORES logical cores" >> README.md
|
||||||
echo -e '\n```\n' >> README.md
|
|
||||||
|
|
||||||
hyperfine --style basic --warmup 5 './target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png' 'optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png' >> README.md
|
|
||||||
echo -e '\n\n' >> README.md
|
echo -e '\n\n' >> README.md
|
||||||
hyperfine --style basic --warmup 5 './target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png' 'optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png' >> README.md
|
|
||||||
|
|
||||||
echo -e '\n```' >> README.md
|
hyperfine --warmup 5 './target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png' 'optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png' | ./node_modules/.bin/strip-ansi >> README.md
|
||||||
|
hyperfine --warmup 5 './target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png' 'optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png' | ./node_modules/.bin/strip-ansi >> README.md
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
#!/bin/sh -eu
|
|
||||||
cargo build
|
|
||||||
cargo xtask mangen
|
|
||||||
|
|
||||||
./target/debug/oxipng -V > MANUAL.txt
|
|
||||||
#Redirect all streams to prevent detection of the terminal width and force an internal default of 100
|
|
||||||
./target/debug/oxipng --help >> MANUAL.txt 2>/dev/null </dev/null
|
|
||||||
72
src/apng.rs
|
|
@ -1,72 +0,0 @@
|
||||||
use crate::{
|
|
||||||
PngResult,
|
|
||||||
error::PngError,
|
|
||||||
headers::{read_be_u16, read_be_u32},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
/// Animated PNG frame
|
|
||||||
pub struct Frame {
|
|
||||||
/// Width of the frame
|
|
||||||
pub width: u32,
|
|
||||||
/// Height of the frame
|
|
||||||
pub height: u32,
|
|
||||||
/// X offset of the frame
|
|
||||||
pub x_offset: u32,
|
|
||||||
/// Y offset of the frame
|
|
||||||
pub y_offset: u32,
|
|
||||||
/// Frame delay numerator
|
|
||||||
pub delay_num: u16,
|
|
||||||
/// Frame delay denominator
|
|
||||||
pub delay_den: u16,
|
|
||||||
/// Frame disposal operation
|
|
||||||
pub dispose_op: u8,
|
|
||||||
/// Frame blend operation
|
|
||||||
pub blend_op: u8,
|
|
||||||
/// Frame data, from fdAT chunks
|
|
||||||
pub data: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Frame {
|
|
||||||
/// Construct a new Frame from the data in a fcTL chunk
|
|
||||||
pub fn from_fctl_data(byte_data: &[u8]) -> PngResult<Self> {
|
|
||||||
if byte_data.len() < 26 {
|
|
||||||
return Err(PngError::TruncatedData);
|
|
||||||
}
|
|
||||||
Ok(Self {
|
|
||||||
width: read_be_u32(&byte_data[4..8]),
|
|
||||||
height: read_be_u32(&byte_data[8..12]),
|
|
||||||
x_offset: read_be_u32(&byte_data[12..16]),
|
|
||||||
y_offset: read_be_u32(&byte_data[16..20]),
|
|
||||||
delay_num: read_be_u16(&byte_data[20..22]),
|
|
||||||
delay_den: read_be_u16(&byte_data[22..24]),
|
|
||||||
dispose_op: byte_data[24],
|
|
||||||
blend_op: byte_data[25],
|
|
||||||
data: vec![],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct the data for a fcTL chunk using the given sequence number
|
|
||||||
#[must_use]
|
|
||||||
pub fn fctl_data(&self, sequence_number: u32) -> Vec<u8> {
|
|
||||||
let mut byte_data = Vec::with_capacity(26);
|
|
||||||
byte_data.extend_from_slice(&sequence_number.to_be_bytes());
|
|
||||||
byte_data.extend_from_slice(&self.width.to_be_bytes());
|
|
||||||
byte_data.extend_from_slice(&self.height.to_be_bytes());
|
|
||||||
byte_data.extend_from_slice(&self.x_offset.to_be_bytes());
|
|
||||||
byte_data.extend_from_slice(&self.y_offset.to_be_bytes());
|
|
||||||
byte_data.extend_from_slice(&self.delay_num.to_be_bytes());
|
|
||||||
byte_data.extend_from_slice(&self.delay_den.to_be_bytes());
|
|
||||||
byte_data.extend_from_slice(&[self.dispose_op, self.blend_op]);
|
|
||||||
byte_data
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct the data for a fdAT chunk using the given sequence number
|
|
||||||
#[must_use]
|
|
||||||
pub fn fdat_data(&self, sequence_number: u32) -> Vec<u8> {
|
|
||||||
let mut byte_data = Vec::with_capacity(4 + self.data.len());
|
|
||||||
byte_data.extend_from_slice(&sequence_number.to_be_bytes());
|
|
||||||
byte_data.extend_from_slice(&self.data);
|
|
||||||
byte_data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +1,39 @@
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
|
use std::sync::atomic::AtomicUsize;
|
||||||
|
use std::sync::atomic::Ordering::{Relaxed, SeqCst};
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AtomicMin {
|
pub struct AtomicMin {
|
||||||
val: AtomicUsize,
|
val: AtomicUsize,
|
||||||
}
|
}
|
||||||
|
|
||||||
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_value())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 { None } else { Some(val) }
|
if val == usize::max_value() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(val)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try a new value, returning true if it is the new minimum
|
pub fn set_min(&self, new_val: usize) {
|
||||||
pub fn set_min(&self, new_val: usize) -> bool {
|
let mut current_val = self.val.load(Relaxed);
|
||||||
new_val < self.val.fetch_min(new_val, SeqCst)
|
loop {
|
||||||
|
if new_val < current_val {
|
||||||
|
if let Err(v) = self
|
||||||
|
.val
|
||||||
|
.compare_exchange(current_val, new_val, SeqCst, Relaxed)
|
||||||
|
{
|
||||||
|
current_val = v;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
450
src/cli.rs
|
|
@ -1,450 +0,0 @@
|
||||||
use std::{num::NonZeroU64, path::PathBuf};
|
|
||||||
|
|
||||||
use clap::builder::Styles;
|
|
||||||
use clap::builder::styling::{AnsiColor, Effects};
|
|
||||||
use clap::{Arg, ArgAction, Command, builder::ArgPredicate, value_parser};
|
|
||||||
use parse_size::parse_size;
|
|
||||||
|
|
||||||
include!("display_chunks.rs");
|
|
||||||
|
|
||||||
const STYLES: Styles = Styles::styled()
|
|
||||||
.header(AnsiColor::Green.on_default().effects(Effects::BOLD))
|
|
||||||
.usage(AnsiColor::Green.on_default().effects(Effects::BOLD))
|
|
||||||
.literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD))
|
|
||||||
.placeholder(AnsiColor::Cyan.on_default());
|
|
||||||
|
|
||||||
pub fn build_command() -> Command {
|
|
||||||
// Note: clap 'wrap_help' is enabled to automatically wrap lines according to terminal width.
|
|
||||||
// To keep things tidy though, short help descriptions should be no more than 54 characters,
|
|
||||||
// so that they can fit on a single line in an 80 character terminal.
|
|
||||||
// Long help descriptions are soft wrapped here at 90 characters (column 91) but this does not
|
|
||||||
// affect output, it simply matches what is rendered when help is output to a file.
|
|
||||||
Command::new("oxipng")
|
|
||||||
.version(env!("CARGO_PKG_VERSION"))
|
|
||||||
.author("Joshua Holmer <jholmer.in@gmail.com>")
|
|
||||||
.about("Losslessly improve compression of PNG files")
|
|
||||||
.styles(STYLES)
|
|
||||||
.arg(
|
|
||||||
Arg::new("files")
|
|
||||||
.help("File(s) to compress (use '-' for stdin)")
|
|
||||||
.index(1)
|
|
||||||
.num_args(1..)
|
|
||||||
.use_value_delimiter(false)
|
|
||||||
.required(true)
|
|
||||||
.value_parser(value_parser!(PathBuf)),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("optimization")
|
|
||||||
.help("Optimization level (0-6, or max)")
|
|
||||||
.long_help("\
|
|
||||||
Set the optimization level preset. The default level 2 is quite fast and provides good \
|
|
||||||
compression. Lower levels are faster, higher levels provide better compression, though \
|
|
||||||
with increasingly diminishing returns.
|
|
||||||
|
|
||||||
0 => --zc 5 --fast (filter chosen heuristically)
|
|
||||||
1 => --zc 10 --fast (filter chosen heuristically)
|
|
||||||
2 => --zc 11 -f 0,1,6,7 --fast
|
|
||||||
3 => --zc 11 -f 0,7,8,9 --brute-level 1 --brute-lines 3
|
|
||||||
4 => --zc 12 -f 0,7,8,9 --brute-level 1 --brute-lines 4
|
|
||||||
5 => --zc 12 -f 0,1,2,5,6,7,8,9 --brute-level 4 --brute-lines 4
|
|
||||||
6 => --zc 12 -f 0-9 --brute-level 5 --brute-lines 8
|
|
||||||
max => (stable alias for the maximum level)
|
|
||||||
|
|
||||||
Manually specifying a compression option (zc, f, etc.) will override the optimization \
|
|
||||||
preset, regardless of the order you write the arguments.")
|
|
||||||
.short('o')
|
|
||||||
.long("opt")
|
|
||||||
.value_name("level")
|
|
||||||
.default_value("2")
|
|
||||||
.value_parser(["0", "1", "2", "3", "4", "5", "6", "max"])
|
|
||||||
.hide_possible_values(true),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("recursive")
|
|
||||||
.help("Recurse input directories, optimizing all PNG files")
|
|
||||||
.long_help("\
|
|
||||||
When directories are given as input, traverse the directory trees and optimize all PNG \
|
|
||||||
files found (files with “.png” or “.apng” extension).")
|
|
||||||
.short('r')
|
|
||||||
.long("recursive")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("output_dir")
|
|
||||||
.help("Write output file(s) to <directory>")
|
|
||||||
.long_help("\
|
|
||||||
Write output file(s) to <directory>. If the directory does not exist, it will be created. \
|
|
||||||
Note that this will not preserve the directory structure of the input files when used with \
|
|
||||||
'--recursive'.")
|
|
||||||
.long("dir")
|
|
||||||
.value_name("directory")
|
|
||||||
.value_parser(value_parser!(PathBuf))
|
|
||||||
.conflicts_with("output_file")
|
|
||||||
.conflicts_with("stdout"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("output_file")
|
|
||||||
.help("Write output file to <file>")
|
|
||||||
.long("out")
|
|
||||||
.value_name("file")
|
|
||||||
.value_parser(value_parser!(PathBuf))
|
|
||||||
.conflicts_with("output_dir")
|
|
||||||
.conflicts_with("stdout"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("stdout")
|
|
||||||
.help("Write output to stdout")
|
|
||||||
.long("stdout")
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
.conflicts_with("output_dir")
|
|
||||||
.conflicts_with("output_file"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("preserve")
|
|
||||||
.help("Preserve file permissions and timestamps if possible")
|
|
||||||
.short('p')
|
|
||||||
.long("preserve")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("dry-run")
|
|
||||||
.help("Do not write any files, only show compression results")
|
|
||||||
.short('d')
|
|
||||||
.long("dry-run")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("strip-safe")
|
|
||||||
.help("Strip safely-removable chunks, same as '--strip safe'")
|
|
||||||
.short('s')
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
.conflicts_with("strip"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("strip")
|
|
||||||
.help("Strip metadata (safe, all, or comma-separated list)\nCAUTION: 'all' will convert APNGs to standard PNGs")
|
|
||||||
.long_help(format!("\
|
|
||||||
Strip metadata chunks, where <mode> is one of:
|
|
||||||
|
|
||||||
safe => Strip all non-critical chunks, except for the following:
|
|
||||||
{}
|
|
||||||
all => Strip all non-critical chunks
|
|
||||||
<list> => Strip chunks in the comma-separated list, e.g. 'bKGD,cHRM'
|
|
||||||
|
|
||||||
CAUTION: 'all' will convert APNGs to standard PNGs.
|
|
||||||
|
|
||||||
Please note that regardless of any options set, some chunks will necessarily be stripped \
|
|
||||||
when invalidated by the optimization:
|
|
||||||
bKGD, sBIT, hIST: Stripped if the color type or bit depth changes.
|
|
||||||
iDOT: Always stripped.
|
|
||||||
caBX: Stripped if it contains C2PA metadata. If explicitly retained by `--keep`, \
|
|
||||||
optimization will be aborted.
|
|
||||||
|
|
||||||
The default when --strip is not passed is to keep all chunks that remain valid.",
|
|
||||||
DISPLAY_CHUNKS
|
|
||||||
.iter()
|
|
||||||
.map(|c| String::from_utf8_lossy(c))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")))
|
|
||||||
.long("strip")
|
|
||||||
.value_name("mode")
|
|
||||||
.conflicts_with("strip-safe"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("keep")
|
|
||||||
.help("Strip all metadata except in the comma-separated list")
|
|
||||||
.long_help("\
|
|
||||||
Strip all metadata chunks except those in the comma-separated list. The special value \
|
|
||||||
'display' includes chunks that affect the image appearance, equivalent to '--strip safe'.
|
|
||||||
|
|
||||||
E.g. '--keep eXIf,display' will strip chunks, keeping only eXIf and those that affect the \
|
|
||||||
image appearance.")
|
|
||||||
.long("keep")
|
|
||||||
.value_name("list")
|
|
||||||
.conflicts_with("strip")
|
|
||||||
.conflicts_with("strip-safe"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("alpha")
|
|
||||||
.help("Perform additional alpha channel optimization")
|
|
||||||
.long_help("\
|
|
||||||
Perform additional optimization on images with an alpha channel, by altering the color \
|
|
||||||
values of fully transparent pixels. This is generally recommended for better compression, \
|
|
||||||
but take care as while this is “visually lossless”, it is technically a lossy \
|
|
||||||
transformation and may be unsuitable for some applications.")
|
|
||||||
.short('a')
|
|
||||||
.long("alpha")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("interlace")
|
|
||||||
.help("Set PNG interlacing (off, on, keep)")
|
|
||||||
.long_help("\
|
|
||||||
Set the PNG interlacing mode, where <mode> is one of:
|
|
||||||
|
|
||||||
off => Remove interlacing from all images that are processed
|
|
||||||
on => Apply Adam7 interlacing on all images that are processed
|
|
||||||
keep => Keep the existing interlacing mode of each image
|
|
||||||
|
|
||||||
Note that interlacing can add 25-50% to the size of an optimized image. Only use it if you \
|
|
||||||
believe the benefits outweigh the costs for your use case.")
|
|
||||||
.short('i')
|
|
||||||
.long("interlace")
|
|
||||||
.value_name("mode")
|
|
||||||
.value_parser(["off", "on", "keep", "0", "1"])
|
|
||||||
.default_value("off")
|
|
||||||
.default_value_if("no-reductions", ArgPredicate::IsPresent, "keep")
|
|
||||||
.hide_possible_values(true),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("scale16")
|
|
||||||
.help("Forcibly reduce 16-bit images to 8-bit (lossy)")
|
|
||||||
.long_help("\
|
|
||||||
Forcibly reduce images with 16 bits per channel to 8 bits per channel. This is a lossy \
|
|
||||||
operation but can provide significant savings when you have no need for higher depth. \
|
|
||||||
Reduction is performed by scaling the values such that, e.g. 0x00FF is reduced to 0x01 \
|
|
||||||
rather than 0x00.
|
|
||||||
|
|
||||||
Without this flag, 16-bit images will only be reduced in depth if it can be done \
|
|
||||||
losslessly.")
|
|
||||||
.long("scale16")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("verbose")
|
|
||||||
.help("Show per-file info (use multiple times for more detail)")
|
|
||||||
.short('v')
|
|
||||||
.long("verbose")
|
|
||||||
.action(ArgAction::Count)
|
|
||||||
.conflicts_with("quiet"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("quiet")
|
|
||||||
.help("Suppress all output messages")
|
|
||||||
.short('q')
|
|
||||||
.long("quiet")
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
.conflicts_with("verbose"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("json")
|
|
||||||
.help("Print results as JSON")
|
|
||||||
.short('j')
|
|
||||||
.long("json")
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
.conflicts_with("stdout"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("filters")
|
|
||||||
.help("Filters to try (0-9; see '--help' for details)")
|
|
||||||
.long_help("\
|
|
||||||
Perform compression trials with each of the given filter types. You can specify a \
|
|
||||||
comma-separated list, or a range of values. E.g. '-f 0-3' is the same as '-f 0,1,2,3'.
|
|
||||||
|
|
||||||
PNG delta filters (apply the same filter to every line)
|
|
||||||
0 => None (recommended to always include this filter)
|
|
||||||
1 => Sub
|
|
||||||
2 => Up
|
|
||||||
3 => Average
|
|
||||||
4 => Paeth
|
|
||||||
|
|
||||||
Heuristic strategies (try to find the best delta filter for each line)
|
|
||||||
5 => MinSum Minimum sum of absolute differences
|
|
||||||
6 => Entropy Smallest Shannon entropy
|
|
||||||
7 => Bigrams Lowest count of distinct bigrams
|
|
||||||
8 => BigEnt Smallest Shannon entropy of bigrams
|
|
||||||
9 => Brute Smallest compressed size (slow)
|
|
||||||
|
|
||||||
The default value depends on the optimization level preset.")
|
|
||||||
.short('f')
|
|
||||||
.long("filters")
|
|
||||||
.value_name("list"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("fast")
|
|
||||||
.help("Use fast filter evaluation")
|
|
||||||
.long_help("\
|
|
||||||
Perform a fast compression evaluation of each enabled filter, followed by a single main \
|
|
||||||
compression trial of the best result. Recommended if you have more filters enabled than \
|
|
||||||
CPU cores.")
|
|
||||||
.long("fast")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("compression")
|
|
||||||
.help("Deflate compression level (0-12)")
|
|
||||||
.long_help("\
|
|
||||||
Deflate compression level (0-12) for main compression trials. The levels here are defined \
|
|
||||||
by the libdeflate compression library.
|
|
||||||
|
|
||||||
The default value depends on the optimization level preset.")
|
|
||||||
.long("zc")
|
|
||||||
.value_name("level")
|
|
||||||
.value_parser(0..=12)
|
|
||||||
.conflicts_with("zopfli"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("no-bit-reduction")
|
|
||||||
.help("Do not change bit depth")
|
|
||||||
.long("nb")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("no-color-reduction")
|
|
||||||
.help("Do not change color type")
|
|
||||||
.long("nc")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("no-palette-reduction")
|
|
||||||
.help("Do not change color palette")
|
|
||||||
.long_help("\
|
|
||||||
Do not convert to indexed and do not modify an existing color palette.")
|
|
||||||
.long("np")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("no-grayscale-reduction")
|
|
||||||
.help("Do not change to or from grayscale")
|
|
||||||
.long("ng")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("no-reductions")
|
|
||||||
.help("Do not perform any transformations")
|
|
||||||
.long_help("\
|
|
||||||
Do not perform any transformations and do not deinterlace by default.")
|
|
||||||
.long("nx")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("no-recoding")
|
|
||||||
.help("Do not recompress unless transformations occur")
|
|
||||||
.long_help("\
|
|
||||||
Do not recompress IDAT unless required due to transformations. Recompression of other \
|
|
||||||
compressed chunks (such as iCCP) will also be disabled. Note that the combination of \
|
|
||||||
'--nx' and '--nz' will fully disable all optimization.")
|
|
||||||
.long("nz")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("fix")
|
|
||||||
.help("Disable checksum validation")
|
|
||||||
.long_help("\
|
|
||||||
Do not perform checksum validation of PNG chunks. This may allow some files with errors to \
|
|
||||||
be processed successfully. The output will always have correct checksums.")
|
|
||||||
.long("fix")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("force")
|
|
||||||
.help("Write the output even if it is larger than the input")
|
|
||||||
.long("force")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("zopfli")
|
|
||||||
.help("Use the much slower but stronger Zopfli compressor")
|
|
||||||
.long_help("\
|
|
||||||
Use the much slower but stronger Zopfli compressor for main compression trials. \
|
|
||||||
Recommended use is with '-o max' and '--fast'.")
|
|
||||||
.short('z')
|
|
||||||
.short_alias('Z') // Kept for backwards compatibility
|
|
||||||
.long("zopfli")
|
|
||||||
.action(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("iterations")
|
|
||||||
.help("Number of Zopfli iterations")
|
|
||||||
.long_help("\
|
|
||||||
Set the number of iterations to use for Zopfli compression. Using fewer iterations may \
|
|
||||||
speed up compression for large files. This option requires '--zopfli' to be set.")
|
|
||||||
.long("zi")
|
|
||||||
.value_name("iterations")
|
|
||||||
.default_value("15")
|
|
||||||
.value_parser(value_parser!(NonZeroU64))
|
|
||||||
.requires("zopfli"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("iterations-without-improvement")
|
|
||||||
.hide_short_help(true)
|
|
||||||
.long_help("\
|
|
||||||
Stop Zopfli compression after this number of iterations without improvement. Use this in \
|
|
||||||
conjunction with a high value for '--zi' to achieve better compression in reasonable time.")
|
|
||||||
.long("ziwi")
|
|
||||||
.value_name("iterations")
|
|
||||||
.value_parser(value_parser!(NonZeroU64))
|
|
||||||
.requires("zopfli"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("brute-level")
|
|
||||||
.hide_short_help(true)
|
|
||||||
.long_help("\
|
|
||||||
Set the libdeflate compression level to use with the Brute filter strategy. Sane values \
|
|
||||||
are 1-5. Higher values are not necessarily better.")
|
|
||||||
.long("brute-level")
|
|
||||||
.value_name("level")
|
|
||||||
.value_parser(1..=12),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("brute-lines")
|
|
||||||
.hide_short_help(true)
|
|
||||||
.long_help("\
|
|
||||||
Set the number of lines to compress at once with the Brute filter strategy. Sane values \
|
|
||||||
are 2-16. Higher values are not necessarily better.")
|
|
||||||
.long("brute-lines")
|
|
||||||
.value_name("lines")
|
|
||||||
.value_parser(value_parser!(usize)),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("timeout")
|
|
||||||
.help("Maximum amount of time to spend on optimizations")
|
|
||||||
.long_help("\
|
|
||||||
Maximum amount of time, in seconds, to spend on optimizations. Oxipng will check the \
|
|
||||||
timeout before each transformation or compression trial, and will stop trying to optimize \
|
|
||||||
the file if the timeout is exceeded. Note that this does not cut short any operations that \
|
|
||||||
are already in progress, so it is currently of limited effectiveness for large files with \
|
|
||||||
high compression levels.")
|
|
||||||
.long("timeout")
|
|
||||||
.value_name("secs")
|
|
||||||
.value_parser(value_parser!(u64)),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("max-size")
|
|
||||||
.help("Skip image if the decompressed size exceeds this limit")
|
|
||||||
.long_help("\
|
|
||||||
Maximum size to allow for the input image. If the raw, decompressed image data (or the \
|
|
||||||
file size) of the image exceeds this size, it will be skipped. This is useful for limiting \
|
|
||||||
memory usage or avoiding long processing times on large images. The value may be specified \
|
|
||||||
with a unit suffix such as k, KB, m, MB, etc.
|
|
||||||
|
|
||||||
The decompressed size of an image is roughly equal to width * height * bit-depth / 8. E.g. \
|
|
||||||
a 1920x1080 image with 24-bit color depth would be roughly 6MB.")
|
|
||||||
.long("max-raw-size")
|
|
||||||
.value_name("bytes")
|
|
||||||
.value_parser(|s: &str| parse_size(s)),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("threads")
|
|
||||||
.help("Number of threads to use [default: num logical CPUs]")
|
|
||||||
.long_help("\
|
|
||||||
Set the maximum number of threads to use. Oxipng uses multithreading to evaluate multiple \
|
|
||||||
optimizations on the same file in parallel as well as process multiple files in parallel. \
|
|
||||||
You can set this to a lower value if you need to limit memory or CPU usage.
|
|
||||||
|
|
||||||
[default: num logical CPUs]")
|
|
||||||
.short('t')
|
|
||||||
.long("threads")
|
|
||||||
.value_name("num")
|
|
||||||
.value_parser(value_parser!(usize)),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("parallel-files")
|
|
||||||
.help("Process multiple files sequentially")
|
|
||||||
.long_help("\
|
|
||||||
Process multiple files sequentially rather than in parallel. Use this if you need \
|
|
||||||
determinism in the processing order. Note this is not necessary if using '--threads 1'.")
|
|
||||||
.long("sequential")
|
|
||||||
.action(ArgAction::SetFalse),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
186
src/colors.rs
|
|
@ -1,129 +1,145 @@
|
||||||
use std::{fmt, fmt::Display};
|
use std::fmt;
|
||||||
|
|
||||||
use rgb::{RGB16, RGBA8};
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
|
|
||||||
use crate::PngError;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
/// The color type used to represent this image
|
/// The color type used to represent this image
|
||||||
pub enum ColorType {
|
pub enum ColorType {
|
||||||
/// Grayscale, with one color channel
|
/// Grayscale, with one color channel
|
||||||
Grayscale {
|
Grayscale,
|
||||||
/// Optional shade of gray that should be rendered as transparent
|
|
||||||
transparent_shade: Option<u16>,
|
|
||||||
},
|
|
||||||
/// RGB, with three color channels
|
/// RGB, with three color channels
|
||||||
RGB {
|
RGB,
|
||||||
/// Optional color value that should be rendered as transparent
|
/// Indexed, with one byte per pixel representing one of up to 256 colors in the image
|
||||||
transparent_color: Option<RGB16>,
|
Indexed,
|
||||||
},
|
|
||||||
/// Indexed, with one byte per pixel representing a color from the palette
|
|
||||||
Indexed {
|
|
||||||
/// The palette containing the colors used, up to 256 entries
|
|
||||||
palette: Vec<RGBA8>,
|
|
||||||
},
|
|
||||||
/// Grayscale + Alpha, with two color channels
|
/// Grayscale + Alpha, with two color channels
|
||||||
GrayscaleAlpha,
|
GrayscaleAlpha,
|
||||||
/// RGBA, with four color channels
|
/// RGBA, with four color channels
|
||||||
RGBA,
|
RGBA,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ColorType {
|
impl fmt::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 {
|
write!(
|
||||||
Self::Grayscale { .. } => write!(f, "Grayscale"),
|
f,
|
||||||
Self::RGB { .. } => write!(f, "RGB"),
|
"{}",
|
||||||
Self::Indexed { palette } => write!(f, "Indexed ({} colors)", palette.len()),
|
match *self {
|
||||||
Self::GrayscaleAlpha => write!(f, "Grayscale + Alpha"),
|
ColorType::Grayscale => "Grayscale",
|
||||||
Self::RGBA => write!(f, "RGB + Alpha"),
|
ColorType::RGB => "RGB",
|
||||||
}
|
ColorType::Indexed => "Indexed",
|
||||||
|
ColorType::GrayscaleAlpha => "Grayscale + Alpha",
|
||||||
|
ColorType::RGBA => "RGB + Alpha",
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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]
|
||||||
#[must_use]
|
pub fn png_header_code(self) -> u8 {
|
||||||
pub const fn png_header_code(&self) -> u8 {
|
|
||||||
match self {
|
match self {
|
||||||
Self::Grayscale { .. } => 0,
|
ColorType::Grayscale => 0,
|
||||||
Self::RGB { .. } => 2,
|
ColorType::RGB => 2,
|
||||||
Self::Indexed { .. } => 3,
|
ColorType::Indexed => 3,
|
||||||
Self::GrayscaleAlpha => 4,
|
ColorType::GrayscaleAlpha => 4,
|
||||||
Self::RGBA => 6,
|
ColorType::RGBA => 6,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) const fn channels_per_pixel(&self) -> u8 {
|
pub fn channels_per_pixel(self) -> u8 {
|
||||||
match self {
|
match self {
|
||||||
Self::Grayscale { .. } | Self::Indexed { .. } => 1,
|
ColorType::Grayscale | ColorType::Indexed => 1,
|
||||||
Self::GrayscaleAlpha => 2,
|
ColorType::GrayscaleAlpha => 2,
|
||||||
Self::RGB { .. } => 3,
|
ColorType::RGB => 3,
|
||||||
Self::RGBA => 4,
|
ColorType::RGBA => 4,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) const fn is_rgb(&self) -> bool {
|
|
||||||
matches!(self, Self::RGB { .. } | Self::RGBA)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) const fn is_gray(&self) -> bool {
|
|
||||||
matches!(self, Self::Grayscale { .. } | Self::GrayscaleAlpha)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) const fn has_alpha(&self) -> bool {
|
|
||||||
matches!(self, Self::GrayscaleAlpha | Self::RGBA)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) const fn has_trns(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::Grayscale { transparent_shade } => transparent_shade.is_some(),
|
|
||||||
Self::RGB { transparent_color } => transparent_color.is_some(),
|
|
||||||
_ => false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(u8)]
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
|
|
||||||
/// The number of bits to be used per channel per pixel
|
/// The number of bits to be used per channel per pixel
|
||||||
pub enum BitDepth {
|
pub enum BitDepth {
|
||||||
/// One bit per channel per pixel
|
/// One bit per channel per pixel
|
||||||
One = 1,
|
One,
|
||||||
/// Two bits per channel per pixel
|
/// Two bits per channel per pixel
|
||||||
Two = 2,
|
Two,
|
||||||
/// Four bits per channel per pixel
|
/// Four bits per channel per pixel
|
||||||
Four = 4,
|
Four,
|
||||||
/// Eight bits per channel per pixel
|
/// Eight bits per channel per pixel
|
||||||
Eight = 8,
|
Eight,
|
||||||
/// Sixteen bits per channel per pixel
|
/// Sixteen bits per channel per pixel
|
||||||
Sixteen = 16,
|
Sixteen,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for BitDepth {
|
impl fmt::Display for BitDepth {
|
||||||
type Error = PngError;
|
#[inline]
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match *self {
|
||||||
|
BitDepth::One => "1",
|
||||||
|
BitDepth::Two => "2",
|
||||||
|
BitDepth::Four => "4",
|
||||||
|
BitDepth::Eight => "8",
|
||||||
|
BitDepth::Sixteen => "16",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
impl BitDepth {
|
||||||
match value {
|
/// Retrieve the number of bits per channel per pixel as a `u8`
|
||||||
1 => Ok(Self::One),
|
#[inline]
|
||||||
2 => Ok(Self::Two),
|
pub fn as_u8(self) -> u8 {
|
||||||
4 => Ok(Self::Four),
|
match self {
|
||||||
8 => Ok(Self::Eight),
|
BitDepth::One => 1,
|
||||||
16 => Ok(Self::Sixteen),
|
BitDepth::Two => 2,
|
||||||
_ => Err(PngError::InvalidData),
|
BitDepth::Four => 4,
|
||||||
|
BitDepth::Eight => 8,
|
||||||
|
BitDepth::Sixteen => 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Parse a number of bits per channel per pixel into a `BitDepth`
|
||||||
|
#[inline]
|
||||||
|
pub fn from_u8(depth: u8) -> BitDepth {
|
||||||
|
match depth {
|
||||||
|
1 => BitDepth::One,
|
||||||
|
2 => BitDepth::Two,
|
||||||
|
4 => BitDepth::Four,
|
||||||
|
8 => BitDepth::Eight,
|
||||||
|
16 => BitDepth::Sixteen,
|
||||||
|
_ => panic!("Unsupported bit depth"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for BitDepth {
|
#[derive(Debug, PartialEq, Clone, Copy, Eq, Hash)]
|
||||||
#[inline]
|
/// Potential optimization methods for alpha channel
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
pub enum AlphaOptim {
|
||||||
Display::fmt(&(*self as u8).to_string(), f)
|
NoOp,
|
||||||
|
Black,
|
||||||
|
White,
|
||||||
|
Up,
|
||||||
|
Right,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for AlphaOptim {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match *self {
|
||||||
|
AlphaOptim::NoOp => "_",
|
||||||
|
AlphaOptim::Black => "B",
|
||||||
|
AlphaOptim::White => "W",
|
||||||
|
AlphaOptim::Up => "U",
|
||||||
|
AlphaOptim::Right => "R",
|
||||||
|
AlphaOptim::Down => "D",
|
||||||
|
AlphaOptim::Left => "L",
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
use libdeflater::*;
|
|
||||||
|
|
||||||
use crate::{PngError, PngResult};
|
|
||||||
|
|
||||||
pub fn deflate(data: &[u8], level: u8, max_size: Option<usize>) -> PngResult<Vec<u8>> {
|
|
||||||
let mut compressor = Compressor::new(CompressionLvl::new(level.into()).unwrap());
|
|
||||||
let capacity = max_size.unwrap_or_else(|| compressor.zlib_compress_bound(data.len()));
|
|
||||||
let mut dest = vec![0; capacity];
|
|
||||||
let len = compressor
|
|
||||||
.zlib_compress(data, &mut dest)
|
|
||||||
.map_err(|err| match err {
|
|
||||||
CompressionError::InsufficientSpace => PngError::DeflatedDataTooLong(capacity),
|
|
||||||
})?;
|
|
||||||
dest.truncate(len);
|
|
||||||
Ok(dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn inflate(data: &[u8], out_size: usize) -> PngResult<Vec<u8>> {
|
|
||||||
let mut decompressor = Decompressor::new();
|
|
||||||
let mut dest = vec![0; out_size];
|
|
||||||
let len = decompressor
|
|
||||||
.zlib_decompress(data, &mut dest)
|
|
||||||
.map_err(|err| match err {
|
|
||||||
DecompressionError::BadData => PngError::InvalidData,
|
|
||||||
DecompressionError::InsufficientSpace => PngError::InflatedDataTooLong(out_size),
|
|
||||||
})?;
|
|
||||||
dest.truncate(len);
|
|
||||||
Ok(dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn crc32(data: &[u8]) -> u32 {
|
|
||||||
let mut crc = Crc::new();
|
|
||||||
crc.update(data);
|
|
||||||
crc.sum()
|
|
||||||
}
|
|
||||||
67
src/deflate/miniz_stream.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
use atomicmin::AtomicMin;
|
||||||
|
use error::PngError;
|
||||||
|
use miniz_oxide::deflate::core::*;
|
||||||
|
use PngResult;
|
||||||
|
|
||||||
|
pub fn compress_to_vec_oxipng(
|
||||||
|
input: &[u8],
|
||||||
|
level: u8,
|
||||||
|
window_bits: i32,
|
||||||
|
strategy: i32,
|
||||||
|
max_size: &AtomicMin,
|
||||||
|
) -> PngResult<Vec<u8>> {
|
||||||
|
// The comp flags function sets the zlib flag if the window_bits parameter is > 0.
|
||||||
|
let flags = create_comp_flags_from_zip_params(level.into(), window_bits, strategy);
|
||||||
|
let mut compressor = CompressorOxide::new(flags);
|
||||||
|
// if max size is known, then expect that much data (but no more than input.len())
|
||||||
|
let mut output = Vec::with_capacity(max_size.get().unwrap_or(input.len() / 2).min(input.len()));
|
||||||
|
// # Unsafe
|
||||||
|
// We trust compress to not read the uninitialized bytes.
|
||||||
|
unsafe {
|
||||||
|
let cap = output.capacity();
|
||||||
|
output.set_len(cap);
|
||||||
|
}
|
||||||
|
let mut in_pos = 0;
|
||||||
|
let mut out_pos = 0;
|
||||||
|
loop {
|
||||||
|
let (status, bytes_in, bytes_out) = compress(
|
||||||
|
&mut compressor,
|
||||||
|
&input[in_pos..],
|
||||||
|
&mut output[out_pos..],
|
||||||
|
TDEFLFlush::Finish,
|
||||||
|
);
|
||||||
|
|
||||||
|
out_pos += bytes_out;
|
||||||
|
in_pos += bytes_in;
|
||||||
|
|
||||||
|
match status {
|
||||||
|
TDEFLStatus::Done => {
|
||||||
|
output.truncate(out_pos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
TDEFLStatus::Okay => {
|
||||||
|
if let Some(max) = max_size.get() {
|
||||||
|
if output.len() > max {
|
||||||
|
return Err(PngError::DeflatedDataTooLong(output.len()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We need more space, so extend the vector.
|
||||||
|
if output.len().saturating_sub(out_pos) < 30 {
|
||||||
|
let current_len = output.len();
|
||||||
|
output.reserve(current_len);
|
||||||
|
|
||||||
|
// # Unsafe
|
||||||
|
// We trust compress to not read the uninitialized bytes.
|
||||||
|
unsafe {
|
||||||
|
let cap = output.capacity();
|
||||||
|
output.set_len(cap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Not supposed to happen unless there is a bug.
|
||||||
|
_ => panic!("Bug! Unexpectedly failed to compress!"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
@ -1,53 +1,123 @@
|
||||||
mod deflater;
|
use atomicmin::AtomicMin;
|
||||||
use std::{fmt, fmt::Display};
|
use error::PngError;
|
||||||
|
use miniz_oxide;
|
||||||
|
use std::cmp::max;
|
||||||
|
use zopfli;
|
||||||
|
use PngResult;
|
||||||
|
|
||||||
pub use deflater::{crc32, deflate, inflate};
|
#[doc(hidden)]
|
||||||
|
pub mod miniz_stream;
|
||||||
|
|
||||||
use crate::{PngError, PngResult};
|
/// Decompress a data stream using the DEFLATE algorithm
|
||||||
|
pub fn inflate(data: &[u8]) -> PngResult<Vec<u8>> {
|
||||||
#[cfg(feature = "zopfli")]
|
miniz_oxide::inflate::decompress_to_vec_zlib(data)
|
||||||
mod zopfli_oxipng;
|
.map_err(|e| PngError::new(&format!("Error on decompress: {:?}", e)))
|
||||||
#[cfg(feature = "zopfli")]
|
|
||||||
pub use zopfli::Options as ZopfliOptions;
|
|
||||||
#[cfg(feature = "zopfli")]
|
|
||||||
pub use zopfli_oxipng::deflate as zopfli_deflate;
|
|
||||||
|
|
||||||
/// DEFLATE algorithms supported by oxipng (for use in [`Options`][crate::Options])
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
pub enum Deflater {
|
|
||||||
/// Use libdeflater.
|
|
||||||
Libdeflater {
|
|
||||||
/// Which compression level to use on the file (0-12)
|
|
||||||
compression: u8,
|
|
||||||
},
|
|
||||||
#[cfg(feature = "zopfli")]
|
|
||||||
/// Use the better but slower Zopfli implementation
|
|
||||||
Zopfli(ZopfliOptions),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deflater {
|
/// Compress a data stream using the DEFLATE algorithm
|
||||||
pub(crate) fn deflate(self, data: &[u8], max_size: Option<usize>) -> PngResult<Vec<u8>> {
|
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
|
||||||
let compressed = match self {
|
pub fn deflate(data: &[u8], zc: u8, zs: u8, zw: u8, max_size: &AtomicMin) -> PngResult<Vec<u8>> {
|
||||||
Self::Libdeflater { compression } => deflate(data, compression, max_size)?,
|
if is_cfzlib_supported() {
|
||||||
#[cfg(feature = "zopfli")]
|
return cfzlib_deflate(data, zc, zs, zw, max_size);
|
||||||
Self::Zopfli(options) => zopfli_deflate(data, options)?,
|
}
|
||||||
};
|
|
||||||
if let Some(max) = max_size {
|
miniz_stream::compress_to_vec_oxipng(data, zc, zw.into(), zs.into(), max_size)
|
||||||
if compressed.len() > max {
|
}
|
||||||
return Err(PngError::DeflatedDataTooLong(max));
|
|
||||||
|
/// Compress a data stream using the DEFLATE algorithm
|
||||||
|
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
|
||||||
|
pub fn deflate(data: &[u8], zc: u8, zs: u8, zw: u8, max_size: &AtomicMin) -> PngResult<Vec<u8>> {
|
||||||
|
miniz_stream::compress_to_vec_oxipng(data, zc, zw.into(), zs.into(), max_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "x86_64")]
|
||||||
|
fn is_cfzlib_supported() -> bool {
|
||||||
|
if is_x86_feature_detected!("sse4.2") && is_x86_feature_detected!("pclmulqdq") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "aarch64")]
|
||||||
|
fn is_cfzlib_supported() -> bool {
|
||||||
|
if is_arm_feature_detected!("neon") && is_arm_feature_detected!("crc") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
|
||||||
|
pub fn cfzlib_deflate(
|
||||||
|
data: &[u8],
|
||||||
|
level: u8,
|
||||||
|
strategy: u8,
|
||||||
|
window_bits: u8,
|
||||||
|
max_size: &AtomicMin,
|
||||||
|
) -> PngResult<Vec<u8>> {
|
||||||
|
use cloudflare_zlib_sys::*;
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
assert!(data.len() < u32::max_value() as usize);
|
||||||
|
unsafe {
|
||||||
|
let mut stream = mem::zeroed();
|
||||||
|
if Z_OK != deflateInit2(
|
||||||
|
&mut stream,
|
||||||
|
level.into(),
|
||||||
|
Z_DEFLATED,
|
||||||
|
window_bits.into(),
|
||||||
|
MAX_MEM_LEVEL,
|
||||||
|
strategy.into(),
|
||||||
|
) {
|
||||||
|
return Err(PngError::new("deflateInit2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let upper_bound = deflateBound(&mut stream, data.len() as uLong) as usize;
|
||||||
|
let max_size = max_size.get().unwrap_or(upper_bound).min(upper_bound);
|
||||||
|
// it's important to have the capacity pre-allocated,
|
||||||
|
// as unsafe set_len is called later
|
||||||
|
let mut out = Vec::with_capacity(max_size);
|
||||||
|
|
||||||
|
stream.next_in = data.as_ptr() as *mut _;
|
||||||
|
stream.total_in = data.len() as uLong;
|
||||||
|
stream.avail_in = data.len() as uInt;
|
||||||
|
stream.next_out = out.as_mut_ptr();
|
||||||
|
stream.avail_out = out.capacity() as uInt;
|
||||||
|
match deflate(&mut stream, Z_FINISH) {
|
||||||
|
Z_STREAM_END => {}
|
||||||
|
Z_OK | Z_BUF_ERROR => {
|
||||||
|
deflateEnd(&mut stream);
|
||||||
|
return Err(PngError::DeflatedDataTooLong(stream.total_out as usize));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
deflateEnd(&mut stream);
|
||||||
|
return Err(PngError::new("deflate"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(compressed)
|
if Z_OK != deflateEnd(&mut stream) {
|
||||||
|
return Err(PngError::new("deflateEnd"));
|
||||||
|
}
|
||||||
|
debug_assert!(stream.total_out as usize <= out.capacity());
|
||||||
|
out.set_len(stream.total_out as usize);
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Deflater {
|
pub fn zopfli_deflate(data: &[u8]) -> PngResult<Vec<u8>> {
|
||||||
#[inline]
|
let mut output = Vec::with_capacity(max(1024, data.len() / 20));
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
let options = zopfli::Options::default();
|
||||||
match self {
|
match zopfli::compress(&options, &zopfli::Format::Zlib, data, &mut output) {
|
||||||
Self::Libdeflater { compression } => write!(f, "zc = {compression}"),
|
Ok(_) => (),
|
||||||
#[cfg(feature = "zopfli")]
|
Err(_) => return Err(PngError::new("Failed to compress in zopfli")),
|
||||||
Self::Zopfli(options) => write!(f, "zopfli, zi = {}", options.iteration_count),
|
};
|
||||||
}
|
output.shrink_to_fit();
|
||||||
}
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
/// DEFLATE algorithms supported by oxipng
|
||||||
|
pub enum Deflaters {
|
||||||
|
/// Use the Zlib/Miniz DEFLATE implementation
|
||||||
|
Zlib,
|
||||||
|
/// Use the better but slower Zopfli implementation
|
||||||
|
Zopfli,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
use crate::{PngError, PngResult};
|
|
||||||
|
|
||||||
pub fn deflate(data: &[u8], options: zopfli::Options) -> PngResult<Vec<u8>> {
|
|
||||||
let mut output = Vec::with_capacity(data.len());
|
|
||||||
// Since Rust v1.74, passing &[u8] directly into zopfli causes a regression in compressed size
|
|
||||||
// for some files. Wrapping the slice in another Read implementer such as Box fixes it for now.
|
|
||||||
match zopfli::compress(options, zopfli::Format::Zlib, Box::new(data), &mut output) {
|
|
||||||
Ok(()) => (),
|
|
||||||
Err(_) => return Err(PngError::new("Failed to compress in zopfli")),
|
|
||||||
}
|
|
||||||
output.shrink_to_fit();
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
/// List of chunks that affect image display and will be kept when using the `Safe` chunk strip option
|
|
||||||
pub const DISPLAY_CHUNKS: [[u8; 4]; 7] = [
|
|
||||||
*b"cICP", *b"iCCP", *b"sRGB", *b"pHYs", *b"acTL", *b"fcTL", *b"fdAT",
|
|
||||||
];
|
|
||||||
78
src/error.rs
|
|
@ -1,69 +1,49 @@
|
||||||
use std::{error::Error, fmt};
|
use std::error::Error;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
use crate::colors::{BitDepth, ColorType};
|
// TODO: Use `#[non_exhaustive]` once stabilized
|
||||||
|
// https://github.com/rust-lang/rust/issues/44109
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum PngError {
|
pub enum PngError {
|
||||||
APNGOutOfOrder,
|
|
||||||
C2PAMetadataPreventsChanges,
|
|
||||||
ChunkMissing(&'static str),
|
|
||||||
CRCMismatch([u8; 4]),
|
|
||||||
DeflatedDataTooLong(usize),
|
DeflatedDataTooLong(usize),
|
||||||
IncorrectDataLength(usize, usize),
|
|
||||||
InflatedDataTooLong(usize),
|
|
||||||
InvalidData,
|
|
||||||
InvalidDepthForType(BitDepth, ColorType),
|
|
||||||
NotPNG,
|
NotPNG,
|
||||||
ReadFailed(String, std::io::Error),
|
APNGNotSupported,
|
||||||
|
InvalidData,
|
||||||
TruncatedData,
|
TruncatedData,
|
||||||
WriteFailed(String, std::io::Error),
|
ChunkMissing(&'static str),
|
||||||
Other(Box<str>),
|
Other(Box<str>),
|
||||||
|
#[doc(hidden)]
|
||||||
|
_Nonexhaustive,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error for PngError {}
|
impl Error for PngError {
|
||||||
|
// deprecated
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for PngError {
|
impl fmt::Display for PngError {
|
||||||
#[inline]
|
#[inline]
|
||||||
#[cold]
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match *self {
|
match *self {
|
||||||
Self::APNGOutOfOrder => f.write_str("APNG chunks are out of order"),
|
PngError::DeflatedDataTooLong(_) => f.write_str("deflated data too long"),
|
||||||
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 => {
|
||||||
Self::ChunkMissing(s) => write!(f, "Chunk {s} missing or empty"),
|
f.write_str("Missing data in the file; the file is truncated")
|
||||||
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"),
|
PngError::APNGNotSupported => f.write_str("APNG files are not (yet) supported"),
|
||||||
Self::ReadFailed(ref s, ref e) => write!(f, "Failed to read from {s}: {e}"),
|
PngError::ChunkMissing(s) => write!(f, "Chunk {} missing or empty", s),
|
||||||
Self::TruncatedData => f.write_str("Missing data in the file; the file is truncated"),
|
PngError::Other(ref s) => f.write_str(s),
|
||||||
Self::WriteFailed(ref s, ref e) => write!(f, "Failed to write to {s}: {e}"),
|
PngError::_Nonexhaustive => unreachable!(),
|
||||||
Self::Other(ref s) => f.write_str(s),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PngError {
|
impl PngError {
|
||||||
#[cold]
|
#[inline]
|
||||||
#[must_use]
|
pub fn new(description: &str) -> PngError {
|
||||||
pub fn new(description: &str) -> Self {
|
PngError::Other(description.into())
|
||||||
Self::Other(description.into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
202
src/evaluate.rs
|
|
@ -1,202 +0,0 @@
|
||||||
//! Check if a reduction makes file smaller, and keep best reductions.
|
|
||||||
//! Works asynchronously when possible
|
|
||||||
|
|
||||||
#[cfg(not(feature = "parallel"))]
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::sync::{
|
|
||||||
Arc,
|
|
||||||
atomic::{AtomicUsize, Ordering::*},
|
|
||||||
};
|
|
||||||
|
|
||||||
use deflate::Deflater;
|
|
||||||
use indexmap::IndexSet;
|
|
||||||
use log::trace;
|
|
||||||
use rayon::prelude::*;
|
|
||||||
#[cfg(feature = "parallel")]
|
|
||||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
|
||||||
|
|
||||||
#[cfg(not(feature = "parallel"))]
|
|
||||||
use crate::rayon;
|
|
||||||
use crate::{
|
|
||||||
Deadline, PngError, atomicmin::AtomicMin, deflate, filters::FilterStrategy, png::PngImage,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) struct Candidate {
|
|
||||||
pub image: Arc<PngImage>,
|
|
||||||
pub idat_data: Option<Vec<u8>>,
|
|
||||||
pub estimated_output_size: usize,
|
|
||||||
/// The input filter, which is retained for printing and for APNG frames.
|
|
||||||
pub filter: FilterStrategy,
|
|
||||||
/// The filter returned by the filter function, which may be Predefined.
|
|
||||||
/// Use this for the next round to avoid recomputing the filter.
|
|
||||||
pub filter_used: FilterStrategy,
|
|
||||||
/// For determining tie-breaker
|
|
||||||
nth: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Candidate {
|
|
||||||
fn cmp_key(&self) -> impl Ord + use<> {
|
|
||||||
(
|
|
||||||
self.estimated_output_size,
|
|
||||||
self.image.data.len(),
|
|
||||||
self.filter.clone(),
|
|
||||||
// Prefer the later image added (e.g. baseline, which is always added last)
|
|
||||||
usize::MAX - self.nth,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collect image versions and pick one that compresses best
|
|
||||||
pub(crate) struct Evaluator {
|
|
||||||
deadline: Arc<Deadline>,
|
|
||||||
filters: IndexSet<FilterStrategy>,
|
|
||||||
deflater: Deflater,
|
|
||||||
optimize_alpha: bool,
|
|
||||||
final_round: bool,
|
|
||||||
nth: AtomicUsize,
|
|
||||||
executed: Arc<AtomicUsize>,
|
|
||||||
best_candidate_size: Arc<AtomicMin>,
|
|
||||||
/// images are sent to the caller thread for evaluation
|
|
||||||
#[cfg(feature = "parallel")]
|
|
||||||
eval_channel: (Sender<Candidate>, Receiver<Candidate>),
|
|
||||||
// in non-parallel mode, images are evaluated synchronously
|
|
||||||
#[cfg(not(feature = "parallel"))]
|
|
||||||
eval_best_candidate: RefCell<Option<Candidate>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Evaluator {
|
|
||||||
pub fn new(
|
|
||||||
deadline: Arc<Deadline>,
|
|
||||||
filters: IndexSet<FilterStrategy>,
|
|
||||||
deflater: Deflater,
|
|
||||||
optimize_alpha: bool,
|
|
||||||
final_round: bool,
|
|
||||||
) -> Self {
|
|
||||||
#[cfg(feature = "parallel")]
|
|
||||||
let eval_channel = channel();
|
|
||||||
Self {
|
|
||||||
deadline,
|
|
||||||
filters,
|
|
||||||
deflater,
|
|
||||||
optimize_alpha,
|
|
||||||
final_round,
|
|
||||||
nth: AtomicUsize::new(0),
|
|
||||||
executed: Arc::new(AtomicUsize::new(0)),
|
|
||||||
best_candidate_size: Arc::new(AtomicMin::new(None)),
|
|
||||||
#[cfg(feature = "parallel")]
|
|
||||||
eval_channel,
|
|
||||||
#[cfg(not(feature = "parallel"))]
|
|
||||||
eval_best_candidate: RefCell::new(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wait for all evaluations to finish and return smallest reduction
|
|
||||||
/// Or `None` if the queue is empty.
|
|
||||||
#[cfg(feature = "parallel")]
|
|
||||||
pub fn get_best_candidate(self) -> Option<Candidate> {
|
|
||||||
let (eval_send, eval_recv) = self.eval_channel;
|
|
||||||
// Disconnect the sender, breaking the loop in the thread
|
|
||||||
drop(eval_send);
|
|
||||||
let nth = self.nth.load(SeqCst);
|
|
||||||
// Yield to ensure all evaluations are executed
|
|
||||||
// This can prevent deadlocks when run within an existing rayon thread pool
|
|
||||||
while self.executed.load(Relaxed) < nth {
|
|
||||||
rayon::yield_local();
|
|
||||||
}
|
|
||||||
eval_recv.into_iter().min_by_key(Candidate::cmp_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "parallel"))]
|
|
||||||
pub fn get_best_candidate(self) -> Option<Candidate> {
|
|
||||||
self.eval_best_candidate.into_inner()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set best size, if known in advance
|
|
||||||
pub fn set_best_size(&self, size: usize) {
|
|
||||||
self.best_candidate_size.set_min(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the image is smaller than others
|
|
||||||
pub fn try_image(&self, image: Arc<PngImage>) {
|
|
||||||
let description = image.ihdr.color_type.to_string();
|
|
||||||
self.try_image_with_description(image, &description);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the image is smaller than others, with a description for verbose mode
|
|
||||||
pub fn try_image_with_description(&self, image: Arc<PngImage>, description: &str) {
|
|
||||||
let nth = self.nth.fetch_add(1, SeqCst);
|
|
||||||
// These clones are only cheap refcounts
|
|
||||||
let deadline = self.deadline.clone();
|
|
||||||
let filters = self.filters.clone();
|
|
||||||
let deflater = self.deflater;
|
|
||||||
let optimize_alpha = self.optimize_alpha;
|
|
||||||
let final_round = self.final_round;
|
|
||||||
let executed = self.executed.clone();
|
|
||||||
let best_candidate_size = self.best_candidate_size.clone();
|
|
||||||
let description = description.to_string();
|
|
||||||
// sends it off asynchronously for compression,
|
|
||||||
// but results will be collected via the message queue
|
|
||||||
#[cfg(feature = "parallel")]
|
|
||||||
let eval_send = self.eval_channel.0.clone();
|
|
||||||
rayon::spawn(move || {
|
|
||||||
executed.fetch_add(1, Relaxed);
|
|
||||||
let filters_iter = filters.par_iter().with_max_len(1);
|
|
||||||
|
|
||||||
// Updating of best result inside the parallel loop would require locks,
|
|
||||||
// which are dangerous to do in side Rayon's loop.
|
|
||||||
// Instead, only update (atomic) best size in real time,
|
|
||||||
// and the best result later without need for locks.
|
|
||||||
filters_iter.for_each(|filter| {
|
|
||||||
if deadline.passed() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let (filtered, filter_used) = image.filter_image(filter.clone(), optimize_alpha);
|
|
||||||
let idat_data = deflater.deflate(&filtered, best_candidate_size.get());
|
|
||||||
if let Ok(idat_data) = idat_data {
|
|
||||||
let estimated_output_size = image.estimated_output_size(&idat_data);
|
|
||||||
trace!(
|
|
||||||
"Eval: {}-bit {:23} {:8} {} bytes",
|
|
||||||
image.ihdr.bit_depth, description, filter, estimated_output_size
|
|
||||||
);
|
|
||||||
|
|
||||||
// Skip if it exceeds best known size. (This is important to ensure
|
|
||||||
// the evaluator returns no result when all candidates are too large.)
|
|
||||||
if let Some(max) = best_candidate_size.get() {
|
|
||||||
if estimated_output_size > max {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We only need to retain the IDAT data in the final round
|
|
||||||
let new = Candidate {
|
|
||||||
image: image.clone(),
|
|
||||||
idat_data: if final_round { Some(idat_data) } else { None },
|
|
||||||
estimated_output_size,
|
|
||||||
filter: filter.clone(),
|
|
||||||
filter_used,
|
|
||||||
nth,
|
|
||||||
};
|
|
||||||
best_candidate_size.set_min(estimated_output_size);
|
|
||||||
|
|
||||||
#[cfg(feature = "parallel")]
|
|
||||||
{
|
|
||||||
eval_send.send(new).expect("send");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "parallel"))]
|
|
||||||
{
|
|
||||||
match &mut *self.eval_best_candidate.borrow_mut() {
|
|
||||||
Some(prev) if prev.cmp_key() < new.cmp_key() => {}
|
|
||||||
best => *best = Some(new),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if let Err(PngError::DeflatedDataTooLong(size)) = idat_data {
|
|
||||||
trace!(
|
|
||||||
"Eval: {}-bit {:23} {:8} >{} bytes",
|
|
||||||
image.ihdr.bit_depth, description, filter, size
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
394
src/filters.rs
|
|
@ -1,278 +1,148 @@
|
||||||
use std::{fmt, fmt::Display, mem::transmute};
|
pub fn filter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec<u8> {
|
||||||
|
let mut filtered = Vec::with_capacity(data.len());
|
||||||
/// Filtering strategy for use in [`Options`][crate::Options]
|
match filter {
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
|
0 => {
|
||||||
pub enum FilterStrategy {
|
filtered.extend_from_slice(data);
|
||||||
/// 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),
|
|
||||||
}
|
}
|
||||||
}
|
1 => {
|
||||||
}
|
filtered.extend_from_slice(&data[0..bpp]);
|
||||||
|
filtered.extend(
|
||||||
/// PNG delta filters
|
data.iter()
|
||||||
#[repr(u8)]
|
.skip(bpp)
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
|
.zip(data.iter())
|
||||||
pub enum RowFilter {
|
.map(|(cur, last)| cur.wrapping_sub(*last)),
|
||||||
None,
|
);
|
||||||
Sub,
|
|
||||||
Up,
|
|
||||||
Average,
|
|
||||||
Paeth,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<u8> for RowFilter {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
|
||||||
if value > 4 {
|
|
||||||
return Err(());
|
|
||||||
}
|
}
|
||||||
unsafe { transmute(value as i8) }
|
2 => {
|
||||||
}
|
if last_line.is_empty() {
|
||||||
}
|
filtered.extend_from_slice(data);
|
||||||
|
} else {
|
||||||
impl Display for RowFilter {
|
filtered.extend(
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
Display::fmt(
|
|
||||||
match self {
|
|
||||||
Self::None => "None",
|
|
||||||
Self::Sub => "Sub",
|
|
||||||
Self::Up => "Up",
|
|
||||||
Self::Average => "Average",
|
|
||||||
Self::Paeth => "Paeth",
|
|
||||||
},
|
|
||||||
f,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RowFilter {
|
|
||||||
pub(crate) const ALL: [Self; 5] = [Self::None, Self::Sub, Self::Up, Self::Average, Self::Paeth];
|
|
||||||
pub(crate) const SINGLE_LINE: [Self; 2] = [Self::None, Self::Sub];
|
|
||||||
|
|
||||||
pub(crate) fn filter_line(
|
|
||||||
self,
|
|
||||||
bpp: usize,
|
|
||||||
data: &mut [u8],
|
|
||||||
prev_line: &[u8],
|
|
||||||
buf: &mut Vec<u8>,
|
|
||||||
alpha_bytes: usize,
|
|
||||||
) {
|
|
||||||
assert!(data.len() >= bpp);
|
|
||||||
assert_eq!(data.len(), prev_line.len());
|
|
||||||
|
|
||||||
if alpha_bytes != 0 {
|
|
||||||
self.optimize_alpha(bpp, data, prev_line, bpp - alpha_bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.clear();
|
|
||||||
buf.reserve(data.len() + 1);
|
|
||||||
buf.push(self as u8);
|
|
||||||
match self {
|
|
||||||
Self::None => {
|
|
||||||
buf.extend_from_slice(data);
|
|
||||||
}
|
|
||||||
Self::Sub => {
|
|
||||||
buf.extend_from_slice(&data[0..bpp]);
|
|
||||||
buf.extend(
|
|
||||||
data.iter()
|
data.iter()
|
||||||
.skip(bpp)
|
.zip(last_line.iter())
|
||||||
.zip(data.iter())
|
|
||||||
.map(|(cur, last)| cur.wrapping_sub(*last)),
|
.map(|(cur, last)| cur.wrapping_sub(*last)),
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
Self::Up => {
|
|
||||||
buf.extend(
|
|
||||||
data.iter()
|
|
||||||
.zip(prev_line.iter())
|
|
||||||
.map(|(cur, last)| cur.wrapping_sub(*last)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Self::Average => {
|
|
||||||
for (i, byte) in data.iter().enumerate() {
|
|
||||||
buf.push(byte.wrapping_sub(i.checked_sub(bpp).map_or_else(
|
|
||||||
|| prev_line[i] >> 1,
|
|
||||||
|x| ((u16::from(data[x]) + u16::from(prev_line[i])) >> 1) as u8,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::Paeth => {
|
|
||||||
for (i, byte) in data.iter().enumerate() {
|
|
||||||
buf.push(byte.wrapping_sub(i.checked_sub(bpp).map_or_else(
|
|
||||||
|| prev_line[i],
|
|
||||||
|x| paeth_predictor(data[x], prev_line[i], prev_line[x]),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
3 => for (i, byte) in data.iter().enumerate() {
|
||||||
|
if last_line.is_empty() {
|
||||||
|
filtered.push(match i.checked_sub(bpp) {
|
||||||
|
Some(x) => byte.wrapping_sub(data[x] >> 1),
|
||||||
|
None => *byte,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
filtered.push(match i.checked_sub(bpp) {
|
||||||
|
Some(x) => byte
|
||||||
|
.wrapping_sub(((u16::from(data[x]) + u16::from(last_line[i])) >> 1) as u8),
|
||||||
|
None => byte.wrapping_sub(last_line[i] >> 1),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
4 => for (i, byte) in data.iter().enumerate() {
|
||||||
|
if last_line.is_empty() {
|
||||||
|
filtered.push(match i.checked_sub(bpp) {
|
||||||
|
Some(x) => byte.wrapping_sub(data[x]),
|
||||||
|
None => *byte,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
filtered.push(match i.checked_sub(bpp) {
|
||||||
|
Some(x) => {
|
||||||
|
byte.wrapping_sub(paeth_predictor(data[x], last_line[i], last_line[x]))
|
||||||
|
}
|
||||||
|
None => byte.wrapping_sub(last_line[i]),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
filtered
|
||||||
|
}
|
||||||
|
|
||||||
// Optimize fully transparent pixels of a scanline such that they will be zeroed when filtered
|
pub fn unfilter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec<u8> {
|
||||||
fn optimize_alpha(self, bpp: usize, data: &mut [u8], prev_line: &[u8], color_bytes: usize) {
|
let mut unfiltered = Vec::with_capacity(data.len());
|
||||||
if self == Self::None {
|
match filter {
|
||||||
// Assume transparent pixels already set to 0
|
0 => {
|
||||||
return;
|
unfiltered.extend_from_slice(data);
|
||||||
}
|
}
|
||||||
|
1 => for (i, byte) in data.iter().enumerate() {
|
||||||
let mut pixels: Vec<_> = data.chunks_exact_mut(bpp).collect();
|
match i.checked_sub(bpp) {
|
||||||
let prev_pixels: Vec<_> = prev_line.chunks_exact(bpp).collect();
|
Some(x) => {
|
||||||
for i in 0..pixels.len() {
|
let b = unfiltered[x];
|
||||||
if pixels[i].iter().skip(color_bytes).all(|b| *b == 0) {
|
unfiltered.push(byte.wrapping_add(b));
|
||||||
// If the first pixel in the row is transparent, find the next non-transparent pixel and pretend
|
}
|
||||||
// it is the previous one. This can help improve effectiveness of the Sub and Paeth filters.
|
None => {
|
||||||
let prev = match i {
|
unfiltered.push(*byte);
|
||||||
0 => pixels
|
}
|
||||||
.iter()
|
};
|
||||||
.position(|px| px.iter().skip(color_bytes).any(|b| *b != 0))
|
},
|
||||||
.unwrap_or(i),
|
2 => {
|
||||||
_ => i - 1,
|
if last_line.is_empty() {
|
||||||
|
unfiltered.extend_from_slice(data);
|
||||||
|
} else {
|
||||||
|
unfiltered.extend(
|
||||||
|
data.iter()
|
||||||
|
.zip(last_line.iter())
|
||||||
|
.map(|(cur, last)| cur.wrapping_add(*last)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
for (i, byte) in data.iter().enumerate() {
|
||||||
|
if last_line.is_empty() {
|
||||||
|
match i.checked_sub(bpp) {
|
||||||
|
Some(x) => {
|
||||||
|
let b = unfiltered[x];
|
||||||
|
unfiltered.push(byte.wrapping_add(b >> 1));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
unfiltered.push(*byte);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
match i.checked_sub(bpp) {
|
||||||
|
Some(x) => {
|
||||||
|
let b = unfiltered[x];
|
||||||
|
unfiltered.push(byte.wrapping_add(
|
||||||
|
((u16::from(b) + u16::from(last_line[i])) >> 1) as u8,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
unfiltered.push(byte.wrapping_add(last_line[i] >> 1));
|
||||||
|
}
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// These assertions help eliminate a few bounds checks in the slice accesses below
|
|
||||||
assert!(prev < pixels.len());
|
|
||||||
assert!(i < prev_pixels.len());
|
|
||||||
|
|
||||||
match self {
|
|
||||||
Self::None => unreachable!(),
|
|
||||||
Self::Sub => {
|
|
||||||
// The code below is roughly equivalent to pixels[i][0..color_bytes].copy_from_slice(&pixels[prev][0..color_bytes]),
|
|
||||||
// if such a thing was possible to do without violating Rust aliasing rules. See:
|
|
||||||
// https://users.rust-lang.org/t/problem-borrowing-two-elements-of-vec-mutably/21446/2
|
|
||||||
|
|
||||||
if prev < i {
|
|
||||||
let (pixels_head, pixels_tail) = pixels.split_at_mut(prev + 1);
|
|
||||||
pixels_tail[i - prev - 1][0..color_bytes]
|
|
||||||
.copy_from_slice(&pixels_head[prev][0..color_bytes]);
|
|
||||||
} else if prev > i {
|
|
||||||
let (pixels_head, pixels_tail) = pixels.split_at_mut(i + 1);
|
|
||||||
pixels_head[i][0..color_bytes]
|
|
||||||
.copy_from_slice(&pixels_tail[prev - i - 1][0..color_bytes]);
|
|
||||||
} else {
|
|
||||||
// If prev == i, we'd be copying the pixels onto themselves, which is useless
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::Up => {
|
|
||||||
pixels[i][0..color_bytes].copy_from_slice(&prev_pixels[i][0..color_bytes]);
|
|
||||||
}
|
|
||||||
Self::Average => {
|
|
||||||
for j in 0..color_bytes {
|
|
||||||
pixels[i][j] = match i {
|
|
||||||
0 => prev_pixels[i][j] >> 1,
|
|
||||||
_ => {
|
|
||||||
((u16::from(pixels[i - 1][j]) + u16::from(prev_pixels[i][j]))
|
|
||||||
>> 1) as u8
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::Paeth => {
|
|
||||||
for j in 0..color_bytes {
|
|
||||||
pixels[i][j] = match i {
|
|
||||||
0 => pixels[prev][j].min(prev_pixels[i][j]),
|
|
||||||
_ => paeth_predictor(
|
|
||||||
pixels[i - 1][j],
|
|
||||||
prev_pixels[i][j],
|
|
||||||
prev_pixels[i - 1][j],
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn unfilter_line(
|
|
||||||
self,
|
|
||||||
bpp: usize,
|
|
||||||
data: &[u8],
|
|
||||||
prev_line: &[u8],
|
|
||||||
buf: &mut Vec<u8>,
|
|
||||||
) {
|
|
||||||
buf.clear();
|
|
||||||
buf.reserve(data.len());
|
|
||||||
assert!(data.len() >= bpp);
|
|
||||||
assert_eq!(data.len(), prev_line.len());
|
|
||||||
match self {
|
|
||||||
Self::None => {
|
|
||||||
buf.extend_from_slice(data);
|
|
||||||
}
|
|
||||||
Self::Sub => {
|
|
||||||
for (i, &cur) in data.iter().enumerate() {
|
|
||||||
let prev_byte = i.checked_sub(bpp).and_then(|x| buf.get(x).copied());
|
|
||||||
buf.push(prev_byte.map_or(cur, |b| cur.wrapping_add(b)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::Up => {
|
|
||||||
buf.extend(
|
|
||||||
data.iter()
|
|
||||||
.zip(prev_line)
|
|
||||||
.map(|(&cur, &last)| cur.wrapping_add(last)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Self::Average => {
|
|
||||||
for (i, (&cur, &last)) in data.iter().zip(prev_line).enumerate() {
|
|
||||||
let prev_byte = i.checked_sub(bpp).and_then(|x| buf.get(x).copied());
|
|
||||||
buf.push(cur.wrapping_add(prev_byte.map_or_else(
|
|
||||||
|| last >> 1,
|
|
||||||
|b| ((u16::from(b) + u16::from(last)) >> 1) as u8,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::Paeth => {
|
|
||||||
for (i, (&cur, &up)) in data.iter().zip(prev_line).enumerate() {
|
|
||||||
buf.push(
|
|
||||||
match i
|
|
||||||
.checked_sub(bpp)
|
|
||||||
.map(|x| (buf.get(x).copied(), prev_line.get(x).copied()))
|
|
||||||
{
|
|
||||||
Some((Some(left), Some(left_up))) => {
|
|
||||||
cur.wrapping_add(paeth_predictor(left, up, left_up))
|
|
||||||
}
|
|
||||||
_ => cur.wrapping_add(up),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4 => for (i, byte) in data.iter().enumerate() {
|
||||||
|
if last_line.is_empty() {
|
||||||
|
match i.checked_sub(bpp) {
|
||||||
|
Some(x) => {
|
||||||
|
let b = unfiltered[x];
|
||||||
|
unfiltered.push(byte.wrapping_add(b));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
unfiltered.push(*byte);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
match i.checked_sub(bpp) {
|
||||||
|
Some(x) => {
|
||||||
|
let b = unfiltered[x];
|
||||||
|
unfiltered.push(byte.wrapping_add(paeth_predictor(
|
||||||
|
b,
|
||||||
|
last_line[i],
|
||||||
|
last_line[x],
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
unfiltered.push(byte.wrapping_add(last_line[i]));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
unfiltered
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paeth_predictor(a: u8, b: u8, c: u8) -> u8 {
|
fn paeth_predictor(a: u8, b: u8, c: u8) -> u8 {
|
||||||
|
|
|
||||||
436
src/headers.rs
|
|
@ -1,16 +1,12 @@
|
||||||
use indexmap::IndexSet;
|
use byteorder::{BigEndian, ReadBytesExt};
|
||||||
use log::{debug, trace, warn};
|
use colors::{BitDepth, ColorType};
|
||||||
use rgb::{RGB16, RGBA8};
|
use crc::crc32;
|
||||||
|
use error::PngError;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use PngResult;
|
||||||
|
|
||||||
use crate::{
|
#[derive(Debug, Clone, Copy)]
|
||||||
Deflater, Options, PngResult,
|
|
||||||
colors::{BitDepth, ColorType},
|
|
||||||
deflate::{crc32, inflate},
|
|
||||||
display_chunks::DISPLAY_CHUNKS,
|
|
||||||
error::PngError,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
/// Headers from the IHDR chunk of the image
|
/// Headers from the IHDR chunk of the image
|
||||||
pub struct IhdrData {
|
pub struct IhdrData {
|
||||||
/// The width of the image in pixels
|
/// The width of the image in pixels
|
||||||
|
|
@ -21,81 +17,38 @@ 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,
|
||||||
/// Whether the image is interlaced
|
/// The compression method used for this image (0 for DEFLATE)
|
||||||
pub interlaced: bool,
|
pub compression: u8,
|
||||||
|
/// The filter mode used for this image (currently only 0 is valid)
|
||||||
|
pub filter: u8,
|
||||||
|
/// The interlacing mode of the image (0 = None, 1 = Adam7)
|
||||||
|
pub interlaced: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IhdrData {
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
/// Bits per pixel
|
/// Options to use for performing operations on headers (such as stripping)
|
||||||
#[must_use]
|
pub enum Headers {
|
||||||
#[inline]
|
|
||||||
pub const fn bpp(&self) -> usize {
|
|
||||||
self.bit_depth as usize * self.color_type.channels_per_pixel() as usize
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Byte length of IDAT that is correct for this IHDR
|
|
||||||
#[must_use]
|
|
||||||
pub const fn raw_data_size(&self) -> usize {
|
|
||||||
let w = self.width as usize;
|
|
||||||
let h = self.height as usize;
|
|
||||||
let bpp = self.bpp();
|
|
||||||
|
|
||||||
const fn bitmap_size(bpp: usize, w: usize, h: usize) -> usize {
|
|
||||||
(w * bpp).div_ceil(8) * h
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.interlaced {
|
|
||||||
let mut size = bitmap_size(bpp, (w + 7) >> 3, (h + 7) >> 3) + ((h + 7) >> 3);
|
|
||||||
if w > 4 {
|
|
||||||
size += bitmap_size(bpp, (w + 3) >> 3, (h + 7) >> 3) + ((h + 7) >> 3);
|
|
||||||
}
|
|
||||||
size += bitmap_size(bpp, (w + 3) >> 2, (h + 3) >> 3) + ((h + 3) >> 3);
|
|
||||||
if w > 2 {
|
|
||||||
size += bitmap_size(bpp, (w + 1) >> 2, (h + 3) >> 2) + ((h + 3) >> 2);
|
|
||||||
}
|
|
||||||
size += bitmap_size(bpp, (w + 1) >> 1, (h + 1) >> 2) + ((h + 1) >> 2);
|
|
||||||
if w > 1 {
|
|
||||||
size += bitmap_size(bpp, w >> 1, (h + 1) >> 1) + ((h + 1) >> 1);
|
|
||||||
}
|
|
||||||
size + bitmap_size(bpp, w, h >> 1) + (h >> 1)
|
|
||||||
} else {
|
|
||||||
bitmap_size(bpp, w, h) + h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Chunk {
|
|
||||||
pub name: [u8; 4],
|
|
||||||
pub data: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [`Options`][crate::Options] to use when stripping chunks (metadata)
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub enum StripChunks {
|
|
||||||
/// 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(Vec<String>),
|
||||||
/// Remove all chunks that won't affect image display
|
/// Headers that won't affect rendering (all but cHRM, gAMA, iCCP, sBIT, sRGB, bKGD, hIST, pHYs, sPLT)
|
||||||
Safe,
|
Safe,
|
||||||
/// Remove all non-critical chunks except these
|
/// Remove all non-critical chunks except these
|
||||||
Keep(IndexSet<[u8; 4]>),
|
Keep(HashSet<String>),
|
||||||
/// All non-critical chunks
|
/// All non-critical headers
|
||||||
All,
|
All,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StripChunks {
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) fn keep(&self, name: &[u8; 4]) -> bool {
|
pub struct Header {
|
||||||
match &self {
|
pub key: String,
|
||||||
Self::None => true,
|
pub data: Vec<u8>,
|
||||||
Self::Keep(names) => names.contains(name),
|
}
|
||||||
Self::Strip(names) => !names.contains(name),
|
|
||||||
Self::Safe => DISPLAY_CHUNKS.contains(name),
|
impl Header {
|
||||||
Self::All => false,
|
pub fn new(key: String, data: Vec<u8>) -> Self {
|
||||||
}
|
Header { key, data }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,319 +60,84 @@ pub fn file_header_is_valid(bytes: &[u8]) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct RawChunk<'a> {
|
pub struct RawHeader<'a> {
|
||||||
pub name: [u8; 4],
|
pub name: [u8; 4],
|
||||||
pub data: &'a [u8],
|
pub data: &'a [u8],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RawChunk<'_> {
|
pub fn parse_next_header<'a>(
|
||||||
// Is it a chunk for C2PA/CAI JUMBF metadata
|
|
||||||
pub(crate) fn is_c2pa(&self) -> bool {
|
|
||||||
if self.name == *b"caBX" {
|
|
||||||
if let Some((b"jumb", data)) = parse_jumbf_box(self.data) {
|
|
||||||
if let Some((b"jumd", data)) = parse_jumbf_box(data) {
|
|
||||||
if data.get(..4) == Some(b"c2pa") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_jumbf_box(data: &[u8]) -> Option<(&[u8], &[u8])> {
|
|
||||||
if data.len() < 8 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let (len, rest) = data.split_at(4);
|
|
||||||
let len = read_be_u32(len) as usize;
|
|
||||||
if len < 8 || len > data.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let (box_name, data) = rest.split_at(4);
|
|
||||||
let data = data.get(..len - 8)?;
|
|
||||||
Some((box_name, data))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_next_chunk<'a>(
|
|
||||||
byte_data: &'a [u8],
|
byte_data: &'a [u8],
|
||||||
byte_offset: &mut usize,
|
byte_offset: &mut usize,
|
||||||
fix_errors: bool,
|
fix_errors: bool,
|
||||||
) -> PngResult<Option<RawChunk<'a>>> {
|
) -> PngResult<Option<RawHeader<'a>>> {
|
||||||
let length = read_be_u32(
|
let mut rdr = Cursor::new(
|
||||||
byte_data
|
byte_data
|
||||||
.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 {
|
let length = rdr.read_u32::<BigEndian>().unwrap();
|
||||||
return Err(PngError::TruncatedData);
|
|
||||||
}
|
|
||||||
*byte_offset += 4;
|
*byte_offset += 4;
|
||||||
|
|
||||||
let chunk_start = *byte_offset;
|
let header_start = *byte_offset;
|
||||||
let chunk_name = &byte_data[chunk_start..chunk_start + 4];
|
let chunk_name = byte_data
|
||||||
|
.get(header_start..header_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[*byte_offset..*byte_offset + length as usize];
|
let data = byte_data
|
||||||
|
.get(*byte_offset..*byte_offset + length as usize)
|
||||||
|
.ok_or(PngError::TruncatedData)?;
|
||||||
*byte_offset += length as usize;
|
*byte_offset += length as usize;
|
||||||
let crc = read_be_u32(&byte_data[*byte_offset..*byte_offset + 4]);
|
let mut rdr = Cursor::new(
|
||||||
|
byte_data
|
||||||
|
.get(*byte_offset..*byte_offset + 4)
|
||||||
|
.ok_or(PngError::TruncatedData)?,
|
||||||
|
);
|
||||||
|
let crc = rdr.read_u32::<BigEndian>().unwrap();
|
||||||
*byte_offset += 4;
|
*byte_offset += 4;
|
||||||
|
|
||||||
let chunk_bytes = &byte_data[chunk_start..chunk_start + 4 + length as usize];
|
let header_bytes = byte_data
|
||||||
if !fix_errors && crc32(chunk_bytes) != crc {
|
.get(header_start..header_start + 4 + length as usize)
|
||||||
return Err(PngError::CRCMismatch(chunk_name.try_into().unwrap()));
|
.ok_or(PngError::TruncatedData)?;
|
||||||
|
if !fix_errors && crc32::checksum_ieee(header_bytes) != crc {
|
||||||
|
return Err(PngError::new(&format!(
|
||||||
|
"CRC Mismatch in {} header; May be recoverable by using --fix",
|
||||||
|
String::from_utf8_lossy(chunk_name)
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let name: [u8; 4] = chunk_name.try_into().unwrap();
|
let mut name = [0u8; 4];
|
||||||
Ok(Some(RawChunk { name, data }))
|
name.copy_from_slice(chunk_name);
|
||||||
|
Ok(Some(RawHeader { name, data }))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_ihdr_chunk(
|
pub fn parse_ihdr_header(byte_data: &[u8]) -> PngResult<IhdrData> {
|
||||||
byte_data: &[u8],
|
let mut rdr = Cursor::new(&byte_data[0..8]);
|
||||||
palette_data: Option<Vec<u8>>,
|
|
||||||
trns_data: Option<Vec<u8>>,
|
|
||||||
) -> PngResult<IhdrData> {
|
|
||||||
// This eliminates bounds checks for the rest of the function
|
|
||||||
let interlaced = byte_data.get(12).copied().ok_or(PngError::TruncatedData)?;
|
|
||||||
Ok(IhdrData {
|
Ok(IhdrData {
|
||||||
color_type: match byte_data[9] {
|
color_type: match byte_data[9] {
|
||||||
0 => ColorType::Grayscale {
|
0 => ColorType::Grayscale,
|
||||||
transparent_shade: trns_data
|
2 => ColorType::RGB,
|
||||||
.filter(|t| t.len() >= 2)
|
3 => ColorType::Indexed,
|
||||||
.map(|t| read_be_u16(&t[0..2])),
|
|
||||||
},
|
|
||||||
2 => ColorType::RGB {
|
|
||||||
transparent_color: trns_data.filter(|t| t.len() >= 6).map(|t| RGB16 {
|
|
||||||
r: read_be_u16(&t[0..2]),
|
|
||||||
g: read_be_u16(&t[2..4]),
|
|
||||||
b: read_be_u16(&t[4..6]),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
3 => ColorType::Indexed {
|
|
||||||
palette: palette_to_rgba(palette_data, trns_data).unwrap_or_default(),
|
|
||||||
},
|
|
||||||
4 => ColorType::GrayscaleAlpha,
|
4 => ColorType::GrayscaleAlpha,
|
||||||
6 => ColorType::RGBA,
|
6 => ColorType::RGBA,
|
||||||
_ => return Err(PngError::InvalidData),
|
_ => return Err(PngError::new("Unexpected color type in header")),
|
||||||
},
|
},
|
||||||
bit_depth: byte_data[8].try_into()?,
|
bit_depth: match byte_data[8] {
|
||||||
width: read_be_u32(&byte_data[0..4]),
|
1 => BitDepth::One,
|
||||||
height: read_be_u32(&byte_data[4..8]),
|
2 => BitDepth::Two,
|
||||||
interlaced: match interlaced {
|
4 => BitDepth::Four,
|
||||||
0 => false,
|
8 => BitDepth::Eight,
|
||||||
1 => true,
|
16 => BitDepth::Sixteen,
|
||||||
_ => return Err(PngError::InvalidData),
|
_ => return Err(PngError::new("Unexpected bit depth in header")),
|
||||||
},
|
},
|
||||||
|
width: rdr.read_u32::<BigEndian>().unwrap(),
|
||||||
|
height: rdr.read_u32::<BigEndian>().unwrap(),
|
||||||
|
compression: byte_data[10],
|
||||||
|
filter: byte_data[11],
|
||||||
|
interlaced: byte_data[12],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct an RGBA palette from the raw palette and transparency data
|
|
||||||
fn palette_to_rgba(
|
|
||||||
palette_data: Option<Vec<u8>>,
|
|
||||||
trns_data: Option<Vec<u8>>,
|
|
||||||
) -> Result<Vec<RGBA8>, PngError> {
|
|
||||||
let palette_data = palette_data.ok_or(PngError::ChunkMissing("PLTE"))?;
|
|
||||||
let mut palette: Vec<_> = palette_data
|
|
||||||
.chunks_exact(3)
|
|
||||||
.map(|color| RGBA8::new(color[0], color[1], color[2], 255))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if let Some(trns_data) = trns_data {
|
|
||||||
for (color, trns) in palette.iter_mut().zip(trns_data) {
|
|
||||||
color.a = trns;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(palette)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn read_be_u16(bytes: &[u8]) -> u16 {
|
|
||||||
u16::from_be_bytes(bytes.try_into().unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn read_be_u32(bytes: &[u8]) -> u32 {
|
|
||||||
u32::from_be_bytes(bytes.try_into().unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract and decompress the ICC profile from an iCCP chunk
|
|
||||||
pub fn extract_icc(iccp: &Chunk, max_size: Option<usize>) -> Option<Vec<u8>> {
|
|
||||||
// Skip (useless) profile name
|
|
||||||
let mut data = iccp.data.as_slice();
|
|
||||||
loop {
|
|
||||||
let (&n, rest) = data.split_first()?;
|
|
||||||
data = rest;
|
|
||||||
if n == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (&compression_method, compressed_data) = data.split_first()?;
|
|
||||||
if compression_method != 0 {
|
|
||||||
return None; // The profile is supposed to be compressed (method 0)
|
|
||||||
}
|
|
||||||
// Libdeflate works with a fixed size buffer. Since the decompressed size is unknown we have to
|
|
||||||
// guess the required buffer size. We allow a fairly generous 10x factor with a minimum of 1000.
|
|
||||||
let mut out_size = (compressed_data.len() * 10).max(1000);
|
|
||||||
// For sanity, impose a default limit of 1MB.
|
|
||||||
out_size = out_size.min(max_size.unwrap_or(1_000_000));
|
|
||||||
match inflate(compressed_data, out_size) {
|
|
||||||
Ok(icc) => Some(icc),
|
|
||||||
Err(e) => {
|
|
||||||
// Log the error so we can know if the buffer size needs to be adjusted
|
|
||||||
warn!("Failed to decompress icc: {e}");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Make an iCCP chunk by compressing the ICC profile
|
|
||||||
pub fn make_iccp(icc: &[u8], deflater: Deflater, max_size: Option<usize>) -> PngResult<Chunk> {
|
|
||||||
let mut compressed = deflater.deflate(icc, max_size)?;
|
|
||||||
let mut data = Vec::with_capacity(compressed.len() + 5);
|
|
||||||
data.extend(b"icc"); // Profile name - generally unused, can be anything
|
|
||||||
data.extend([0, 0]); // Null separator, zlib compression method
|
|
||||||
data.append(&mut compressed);
|
|
||||||
Ok(Chunk {
|
|
||||||
name: *b"iCCP",
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If the profile is sRGB, extracts the rendering intent value from it
|
|
||||||
pub fn srgb_rendering_intent(icc_data: &[u8]) -> Option<u8> {
|
|
||||||
let rendering_intent = *icc_data.get(67)?;
|
|
||||||
|
|
||||||
// The known profiles are the same as in libpng's `png_sRGB_checks`.
|
|
||||||
// The Profile ID header of ICC has a fixed layout,
|
|
||||||
// and is supposed to contain MD5 of profile data at this offset
|
|
||||||
match icc_data.get(84..100)? {
|
|
||||||
b"\x29\xf8\x3d\xde\xaf\xf2\x55\xae\x78\x42\xfa\xe4\xca\x83\x39\x0d"
|
|
||||||
| b"\xc9\x5b\xd6\x37\xe9\x5d\x8a\x3b\x0d\xf3\x8f\x99\xc1\x32\x03\x89"
|
|
||||||
| b"\xfc\x66\x33\x78\x37\xe2\x88\x6b\xfd\x72\xe9\x83\x82\x28\xf1\xb8"
|
|
||||||
| b"\x34\x56\x2a\xbf\x99\x4c\xcd\x06\x6d\x2c\x57\x21\xd0\xd6\x8c\x5d" => {
|
|
||||||
Some(rendering_intent)
|
|
||||||
}
|
|
||||||
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" => {
|
|
||||||
// Known-bad profiles are identified by their CRC
|
|
||||||
match (crc32(icc_data), icc_data.len()) {
|
|
||||||
(0x5d51_29ce, 3024) | (0x182e_a552, 3144) | (0xf29e_526d, 3144) => {
|
|
||||||
Some(rendering_intent)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Process aux chunks and potentially adjust options before optimizing
|
|
||||||
pub fn preprocess_chunks(aux_chunks: &mut Vec<Chunk>, opts: &mut Options) {
|
|
||||||
let has_srgb = aux_chunks.iter().any(|c| &c.name == b"sRGB");
|
|
||||||
// Grayscale conversion should not be performed if the image is not in the sRGB colorspace
|
|
||||||
// An sRGB profile would need to be stripped on conversion, so disallow if stripping is disabled
|
|
||||||
let mut allow_grayscale = !has_srgb || opts.strip != StripChunks::None;
|
|
||||||
|
|
||||||
if let Some(iccp_idx) = aux_chunks.iter().position(|c| &c.name == b"iCCP") {
|
|
||||||
allow_grayscale = false;
|
|
||||||
// See if we can replace an iCCP chunk with an sRGB chunk
|
|
||||||
let may_replace_iccp = opts.strip != StripChunks::None && opts.strip.keep(b"sRGB");
|
|
||||||
if may_replace_iccp && has_srgb {
|
|
||||||
// Files aren't supposed to have both chunks, so we chose to honor sRGB
|
|
||||||
trace!("Removing iCCP chunk due to conflict with sRGB chunk");
|
|
||||||
aux_chunks.remove(iccp_idx);
|
|
||||||
allow_grayscale = true;
|
|
||||||
} else if let Some(icc) = extract_icc(&aux_chunks[iccp_idx], opts.max_decompressed_size) {
|
|
||||||
let intent = if may_replace_iccp {
|
|
||||||
srgb_rendering_intent(&icc)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
// sRGB-like profile can be replaced with an sRGB chunk with the same rendering intent
|
|
||||||
if let Some(intent) = intent {
|
|
||||||
trace!("Replacing iCCP chunk with equivalent sRGB chunk");
|
|
||||||
aux_chunks[iccp_idx] = Chunk {
|
|
||||||
name: *b"sRGB",
|
|
||||||
data: vec![intent],
|
|
||||||
};
|
|
||||||
allow_grayscale = true;
|
|
||||||
} else if opts.idat_recoding {
|
|
||||||
// Try recompressing the profile
|
|
||||||
let cur_len = aux_chunks[iccp_idx].data.len();
|
|
||||||
if let Ok(iccp) = make_iccp(&icc, opts.deflater, Some(cur_len - 1)) {
|
|
||||||
debug!(
|
|
||||||
"Recompressed iCCP chunk: {} ({} bytes decrease)",
|
|
||||||
iccp.data.len(),
|
|
||||||
cur_len - iccp.data.len()
|
|
||||||
);
|
|
||||||
aux_chunks[iccp_idx] = iccp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !allow_grayscale && opts.grayscale_reduction {
|
|
||||||
debug!("Disabling grayscale reduction due to presence of sRGB or iCCP chunk");
|
|
||||||
opts.grayscale_reduction = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for APNG by presence of acTL chunk
|
|
||||||
if aux_chunks.iter().any(|c| &c.name == b"acTL") {
|
|
||||||
warn!("APNG detected, disabling all reductions");
|
|
||||||
opts.interlace = None;
|
|
||||||
opts.bit_depth_reduction = false;
|
|
||||||
opts.color_type_reduction = false;
|
|
||||||
opts.palette_reduction = false;
|
|
||||||
opts.grayscale_reduction = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perform cleanup of certain aux chunks after optimization has been completed
|
|
||||||
pub fn postprocess_chunks(aux_chunks: &mut Vec<Chunk>, ihdr: &IhdrData, orig_ihdr: &IhdrData) {
|
|
||||||
// If the depth/color type has changed, some chunks may be invalid and should be dropped
|
|
||||||
// While these could potentially be converted, they have no known use case today and are
|
|
||||||
// generally more trouble than they're worth
|
|
||||||
if orig_ihdr.bit_depth != ihdr.bit_depth || orig_ihdr.color_type != ihdr.color_type {
|
|
||||||
aux_chunks.retain(|c| {
|
|
||||||
let invalid = &c.name == b"bKGD" || &c.name == b"sBIT" || &c.name == b"hIST";
|
|
||||||
if invalid {
|
|
||||||
warn!(
|
|
||||||
"Removing {} chunk as it no longer matches the image data",
|
|
||||||
std::str::from_utf8(&c.name).unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
!invalid
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove any sRGB or iCCP chunks if the image was converted to or from grayscale
|
|
||||||
if orig_ihdr.color_type.is_gray() != ihdr.color_type.is_gray() {
|
|
||||||
aux_chunks.retain(|c| {
|
|
||||||
let invalid = &c.name == b"sRGB" || &c.name == b"iCCP";
|
|
||||||
if invalid {
|
|
||||||
trace!(
|
|
||||||
"Removing {} chunk as it no longer matches the color type",
|
|
||||||
std::str::from_utf8(&c.name).unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
!invalid
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove iDOT which will necessarily be invalid after successful optimization
|
|
||||||
aux_chunks.retain(|c| {
|
|
||||||
let invalid = &c.name == b"iDOT";
|
|
||||||
if invalid {
|
|
||||||
trace!(
|
|
||||||
"Removing {} chunk as it no longer matches the IDAT",
|
|
||||||
std::str::from_utf8(&c.name).unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
!invalid
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
189
src/interlace.rs
|
|
@ -1,20 +1,51 @@
|
||||||
use bitvec::prelude::*;
|
use bit_vec::BitVec;
|
||||||
|
use png::PngData;
|
||||||
|
|
||||||
use crate::{headers::IhdrData, png::PngImage};
|
pub fn interlace_image(png: &mut PngData) {
|
||||||
|
let mut passes: Vec<BitVec> = vec![BitVec::new(); 7];
|
||||||
#[must_use]
|
let bits_per_pixel = png.ihdr_data.bit_depth.as_u8() * png.channels_per_pixel();
|
||||||
pub fn interlace_image(png: &PngImage) -> PngImage {
|
for (index, line) in png.scan_lines().enumerate() {
|
||||||
let mut passes: Vec<BitVec<u8, Msb0>> = vec![BitVec::new(); 7];
|
match index % 8 {
|
||||||
let bits_per_pixel = png.ihdr.bpp();
|
// Add filter bytes to passes that will be in the output image
|
||||||
for (index, line) in png.scan_lines(false).enumerate() {
|
0 => {
|
||||||
let bit_vec = line.data.view_bits::<Msb0>();
|
passes[0].extend(BitVec::from_elem(8, false));
|
||||||
for (i, bit) in bit_vec.iter().by_vals().enumerate() {
|
if png.ihdr_data.width >= 5 {
|
||||||
|
passes[1].extend(BitVec::from_elem(8, false));
|
||||||
|
}
|
||||||
|
if png.ihdr_data.width >= 3 {
|
||||||
|
passes[3].extend(BitVec::from_elem(8, false));
|
||||||
|
}
|
||||||
|
if png.ihdr_data.width >= 2 {
|
||||||
|
passes[5].extend(BitVec::from_elem(8, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
4 => {
|
||||||
|
passes[2].extend(BitVec::from_elem(8, false));
|
||||||
|
if png.ihdr_data.width >= 3 {
|
||||||
|
passes[3].extend(BitVec::from_elem(8, false));
|
||||||
|
}
|
||||||
|
if png.ihdr_data.width >= 2 {
|
||||||
|
passes[5].extend(BitVec::from_elem(8, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 | 6 => {
|
||||||
|
passes[4].extend(BitVec::from_elem(8, false));
|
||||||
|
if png.ihdr_data.width >= 2 {
|
||||||
|
passes[5].extend(BitVec::from_elem(8, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
passes[6].extend(BitVec::from_elem(8, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let bit_vec = BitVec::from_bytes(&line.data);
|
||||||
|
for (i, bit) in bit_vec.iter().enumerate() {
|
||||||
// Avoid moving padded 0's into new image
|
// Avoid moving padded 0's into new image
|
||||||
if i >= (png.ihdr.width as usize * bits_per_pixel) {
|
if i >= (png.ihdr_data.width * u32::from(bits_per_pixel)) as usize {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Copy pixels into interlaced passes
|
// Copy pixels into interlaced passes
|
||||||
let pix_modulo = (i / bits_per_pixel) % 8;
|
let pix_modulo = (i / bits_per_pixel as usize) % 8;
|
||||||
match index % 8 {
|
match index % 8 {
|
||||||
0 => match pix_modulo {
|
0 => match pix_modulo {
|
||||||
0 => passes[0].push(bit),
|
0 => passes[0].push(bit),
|
||||||
|
|
@ -43,136 +74,64 @@ pub fn interlace_image(png: &PngImage) -> PngImage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let mut output = Vec::new();
|
||||||
let mut output = Vec::with_capacity(png.data.len());
|
|
||||||
for pass in &passes {
|
for pass in &passes {
|
||||||
output.extend_from_slice(pass.as_raw_slice());
|
output.extend(pass.to_bytes());
|
||||||
}
|
|
||||||
|
|
||||||
PngImage {
|
|
||||||
data: output,
|
|
||||||
ihdr: IhdrData {
|
|
||||||
color_type: png.ihdr.color_type.clone(),
|
|
||||||
interlaced: true,
|
|
||||||
..png.ihdr
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
png.raw_data = output;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinterlace_image(png: &PngImage) -> PngImage {
|
pub fn deinterlace_image(png: &mut PngData) {
|
||||||
PngImage {
|
let bits_per_pixel = png.ihdr_data.bit_depth.as_u8() * png.channels_per_pixel();
|
||||||
data: match png.ihdr.bpp() {
|
let bits_per_line = 8 + bits_per_pixel as usize * png.ihdr_data.width as usize;
|
||||||
8.. => deinterlace_bytes(png),
|
// Initialize each output line with a starting filter byte of 0
|
||||||
_ => deinterlace_bits(png),
|
// as well as some blank data
|
||||||
},
|
let mut lines: Vec<BitVec> =
|
||||||
ihdr: IhdrData {
|
vec![BitVec::from_elem(bits_per_line, false); png.ihdr_data.height as usize];
|
||||||
color_type: png.ihdr.color_type.clone(),
|
|
||||||
interlaced: false,
|
|
||||||
..png.ihdr
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deinterlace by bits, for images with less than 8bpp
|
|
||||||
fn deinterlace_bits(png: &PngImage) -> Vec<u8> {
|
|
||||||
let bits_per_pixel = png.ihdr.bpp();
|
|
||||||
let bits_per_line = bits_per_pixel * png.ihdr.width as usize;
|
|
||||||
// Initialize each output line with blank data
|
|
||||||
let mut lines: Vec<BitVec<u8, Msb0>> =
|
|
||||||
vec![bitvec![u8, Msb0; 0; bits_per_line]; png.ihdr.height as usize];
|
|
||||||
let mut current_pass = 1;
|
let mut current_pass = 1;
|
||||||
let mut pass_constants = interlaced_constants(current_pass);
|
let mut pass_constants = interlaced_constants(current_pass);
|
||||||
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() {
|
||||||
let bit_vec = line.data.view_bits::<Msb0>();
|
let bit_vec = BitVec::from_bytes(&line.data);
|
||||||
let bits_in_line = (png.ihdr.width - u32::from(pass_constants.x_shift))
|
let bits_in_line = ((png.ihdr_data.width - u32::from(pass_constants.x_shift)) as f32
|
||||||
.div_ceil(u32::from(pass_constants.x_step)) as usize
|
/ f32::from(pass_constants.x_step)).ceil() as usize
|
||||||
* bits_per_pixel;
|
* bits_per_pixel as usize;
|
||||||
for (i, bit) in bit_vec.iter().by_vals().enumerate() {
|
for (i, bit) in bit_vec.iter().enumerate() {
|
||||||
// Avoid moving padded 0's into new image
|
// Avoid moving padded 0's into new image
|
||||||
if i >= bits_in_line {
|
if i >= bits_in_line {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let current_x: usize = pass_constants.x_shift as usize
|
let current_x: usize = pass_constants.x_shift as usize
|
||||||
+ (i / bits_per_pixel) * pass_constants.x_step as usize;
|
+ (i / bits_per_pixel as usize) * pass_constants.x_step as usize;
|
||||||
// Copy this bit into the output line
|
// Copy this bit into the output line, offset by 8 because of filter byte
|
||||||
let index = (i % bits_per_pixel) + current_x * bits_per_pixel;
|
let index = 8 + (i % bits_per_pixel as usize) + current_x * bits_per_pixel as usize;
|
||||||
lines[current_y].set(index, bit);
|
lines[current_y].set(index, bit);
|
||||||
}
|
}
|
||||||
// Calculate the next line and move to next pass if necessary
|
// Calculate the next line and move to next pass if necessary
|
||||||
current_y += pass_constants.y_step as usize;
|
current_y += pass_constants.y_step as usize;
|
||||||
if current_y >= png.ihdr.height as usize {
|
if current_y >= png.ihdr_data.height as usize {
|
||||||
if !increment_pass(&mut current_pass, &png.ihdr) {
|
if current_pass == 7 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
current_pass += 1;
|
||||||
|
if current_pass == 2 && png.ihdr_data.width <= 4 {
|
||||||
|
current_pass += 1;
|
||||||
|
}
|
||||||
|
if current_pass == 3 && png.ihdr_data.height <= 4 {
|
||||||
|
current_pass += 1;
|
||||||
|
}
|
||||||
pass_constants = interlaced_constants(current_pass);
|
pass_constants = interlaced_constants(current_pass);
|
||||||
current_y = pass_constants.y_shift as usize;
|
current_y = pass_constants.y_shift as usize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut output = Vec::with_capacity(png.data.len());
|
let mut output = Vec::new();
|
||||||
for line in &mut lines {
|
for line in &mut lines {
|
||||||
while line.len() % 8 != 0 {
|
while line.len() % 8 != 0 {
|
||||||
line.push(false);
|
line.push(false);
|
||||||
}
|
}
|
||||||
output.extend_from_slice(line.as_raw_slice());
|
output.extend(line.to_bytes());
|
||||||
}
|
}
|
||||||
output
|
png.raw_data = output;
|
||||||
}
|
|
||||||
|
|
||||||
/// Deinterlace by bytes, for images with at least 8bpp
|
|
||||||
fn deinterlace_bytes(png: &PngImage) -> Vec<u8> {
|
|
||||||
let bytes_per_pixel = png.ihdr.bpp() / 8;
|
|
||||||
let bytes_per_line = bytes_per_pixel * png.ihdr.width as usize;
|
|
||||||
// Initialize each output line with some blank data
|
|
||||||
let mut lines: Vec<Vec<u8>> = vec![vec![0; bytes_per_line]; png.ihdr.height as usize];
|
|
||||||
let mut current_pass = 1;
|
|
||||||
let mut pass_constants = interlaced_constants(current_pass);
|
|
||||||
let mut current_y: usize = pass_constants.y_shift as usize;
|
|
||||||
for line in png.scan_lines(false) {
|
|
||||||
for (i, byte) in line.data.iter().enumerate() {
|
|
||||||
let current_x: usize = pass_constants.x_shift as usize
|
|
||||||
+ (i / bytes_per_pixel) * pass_constants.x_step as usize;
|
|
||||||
// Copy this byte into the output line
|
|
||||||
let index = (i % bytes_per_pixel) + current_x * bytes_per_pixel;
|
|
||||||
lines[current_y][index] = *byte;
|
|
||||||
}
|
|
||||||
// Calculate the next line and move to next pass if necessary
|
|
||||||
current_y += pass_constants.y_step as usize;
|
|
||||||
if current_y >= png.ihdr.height as usize {
|
|
||||||
if !increment_pass(&mut current_pass, &png.ihdr) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
pass_constants = interlaced_constants(current_pass);
|
|
||||||
current_y = pass_constants.y_shift as usize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lines.concat()
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn increment_pass(current_pass: &mut u8, ihdr: &IhdrData) -> bool {
|
|
||||||
if *current_pass == 7 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
*current_pass += 1;
|
|
||||||
if *current_pass == 2 && ihdr.width <= 4 {
|
|
||||||
*current_pass += 1;
|
|
||||||
}
|
|
||||||
if *current_pass == 3 && ihdr.height <= 4 {
|
|
||||||
*current_pass += 1;
|
|
||||||
}
|
|
||||||
if *current_pass == 4 && ihdr.width <= 2 {
|
|
||||||
*current_pass += 1;
|
|
||||||
}
|
|
||||||
if *current_pass == 5 && ihdr.height <= 2 {
|
|
||||||
*current_pass += 1;
|
|
||||||
}
|
|
||||||
if *current_pass == 6 && ihdr.width == 1 {
|
|
||||||
*current_pass += 1;
|
|
||||||
}
|
|
||||||
if *current_pass == 7 && ihdr.height == 1 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
|
|
|
||||||
1301
src/lib.rs
911
src/main.rs
303
src/options.rs
|
|
@ -1,303 +0,0 @@
|
||||||
use std::{
|
|
||||||
fmt,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use indexmap::{IndexSet, indexset};
|
|
||||||
use log::warn;
|
|
||||||
|
|
||||||
use crate::{deflate::Deflater, filters::FilterStrategy, headers::StripChunks};
|
|
||||||
|
|
||||||
/// Write destination for [`optimize`][crate::optimize].
|
|
||||||
/// You can use [`optimize_from_memory`](crate::optimize_from_memory) to avoid external I/O.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum OutFile {
|
|
||||||
/// Don't actually write any output, just calculate the best results.
|
|
||||||
None,
|
|
||||||
/// Write output to a file.
|
|
||||||
///
|
|
||||||
/// * `path`: Path to write the output file. `None` means same as input.
|
|
||||||
/// * `preserve_attrs`: Ensure the output file has the same permissions & timestamps as the input file.
|
|
||||||
Path {
|
|
||||||
path: Option<PathBuf>,
|
|
||||||
preserve_attrs: bool,
|
|
||||||
},
|
|
||||||
/// Write to standard output.
|
|
||||||
StdOut,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OutFile {
|
|
||||||
/// Construct a new `OutFile` with the given path.
|
|
||||||
///
|
|
||||||
/// This is a convenience method for `OutFile::Path { path: Some(path), preserve_attrs: false }`.
|
|
||||||
#[must_use]
|
|
||||||
pub const fn from_path(path: PathBuf) -> Self {
|
|
||||||
Self::Path {
|
|
||||||
path: Some(path),
|
|
||||||
preserve_attrs: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn path(&self) -> Option<&Path> {
|
|
||||||
match *self {
|
|
||||||
Self::Path {
|
|
||||||
path: Some(ref p), ..
|
|
||||||
} => Some(p.as_path()),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Where to read images from in [`optimize`][crate::optimize].
|
|
||||||
/// You can use [`optimize_from_memory`](crate::optimize_from_memory) to avoid external I/O.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum InFile {
|
|
||||||
Path(PathBuf),
|
|
||||||
StdIn,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InFile {
|
|
||||||
#[must_use]
|
|
||||||
pub fn path(&self) -> Option<&Path> {
|
|
||||||
match *self {
|
|
||||||
Self::Path(ref p) => Some(p.as_path()),
|
|
||||||
Self::StdIn => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for InFile {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
Self::Path(ref p) => write!(f, "{}", p.display()),
|
|
||||||
Self::StdIn => f.write_str("stdin"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Into<PathBuf>> From<T> for InFile {
|
|
||||||
fn from(s: T) -> Self {
|
|
||||||
Self::Path(s.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
/// Options controlling the output of the `optimize` function
|
|
||||||
pub struct Options {
|
|
||||||
/// Attempt to fix errors when decoding the input file rather than returning an `Err`.
|
|
||||||
///
|
|
||||||
/// Default: `false`
|
|
||||||
pub fix_errors: bool,
|
|
||||||
/// Write to output even if there was no improvement in compression.
|
|
||||||
///
|
|
||||||
/// Default: `false`
|
|
||||||
pub force: bool,
|
|
||||||
/// Which `FilterStrategy` to try on the file
|
|
||||||
///
|
|
||||||
/// Default: `None,Sub,Entropy,Bigrams`
|
|
||||||
pub filters: IndexSet<FilterStrategy>,
|
|
||||||
/// Whether to change the interlacing of the file.
|
|
||||||
///
|
|
||||||
/// - `None` will not change the current interlacing.
|
|
||||||
/// - `Some(x)` will turn interlacing on or off.
|
|
||||||
///
|
|
||||||
/// Default: `Some(false)`
|
|
||||||
pub interlace: Option<bool>,
|
|
||||||
/// Whether to allow transparent pixels to be altered to improve compression.
|
|
||||||
///
|
|
||||||
/// Default: `false`
|
|
||||||
pub optimize_alpha: bool,
|
|
||||||
/// Whether to attempt bit depth reduction
|
|
||||||
///
|
|
||||||
/// Default: `true`
|
|
||||||
pub bit_depth_reduction: bool,
|
|
||||||
/// Whether to attempt color type reduction
|
|
||||||
///
|
|
||||||
/// Default: `true`
|
|
||||||
pub color_type_reduction: bool,
|
|
||||||
/// Whether to attempt palette reduction
|
|
||||||
///
|
|
||||||
/// Default: `true`
|
|
||||||
pub palette_reduction: bool,
|
|
||||||
/// Whether to attempt grayscale reduction
|
|
||||||
///
|
|
||||||
/// Default: `true`
|
|
||||||
pub grayscale_reduction: bool,
|
|
||||||
/// Whether to perform recoding of IDAT and other compressed chunks
|
|
||||||
///
|
|
||||||
/// If any type of reduction is performed, IDAT recoding will be performed
|
|
||||||
/// regardless of this setting
|
|
||||||
///
|
|
||||||
/// Default: `true`
|
|
||||||
pub idat_recoding: bool,
|
|
||||||
/// Whether to forcibly reduce 16-bit to 8-bit by scaling
|
|
||||||
///
|
|
||||||
/// Default: `false`
|
|
||||||
pub scale_16: bool,
|
|
||||||
/// Which chunks to strip from the PNG file, if any
|
|
||||||
///
|
|
||||||
/// Default: `None`
|
|
||||||
pub strip: StripChunks,
|
|
||||||
/// Which DEFLATE (zlib) algorithm to use
|
|
||||||
#[cfg_attr(feature = "zopfli", doc = "(e.g. Zopfli)")]
|
|
||||||
///
|
|
||||||
/// Default: `Libdeflater`
|
|
||||||
pub deflater: Deflater,
|
|
||||||
/// Whether to use fast evaluation to pick the best filter
|
|
||||||
///
|
|
||||||
/// Default: `true`
|
|
||||||
pub fast_evaluation: bool,
|
|
||||||
/// Maximum amount of time to spend on optimizations.
|
|
||||||
/// Further potential optimizations are skipped if the timeout is exceeded.
|
|
||||||
///
|
|
||||||
/// Default: `None`
|
|
||||||
pub timeout: Option<Duration>,
|
|
||||||
/// Maximum decompressed size of the input IDAT.
|
|
||||||
/// If decompression would exceed this size, it will be rejected.
|
|
||||||
///
|
|
||||||
/// Default: `None`
|
|
||||||
pub max_decompressed_size: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Options {
|
|
||||||
#[must_use]
|
|
||||||
pub fn from_preset(level: u8) -> Self {
|
|
||||||
let opts = Self::default();
|
|
||||||
match level {
|
|
||||||
0 => opts.apply_preset_0(),
|
|
||||||
1 => opts.apply_preset_1(),
|
|
||||||
2 => opts.apply_preset_2(),
|
|
||||||
3 => opts.apply_preset_3(),
|
|
||||||
4 => opts.apply_preset_4(),
|
|
||||||
5 => opts.apply_preset_5(),
|
|
||||||
6 => opts.apply_preset_6(),
|
|
||||||
_ => {
|
|
||||||
warn!("Level 7 and above don't exist yet and are identical to level 6");
|
|
||||||
opts.apply_preset_6()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_compression() -> Self {
|
|
||||||
Self::from_preset(6)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following methods make assumptions that they are operating
|
|
||||||
// on an `Options` struct generated by the `default` method.
|
|
||||||
fn apply_preset_0(mut self) -> Self {
|
|
||||||
self.filters.clear();
|
|
||||||
self.deflater = Deflater::Libdeflater { compression: 5 };
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_preset_1(mut self) -> Self {
|
|
||||||
self.filters.clear();
|
|
||||||
self.deflater = Deflater::Libdeflater { compression: 10 };
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn apply_preset_2(self) -> Self {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_preset_3(mut self) -> Self {
|
|
||||||
self.fast_evaluation = false;
|
|
||||||
self.filters = indexset! {
|
|
||||||
FilterStrategy::NONE,
|
|
||||||
FilterStrategy::Bigrams,
|
|
||||||
FilterStrategy::BigEnt,
|
|
||||||
FilterStrategy::Brute {
|
|
||||||
num_lines: 3,
|
|
||||||
level: 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_preset_4(mut self) -> Self {
|
|
||||||
self.fast_evaluation = false;
|
|
||||||
self.filters = indexset! {
|
|
||||||
FilterStrategy::NONE,
|
|
||||||
FilterStrategy::Bigrams,
|
|
||||||
FilterStrategy::BigEnt,
|
|
||||||
FilterStrategy::Brute {
|
|
||||||
num_lines: 4,
|
|
||||||
level: 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
self.deflater = Deflater::Libdeflater { compression: 12 };
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_preset_5(mut self) -> Self {
|
|
||||||
self.fast_evaluation = false;
|
|
||||||
self.filters = indexset! {
|
|
||||||
FilterStrategy::NONE,
|
|
||||||
FilterStrategy::SUB,
|
|
||||||
FilterStrategy::UP,
|
|
||||||
FilterStrategy::MinSum,
|
|
||||||
FilterStrategy::Entropy,
|
|
||||||
FilterStrategy::Bigrams,
|
|
||||||
FilterStrategy::BigEnt,
|
|
||||||
FilterStrategy::Brute {
|
|
||||||
num_lines: 4,
|
|
||||||
level: 4,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
self.deflater = Deflater::Libdeflater { compression: 12 };
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_preset_6(mut self) -> Self {
|
|
||||||
self.fast_evaluation = false;
|
|
||||||
self.filters = indexset! {
|
|
||||||
FilterStrategy::NONE,
|
|
||||||
FilterStrategy::SUB,
|
|
||||||
FilterStrategy::UP,
|
|
||||||
FilterStrategy::AVERAGE,
|
|
||||||
FilterStrategy::PAETH,
|
|
||||||
FilterStrategy::MinSum,
|
|
||||||
FilterStrategy::Entropy,
|
|
||||||
FilterStrategy::Bigrams,
|
|
||||||
FilterStrategy::BigEnt,
|
|
||||||
FilterStrategy::Brute {
|
|
||||||
num_lines: 8,
|
|
||||||
level: 5,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
self.deflater = Deflater::Libdeflater { compression: 12 };
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Options {
|
|
||||||
fn default() -> Self {
|
|
||||||
// Default settings based on -o 2 from the CLI interface
|
|
||||||
Self {
|
|
||||||
fix_errors: false,
|
|
||||||
force: false,
|
|
||||||
filters: indexset! {
|
|
||||||
FilterStrategy::NONE,
|
|
||||||
FilterStrategy::SUB,
|
|
||||||
FilterStrategy::Entropy,
|
|
||||||
FilterStrategy::Bigrams
|
|
||||||
},
|
|
||||||
interlace: Some(false),
|
|
||||||
optimize_alpha: false,
|
|
||||||
bit_depth_reduction: true,
|
|
||||||
color_type_reduction: true,
|
|
||||||
palette_reduction: true,
|
|
||||||
grayscale_reduction: true,
|
|
||||||
idat_recoding: true,
|
|
||||||
scale_16: false,
|
|
||||||
strip: StripChunks::None,
|
|
||||||
deflater: Deflater::Libdeflater { compression: 11 },
|
|
||||||
fast_evaluation: true,
|
|
||||||
timeout: None,
|
|
||||||
max_decompressed_size: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
81
src/png/apng.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
use byteorder::{BigEndian, ReadBytesExt};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum DisposalType {
|
||||||
|
None = 0,
|
||||||
|
Background = 1,
|
||||||
|
Previous = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for DisposalType {
|
||||||
|
fn from(val: u8) -> Self {
|
||||||
|
match val {
|
||||||
|
0 => DisposalType::None,
|
||||||
|
1 => DisposalType::Background,
|
||||||
|
2 => DisposalType::Previous,
|
||||||
|
_ => panic!("Unrecognized disposal type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum BlendType {
|
||||||
|
Source = 0,
|
||||||
|
Over = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for BlendType {
|
||||||
|
fn from(val: u8) -> Self {
|
||||||
|
match val {
|
||||||
|
0 => BlendType::Source,
|
||||||
|
1 => BlendType::Over,
|
||||||
|
_ => panic!("Unrecognized blend type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ApngFrame {
|
||||||
|
pub sequence_number: u32,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub x_offset: u32,
|
||||||
|
pub y_offset: u32,
|
||||||
|
pub delay_num: u16,
|
||||||
|
pub delay_den: u16,
|
||||||
|
pub dispose_op: DisposalType,
|
||||||
|
pub blend_op: BlendType,
|
||||||
|
/// The compressed, filtered data from the fdAT chunks
|
||||||
|
pub frame_data: Vec<u8>,
|
||||||
|
/// The uncompressed, optionally filtered data from the fdAT chunks
|
||||||
|
pub raw_data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a [u8]> for ApngFrame {
|
||||||
|
/// Converts a fcTL header to an `ApngFrame`. Will panic if `data` is less than 26 bytes.
|
||||||
|
fn from(data: &[u8]) -> Self {
|
||||||
|
let mut cursor = Cursor::new(data);
|
||||||
|
ApngFrame {
|
||||||
|
sequence_number: cursor.read_u32::<BigEndian>().unwrap(),
|
||||||
|
width: cursor.read_u32::<BigEndian>().unwrap(),
|
||||||
|
height: cursor.read_u32::<BigEndian>().unwrap(),
|
||||||
|
x_offset: cursor.read_u32::<BigEndian>().unwrap(),
|
||||||
|
y_offset: cursor.read_u32::<BigEndian>().unwrap(),
|
||||||
|
delay_num: cursor.read_u16::<BigEndian>().unwrap(),
|
||||||
|
delay_den: cursor.read_u16::<BigEndian>().unwrap(),
|
||||||
|
dispose_op: cursor.read_u8().unwrap().into(),
|
||||||
|
blend_op: cursor.read_u8().unwrap().into(),
|
||||||
|
frame_data: Vec::new(),
|
||||||
|
raw_data: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct ApngHeaders {
|
||||||
|
pub frames: u32,
|
||||||
|
pub plays: u32,
|
||||||
|
}
|
||||||
1287
src/png/mod.rs
|
|
@ -1,168 +1,131 @@
|
||||||
use crate::png::PngImage;
|
use super::PngData;
|
||||||
|
|
||||||
/// An iterator over the scan lines of a PNG image
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
/// An iterator over the scan lines of a PNG image
|
||||||
pub struct ScanLines<'a> {
|
pub struct ScanLines<'a> {
|
||||||
iter: ScanLineRanges,
|
|
||||||
/// A reference to the PNG image being iterated upon
|
/// A reference to the PNG image being iterated upon
|
||||||
raw_data: &'a [u8],
|
pub png: &'a PngData,
|
||||||
/// Whether the raw data contains filter bytes
|
pub start: usize,
|
||||||
has_filter: bool,
|
pub end: usize,
|
||||||
}
|
/// Current pass number, and 0-indexed row within the pass
|
||||||
|
pub pass: Option<(u8, u32)>,
|
||||||
impl<'a> ScanLines<'a> {
|
|
||||||
#[must_use]
|
|
||||||
pub fn new(png: &'a PngImage, has_filter: bool) -> Self {
|
|
||||||
Self {
|
|
||||||
iter: ScanLineRanges::new(png, has_filter),
|
|
||||||
raw_data: &png.data,
|
|
||||||
has_filter,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Iterator for ScanLines<'a> {
|
impl<'a> Iterator for ScanLines<'a> {
|
||||||
type Item = ScanLine<'a>;
|
type Item = ScanLine<'a>;
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
let (len, pass, num_pixels) = self.iter.next()?;
|
if self.end == self.png.raw_data.len() {
|
||||||
debug_assert!(self.raw_data.len() >= len);
|
None
|
||||||
debug_assert!(!self.has_filter || len > 1);
|
} else if self.png.ihdr_data.interlaced == 1 {
|
||||||
// The data length should always be correct here but this check assures
|
|
||||||
// the compiler that it doesn't need to account for a potential panic
|
|
||||||
if self.raw_data.len() < len {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let (data, rest) = self.raw_data.split_at(len);
|
|
||||||
self.raw_data = rest;
|
|
||||||
let (&filter, data) = if self.has_filter {
|
|
||||||
data.split_first().unwrap()
|
|
||||||
} else {
|
|
||||||
(&0, data)
|
|
||||||
};
|
|
||||||
Some(ScanLine {
|
|
||||||
filter,
|
|
||||||
data,
|
|
||||||
pass,
|
|
||||||
num_pixels,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
/// An iterator over the scan line locations of a PNG image
|
|
||||||
struct ScanLineRanges {
|
|
||||||
/// Current pass number, and 0-indexed row within the pass
|
|
||||||
pass: Option<(u8, u32)>,
|
|
||||||
bits_per_pixel: usize,
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
left: usize,
|
|
||||||
has_filter: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScanLineRanges {
|
|
||||||
pub fn new(png: &PngImage, has_filter: bool) -> Self {
|
|
||||||
Self {
|
|
||||||
bits_per_pixel: png.ihdr.bpp(),
|
|
||||||
width: png.ihdr.width,
|
|
||||||
height: png.ihdr.height,
|
|
||||||
left: png.data.len(),
|
|
||||||
pass: if png.ihdr.interlaced {
|
|
||||||
Some((1, 0))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
has_filter,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Iterator for ScanLineRanges {
|
|
||||||
type Item = (usize, Option<u8>, usize);
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
if self.left == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let (pixels_per_line, current_pass) = if let Some(ref mut pass) = self.pass {
|
|
||||||
// Scanlines for interlaced PNG files
|
// Scanlines for interlaced PNG files
|
||||||
|
if self.pass.is_none() {
|
||||||
|
self.pass = Some((1, 0));
|
||||||
|
}
|
||||||
// Handle edge cases for images smaller than 5 pixels in either direction
|
// Handle edge cases for images smaller than 5 pixels in either direction
|
||||||
// No extra case needed for skipping pass 7 as this is already handled by the
|
if self.png.ihdr_data.width < 5 && self.pass.unwrap().0 == 2 {
|
||||||
// self.left == 0 check above
|
if let Some(pass) = self.pass.as_mut() {
|
||||||
if self.width < 5 && pass.0 == 2 {
|
pass.0 = 3;
|
||||||
pass.0 = 3;
|
pass.1 = 4;
|
||||||
pass.1 = 4;
|
}
|
||||||
}
|
}
|
||||||
// Intentionally keep these separate so that they can be applied one after another
|
// Intentionally keep these separate so that they can be applied one after another
|
||||||
if self.height < 5 && pass.0 == 3 {
|
if self.png.ihdr_data.height < 5 && self.pass.unwrap().0 == 3 {
|
||||||
pass.0 = 4;
|
if let Some(pass) = self.pass.as_mut() {
|
||||||
pass.1 = 0;
|
pass.0 = 4;
|
||||||
|
pass.1 = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if self.width < 3 && pass.0 == 4 {
|
let bits_per_pixel = u32::from(self.png.ihdr_data.bit_depth.as_u8())
|
||||||
pass.0 = 5;
|
* u32::from(self.png.channels_per_pixel());
|
||||||
pass.1 = 2;
|
let y_steps;
|
||||||
}
|
let pixels_factor;
|
||||||
if self.height < 3 && pass.0 == 5 {
|
match self.pass {
|
||||||
pass.0 = 6;
|
Some((1, _)) | Some((2, _)) => {
|
||||||
pass.1 = 0;
|
pixels_factor = 8;
|
||||||
}
|
y_steps = 8;
|
||||||
if self.width == 1 && pass.0 == 6 {
|
}
|
||||||
pass.0 = 7;
|
Some((3, _)) => {
|
||||||
pass.1 = 1;
|
pixels_factor = 4;
|
||||||
}
|
y_steps = 8;
|
||||||
let (pixels_factor, y_steps) = match pass.0 {
|
}
|
||||||
1 | 2 => (8, 8),
|
Some((4, _)) => {
|
||||||
3 => (4, 8),
|
pixels_factor = 4;
|
||||||
4 => (4, 4),
|
y_steps = 4;
|
||||||
5 => (2, 4),
|
}
|
||||||
6 => (2, 2),
|
Some((5, _)) => {
|
||||||
7 => (1, 2),
|
pixels_factor = 2;
|
||||||
|
y_steps = 4;
|
||||||
|
}
|
||||||
|
Some((6, _)) => {
|
||||||
|
pixels_factor = 2;
|
||||||
|
y_steps = 2;
|
||||||
|
}
|
||||||
|
Some((7, _)) => {
|
||||||
|
pixels_factor = 1;
|
||||||
|
y_steps = 2;
|
||||||
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
}
|
||||||
let mut pixels_per_line = self.width / pixels_factor;
|
let mut pixels_per_line = self.png.ihdr_data.width / pixels_factor as u32;
|
||||||
// Determine whether to add pixels if there is a final, incomplete 8x8 block
|
// Determine whether to add pixels if there is a final, incomplete 8x8 block
|
||||||
let gap = self.width % pixels_factor;
|
let gap = self.png.ihdr_data.width % pixels_factor;
|
||||||
match pass.0 {
|
if gap > 0 {
|
||||||
1 | 3 | 5 if gap > 0 => {
|
match self.pass.unwrap().0 {
|
||||||
pixels_per_line += 1;
|
1 | 3 | 5 => {
|
||||||
}
|
pixels_per_line += 1;
|
||||||
2 if gap >= 5 => {
|
}
|
||||||
pixels_per_line += 1;
|
2 if gap >= 5 => {
|
||||||
}
|
pixels_per_line += 1;
|
||||||
4 if gap >= 3 => {
|
}
|
||||||
pixels_per_line += 1;
|
4 if gap >= 3 => {
|
||||||
}
|
pixels_per_line += 1;
|
||||||
6 if gap >= 2 => {
|
}
|
||||||
pixels_per_line += 1;
|
6 if gap >= 2 => {
|
||||||
}
|
pixels_per_line += 1;
|
||||||
_ => (),
|
}
|
||||||
}
|
_ => (),
|
||||||
let current_pass = Some(pass.0);
|
|
||||||
if pass.1 + y_steps >= self.height {
|
|
||||||
pass.0 += 1;
|
|
||||||
pass.1 = match pass.0 {
|
|
||||||
3 => 4,
|
|
||||||
5 => 2,
|
|
||||||
7 => 1,
|
|
||||||
_ => 0,
|
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
pass.1 += y_steps;
|
|
||||||
}
|
}
|
||||||
(pixels_per_line, current_pass)
|
let current_pass = if let Some(pass) = self.pass {
|
||||||
|
Some(pass.0)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let bytes_per_line = ((pixels_per_line * bits_per_pixel + 7) / 8) as usize;
|
||||||
|
self.start = self.end;
|
||||||
|
self.end = self.start + bytes_per_line + 1;
|
||||||
|
if let Some(pass) = self.pass.as_mut() {
|
||||||
|
if pass.1 + y_steps >= self.png.ihdr_data.height {
|
||||||
|
pass.0 += 1;
|
||||||
|
pass.1 = match pass.0 {
|
||||||
|
3 => 4,
|
||||||
|
5 => 2,
|
||||||
|
7 => 1,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
pass.1 += y_steps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(ScanLine {
|
||||||
|
filter: self.png.raw_data[self.start],
|
||||||
|
data: &self.png.raw_data[(self.start + 1)..self.end],
|
||||||
|
pass: current_pass,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// Standard, non-interlaced PNG scanlines
|
// Standard, non-interlaced PNG scanlines
|
||||||
(self.width, None)
|
let bits_per_line = self.png.ihdr_data.width as usize
|
||||||
};
|
* self.png.ihdr_data.bit_depth.as_u8() as usize
|
||||||
let bits_per_line = pixels_per_line as usize * self.bits_per_pixel;
|
* self.png.channels_per_pixel() as usize;
|
||||||
let mut len = bits_per_line.div_ceil(8);
|
let bytes_per_line = (bits_per_line + 7) / 8 as usize;
|
||||||
if self.has_filter {
|
self.start = self.end;
|
||||||
len += 1;
|
self.end = self.start + bytes_per_line + 1;
|
||||||
|
Some(ScanLine {
|
||||||
|
filter: self.png.raw_data[self.start],
|
||||||
|
data: &self.png.raw_data[(self.start + 1)..self.end],
|
||||||
|
pass: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
self.left = self.left.checked_sub(len)?;
|
|
||||||
Some((len, current_pass, pixels_per_line as usize))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,6 +138,4 @@ pub struct ScanLine<'a> {
|
||||||
pub data: &'a [u8],
|
pub data: &'a [u8],
|
||||||
/// The current pass if the image is interlaced
|
/// The current pass if the image is interlaced
|
||||||
pub pass: Option<u8>,
|
pub pass: Option<u8>,
|
||||||
/// The number of pixels in the current scan line
|
|
||||||
pub num_pixels: usize,
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
81
src/rayon.rs
|
|
@ -1,81 +0,0 @@
|
||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
pub mod prelude {
|
|
||||||
pub use super::*;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ParallelIterator: Iterator + Sized {
|
|
||||||
fn with_max_len(self, _l: usize) -> Self {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn reduce_with<OP>(mut self, op: OP) -> Option<Self::Item>
|
|
||||||
where
|
|
||||||
OP: Fn(Self::Item, Self::Item) -> Self::Item + Sync,
|
|
||||||
{
|
|
||||||
self.next().map(|a| self.fold(a, op))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait IntoParallelIterator {
|
|
||||||
type Iter: Iterator<Item = Self::Item>;
|
|
||||||
type Item: Send;
|
|
||||||
fn into_par_iter(self) -> Self::Iter;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait IntoParallelRefIterator<'data> {
|
|
||||||
type Iter: Iterator<Item = Self::Item>;
|
|
||||||
type Item: Send + 'data;
|
|
||||||
fn par_iter(&'data self) -> Self::Iter;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait IntoParallelRefMutIterator<'data> {
|
|
||||||
type Iter: ParallelIterator<Item = Self::Item>;
|
|
||||||
type Item: Send + 'data;
|
|
||||||
fn par_iter_mut(&'data mut self) -> Self::Iter;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<I: IntoIterator> IntoParallelIterator for I
|
|
||||||
where
|
|
||||||
I::Item: Send,
|
|
||||||
{
|
|
||||||
type Iter = I::IntoIter;
|
|
||||||
type Item = I::Item;
|
|
||||||
|
|
||||||
fn into_par_iter(self) -> Self::Iter {
|
|
||||||
self.into_iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'data, I: 'data + ?Sized> IntoParallelRefIterator<'data> for I
|
|
||||||
where
|
|
||||||
&'data I: IntoParallelIterator,
|
|
||||||
{
|
|
||||||
type Iter = <&'data I as IntoParallelIterator>::Iter;
|
|
||||||
type Item = <&'data I as IntoParallelIterator>::Item;
|
|
||||||
|
|
||||||
fn par_iter(&'data self) -> Self::Iter {
|
|
||||||
self.into_par_iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'data, I: 'data + ?Sized> IntoParallelRefMutIterator<'data> for I
|
|
||||||
where
|
|
||||||
&'data mut I: IntoParallelIterator,
|
|
||||||
{
|
|
||||||
type Iter = <&'data mut I as IntoParallelIterator>::Iter;
|
|
||||||
type Item = <&'data mut I as IntoParallelIterator>::Item;
|
|
||||||
|
|
||||||
fn par_iter_mut(&'data mut self) -> Self::Iter {
|
|
||||||
self.into_par_iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<I: Iterator> ParallelIterator for I {}
|
|
||||||
|
|
||||||
pub fn join<A, B>(a: impl FnOnce() -> A, b: impl FnOnce() -> B) -> (A, B) {
|
|
||||||
(a(), b())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn spawn<A>(a: impl FnOnce() -> A) -> A {
|
|
||||||
a()
|
|
||||||
}
|
|
||||||
|
|
@ -1,108 +1,37 @@
|
||||||
use rgb::RGB16;
|
use png::PngData;
|
||||||
|
|
||||||
use crate::{
|
pub fn reduce_alpha_channel(png: &mut PngData, channels: u8) -> Option<Vec<u8>> {
|
||||||
colors::{BitDepth, ColorType},
|
let byte_depth = png.ihdr_data.bit_depth.as_u8() >> 3;
|
||||||
headers::IhdrData,
|
let bpp = channels * byte_depth;
|
||||||
png::PngImage,
|
let bpp_mask = bpp - 1;
|
||||||
};
|
assert_eq!(0, bpp & bpp_mask);
|
||||||
|
|
||||||
/// Clean the alpha channel by setting the color of all fully transparent pixels to black
|
|
||||||
#[must_use]
|
|
||||||
pub fn cleaned_alpha_channel(png: &PngImage) -> Option<PngImage> {
|
|
||||||
if !png.ihdr.color_type.has_alpha() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let byte_depth = png.bytes_per_channel();
|
|
||||||
let bpp = png.channels_per_pixel() * byte_depth;
|
|
||||||
let colored_bytes = bpp - byte_depth;
|
let colored_bytes = bpp - byte_depth;
|
||||||
|
for line in png.scan_lines() {
|
||||||
let mut reduced = Vec::with_capacity(png.data.len());
|
for (i, &byte) in line.data.iter().enumerate() {
|
||||||
for pixel in png.data.chunks_exact(bpp) {
|
if i as u8 & bpp_mask >= colored_bytes && byte != 255 {
|
||||||
if pixel.iter().skip(colored_bytes).all(|b| *b == 0) {
|
return None;
|
||||||
reduced.resize(reduced.len() + bpp, 0);
|
|
||||||
} else {
|
|
||||||
reduced.extend_from_slice(pixel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(PngImage {
|
|
||||||
data: reduced,
|
|
||||||
ihdr: png.ihdr.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option<PngImage> {
|
|
||||||
if !png.ihdr.color_type.has_alpha() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let byte_depth = png.bytes_per_channel();
|
|
||||||
let bpp = png.channels_per_pixel() * byte_depth;
|
|
||||||
let colored_bytes = bpp - byte_depth;
|
|
||||||
|
|
||||||
// If alpha optimisation is enabled, see if the image contains only fully opaque and fully transparent pixels.
|
|
||||||
// In case this occurs, we want to try and find an unused color we can use for the tRNS chunk.
|
|
||||||
// Rather than an exhaustive search, we will just keep track of 256 shades of gray, which should cover many cases.
|
|
||||||
let mut has_transparency = false;
|
|
||||||
let mut used_colors = vec![false; 256];
|
|
||||||
|
|
||||||
for pixel in png.data.chunks_exact(bpp) {
|
|
||||||
if optimize_alpha && pixel.iter().skip(colored_bytes).all(|b| *b == 0) {
|
|
||||||
// Fully transparent, we may be able to reduce with tRNS
|
|
||||||
has_transparency = true;
|
|
||||||
} else if pixel.iter().skip(colored_bytes).any(|b| *b != 255) {
|
|
||||||
// Partially transparent, the image is not reducible
|
|
||||||
return None;
|
|
||||||
} else if optimize_alpha && pixel.iter().take(colored_bytes).all(|b| *b == pixel[0]) {
|
|
||||||
// Opaque shade of gray, we can't use this color for tRNS
|
|
||||||
used_colors[pixel[0] as usize] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let transparency_pixel = if has_transparency {
|
|
||||||
// For grayscale, start by checking 4 specific values in the hope that we may reduce depth
|
|
||||||
let unused = match png.ihdr.color_type {
|
|
||||||
ColorType::GrayscaleAlpha => [0x00, 0xFF, 0x55, 0xAA]
|
|
||||||
.into_iter()
|
|
||||||
.find(|&v| !used_colors[v as usize]),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
.or_else(|| used_colors.iter().position(|&u| !u).map(|v| v as u8));
|
|
||||||
// If no unused color was found we will have to fail here
|
|
||||||
Some(unused?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut raw_data = Vec::with_capacity(png.data.len());
|
|
||||||
for pixel in png.data.chunks_exact(bpp) {
|
|
||||||
match transparency_pixel {
|
|
||||||
Some(trns) if pixel.iter().skip(colored_bytes).all(|b| *b == 0) => {
|
|
||||||
raw_data.resize(raw_data.len() + colored_bytes, trns);
|
|
||||||
}
|
}
|
||||||
_ => raw_data.extend_from_slice(&pixel[0..colored_bytes]),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct the color type with appropriate transparency data
|
let mut reduced = Vec::with_capacity(png.raw_data.len());
|
||||||
let transparent = transparency_pixel.map(|trns| match png.ihdr.bit_depth {
|
for line in png.scan_lines() {
|
||||||
BitDepth::Sixteen => (u16::from(trns) << 8) | u16::from(trns),
|
reduced.push(line.filter);
|
||||||
_ => u16::from(trns),
|
for (i, &byte) in line.data.iter().enumerate() {
|
||||||
});
|
if i as u8 & bpp_mask >= colored_bytes {
|
||||||
let target_color_type = match png.ihdr.color_type {
|
continue;
|
||||||
ColorType::GrayscaleAlpha => ColorType::Grayscale {
|
} else {
|
||||||
transparent_shade: transparent,
|
reduced.push(byte);
|
||||||
},
|
}
|
||||||
_ => ColorType::RGB {
|
}
|
||||||
transparent_color: transparent.map(|t| RGB16::new(t, t, t)),
|
}
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(PngImage {
|
// sBIT contains information about alpha channel's original depth,
|
||||||
data: raw_data,
|
// and alpha has just been removed
|
||||||
ihdr: IhdrData {
|
if let Some(sbit_header) = png.aux_headers.get_mut(b"sBIT") {
|
||||||
color_type: target_color_type,
|
assert_eq!(sbit_header.len(), channels as usize);
|
||||||
..png.ihdr
|
sbit_header.pop();
|
||||||
},
|
}
|
||||||
})
|
|
||||||
|
Some(reduced)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,230 +1,84 @@
|
||||||
use crate::{
|
use bit_vec::BitVec;
|
||||||
colors::{BitDepth, ColorType},
|
use colors::{BitDepth, ColorType};
|
||||||
headers::IhdrData,
|
use png::PngData;
|
||||||
png::PngImage,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Attempt to reduce a 16-bit image to 8-bit, returning the reduced image if successful
|
const ONE_BIT_PERMUTATIONS: [u8; 2] = [0b0000_0000, 0b1111_1111];
|
||||||
#[must_use]
|
const TWO_BIT_PERMUTATIONS: [u8; 5] = [
|
||||||
pub fn reduced_bit_depth_16_to_8(png: &PngImage, force_scale: bool) -> Option<PngImage> {
|
0b0000_0000,
|
||||||
if png.ihdr.bit_depth != BitDepth::Sixteen {
|
0b0000_1111,
|
||||||
return None;
|
0b0011_1100,
|
||||||
}
|
0b1111_0000,
|
||||||
|
0b1111_1111,
|
||||||
|
];
|
||||||
|
const FOUR_BIT_PERMUTATIONS: [u8; 11] = [
|
||||||
|
0b0000_0000,
|
||||||
|
0b0000_0011,
|
||||||
|
0b0000_1100,
|
||||||
|
0b0011_0000,
|
||||||
|
0b1100_0000,
|
||||||
|
0b0000_1111,
|
||||||
|
0b0011_1100,
|
||||||
|
0b1111_0000,
|
||||||
|
0b0011_1111,
|
||||||
|
0b1111_1100,
|
||||||
|
0b1111_1111,
|
||||||
|
];
|
||||||
|
|
||||||
if force_scale {
|
pub fn reduce_bit_depth_8_or_less(png: &mut PngData) -> bool {
|
||||||
return scaled_bit_depth_16_to_8(png);
|
let mut reduced = BitVec::with_capacity(png.raw_data.len() * 8);
|
||||||
}
|
let bit_depth: usize = png.ihdr_data.bit_depth.as_u8() as usize;
|
||||||
|
let mut allowed_bits = 1;
|
||||||
// Reduce from 16 to 8 bits per channel per pixel
|
for line in png.scan_lines() {
|
||||||
if png.data.chunks_exact(2).any(|pair| pair[0] != pair[1]) {
|
let bit_vec = BitVec::from_bytes(&line.data);
|
||||||
// Can't reduce
|
if png.ihdr_data.color_type == ColorType::Indexed {
|
||||||
return None;
|
for (i, bit) in bit_vec.iter().enumerate() {
|
||||||
}
|
let bit_index = bit_depth - (i % bit_depth);
|
||||||
|
if bit && bit_index > allowed_bits {
|
||||||
Some(PngImage {
|
allowed_bits = bit_index.next_power_of_two();
|
||||||
data: png.data.chunks_exact(2).map(|pair| pair[0]).collect(),
|
if allowed_bits == bit_depth {
|
||||||
ihdr: IhdrData {
|
// Not reducable
|
||||||
color_type: png.ihdr.color_type.clone(),
|
return false;
|
||||||
bit_depth: BitDepth::Eight,
|
|
||||||
..png.ihdr
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Forcibly reduce a 16-bit image to 8-bit by scaling, returning the reduced image if successful
|
|
||||||
#[must_use]
|
|
||||||
pub fn scaled_bit_depth_16_to_8(png: &PngImage) -> Option<PngImage> {
|
|
||||||
if png.ihdr.bit_depth != BitDepth::Sixteen {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reduce from 16 to 8 bits per channel per pixel by scaling when necessary
|
|
||||||
let data = png
|
|
||||||
.data
|
|
||||||
.chunks_exact(2)
|
|
||||||
.map(|pair| {
|
|
||||||
if pair[0] == pair[1] {
|
|
||||||
return pair[0];
|
|
||||||
}
|
|
||||||
// See: http://www.libpng.org/pub/png/spec/1.2/PNG-Decoders.html#D.Sample-depth-rescaling
|
|
||||||
// This allows values such as 0x00FF to be rounded to 0x01 rather than truncated to 0x00
|
|
||||||
let val = f32::from(u16::from_be_bytes([pair[0], pair[1]]));
|
|
||||||
(val * (255.0 / 65535.0)).round() as u8
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Some(PngImage {
|
|
||||||
data,
|
|
||||||
ihdr: IhdrData {
|
|
||||||
color_type: png.ihdr.color_type.clone(),
|
|
||||||
bit_depth: BitDepth::Eight,
|
|
||||||
..png.ihdr
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempt to reduce an 8-bit image to a lower bit depth, returning the reduced image if successful
|
|
||||||
#[must_use]
|
|
||||||
pub fn reduced_bit_depth_8_or_less(png: &PngImage) -> Option<PngImage> {
|
|
||||||
if png.ihdr.bit_depth != BitDepth::Eight || png.channels_per_pixel() != 1 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let minimum_bits = if let ColorType::Indexed { palette } = &png.ihdr.color_type {
|
|
||||||
// We can easily determine minimum depth by the palette size
|
|
||||||
match palette.len() {
|
|
||||||
0..=2 => 1,
|
|
||||||
3..=4 => 2,
|
|
||||||
5..=16 => 4,
|
|
||||||
_ => return None,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let mut minimum_bits = 1;
|
|
||||||
|
|
||||||
// Finding minimum depth for grayscale is much more complicated
|
|
||||||
let mut mask = 1;
|
|
||||||
let mut divisions = 1..8;
|
|
||||||
for &b in &png.data {
|
|
||||||
if b == 0 || b == 255 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
'try_depth: loop {
|
|
||||||
// Align the first pixel division with the mask
|
|
||||||
let mut byte = b.rotate_left(minimum_bits as u32);
|
|
||||||
// Each potential division of this pixel must be identical to successfully reduce
|
|
||||||
let compare = byte & mask;
|
|
||||||
for _ in divisions.clone() {
|
|
||||||
// Align the next division with the mask
|
|
||||||
byte = byte.rotate_left(minimum_bits as u32);
|
|
||||||
if byte & mask != compare {
|
|
||||||
// This depth is not possible, try the next one up
|
|
||||||
minimum_bits <<= 1;
|
|
||||||
if minimum_bits == 8 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
mask = (1 << minimum_bits) - 1;
|
|
||||||
divisions = 1..(8 / minimum_bits);
|
|
||||||
continue 'try_depth;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
for byte in bit_vec.to_bytes() {
|
||||||
minimum_bits
|
while allowed_bits < bit_depth {
|
||||||
};
|
let permutations: &[u8] = if allowed_bits == 1 {
|
||||||
|
&ONE_BIT_PERMUTATIONS
|
||||||
let mut reduced = Vec::with_capacity(png.data.len());
|
} else if allowed_bits == 2 {
|
||||||
let mask = (1 << minimum_bits) - 1;
|
&TWO_BIT_PERMUTATIONS
|
||||||
for line in png.scan_lines(false) {
|
} else if allowed_bits == 4 {
|
||||||
// Loop over the data in chunks that will produce 1 byte of output
|
&FOUR_BIT_PERMUTATIONS
|
||||||
for chunk in line.data.chunks(8 / minimum_bits) {
|
} else {
|
||||||
let mut new_byte = 0;
|
return false;
|
||||||
let mut shift = 8;
|
};
|
||||||
for byte in chunk {
|
if permutations.iter().any(|perm| *perm == byte) {
|
||||||
shift -= minimum_bits;
|
break;
|
||||||
// Take the low bits of the pixel and shift them into the output byte
|
} else {
|
||||||
new_byte |= (byte & mask) << shift;
|
allowed_bits <<= 1;
|
||||||
}
|
|
||||||
reduced.push(new_byte);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the image is grayscale we also need to reduce the transparency pixel
|
|
||||||
let color_type = if let ColorType::Grayscale {
|
|
||||||
transparent_shade: Some(trans),
|
|
||||||
} = png.ihdr.color_type
|
|
||||||
{
|
|
||||||
let reduced_trans = (trans & 0xFF) >> (8 - minimum_bits);
|
|
||||||
// Verify the reduction is valid by restoring back to original bit depth
|
|
||||||
let mut check = reduced_trans;
|
|
||||||
let mut bits = minimum_bits;
|
|
||||||
while bits < 8 {
|
|
||||||
check = (check << bits) | check;
|
|
||||||
bits <<= 1;
|
|
||||||
}
|
|
||||||
// If the transparency doesn't fit the new bit depth it is therefore unused - set it to None
|
|
||||||
ColorType::Grayscale {
|
|
||||||
transparent_shade: if trans == check {
|
|
||||||
Some(reduced_trans)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
png.ihdr.color_type.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(PngImage {
|
|
||||||
data: reduced,
|
|
||||||
ihdr: IhdrData {
|
|
||||||
color_type,
|
|
||||||
bit_depth: (minimum_bits as u8).try_into().unwrap(),
|
|
||||||
..png.ihdr
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Expand a 1/2/4-bit image to 8-bit, returning the expanded image if successful
|
|
||||||
#[must_use]
|
|
||||||
pub fn expanded_bit_depth_to_8(png: &PngImage) -> Option<PngImage> {
|
|
||||||
let bit_depth = png.ihdr.bit_depth as u32;
|
|
||||||
if bit_depth >= 8 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
// Calculate the current number of pixels per byte
|
|
||||||
let ppb = 8 / bit_depth;
|
|
||||||
let is_gray = matches!(png.ihdr.color_type, ColorType::Grayscale { .. });
|
|
||||||
|
|
||||||
let mut reduced = Vec::with_capacity((png.ihdr.width * png.ihdr.height) as usize);
|
|
||||||
let mut length = 0;
|
|
||||||
let mask = (1 << bit_depth) - 1;
|
|
||||||
for line in png.scan_lines(false) {
|
|
||||||
for &(mut byte) in line.data {
|
|
||||||
// Loop over each pixel in the byte
|
|
||||||
for _ in 0..ppb {
|
|
||||||
// Align the current pixel with the mask
|
|
||||||
byte = byte.rotate_left(bit_depth);
|
|
||||||
let mut val = byte & mask;
|
|
||||||
if is_gray {
|
|
||||||
// Expand gray by repeating the bits
|
|
||||||
let mut bits = bit_depth;
|
|
||||||
while bits < 8 {
|
|
||||||
val = (val << bits) | val;
|
|
||||||
bits <<= 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reduced.push(val);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Trim any overflow
|
|
||||||
length += line.num_pixels;
|
|
||||||
reduced.truncate(length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the image is grayscale we also need to expand the transparency pixel
|
for line in png.scan_lines() {
|
||||||
let color_type = if let ColorType::Grayscale {
|
reduced.extend(BitVec::from_bytes(&[line.filter]));
|
||||||
transparent_shade: Some(mut trans),
|
let bit_vec = BitVec::from_bytes(&line.data);
|
||||||
} = png.ihdr.color_type
|
for (i, bit) in bit_vec.iter().enumerate() {
|
||||||
{
|
let bit_index = bit_depth - (i % bit_depth);
|
||||||
let mut bits = bit_depth;
|
if bit_index <= allowed_bits {
|
||||||
while bits < 8 {
|
reduced.push(bit);
|
||||||
trans = (trans << bits) | trans;
|
}
|
||||||
bits <<= 1;
|
|
||||||
}
|
}
|
||||||
ColorType::Grayscale {
|
// Pad end of line to get 8 bits per byte
|
||||||
transparent_shade: Some(trans),
|
while reduced.len() % 8 != 0 {
|
||||||
|
reduced.push(false);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
png.ihdr.color_type.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(PngImage {
|
png.raw_data = reduced.to_bytes();
|
||||||
data: reduced,
|
png.ihdr_data.bit_depth = BitDepth::from_u8(allowed_bits as u8);
|
||||||
ihdr: IhdrData {
|
true
|
||||||
color_type,
|
|
||||||
bit_depth: BitDepth::Eight,
|
|
||||||
..png.ihdr
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,206 +1,306 @@
|
||||||
use std::hash::{BuildHasherDefault, Hash};
|
use colors::{BitDepth, ColorType};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use png::PngData;
|
||||||
|
|
||||||
use indexmap::IndexSet;
|
use super::alpha::reduce_alpha_channel;
|
||||||
use rgb::{ComponentMap, FromSlice, RGB, RGBA, alt::Gray};
|
|
||||||
use rustc_hash::FxHasher;
|
|
||||||
|
|
||||||
use crate::{
|
pub fn reduce_rgba_to_rgb(png: &mut PngData) -> bool {
|
||||||
colors::{BitDepth, ColorType},
|
if let Some(reduced) = reduce_alpha_channel(png, 4) {
|
||||||
headers::IhdrData,
|
png.raw_data = reduced;
|
||||||
png::PngImage,
|
png.ihdr_data.color_type = ColorType::RGB;
|
||||||
};
|
true
|
||||||
|
} else {
|
||||||
type FxIndexSet<V> = IndexSet<V, BuildHasherDefault<FxHasher>>;
|
false
|
||||||
|
}
|
||||||
/// Maximum size difference between indexed and channels to consider a candidate for evaluation
|
}
|
||||||
pub const INDEXED_MAX_DIFF: usize = 20000;
|
|
||||||
|
pub fn reduce_rgba_to_grayscale_alpha(png: &mut PngData) -> bool {
|
||||||
fn build_palette<T>(
|
let mut reduced = Vec::with_capacity(png.raw_data.len());
|
||||||
iter: impl IntoIterator<Item = T>,
|
let byte_depth = png.ihdr_data.bit_depth.as_u8() >> 3;
|
||||||
reduced: &mut Vec<u8>,
|
let bpp = 4 * byte_depth;
|
||||||
) -> Option<FxIndexSet<T>>
|
let bpp_mask = bpp - 1;
|
||||||
where
|
assert_eq!(0, bpp & bpp_mask);
|
||||||
T: Eq + Hash,
|
let colored_bytes = bpp - byte_depth;
|
||||||
{
|
for line in png.scan_lines() {
|
||||||
let mut palette = FxIndexSet::default();
|
reduced.push(line.filter);
|
||||||
palette.reserve(257);
|
let mut low_bytes = Vec::with_capacity(4);
|
||||||
for pixel in iter {
|
let mut high_bytes = Vec::with_capacity(4);
|
||||||
let (idx, _) = palette.insert_full(pixel);
|
let mut trans_bytes = Vec::with_capacity(byte_depth as usize);
|
||||||
if idx == 256 {
|
for (i, byte) in line.data.iter().enumerate() {
|
||||||
return None;
|
if i as u8 & bpp_mask < colored_bytes {
|
||||||
}
|
if byte_depth == 1 || i % 2 == 1 {
|
||||||
reduced.push(idx as u8);
|
low_bytes.push(*byte);
|
||||||
}
|
} else {
|
||||||
Some(palette)
|
high_bytes.push(*byte);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
#[must_use]
|
trans_bytes.push(*byte);
|
||||||
pub fn reduced_to_indexed(png: &PngImage, allow_grayscale: bool) -> Option<PngImage> {
|
}
|
||||||
if png.ihdr.bit_depth != BitDepth::Eight {
|
|
||||||
return None;
|
if (i as u8 & bpp_mask) == bpp - 1 {
|
||||||
}
|
if low_bytes.iter().unique().count() > 1 {
|
||||||
if matches!(png.ihdr.color_type, ColorType::Indexed { .. }) {
|
return false;
|
||||||
return None;
|
}
|
||||||
}
|
if byte_depth == 2 {
|
||||||
if !allow_grayscale && png.ihdr.color_type.is_gray() {
|
if high_bytes.iter().unique().count() > 1 {
|
||||||
return None;
|
return false;
|
||||||
}
|
}
|
||||||
|
reduced.push(high_bytes[0]);
|
||||||
let mut raw_data = Vec::with_capacity(png.data.len() / png.channels_per_pixel());
|
high_bytes.clear();
|
||||||
let palette: Vec<_> = match png.ihdr.color_type {
|
}
|
||||||
ColorType::Grayscale { transparent_shade } => {
|
reduced.push(low_bytes[0]);
|
||||||
let pmap = build_palette(png.data.as_gray().iter().copied(), &mut raw_data)?;
|
low_bytes.clear();
|
||||||
// Convert the Gray16 transparency to Gray8
|
reduced.extend_from_slice(&trans_bytes);
|
||||||
let transparency_pixel = transparent_shade.map(|t| Gray::from(t as u8));
|
trans_bytes.clear();
|
||||||
pmap.into_iter()
|
}
|
||||||
.map(|px| {
|
}
|
||||||
RGB::from(px).with_alpha(if Some(px) == transparency_pixel {
|
}
|
||||||
0
|
|
||||||
} else {
|
if let Some(sbit_header) = png.aux_headers.get_mut(b"sBIT") {
|
||||||
255
|
assert_eq!(sbit_header.len(), 4);
|
||||||
})
|
sbit_header.remove(1);
|
||||||
})
|
sbit_header.remove(1);
|
||||||
.collect()
|
}
|
||||||
}
|
if let Some(bkgd_header) = png.aux_headers.get_mut(b"bKGD") {
|
||||||
ColorType::RGB { transparent_color } => {
|
assert_eq!(bkgd_header.len(), 6);
|
||||||
let pmap = build_palette(png.data.as_rgb().iter().copied(), &mut raw_data)?;
|
bkgd_header.truncate(2);
|
||||||
// Convert the RGB16 transparency to RGB8
|
}
|
||||||
let transparency_pixel = transparent_color.map(|t| t.map(|c| c as u8));
|
|
||||||
pmap.into_iter()
|
png.raw_data = reduced;
|
||||||
.map(|px| {
|
png.ihdr_data.color_type = ColorType::GrayscaleAlpha;
|
||||||
px.with_alpha(if Some(px) == transparency_pixel {
|
true
|
||||||
0
|
}
|
||||||
} else {
|
|
||||||
255
|
pub fn reduce_rgba_to_palette(png: &mut PngData) -> bool {
|
||||||
})
|
if png.ihdr_data.bit_depth != BitDepth::Eight {
|
||||||
})
|
return false;
|
||||||
.collect()
|
}
|
||||||
}
|
let mut reduced = Vec::with_capacity(png.raw_data.len());
|
||||||
ColorType::GrayscaleAlpha => {
|
let mut palette = Vec::with_capacity(256);
|
||||||
let pmap = build_palette(png.data.as_gray_alpha().iter().copied(), &mut raw_data)?;
|
let bpp: usize = (4 * png.ihdr_data.bit_depth.as_u8() as usize) >> 3;
|
||||||
pmap.into_iter().map(RGBA::from).collect()
|
for line in png.scan_lines() {
|
||||||
}
|
reduced.push(line.filter);
|
||||||
ColorType::RGBA => {
|
let mut cur_pixel = Vec::with_capacity(bpp);
|
||||||
let pmap = build_palette(png.data.as_rgba().iter().copied(), &mut raw_data)?;
|
for (i, byte) in line.data.iter().enumerate() {
|
||||||
pmap.into_iter().collect()
|
cur_pixel.push(*byte);
|
||||||
}
|
if i % bpp == bpp - 1 {
|
||||||
_ => return None,
|
if let Some(idx) = palette.iter().position(|x| x == &cur_pixel) {
|
||||||
};
|
reduced.push(idx as u8);
|
||||||
|
} else {
|
||||||
Some(PngImage {
|
let len = palette.len();
|
||||||
data: raw_data,
|
if len == 256 {
|
||||||
ihdr: IhdrData {
|
return false;
|
||||||
color_type: ColorType::Indexed { palette },
|
}
|
||||||
..png.ihdr
|
palette.push(cur_pixel);
|
||||||
},
|
reduced.push(len as u8);
|
||||||
})
|
}
|
||||||
}
|
cur_pixel = Vec::with_capacity(bpp);
|
||||||
|
}
|
||||||
#[must_use]
|
}
|
||||||
pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
|
}
|
||||||
if !png.ihdr.color_type.is_rgb() {
|
|
||||||
return None;
|
let mut color_palette = Vec::with_capacity(
|
||||||
}
|
palette.len() * 3 + if png.aux_headers.contains_key(b"bKGD") {
|
||||||
|
6
|
||||||
let mut reduced = Vec::with_capacity(png.data.len());
|
} else {
|
||||||
let byte_depth = png.bytes_per_channel();
|
0
|
||||||
let bpp = png.channels_per_pixel() * byte_depth;
|
},
|
||||||
let last_color = 2 * byte_depth;
|
);
|
||||||
for pixel in png.data.chunks_exact(bpp) {
|
let mut trans_palette = Vec::with_capacity(palette.len());
|
||||||
if byte_depth == 1 {
|
for color in &palette {
|
||||||
if pixel[0] != pixel[1] || pixel[1] != pixel[2] {
|
for (i, byte) in color.iter().enumerate() {
|
||||||
return None;
|
if i < 3 {
|
||||||
}
|
color_palette.push(*byte);
|
||||||
} else if pixel[0..2] != pixel[2..4] || pixel[2..4] != pixel[4..6] {
|
} else {
|
||||||
return None;
|
trans_palette.push(*byte);
|
||||||
}
|
}
|
||||||
reduced.extend_from_slice(&pixel[last_color..]);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let color_type = match png.ihdr.color_type {
|
let headers_size = color_palette.len() + trans_palette.len() + 8;
|
||||||
ColorType::RGB { transparent_color } => ColorType::Grayscale {
|
if reduced.len() + headers_size > png.raw_data.len() * 4 {
|
||||||
// Copy the transparent component if it is also gray
|
// Reduction would result in a larger image
|
||||||
transparent_shade: transparent_color
|
return false;
|
||||||
.filter(|t| t.r == t.g && t.g == t.b)
|
}
|
||||||
.map(|t| t.r),
|
|
||||||
},
|
if let Some(bkgd_header) = png.aux_headers.get_mut(b"bKGD") {
|
||||||
_ => ColorType::GrayscaleAlpha,
|
assert_eq!(bkgd_header.len(), 6);
|
||||||
};
|
let header_pixels = bkgd_header
|
||||||
|
.iter()
|
||||||
Some(PngImage {
|
.skip(1)
|
||||||
data: reduced,
|
.step(2)
|
||||||
ihdr: IhdrData {
|
.cloned()
|
||||||
color_type,
|
.collect::<Vec<u8>>();
|
||||||
..png.ihdr
|
if let Some(entry) = color_palette
|
||||||
},
|
.chunks(3)
|
||||||
})
|
.position(|x| x == header_pixels.as_slice())
|
||||||
}
|
{
|
||||||
|
*bkgd_header = vec![entry as u8];
|
||||||
/// Attempt to convert indexed to a different color type, returning the resulting image if successful
|
} else if color_palette.len() / 3 == 256 {
|
||||||
#[must_use]
|
return false;
|
||||||
pub fn indexed_to_channels(
|
} else {
|
||||||
png: &PngImage,
|
let entry = color_palette.len() / 3;
|
||||||
allow_grayscale: bool,
|
color_palette.extend_from_slice(&header_pixels);
|
||||||
optimize_alpha: bool,
|
*bkgd_header = vec![entry as u8];
|
||||||
) -> Option<PngImage> {
|
}
|
||||||
if png.ihdr.bit_depth != BitDepth::Eight {
|
}
|
||||||
return None;
|
if let Some(sbit_header) = png.aux_headers.get_mut(b"sBIT") {
|
||||||
}
|
assert_eq!(sbit_header.len(), 4);
|
||||||
let mut palette = match &png.ihdr.color_type {
|
sbit_header.pop();
|
||||||
ColorType::Indexed { palette } => palette.clone(),
|
}
|
||||||
_ => return None,
|
|
||||||
};
|
png.raw_data = reduced;
|
||||||
|
png.palette = Some(color_palette);
|
||||||
// Ensure fully transparent colors are black, which can help with grayscale conversion
|
if trans_palette.iter().any(|x| *x != 255) {
|
||||||
if optimize_alpha {
|
while let Some(255) = trans_palette.last().cloned() {
|
||||||
for color in &mut palette {
|
trans_palette.pop();
|
||||||
if color.a == 0 {
|
}
|
||||||
color.r = 0;
|
png.transparency_palette = Some(trans_palette);
|
||||||
color.g = 0;
|
} else {
|
||||||
color.b = 0;
|
png.transparency_palette = None;
|
||||||
}
|
}
|
||||||
}
|
png.ihdr_data.color_type = ColorType::Indexed;
|
||||||
}
|
true
|
||||||
|
}
|
||||||
// Determine which channels are required
|
|
||||||
let is_gray = if allow_grayscale {
|
pub fn reduce_rgb_to_palette(png: &mut PngData) -> bool {
|
||||||
palette.iter().all(|c| c.r == c.g && c.g == c.b)
|
if png.ihdr_data.bit_depth != BitDepth::Eight {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut reduced = Vec::with_capacity(png.raw_data.len());
|
||||||
|
let mut palette = Vec::with_capacity(256);
|
||||||
|
if let Some(ref trns) = png.transparency_pixel {
|
||||||
|
assert_eq!(trns.len(), 6);
|
||||||
|
if trns[0] != trns[1] || trns[2] != trns[3] || trns[4] != trns[5] {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
palette.push(vec![trns[0], trns[2], trns[4]]);
|
||||||
|
}
|
||||||
|
let bpp: usize = (3 * png.ihdr_data.bit_depth.as_u8() as usize) >> 3;
|
||||||
|
for line in png.scan_lines() {
|
||||||
|
reduced.push(line.filter);
|
||||||
|
let mut cur_pixel = Vec::with_capacity(bpp);
|
||||||
|
for (i, byte) in line.data.iter().enumerate() {
|
||||||
|
cur_pixel.push(*byte);
|
||||||
|
if i % bpp == bpp - 1 {
|
||||||
|
if let Some(idx) = palette.iter().position(|x| x == &cur_pixel) {
|
||||||
|
reduced.push(idx as u8);
|
||||||
|
} else {
|
||||||
|
let len = palette.len();
|
||||||
|
if len == 256 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
palette.push(cur_pixel);
|
||||||
|
reduced.push(len as u8);
|
||||||
|
}
|
||||||
|
cur_pixel = Vec::with_capacity(bpp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut color_palette = Vec::with_capacity(palette.len() * 3);
|
||||||
|
for color in &palette {
|
||||||
|
color_palette.extend_from_slice(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers_size = color_palette.len() + 4;
|
||||||
|
if reduced.len() + headers_size > png.raw_data.len() * 3 {
|
||||||
|
// Reduction would result in a larger image
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(bkgd_header) = png.aux_headers.get_mut(b"bKGD") {
|
||||||
|
assert_eq!(bkgd_header.len(), 6);
|
||||||
|
let header_pixels = bkgd_header
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.step(2)
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<u8>>();
|
||||||
|
if let Some(entry) = color_palette
|
||||||
|
.chunks(3)
|
||||||
|
.position(|x| x == header_pixels.as_slice())
|
||||||
|
{
|
||||||
|
*bkgd_header = vec![entry as u8];
|
||||||
|
} else if color_palette.len() == 255 {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
let entry = color_palette.len() / 3;
|
||||||
|
color_palette.extend_from_slice(&header_pixels);
|
||||||
|
*bkgd_header = vec![entry as u8];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
png.raw_data = reduced;
|
||||||
|
png.palette = Some(color_palette);
|
||||||
|
png.ihdr_data.color_type = ColorType::Indexed;
|
||||||
|
if png.transparency_pixel.is_some() {
|
||||||
|
png.transparency_pixel = None;
|
||||||
|
png.transparency_palette = Some(vec![0]);
|
||||||
|
};
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reduce_rgb_to_grayscale(png: &mut PngData) -> bool {
|
||||||
|
let mut reduced = Vec::with_capacity(png.raw_data.len());
|
||||||
|
let byte_depth: u8 = png.ihdr_data.bit_depth.as_u8() >> 3;
|
||||||
|
let bpp: usize = 3 * byte_depth as usize;
|
||||||
|
let mut cur_pixel = Vec::with_capacity(bpp);
|
||||||
|
for line in png.scan_lines() {
|
||||||
|
reduced.push(line.filter);
|
||||||
|
for (i, byte) in line.data.iter().enumerate() {
|
||||||
|
cur_pixel.push(*byte);
|
||||||
|
if i % bpp == bpp - 1 {
|
||||||
|
if bpp == 3 {
|
||||||
|
if cur_pixel.iter().unique().count() > 1 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
reduced.push(cur_pixel[0]);
|
||||||
|
} else {
|
||||||
|
let pixel_bytes = cur_pixel
|
||||||
|
.iter()
|
||||||
|
.step(2)
|
||||||
|
.cloned()
|
||||||
|
.zip(cur_pixel.iter().skip(1).step(2).cloned())
|
||||||
|
.unique()
|
||||||
|
.collect::<Vec<(u8, u8)>>();
|
||||||
|
if pixel_bytes.len() > 1 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
reduced.push(pixel_bytes[0].0);
|
||||||
|
reduced.push(pixel_bytes[0].1);
|
||||||
|
}
|
||||||
|
cur_pixel.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(ref mut trns) = png.transparency_pixel {
|
||||||
|
assert_eq!(trns.len(), 6);
|
||||||
|
if trns[0..2] != trns[2..4] || trns[2..4] != trns[4..6] {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*trns = trns[0..2].to_owned();
|
||||||
|
}
|
||||||
|
if let Some(sbit_header) = png.aux_headers.get_mut(b"sBIT") {
|
||||||
|
assert_eq!(sbit_header.len(), 3);
|
||||||
|
sbit_header.truncate(1);
|
||||||
|
}
|
||||||
|
if let Some(bkgd_header) = png.aux_headers.get_mut(b"bKGD") {
|
||||||
|
assert_eq!(bkgd_header.len(), 6);
|
||||||
|
bkgd_header.truncate(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
png.raw_data = reduced;
|
||||||
|
png.ihdr_data.color_type = ColorType::Grayscale;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reduce_grayscale_alpha_to_grayscale(png: &mut PngData) -> bool {
|
||||||
|
if let Some(reduced) = reduce_alpha_channel(png, 2) {
|
||||||
|
png.raw_data = reduced;
|
||||||
|
png.ihdr_data.color_type = ColorType::Grayscale;
|
||||||
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
|
||||||
let has_alpha = palette.iter().any(|c| c.a != 255);
|
|
||||||
let color_type = match (is_gray, has_alpha) {
|
|
||||||
(false, true) => ColorType::RGBA,
|
|
||||||
(false, false) => ColorType::RGB {
|
|
||||||
transparent_color: None,
|
|
||||||
},
|
|
||||||
(true, true) => ColorType::GrayscaleAlpha,
|
|
||||||
(true, false) => ColorType::Grayscale {
|
|
||||||
transparent_shade: None,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Don't proceed if output would be too much larger
|
|
||||||
let out_size = color_type.channels_per_pixel() as usize * png.data.len();
|
|
||||||
if out_size - png.data.len() > INDEXED_MAX_DIFF {
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct the new data
|
|
||||||
let black = RGBA::new(0, 0, 0, 255);
|
|
||||||
let ch_start = if is_gray { 2 } else { 0 };
|
|
||||||
let ch_end = if has_alpha { 3 } else { 2 };
|
|
||||||
let mut data = Vec::with_capacity(out_size);
|
|
||||||
for b in &png.data {
|
|
||||||
let color = palette.get(*b as usize).unwrap_or(&black);
|
|
||||||
data.extend_from_slice(&color.as_ref()[ch_start..=ch_end]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(PngImage {
|
|
||||||
ihdr: IhdrData {
|
|
||||||
color_type,
|
|
||||||
..png.ihdr
|
|
||||||
},
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,203 +1,3 @@
|
||||||
use std::sync::Arc;
|
mod alpha;
|
||||||
|
|
||||||
use crate::{ColorType, Deadline, Deflater, Options, evaluate::Evaluator, png::PngImage};
|
|
||||||
|
|
||||||
pub mod alpha;
|
|
||||||
use crate::alpha::*;
|
|
||||||
pub mod bit_depth;
|
pub mod bit_depth;
|
||||||
use crate::bit_depth::*;
|
|
||||||
pub mod color;
|
pub mod color;
|
||||||
use crate::color::*;
|
|
||||||
pub mod palette;
|
|
||||||
use crate::palette::*;
|
|
||||||
|
|
||||||
pub(crate) fn perform_reductions(
|
|
||||||
mut png: Arc<PngImage>,
|
|
||||||
opts: &Options,
|
|
||||||
deadline: &Deadline,
|
|
||||||
eval: &Evaluator,
|
|
||||||
) -> Arc<PngImage> {
|
|
||||||
let mut evaluation_added = false;
|
|
||||||
|
|
||||||
// At low compression levels, skip some transformations which are less likely to be effective
|
|
||||||
// This currently affects optimization presets 0-2
|
|
||||||
let cheap = match opts.deflater {
|
|
||||||
Deflater::Libdeflater { compression } => compression < 12 && opts.fast_evaluation,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Interlacing must be processed first in order to evaluate the rest correctly
|
|
||||||
if let Some(interlacing) = opts.interlace {
|
|
||||||
if let Some(reduced) = png.change_interlacing(interlacing) {
|
|
||||||
png = Arc::new(reduced);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If alpha optimization is enabled, clean the alpha channel before continuing
|
|
||||||
// This can allow some color type reductions which may not have been possible otherwise
|
|
||||||
if opts.optimize_alpha && !deadline.passed() {
|
|
||||||
if let Some(reduced) = cleaned_alpha_channel(&png) {
|
|
||||||
png = Arc::new(reduced);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to reduce 16-bit to 8-bit
|
|
||||||
// This is just removal of bytes and does not need to be evaluated
|
|
||||||
if opts.bit_depth_reduction && !deadline.passed() {
|
|
||||||
if let Some(reduced) = reduced_bit_depth_16_to_8(&png, opts.scale_16) {
|
|
||||||
png = Arc::new(reduced);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to reduce RGB to grayscale
|
|
||||||
// This is just removal of bytes and does not need to be evaluated
|
|
||||||
if opts.color_type_reduction && opts.grayscale_reduction && !deadline.passed() {
|
|
||||||
if let Some(reduced) = reduced_rgb_to_grayscale(&png) {
|
|
||||||
png = Arc::new(reduced);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to expand the bit depth to 8
|
|
||||||
// This does need to be evaluated but will be done so later when it gets reduced again
|
|
||||||
if opts.bit_depth_reduction && !deadline.passed() {
|
|
||||||
if let Some(reduced) = expanded_bit_depth_to_8(&png) {
|
|
||||||
png = Arc::new(reduced);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now retain the current png for the evaluator baseline
|
|
||||||
// It will only be entered into the evaluator if there are also others to evaluate
|
|
||||||
let mut baseline = png.clone();
|
|
||||||
|
|
||||||
// Attempt to reduce and sort the palette
|
|
||||||
if opts.palette_reduction && !deadline.passed() {
|
|
||||||
if let Some(reduced) = reduced_palette(&png, opts.optimize_alpha) {
|
|
||||||
png = Arc::new(reduced);
|
|
||||||
// If the palette was reduced but the data is unchanged then this should become the baseline
|
|
||||||
if png.data == baseline.data {
|
|
||||||
baseline = png.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(reduced) = sorted_palette(&png) {
|
|
||||||
png = Arc::new(reduced);
|
|
||||||
}
|
|
||||||
// If either action changed the data then enter this into the evaluator
|
|
||||||
if !Arc::ptr_eq(&png, &baseline) {
|
|
||||||
eval.try_image_with_description(png.clone(), "Indexed (luma sort)");
|
|
||||||
evaluation_added = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt alpha removal
|
|
||||||
if opts.color_type_reduction && !deadline.passed() {
|
|
||||||
if let Some(reduced) = reduced_alpha_channel(&png, opts.optimize_alpha) {
|
|
||||||
png = Arc::new(reduced);
|
|
||||||
// For small differences, if a tRNS chunk is required then enter this into the evaluator
|
|
||||||
// Otherwise it is mostly just removal of bytes and should become the baseline
|
|
||||||
if png.ihdr.color_type.has_trns() && baseline.data.len() - png.data.len() <= 1000 {
|
|
||||||
eval.try_image(png.clone());
|
|
||||||
evaluation_added = true;
|
|
||||||
} else {
|
|
||||||
baseline = png.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to convert from indexed to channels
|
|
||||||
// This may give a better result due to dropping the PLTE chunk
|
|
||||||
if !cheap && opts.color_type_reduction && !deadline.passed() {
|
|
||||||
if let Some(reduced) =
|
|
||||||
indexed_to_channels(&png, opts.grayscale_reduction, opts.optimize_alpha)
|
|
||||||
{
|
|
||||||
// This result should not be passed on to subsequent reductions
|
|
||||||
eval.try_image(Arc::new(reduced));
|
|
||||||
evaluation_added = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to reduce to indexed
|
|
||||||
// Keep the existing `png` var in case it is grayscale - we can test both for depth reduction later
|
|
||||||
let mut indexed = None;
|
|
||||||
if opts.color_type_reduction && opts.palette_reduction && !deadline.passed() {
|
|
||||||
if let Some(reduced) = reduced_to_indexed(&png, opts.grayscale_reduction) {
|
|
||||||
// Make sure the palette gets sorted (but don't bother evaluating both results)
|
|
||||||
let new = Arc::new(sorted_palette(&reduced).unwrap_or(reduced));
|
|
||||||
// For relatively small differences, enter this into the evaluator
|
|
||||||
// Otherwise we're confident enough for it to become the baseline
|
|
||||||
if png.data.len() - new.data.len() <= INDEXED_MAX_DIFF {
|
|
||||||
eval.try_image_with_description(new.clone(), "Indexed (luma sort)");
|
|
||||||
evaluation_added = true;
|
|
||||||
} else {
|
|
||||||
baseline = new.clone();
|
|
||||||
}
|
|
||||||
indexed = Some(new);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt additional palette sorting techniques
|
|
||||||
if !cheap && opts.palette_reduction {
|
|
||||||
// Collect a list of palettes so we can avoid evaluating the same one twice
|
|
||||||
let mut palettes = Vec::new();
|
|
||||||
if let ColorType::Indexed { palette } = &baseline.ihdr.color_type {
|
|
||||||
palettes.push(palette.clone());
|
|
||||||
}
|
|
||||||
// Make sure we use the `indexed` var as input if it exists
|
|
||||||
// This one doesn't need to be kept in the palette list as the sorters will fail if there's no change
|
|
||||||
let input = indexed.as_ref().unwrap_or(&png);
|
|
||||||
|
|
||||||
// Attempt to sort the palette using the battiato method
|
|
||||||
if !deadline.passed() {
|
|
||||||
if let Some(reduced) = sorted_palette_battiato(input) {
|
|
||||||
if let ColorType::Indexed { palette } = &reduced.ihdr.color_type {
|
|
||||||
if !palettes.contains(palette) {
|
|
||||||
palettes.push(palette.clone());
|
|
||||||
eval.try_image_with_description(
|
|
||||||
Arc::new(reduced),
|
|
||||||
"Indexed (battiato sort)",
|
|
||||||
);
|
|
||||||
evaluation_added = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to sort the palette using the mzeng method
|
|
||||||
if !deadline.passed() {
|
|
||||||
if let Some(reduced) = sorted_palette_mzeng(input) {
|
|
||||||
if let ColorType::Indexed { palette } = &reduced.ihdr.color_type {
|
|
||||||
if !palettes.contains(palette) {
|
|
||||||
palettes.push(palette.clone());
|
|
||||||
eval.try_image_with_description(Arc::new(reduced), "Indexed (mzeng sort)");
|
|
||||||
evaluation_added = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to reduce to a lower bit depth
|
|
||||||
if opts.bit_depth_reduction && !deadline.passed() {
|
|
||||||
// First try the `png` var
|
|
||||||
let reduced = reduced_bit_depth_8_or_less(&png);
|
|
||||||
// Then try the `indexed` var, unless we're doing cheap evaluations and already have a reduction
|
|
||||||
if (!cheap || reduced.is_none()) && !deadline.passed() {
|
|
||||||
if let Some(indexed) = indexed.and_then(|png| reduced_bit_depth_8_or_less(&png)) {
|
|
||||||
// Only evaluate this if it's different from the first result (which must be grayscale if it exists)
|
|
||||||
if reduced.as_ref().is_none_or(|r| r.data != indexed.data) {
|
|
||||||
eval.try_image(Arc::new(indexed));
|
|
||||||
evaluation_added = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Enter the first result into the evaluator
|
|
||||||
if let Some(reduced) = reduced {
|
|
||||||
eval.try_image(Arc::new(reduced));
|
|
||||||
evaluation_added = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if evaluation_added {
|
|
||||||
eval.try_image(baseline.clone());
|
|
||||||
}
|
|
||||||
baseline
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,439 +0,0 @@
|
||||||
use indexmap::IndexSet;
|
|
||||||
use rgb::RGBA8;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
colors::{BitDepth, ColorType},
|
|
||||||
headers::IhdrData,
|
|
||||||
png::{PngImage, scan_lines::ScanLine},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Attempt to reduce the number of colors in the palette, returning the reduced image if successful
|
|
||||||
#[must_use]
|
|
||||||
pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option<PngImage> {
|
|
||||||
if png.ihdr.bit_depth != BitDepth::Eight {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let ColorType::Indexed { palette } = &png.ihdr.color_type else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut used = [false; 256];
|
|
||||||
for &byte in &png.data {
|
|
||||||
used[byte as usize] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let black = RGBA8::new(0, 0, 0, 255);
|
|
||||||
let mut condensed = IndexSet::with_capacity(palette.len());
|
|
||||||
let mut byte_map = [0; 256];
|
|
||||||
let mut did_change = false;
|
|
||||||
for (i, used) in used.iter().enumerate() {
|
|
||||||
if !used {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// There are invalid files that use pixel indices beyond palette size
|
|
||||||
let color = *palette.get(i).unwrap_or(&black);
|
|
||||||
byte_map[i] = add_color_to_set(color, &mut condensed, optimize_alpha);
|
|
||||||
if byte_map[i] as usize != i {
|
|
||||||
did_change = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = if did_change {
|
|
||||||
// Reassign data bytes to new indices
|
|
||||||
png.data.iter().map(|b| byte_map[*b as usize]).collect()
|
|
||||||
} else if condensed.len() != palette.len() {
|
|
||||||
// Data is unchanged but palette is different size
|
|
||||||
// Note the new palette could potentially be larger if the original had a missing entry
|
|
||||||
png.data.clone()
|
|
||||||
} else {
|
|
||||||
// Nothing has changed
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let palette: Vec<_> = condensed.into_iter().collect();
|
|
||||||
|
|
||||||
Some(PngImage {
|
|
||||||
ihdr: IhdrData {
|
|
||||||
color_type: ColorType::Indexed { palette },
|
|
||||||
..png.ihdr
|
|
||||||
},
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_color_to_set(mut color: RGBA8, set: &mut IndexSet<RGBA8>, optimize_alpha: bool) -> u8 {
|
|
||||||
// If there are multiple fully transparent entries, reduce them into one
|
|
||||||
if optimize_alpha && color.a == 0 {
|
|
||||||
color.r = 0;
|
|
||||||
color.g = 0;
|
|
||||||
color.b = 0;
|
|
||||||
}
|
|
||||||
let (idx, _) = set.insert_full(color);
|
|
||||||
idx as u8
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempt to sort the colors in the palette by luma, returning the sorted image if successful
|
|
||||||
#[must_use]
|
|
||||||
pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
|
|
||||||
if png.ihdr.bit_depth != BitDepth::Eight {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let palette = match &png.ihdr.color_type {
|
|
||||||
ColorType::Indexed { palette } if palette.len() > 1 => palette,
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut enumerated: Vec<_> = palette.iter().enumerate().collect();
|
|
||||||
// Put the most popular edge color first, which can help slightly if the filter bytes are 0
|
|
||||||
let keep_first = most_popular_edge_color(palette.len(), png);
|
|
||||||
let first = keep_first.map(|f| enumerated.remove(f));
|
|
||||||
|
|
||||||
// Sort the palette
|
|
||||||
enumerated.sort_by(|a, b| {
|
|
||||||
// Sort by ascending alpha and descending luma
|
|
||||||
let color_val = |color: &RGBA8| {
|
|
||||||
let a = i32::from(color.a);
|
|
||||||
// Put 7 high bits of alpha first, then luma, then low bit of alpha
|
|
||||||
// This provides notable improvement in images with a lot of alpha
|
|
||||||
((a & 0xFE) << 18) + (a & 0x01)
|
|
||||||
// These are coefficients for standard sRGB to luma conversion
|
|
||||||
- i32::from(color.r) * 299
|
|
||||||
- i32::from(color.g) * 587
|
|
||||||
- i32::from(color.b) * 114
|
|
||||||
};
|
|
||||||
color_val(a.1).cmp(&color_val(b.1))
|
|
||||||
});
|
|
||||||
if let Some(first) = first {
|
|
||||||
enumerated.insert(0, first);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the new palette and determine if anything changed
|
|
||||||
let (remapping, palette): (Vec<_>, Vec<RGBA8>) = enumerated.into_iter().unzip();
|
|
||||||
if remapping.iter().enumerate().all(|(a, b)| a == *b) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the new mapping and convert the data
|
|
||||||
let mut byte_map = [0; 256];
|
|
||||||
for (i, &v) in remapping.iter().enumerate() {
|
|
||||||
byte_map[v] = i as u8;
|
|
||||||
}
|
|
||||||
let data = png.data.iter().map(|&b| byte_map[b as usize]).collect();
|
|
||||||
|
|
||||||
Some(PngImage {
|
|
||||||
ihdr: IhdrData {
|
|
||||||
color_type: ColorType::Indexed { palette },
|
|
||||||
..png.ihdr
|
|
||||||
},
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sort the colors in the palette using the mzeng technique, returning the sorted image if successful
|
|
||||||
#[must_use]
|
|
||||||
pub fn sorted_palette_mzeng(png: &PngImage) -> Option<PngImage> {
|
|
||||||
// Interlacing not currently supported
|
|
||||||
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let palette = match &png.ihdr.color_type {
|
|
||||||
// Images with only two colors will remain unchanged from previous luma sort
|
|
||||||
ColorType::Indexed { palette } if palette.len() > 2 => palette,
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let matrix = co_occurrence_matrix(palette.len(), png);
|
|
||||||
let edges = weighted_edges(&matrix);
|
|
||||||
let mut remapping = mzeng_reindex(palette.len(), edges, &matrix);
|
|
||||||
|
|
||||||
apply_most_popular_color(png, &mut remapping);
|
|
||||||
|
|
||||||
apply_palette_reorder(png, &remapping)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sort the colors in the palette using the battiato technique, returning the sorted image if successful
|
|
||||||
#[must_use]
|
|
||||||
pub fn sorted_palette_battiato(png: &PngImage) -> Option<PngImage> {
|
|
||||||
// Interlacing not currently supported
|
|
||||||
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let palette = match &png.ihdr.color_type {
|
|
||||||
// Images with only two colors will remain unchanged from previous luma sort
|
|
||||||
ColorType::Indexed { palette } if palette.len() > 2 => palette,
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let matrix = co_occurrence_matrix(palette.len(), png);
|
|
||||||
let edges = weighted_edges(&matrix);
|
|
||||||
let mut remapping = battiato_reindex(palette.len(), edges);
|
|
||||||
|
|
||||||
apply_most_popular_color(png, &mut remapping);
|
|
||||||
|
|
||||||
apply_palette_reorder(png, &remapping)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the palette reordering to the image data
|
|
||||||
fn apply_palette_reorder(png: &PngImage, remapping: &[usize]) -> Option<PngImage> {
|
|
||||||
let ColorType::Indexed { palette } = &png.ihdr.color_type else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if anything changed
|
|
||||||
if remapping.iter().enumerate().all(|(a, b)| a == *b) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the palette and byte maps and convert the data
|
|
||||||
let mut new_palette = Vec::new();
|
|
||||||
let mut byte_map = [0; 256];
|
|
||||||
for (i, &v) in remapping.iter().enumerate() {
|
|
||||||
new_palette.push(palette[v]);
|
|
||||||
byte_map[v] = i as u8;
|
|
||||||
}
|
|
||||||
let data = png.data.iter().map(|&b| byte_map[b as usize]).collect();
|
|
||||||
|
|
||||||
Some(PngImage {
|
|
||||||
ihdr: IhdrData {
|
|
||||||
color_type: ColorType::Indexed {
|
|
||||||
palette: new_palette,
|
|
||||||
},
|
|
||||||
..png.ihdr
|
|
||||||
},
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the most popular color on the image edges (the pixels neighboring the filter bytes)
|
|
||||||
fn most_popular_edge_color(num_colors: usize, png: &PngImage) -> Option<usize> {
|
|
||||||
let mut counts = [0_u32; 256];
|
|
||||||
for line in png.scan_lines(false) {
|
|
||||||
if let &[first, .., last] = line.data {
|
|
||||||
counts[first as usize] += 1;
|
|
||||||
counts[last as usize] += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let max = counts
|
|
||||||
.iter()
|
|
||||||
.take(num_colors)
|
|
||||||
.enumerate()
|
|
||||||
.max_by_key(|&(_, v)| v)
|
|
||||||
.unwrap();
|
|
||||||
// Ensure there's a clear winner - return None if multiple colors are tied
|
|
||||||
let max_equal = counts.iter().filter(|&v| v == max.1).count();
|
|
||||||
if max_equal > 1 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(max.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the most popular color in the image, along with its count
|
|
||||||
fn most_popular_color(num_colors: usize, png: &PngImage) -> (usize, u32) {
|
|
||||||
let mut counts = [0_u32; 256];
|
|
||||||
for &val in &png.data {
|
|
||||||
counts[val as usize] += 1;
|
|
||||||
}
|
|
||||||
counts
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.take(num_colors)
|
|
||||||
.enumerate()
|
|
||||||
.max_by_key(|&(_, v)| v)
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put the most popular color first
|
|
||||||
fn apply_most_popular_color(png: &PngImage, remapping: &mut [usize]) {
|
|
||||||
let most_popular = most_popular_color(remapping.len(), png);
|
|
||||||
// If the most popular color is less than 15% of the image, don't use it
|
|
||||||
if most_popular.1 < png.data.len() as u32 * 3 / 20 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let first_idx = remapping.iter().position(|&i| i == most_popular.0).unwrap();
|
|
||||||
// If the index is past halfway, reverse the order so as to minimize the change
|
|
||||||
if first_idx >= remapping.len() / 2 {
|
|
||||||
remapping.reverse();
|
|
||||||
remapping.rotate_right(first_idx + 1);
|
|
||||||
} else {
|
|
||||||
remapping.rotate_left(first_idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate co-occurences matrix
|
|
||||||
fn co_occurrence_matrix(num_colors: usize, png: &PngImage) -> Vec<Vec<u32>> {
|
|
||||||
let mut matrix = vec![vec![0_u32; num_colors]; num_colors];
|
|
||||||
let mut prev: Option<ScanLine> = None;
|
|
||||||
let mut prev_val = None;
|
|
||||||
for line in png.scan_lines(false) {
|
|
||||||
for i in 0..line.data.len() {
|
|
||||||
let val = line.data[i] as usize;
|
|
||||||
if val > num_colors {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Some(prev_val) = prev_val.replace(val) {
|
|
||||||
matrix[prev_val][val] += 1;
|
|
||||||
matrix[val][prev_val] += 1;
|
|
||||||
}
|
|
||||||
if let Some(prev) = &prev {
|
|
||||||
let prev_val = prev.data[i] as usize;
|
|
||||||
if prev_val > num_colors {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
matrix[prev_val][val] += 1;
|
|
||||||
matrix[val][prev_val] += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prev = Some(line);
|
|
||||||
}
|
|
||||||
matrix
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate edge list sorted by weight
|
|
||||||
fn weighted_edges(matrix: &[Vec<u32>]) -> Vec<(usize, usize)> {
|
|
||||||
let mut edges = Vec::new();
|
|
||||||
for (i, m_row) in matrix.iter().enumerate() {
|
|
||||||
for (j, val) in m_row.iter().enumerate().take(i) {
|
|
||||||
edges.push(((j, i), val));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
edges.sort_by(|(_, w1), (_, w2)| w2.cmp(w1));
|
|
||||||
edges.into_iter().map(|(e, _)| e).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply a greedy index assignment using the modified version of Zeng's techinque from
|
|
||||||
// "A note on Zeng's technique for color reindexing of palette-based images" by Pinho et al
|
|
||||||
// https://ieeexplore.ieee.org/document/1261987
|
|
||||||
// Based on the C implementation in libwebp
|
|
||||||
fn mzeng_reindex(num_colors: usize, edges: Vec<(usize, usize)>, matrix: &[Vec<u32>]) -> Vec<usize> {
|
|
||||||
// Initialize the mapping list with the two best indices.
|
|
||||||
let mut remapping = vec![edges[0].0, edges[0].1];
|
|
||||||
|
|
||||||
// Initialize the sums with the first two remappings and find the best one
|
|
||||||
let mut sums = Vec::new();
|
|
||||||
let mut best_sum_pos = 0;
|
|
||||||
let mut best_sum = (0, 0);
|
|
||||||
for (i, m_row) in matrix.iter().enumerate() {
|
|
||||||
if i == remapping[0] || i == remapping[1] {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let sum = (i, m_row[remapping[0]] + m_row[remapping[1]]);
|
|
||||||
if sum.1 > best_sum.1 {
|
|
||||||
best_sum_pos = sums.len();
|
|
||||||
best_sum = sum;
|
|
||||||
}
|
|
||||||
sums.push(sum);
|
|
||||||
}
|
|
||||||
|
|
||||||
while !sums.is_empty() {
|
|
||||||
let best_index = best_sum.0;
|
|
||||||
// Compute delta to know if we need to prepend or append the best index.
|
|
||||||
let mut delta: isize = 0;
|
|
||||||
let n = (num_colors - sums.len()) as isize;
|
|
||||||
for (i, &index) in remapping.iter().enumerate() {
|
|
||||||
delta += (n - 1 - 2 * i as isize) * matrix[best_index][index] as isize;
|
|
||||||
}
|
|
||||||
if delta > 0 {
|
|
||||||
remapping.insert(0, best_index);
|
|
||||||
} else {
|
|
||||||
remapping.push(best_index);
|
|
||||||
}
|
|
||||||
// Remove best_sum from sums.
|
|
||||||
sums.swap_remove(best_sum_pos);
|
|
||||||
if !sums.is_empty() {
|
|
||||||
// Update all the sums and find the best one.
|
|
||||||
best_sum_pos = 0;
|
|
||||||
best_sum = (0, 0);
|
|
||||||
for (i, sum) in sums.iter_mut().enumerate() {
|
|
||||||
sum.1 += matrix[best_index][sum.0];
|
|
||||||
if sum.1 > best_sum.1 {
|
|
||||||
best_sum_pos = i;
|
|
||||||
best_sum = *sum;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the completed remapping
|
|
||||||
remapping
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate an approximate solution of the Traveling Salesman Problem using the algorithm
|
|
||||||
// from "An efficient Re-indexing algorithm for color-mapped images" by Battiato et al
|
|
||||||
// https://ieeexplore.ieee.org/document/1344033
|
|
||||||
fn battiato_reindex(num_colors: usize, edges: Vec<(usize, usize)>) -> Vec<usize> {
|
|
||||||
let mut chains = Vec::new();
|
|
||||||
// Keep track of the state of each vertex (.0) and it's chain number (.1)
|
|
||||||
// 0 = an unvisited vertex (White)
|
|
||||||
// 1 = an endpoint of a chain (Red)
|
|
||||||
// 2 = part of the middle of a chain (Black)
|
|
||||||
let mut vx = vec![(0, 0); num_colors];
|
|
||||||
|
|
||||||
// Iterate the edges and assemble them into a chain
|
|
||||||
for (i, j) in edges {
|
|
||||||
let vi = vx[i];
|
|
||||||
let vj = vx[j];
|
|
||||||
if vi.0 == 0 && vj.0 == 0 {
|
|
||||||
// Two unvisited vertices - create a new chain
|
|
||||||
vx[i].0 = 1;
|
|
||||||
vx[i].1 = chains.len();
|
|
||||||
vx[j].0 = 1;
|
|
||||||
vx[j].1 = chains.len();
|
|
||||||
chains.push(vec![i, j]);
|
|
||||||
} else if vi.0 == 0 && vj.0 == 1 {
|
|
||||||
// An unvisited vertex connects with an endpoint of an existing chain
|
|
||||||
vx[i].0 = 1;
|
|
||||||
vx[i].1 = vj.1;
|
|
||||||
vx[j].0 = 2;
|
|
||||||
let chain = &mut chains[vj.1];
|
|
||||||
if chain[0] == j {
|
|
||||||
chain.insert(0, i);
|
|
||||||
} else {
|
|
||||||
chain.push(i);
|
|
||||||
}
|
|
||||||
} else if vi.0 == 1 && vj.0 == 0 {
|
|
||||||
// An unvisited vertex connects with an endpoint of an existing chain
|
|
||||||
vx[j].0 = 1;
|
|
||||||
vx[j].1 = vi.1;
|
|
||||||
vx[i].0 = 2;
|
|
||||||
let chain = &mut chains[vi.1];
|
|
||||||
if chain[0] == i {
|
|
||||||
chain.insert(0, j);
|
|
||||||
} else {
|
|
||||||
chain.push(j);
|
|
||||||
}
|
|
||||||
} else if vi.0 == 1 && vj.0 == 1 && vi.1 != vj.1 {
|
|
||||||
// Two endpoints of different chains are connected together
|
|
||||||
vx[i].0 = 2;
|
|
||||||
vx[j].0 = 2;
|
|
||||||
let (a, b) = if vi.1 < vj.1 { (i, j) } else { (j, i) };
|
|
||||||
let ca = vx[a].1;
|
|
||||||
let cb = vx[b].1;
|
|
||||||
let chainb = std::mem::take(&mut chains[cb]);
|
|
||||||
for &v in &chainb {
|
|
||||||
vx[v].1 = ca;
|
|
||||||
}
|
|
||||||
let chaina = &mut chains[ca];
|
|
||||||
if chaina[0] == a && chainb[0] == b {
|
|
||||||
for v in chainb {
|
|
||||||
chaina.insert(0, v);
|
|
||||||
}
|
|
||||||
} else if chaina[0] == a {
|
|
||||||
chaina.splice(0..0, chainb);
|
|
||||||
} else if chainb[0] == b {
|
|
||||||
chaina.extend(chainb);
|
|
||||||
} else {
|
|
||||||
let pos = chaina.len();
|
|
||||||
for v in chainb {
|
|
||||||
chaina.insert(pos, v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if chains[0].len() == num_colors {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the completed chain
|
|
||||||
chains.swap_remove(0)
|
|
||||||
}
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
use std::io::Cursor;
|
|
||||||
|
|
||||||
use image::{codecs::png::PngDecoder, *};
|
|
||||||
use log::{error, warn};
|
|
||||||
|
|
||||||
#[cfg(not(feature = "parallel"))]
|
|
||||||
use crate::rayon;
|
|
||||||
|
|
||||||
/// Validate that the output png data still matches the original image
|
|
||||||
pub fn validate_output(output: &[u8], original_data: &[u8]) -> bool {
|
|
||||||
let (old_frames, new_frames) = rayon::join(
|
|
||||||
|| load_png_image_from_memory(original_data),
|
|
||||||
|| load_png_image_from_memory(output),
|
|
||||||
);
|
|
||||||
|
|
||||||
match (new_frames, old_frames) {
|
|
||||||
(Err(new_err), _) => {
|
|
||||||
error!("Failed to read output image for validation: {}", new_err);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
(_, Err(old_err)) => {
|
|
||||||
// The original image might be invalid if, for example, there is a CRC error,
|
|
||||||
// and we set fix_errors to true. In that case, all we can do is check that the
|
|
||||||
// new image is decodable.
|
|
||||||
warn!("Failed to read input image for validation: {}", old_err);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
(Ok(new_frames), Ok(old_frames)) if new_frames.len() != old_frames.len() => false,
|
|
||||||
(Ok(new_frames), Ok(old_frames)) => {
|
|
||||||
for (a, b) in old_frames.iter().zip(new_frames) {
|
|
||||||
if !images_equal(a, &b) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads a PNG image from memory to frames of [RgbaImage]
|
|
||||||
fn load_png_image_from_memory(png_data: &[u8]) -> Result<Vec<RgbaImage>, image::ImageError> {
|
|
||||||
let decoder = PngDecoder::new(Cursor::new(png_data))?;
|
|
||||||
if decoder.is_apng()? {
|
|
||||||
decoder
|
|
||||||
.apng()?
|
|
||||||
.into_frames()
|
|
||||||
.map(|f| f.map(image::Frame::into_buffer))
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
DynamicImage::from_decoder(decoder).map(|i| vec![i.into_rgba8()])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compares images pixel by pixel for equivalent content
|
|
||||||
fn images_equal(old_png: &RgbaImage, new_png: &RgbaImage) -> bool {
|
|
||||||
let a = old_png.pixels().filter(|x| {
|
|
||||||
let p = x.channels();
|
|
||||||
!(p.len() == 4 && p[3] == 0)
|
|
||||||
});
|
|
||||||
let b = new_png.pixels().filter(|x| {
|
|
||||||
let p = x.channels();
|
|
||||||
!(p.len() == 4 && p[3] == 0)
|
|
||||||
});
|
|
||||||
a.eq(b)
|
|
||||||
}
|
|
||||||
346
tests/alpha.rs
|
|
@ -1,346 +0,0 @@
|
||||||
use std::{
|
|
||||||
fs::remove_file,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use oxipng::{internal_tests::*, *};
|
|
||||||
|
|
||||||
const GRAYSCALE_ALPHA: u8 = 4;
|
|
||||||
const RGBA: u8 = 6;
|
|
||||||
|
|
||||||
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
|
|
||||||
let options = oxipng::Options {
|
|
||||||
force: true,
|
|
||||||
optimize_alpha: true,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
(OutFile::from_path(input.with_extension("out.png")), options)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_it_converts(
|
|
||||||
input: &str,
|
|
||||||
filter: FilterStrategy,
|
|
||||||
color_type_in: u8,
|
|
||||||
bit_depth_in: BitDepth,
|
|
||||||
color_type_out: u8,
|
|
||||||
bit_depth_out: BitDepth,
|
|
||||||
) {
|
|
||||||
let input = PathBuf::from(input);
|
|
||||||
|
|
||||||
let (output, mut opts) = get_opts(&input);
|
|
||||||
let png = PngData::new(&input, &opts).unwrap();
|
|
||||||
opts.filters = indexset! {filter};
|
|
||||||
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
|
|
||||||
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);
|
|
||||||
|
|
||||||
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(x) => panic!("{}", x),
|
|
||||||
}
|
|
||||||
let output = output.path().unwrap();
|
|
||||||
assert!(output.exists());
|
|
||||||
|
|
||||||
let png = match PngData::new(output, &opts) {
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(x) => {
|
|
||||||
remove_file(output).ok();
|
|
||||||
panic!("{}", x)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_out);
|
|
||||||
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out);
|
|
||||||
if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type {
|
|
||||||
assert!(palette.len() <= 1 << (png.raw.ihdr.bit_depth as u8));
|
|
||||||
}
|
|
||||||
|
|
||||||
remove_file(output).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_0_for_rgba_16() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_0_for_rgba_16.png",
|
|
||||||
FilterStrategy::NONE,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_1_for_rgba_16() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_1_for_rgba_16.png",
|
|
||||||
FilterStrategy::SUB,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_2_for_rgba_16() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_2_for_rgba_16.png",
|
|
||||||
FilterStrategy::UP,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_3_for_rgba_16() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_3_for_rgba_16.png",
|
|
||||||
FilterStrategy::AVERAGE,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_4_for_rgba_16() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_4_for_rgba_16.png",
|
|
||||||
FilterStrategy::PAETH,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_5_for_rgba_16() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_5_for_rgba_16.png",
|
|
||||||
FilterStrategy::MinSum,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_0_for_rgba_8() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_0_for_rgba_8.png",
|
|
||||||
FilterStrategy::NONE,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_1_for_rgba_8() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_1_for_rgba_8.png",
|
|
||||||
FilterStrategy::SUB,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_2_for_rgba_8() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_2_for_rgba_8.png",
|
|
||||||
FilterStrategy::UP,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_3_for_rgba_8() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_3_for_rgba_8.png",
|
|
||||||
FilterStrategy::AVERAGE,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_4_for_rgba_8() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_4_for_rgba_8.png",
|
|
||||||
FilterStrategy::PAETH,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_5_for_rgba_8() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_5_for_rgba_8.png",
|
|
||||||
FilterStrategy::MinSum,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
RGBA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_0_for_grayscale_alpha_16() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_0_for_grayscale_alpha_16.png",
|
|
||||||
FilterStrategy::NONE,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_1_for_grayscale_alpha_16() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_1_for_grayscale_alpha_16.png",
|
|
||||||
FilterStrategy::SUB,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_2_for_grayscale_alpha_16() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_2_for_grayscale_alpha_16.png",
|
|
||||||
FilterStrategy::UP,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_3_for_grayscale_alpha_16() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_3_for_grayscale_alpha_16.png",
|
|
||||||
FilterStrategy::AVERAGE,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_4_for_grayscale_alpha_16() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_4_for_grayscale_alpha_16.png",
|
|
||||||
FilterStrategy::PAETH,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_5_for_grayscale_alpha_16() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_5_for_grayscale_alpha_16.png",
|
|
||||||
FilterStrategy::MinSum,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Sixteen,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_0_for_grayscale_alpha_8() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_0_for_grayscale_alpha_8.png",
|
|
||||||
FilterStrategy::NONE,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_1_for_grayscale_alpha_8() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_1_for_grayscale_alpha_8.png",
|
|
||||||
FilterStrategy::SUB,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_2_for_grayscale_alpha_8() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_2_for_grayscale_alpha_8.png",
|
|
||||||
FilterStrategy::UP,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_3_for_grayscale_alpha_8() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_3_for_grayscale_alpha_8.png",
|
|
||||||
FilterStrategy::AVERAGE,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_4_for_grayscale_alpha_8() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_4_for_grayscale_alpha_8.png",
|
|
||||||
FilterStrategy::PAETH,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_filter_5_for_grayscale_alpha_8() {
|
|
||||||
test_it_converts(
|
|
||||||
"tests/files/filter_5_for_grayscale_alpha_8.png",
|
|
||||||
FilterStrategy::MinSum,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
GRAYSCALE_ALPHA,
|
|
||||||
BitDepth::Eight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 296 KiB After Width: | Height: | Size: 328 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 1 KiB |
|
Before Width: | Height: | Size: 285 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 546 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 296 KiB After Width: | Height: | Size: 328 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 1 KiB |
|
Before Width: | Height: | Size: 285 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 546 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 296 KiB After Width: | Height: | Size: 328 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 1 KiB |
|
Before Width: | Height: | Size: 285 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 546 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 296 KiB After Width: | Height: | Size: 328 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 1 KiB |