Compare commits
47 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0466546981 | ||
|
|
4e80dd009a | ||
|
|
028294cc8c | ||
|
|
628e241e23 | ||
|
|
e167b04b85 | ||
|
|
fbae35922d | ||
|
|
55b9758575 | ||
|
|
010b76eada | ||
|
|
d239592b58 | ||
|
|
0cf8b782ff | ||
|
|
25e147f6a0 | ||
|
|
a75343800c | ||
|
|
7d52fd66d0 | ||
|
|
5bbd826ae3 | ||
|
|
e4a7d0fd99 | ||
|
|
a1a18ffd21 | ||
|
|
587fd03d0e | ||
|
|
ce52ab8748 | ||
|
|
494edba245 | ||
|
|
2e861b95db | ||
|
|
7834e4709e | ||
|
|
8284676f50 | ||
|
|
2a79fa4c6c | ||
|
|
00b9e33508 | ||
|
|
2585529089 | ||
|
|
6a99181fec | ||
|
|
ea9caac249 | ||
|
|
764d057c32 | ||
|
|
4a537c087d | ||
|
|
015d31e6a1 | ||
|
|
854510a864 | ||
|
|
8466e390b8 | ||
|
|
baaafee4e4 | ||
|
|
9169f99674 | ||
|
|
47d5628a68 | ||
|
|
d54e7c04ea | ||
|
|
ff168f5d2d | ||
|
|
a8edb2871c | ||
|
|
d48b147b3d | ||
|
|
155eeb3637 | ||
|
|
8665b2da8c | ||
|
|
e5e1568196 | ||
|
|
8754013cbc | ||
|
|
a0f4e751bf | ||
|
|
fa2c69071c | ||
|
|
f99ce277da | ||
|
|
0b68c08852 |
21 changed files with 748 additions and 609 deletions
|
|
@ -1,4 +1,7 @@
|
||||||
scripts
|
|
||||||
.github
|
|
||||||
.editorconfig
|
.editorconfig
|
||||||
.pre-commit-hooks.yaml
|
.gitignore
|
||||||
|
/.git/
|
||||||
|
/.github/
|
||||||
|
/.pre-commit-hooks.yaml
|
||||||
|
/scripts/
|
||||||
|
/target/
|
||||||
|
|
|
||||||
19
.github/workflows/deploy.yml
vendored
19
.github/workflows/deploy.yml
vendored
|
|
@ -3,7 +3,12 @@ name: deploy
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*.*.*"
|
- v*.*.*
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
# If we ever end up with two concurrent jobs for releasing the same tag, the former should be cancelled
|
||||||
|
group: release-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
|
|
@ -54,15 +59,15 @@ jobs:
|
||||||
|
|
||||||
- name: Get the Oxipng version
|
- name: Get the Oxipng version
|
||||||
id: oxipngMeta
|
id: oxipngMeta
|
||||||
run:
|
run: >
|
||||||
echo "version=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "oxipng").version')"
|
echo "version=$(cargo metadata --format-version 1 --no-deps |
|
||||||
>> "$GITHUB_OUTPUT"
|
jq -r '.packages[] | select(.name == "oxipng").version')" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Retrieve ${{ matrix.target }} binary
|
- name: Retrieve ${{ matrix.target }} binary
|
||||||
uses: dawidd6/action-download-artifact@v11
|
uses: dawidd6/action-download-artifact@v21
|
||||||
with:
|
with:
|
||||||
workflow: oxipng.yml
|
workflow: oxipng.yml
|
||||||
commit: ${{ env.GITHUB_SHA }}
|
commit: ${{ github.sha }}
|
||||||
name: Oxipng binary (${{ matrix.target }})
|
name: Oxipng binary (${{ matrix.target }})
|
||||||
path: target
|
path: target
|
||||||
|
|
||||||
|
|
@ -123,7 +128,7 @@ jobs:
|
||||||
run: tail -n +3 CHANGELOG.md | sed -e '/^$/,$d' > RELEASE_NOTES.txt
|
run: tail -n +3 CHANGELOG.md | sed -e '/^$/,$d' > RELEASE_NOTES.txt
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v3
|
||||||
with:
|
with:
|
||||||
name: v${{ steps.oxipngMeta.outputs.version }}
|
name: v${{ steps.oxipngMeta.outputs.version }}
|
||||||
body_path: RELEASE_NOTES.txt
|
body_path: RELEASE_NOTES.txt
|
||||||
|
|
|
||||||
90
.github/workflows/docker.yml
vendored
90
.github/workflows/docker.yml
vendored
|
|
@ -2,9 +2,9 @@ name: docker
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- master
|
||||||
tags:
|
tags:
|
||||||
- 'v*.*.*'
|
- v*.*.*
|
||||||
pull_request:
|
pull_request:
|
||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
|
|
@ -17,76 +17,44 @@ on:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
|
|
||||||
env:
|
concurrency:
|
||||||
REGISTRY: ghcr.io
|
# If we ever end up with two concurrent Docker build jobs for the same commit
|
||||||
# ghcr.io/OWNER/REPO
|
# and same versioning context, the former should be cancelled
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
group: docker-${{ github.sha }}-${{ github.ref_type }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
uses: docker/github-builder/.github/workflows/build.yml@v1
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
id-token: write
|
id-token: write
|
||||||
attestations: write
|
attestations: write
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
# Workaround: https://github.com/docker/build-push-action/issues/461
|
|
||||||
- name: Setup Docker buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
# Login against a Docker registry except on PR
|
|
||||||
# https://github.com/docker/login-action
|
|
||||||
- name: Log into registry ${{ env.REGISTRY }}
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
output: image
|
||||||
username: ${{ github.actor }}
|
platforms: linux/amd64,linux/arm64
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
cache: ${{ github.event_name != 'workflow_dispatch' || inputs.use_cache }}
|
||||||
# Extract metadata (tags, labels) for Docker
|
# 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
|
||||||
# For some reason the title have to be set manually
|
# https://github.com/docker/github-builder/issues/109#issuecomment-3885082486
|
||||||
# https://github.com/docker/metadata-action
|
# https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#backwards-compatibility
|
||||||
- name: Extract Docker metadata
|
sign: false
|
||||||
id: meta
|
sbom: true
|
||||||
uses: docker/metadata-action@v5
|
meta-images: ghcr.io/${{ github.repository }}
|
||||||
with:
|
meta-tags: |
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{raw}}
|
||||||
|
set-meta-annotations: true
|
||||||
|
set-meta-labels: true
|
||||||
labels: |
|
labels: |
|
||||||
org.opencontainers.image.title=Oxipng
|
org.opencontainers.image.title=Oxipng
|
||||||
annotations: |
|
annotations: |
|
||||||
org.opencontainers.image.title=Oxipng
|
org.opencontainers.image.title=Oxipng
|
||||||
|
|
||||||
# Build and push Docker image with Buildx (don't push on PR)
|
secrets:
|
||||||
# Cache isn't used for tags and on workflow_dispatch if specified
|
registry-auths: |
|
||||||
# https://github.com/docker/build-push-action
|
- registry: ghcr.io
|
||||||
- name: Build and push Docker image
|
username: ${{ github.actor }}
|
||||||
id: build-and-push
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
no-cache: ${{ (github.event_name == 'workflow_dispatch' && !inputs.use_cache) || startsWith(github.ref, 'refs/tags/') }}
|
|
||||||
|
|
||||||
# Attest the build provenance
|
|
||||||
# TODO: enable push to registry when referrers API will be supported by ghcr.io
|
|
||||||
- name: Attest Build Provenance
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
uses: actions/attest-build-provenance@v3
|
|
||||||
with:
|
|
||||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
||||||
subject-digest: ${{ steps.build-and-push.outputs.digest }}
|
|
||||||
push-to-registry: false
|
|
||||||
|
|
|
||||||
14
.github/workflows/oxipng.yml
vendored
14
.github/workflows/oxipng.yml
vendored
|
|
@ -8,6 +8,11 @@ on:
|
||||||
- synchronize
|
- synchronize
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
# If we ever end up with two concurrent CI jobs for the same commit, the former should be cancelled
|
||||||
|
group: ci-${{ github.sha }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
name: CI
|
name: CI
|
||||||
|
|
@ -55,7 +60,6 @@ jobs:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_BUILD_TARGET: ${{ matrix.target }}
|
CARGO_BUILD_TARGET: ${{ matrix.target }}
|
||||||
RUSTFLAGS: '-Zlocation-detail=none'
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source
|
- name: Checkout source
|
||||||
|
|
@ -74,7 +78,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
target: ${{ env.CARGO_BUILD_TARGET }}
|
target: ${{ env.CARGO_BUILD_TARGET }}
|
||||||
components: clippy, rustfmt
|
components: clippy, rustfmt, rust-src
|
||||||
cache-bin: false
|
cache-bin: false
|
||||||
cache-shared-key: cache
|
cache-shared-key: cache
|
||||||
|
|
||||||
|
|
@ -125,10 +129,12 @@ jobs:
|
||||||
run: cargo doc --release --no-deps
|
run: cargo doc --release --no-deps
|
||||||
|
|
||||||
- name: Build CLI binary
|
- name: Build CLI binary
|
||||||
run: cargo build --release
|
run: cargo build --release -Zbuild-std
|
||||||
|
env:
|
||||||
|
RUSTFLAGS: "-Zlocation-detail=none -Zunstable-options -Cpanic=immediate-abort"
|
||||||
|
|
||||||
- name: Upload CLI binary as artifact
|
- name: Upload CLI binary as artifact
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: Oxipng binary (${{ matrix.target }})
|
name: Oxipng binary (${{ matrix.target }})
|
||||||
path: |
|
path: |
|
||||||
|
|
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
target
|
|
||||||
*.bk
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
*.bk
|
||||||
*.out.png
|
*.out.png
|
||||||
/.idea
|
/.idea/
|
||||||
/node_modules
|
/node_modules/
|
||||||
|
/target/
|
||||||
|
|
|
||||||
16
CHANGELOG.md
16
CHANGELOG.md
|
|
@ -1,7 +1,21 @@
|
||||||
|
## 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
|
## Version 10.0.0
|
||||||
|
|
||||||
- [Breaking] CLI: Change short zopfli flag from `-Z` to `-z`.
|
- [Breaking] CLI: Change short zopfli flag from `-Z` to `-z`.
|
||||||
- [Breaking] CLI: Change `--pretend`/`-P` to `--dry-run`/`-D`.
|
- [Breaking] CLI: Change `--pretend`/`-P` to `--dry-run`/`-d`.
|
||||||
- [Breaking] CLI: Change `--interlace` options from `1`/`0` to `on`/`off`.
|
- [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.interlace: Option<Interlacing>` to `Options.interlace: Option<bool>`.
|
||||||
- [Breaking] API: Change `Options.filter: IndexSet<RowFilter>` to `Options.filters: IndexSet<FilterStrategy>`.
|
- [Breaking] API: Change `Options.filter: IndexSet<RowFilter>` to `Options.filters: IndexSet<FilterStrategy>`.
|
||||||
|
|
|
||||||
390
Cargo.lock
generated
390
Cargo.lock
generated
|
|
@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.20"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
|
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"anstyle-parse",
|
"anstyle-parse",
|
||||||
|
|
@ -25,37 +25,37 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle"
|
name = "anstyle"
|
||||||
version = "1.0.11"
|
version = "1.0.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
|
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-parse"
|
name = "anstyle-parse"
|
||||||
version = "0.2.7"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"utf8parse",
|
"utf8parse",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-query"
|
name = "anstyle-query"
|
||||||
version = "1.1.4"
|
version = "1.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.60.2",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-wincon"
|
name = "anstyle-wincon"
|
||||||
version = "3.0.10"
|
version = "3.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
|
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"once_cell_polyfill",
|
"once_cell_polyfill",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -66,9 +66,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.9.4"
|
version = "2.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
|
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitvec"
|
name = "bitvec"
|
||||||
|
|
@ -84,15 +84,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.19.0"
|
version = "3.20.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.23.2"
|
version = "1.25.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677"
|
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder-lite"
|
name = "byteorder-lite"
|
||||||
|
|
@ -102,9 +102,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.38"
|
version = "1.2.60"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9"
|
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
|
|
@ -112,24 +112,24 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.3"
|
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 = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.53"
|
version = "4.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
|
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.53"
|
version = "4.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
|
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
|
|
@ -140,15 +140,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_lex"
|
name = "clap_lex"
|
||||||
version = "0.7.5"
|
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 = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.4"
|
version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
|
|
@ -159,15 +159,6 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-channel"
|
|
||||||
version = "0.5.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-deque"
|
name = "crossbeam-deque"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
|
|
@ -201,18 +192,18 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_filter"
|
name = "env_filter"
|
||||||
version = "0.1.3"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_logger"
|
name = "env_logger"
|
||||||
version = "0.11.8"
|
version = "0.11.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
|
|
@ -233,7 +224,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.0",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -245,29 +236,17 @@ dependencies = [
|
||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "filetime"
|
|
||||||
version = "0.2.26"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"libredox",
|
|
||||||
"windows-sys 0.60.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.2"
|
version = "0.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959"
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.2"
|
version = "1.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
|
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
|
|
@ -287,9 +266,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.16.1"
|
version = "0.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
|
|
@ -306,9 +285,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.12.1"
|
version = "2.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
|
|
@ -317,21 +296,27 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.1"
|
version = "1.70.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.175"
|
version = "0.2.185"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libdeflate-sys"
|
name = "libdeflate-sys"
|
||||||
version = "1.25.0"
|
version = "1.25.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23bd6304ebf75390d8a99b88bdf2a266f62647838140cb64af8e6702f6e3fddc"
|
checksum = "72753e0008ea87963d2f0770042d0df7abe51fafbb8dcaf618ac440f2f1fec0a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
|
|
@ -339,35 +324,30 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libdeflater"
|
name = "libdeflater"
|
||||||
version = "1.25.0"
|
version = "1.25.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d5d4880e6d634d3d029d65fa016038e788cc728a17b782684726fb34ee140caf"
|
checksum = "d1ee41cf6fb1bb6030dfb59ffb7bc01ab26aade44142084c87f0fc7a1658fe71"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libdeflate-sys",
|
"libdeflate-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libredox"
|
|
||||||
version = "0.1.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"libc",
|
|
||||||
"redox_syscall",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.11.0"
|
version = "0.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.28"
|
version = "0.4.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
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"
|
||||||
|
|
@ -381,9 +361,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "moxcms"
|
name = "moxcms"
|
||||||
version = "0.7.5"
|
version = "0.7.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08"
|
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"pxfm",
|
"pxfm",
|
||||||
|
|
@ -400,19 +380,17 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell_polyfill"
|
name = "once_cell_polyfill"
|
||||||
version = "1.70.1"
|
version = "1.70.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxipng"
|
name = "oxipng"
|
||||||
version = "10.0.0"
|
version = "10.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitvec",
|
"bitvec",
|
||||||
"clap",
|
"clap",
|
||||||
"crossbeam-channel",
|
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"filetime",
|
|
||||||
"glob",
|
"glob",
|
||||||
"image",
|
"image",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
|
|
@ -422,6 +400,7 @@ dependencies = [
|
||||||
"rayon",
|
"rayon",
|
||||||
"rgb",
|
"rgb",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
|
"serde_json",
|
||||||
"zopfli",
|
"zopfli",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -433,15 +412,15 @@ checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.32"
|
version = "0.3.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "png"
|
name = "png"
|
||||||
version = "0.18.0"
|
version = "0.18.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
|
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
|
|
@ -451,12 +430,27 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pxfm"
|
name = "proc-macro2"
|
||||||
version = "0.1.24"
|
version = "1.0.106"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde"
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-traits",
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pxfm"
|
||||||
|
version = "0.1.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -467,9 +461,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon"
|
name = "rayon"
|
||||||
version = "1.11.0"
|
version = "1.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"either",
|
"either",
|
||||||
"rayon-core",
|
"rayon-core",
|
||||||
|
|
@ -485,41 +479,74 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redox_syscall"
|
|
||||||
version = "0.5.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rgb"
|
name = "rgb"
|
||||||
version = "0.8.52"
|
version = "0.8.53"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
|
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.1"
|
version = "2.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.2"
|
version = "1.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
"windows-sys 0.61.0",
|
"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]]
|
[[package]]
|
||||||
|
|
@ -530,9 +557,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simd-adler32"
|
name = "simd-adler32"
|
||||||
version = "0.3.7"
|
version = "0.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
|
|
@ -540,6 +567,17 @@ version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tap"
|
name = "tap"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
@ -548,14 +586,20 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "terminal_size"
|
name = "terminal_size"
|
||||||
version = "0.4.3"
|
version = "0.4.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
|
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
|
@ -564,99 +608,19 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.1.3"
|
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 = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-link"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.60.2"
|
version = "0.61.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-targets",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.61.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa"
|
|
||||||
dependencies = [
|
|
||||||
"windows-link 0.2.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-targets"
|
|
||||||
version = "0.53.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
|
|
||||||
dependencies = [
|
|
||||||
"windows-link 0.1.3",
|
|
||||||
"windows_aarch64_gnullvm",
|
|
||||||
"windows_aarch64_msvc",
|
|
||||||
"windows_i686_gnu",
|
|
||||||
"windows_i686_gnullvm",
|
|
||||||
"windows_i686_msvc",
|
|
||||||
"windows_x86_64_gnu",
|
|
||||||
"windows_x86_64_gnullvm",
|
|
||||||
"windows_x86_64_msvc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_gnullvm"
|
|
||||||
version = "0.53.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.53.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.53.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnullvm"
|
|
||||||
version = "0.53.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.53.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.53.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnullvm"
|
|
||||||
version = "0.53.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.53.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wyz"
|
name = "wyz"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|
@ -666,6 +630,12 @@ dependencies = [
|
||||||
"tap",
|
"tap",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zopfli"
|
name = "zopfli"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
|
|
||||||
63
Cargo.toml
63
Cargo.toml
|
|
@ -20,7 +20,7 @@ homepage = "https://github.com/oxipng/oxipng"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
name = "oxipng"
|
name = "oxipng"
|
||||||
repository = "https://github.com/oxipng/oxipng"
|
repository = "https://github.com/oxipng/oxipng"
|
||||||
version = "10.0.0"
|
version = "10.1.1"
|
||||||
rust-version = "1.85.1"
|
rust-version = "1.85.1"
|
||||||
|
|
||||||
[badges]
|
[badges]
|
||||||
|
|
@ -38,59 +38,32 @@ name = "zopfli"
|
||||||
required-features = ["zopfli"]
|
required-features = ["zopfli"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
zopfli = { version = "0.8.3", optional = true, default-features = false, features = ["std", "zlib"] }
|
|
||||||
rgb = "0.8.52"
|
|
||||||
indexmap = "2.12.1"
|
|
||||||
libdeflater = "1.25.0"
|
|
||||||
log = "0.4.28"
|
|
||||||
bitvec = "1.0.1"
|
bitvec = "1.0.1"
|
||||||
rustc-hash = "2.1.1"
|
clap = { version = "4.6.0", optional = true, features = ["wrap_help"] }
|
||||||
|
env_logger = { version = "0.11.10", optional = true, default-features = false, features = ["auto-color"] }
|
||||||
|
image = { version = "0.25.9", optional = true, default-features = false, features = ["png"] }
|
||||||
|
indexmap = "2.14.0"
|
||||||
|
libdeflater = "1.25.2"
|
||||||
|
log = "0.4.30"
|
||||||
|
parse-size = { version = "1.1.0", optional = true }
|
||||||
|
rayon = { version = "1.11.0", optional = true }
|
||||||
|
rgb = "0.8.53"
|
||||||
|
rustc-hash = "2.1.2"
|
||||||
|
zopfli = { version = "0.8.3", optional = true, default-features = false, features = ["std", "zlib"] }
|
||||||
|
|
||||||
[dependencies.env_logger]
|
[target.'cfg(windows)'.dependencies]
|
||||||
optional = true
|
glob = { version = "0.3.3", optional = true }
|
||||||
default-features = false
|
|
||||||
features = ["auto-color"]
|
|
||||||
version = "0.11.8"
|
|
||||||
|
|
||||||
[dependencies.crossbeam-channel]
|
[dev-dependencies]
|
||||||
optional = true
|
serde_json = "1.0.150"
|
||||||
version = "0.5.15"
|
|
||||||
|
|
||||||
[dependencies.filetime]
|
|
||||||
optional = true
|
|
||||||
version = "0.2.26"
|
|
||||||
|
|
||||||
[dependencies.rayon]
|
|
||||||
optional = true
|
|
||||||
version = "1.11.0"
|
|
||||||
|
|
||||||
[dependencies.clap]
|
|
||||||
optional = true
|
|
||||||
version = "4.5.53"
|
|
||||||
features = ["wrap_help"]
|
|
||||||
|
|
||||||
[dependencies.parse-size]
|
|
||||||
optional = true
|
|
||||||
version = "1.1.0"
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies.glob]
|
|
||||||
optional = true
|
|
||||||
version = "0.3.3"
|
|
||||||
|
|
||||||
[dependencies.image]
|
|
||||||
optional = true
|
|
||||||
default-features = false
|
|
||||||
features = ["png"]
|
|
||||||
version = "0.25.9"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
binary = ["dep:clap", "dep:glob", "dep:env_logger", "dep:parse-size"]
|
binary = ["dep:clap", "dep:glob", "dep:env_logger", "dep:parse-size"]
|
||||||
default = ["binary", "parallel", "zopfli", "filetime"]
|
default = ["binary", "parallel", "zopfli"]
|
||||||
parallel = ["dep:rayon", "indexmap/rayon", "dep:crossbeam-channel"]
|
parallel = ["dep:rayon", "indexmap/rayon"]
|
||||||
freestanding = ["libdeflater/freestanding"]
|
freestanding = ["libdeflater/freestanding"]
|
||||||
sanity-checks = ["dep:image"]
|
sanity-checks = ["dep:image"]
|
||||||
zopfli = ["dep:zopfli"]
|
zopfli = ["dep:zopfli"]
|
||||||
filetime = ["dep:filetime"]
|
|
||||||
system-libdeflate = ["libdeflater/dynamic"]
|
system-libdeflate = ["libdeflater/dynamic"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
oxipng 10.0.0
|
oxipng 10.1.0
|
||||||
Losslessly improve compression of PNG files
|
Losslessly improve compression of PNG files
|
||||||
|
|
||||||
Usage: oxipng [OPTIONS] <files>...
|
Usage: oxipng [OPTIONS] <files>...
|
||||||
|
|
@ -105,10 +105,13 @@ Options:
|
||||||
losslessly.
|
losslessly.
|
||||||
|
|
||||||
-v, --verbose...
|
-v, --verbose...
|
||||||
Run in verbose mode (use twice to increase verbosity)
|
Show per-file info (use multiple times for more detail)
|
||||||
|
|
||||||
-q, --quiet
|
-q, --quiet
|
||||||
Run in quiet mode
|
Suppress all output messages
|
||||||
|
|
||||||
|
-j, --json
|
||||||
|
Print results as JSON
|
||||||
|
|
||||||
-f, --filters <list>
|
-f, --filters <list>
|
||||||
Perform compression trials with each of the given filter types. You can specify a
|
Perform compression trials with each of the given filter types. You can specify a
|
||||||
|
|
|
||||||
66
README.md
66
README.md
|
|
@ -8,7 +8,7 @@
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Oxipng is a multithreaded lossless PNG/APNG compression optimizer. It can be used via a command-line
|
Oxipng is a multithreaded lossless PNG/APNG compression optimizer. It can be used via a command-line
|
||||||
interface or as a library in other Rust programs.
|
interface or as a library in other Rust programs. It is fast and highly effective.
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
|
|
||||||
|
|
@ -76,7 +76,14 @@ All options are case-sensitive.
|
||||||
\* Note that oxipng is not a brute-force optimizer. This means that while higher optimization levels
|
\* 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
|
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
|
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 without.
|
compression (`-z`) is not guaranteed to always be better than default compression.
|
||||||
|
|
||||||
|
## APNG support
|
||||||
|
|
||||||
|
Oxipng currently only supports limited optimization of animated PNGs (APNGs). It can perform
|
||||||
|
alpha-optimization, refiltering and recompression of all frames, but all transformations will be
|
||||||
|
disabled. For best results, it is recommended to use another tool such as
|
||||||
|
[apngopt](https://sourceforge.net/projects/apng/files/APNG_Optimizer/) before running Oxipng.
|
||||||
|
|
||||||
## Git integration via [pre-commit]
|
## Git integration via [pre-commit]
|
||||||
|
|
||||||
|
|
@ -131,17 +138,13 @@ and specifying the desired ones, for example:
|
||||||
|
|
||||||
## History
|
## History
|
||||||
|
|
||||||
Oxipng began as a complete rewrite of the OptiPNG project,
|
Oxipng began in 2015 as a rewrite of the OptiPNG project. The core goal was to implement
|
||||||
which was assumed to be dead as no commit had been made to it since March 2014.
|
multithreading, which would have been very difficult to do within the existing C codebase of OptiPNG.
|
||||||
(OptiPNG has since released a new version, after Oxipng was first released.)
|
|
||||||
The name has been changed to avoid confusion and potential legal issues.
|
|
||||||
|
|
||||||
The core goal of rewriting OptiPNG was to implement multithreading,
|
|
||||||
which would be very difficult to do within the existing C codebase of OptiPNG.
|
|
||||||
This also served as an opportunity to choose a more modern, safer language (Rust).
|
This also served as an opportunity to choose a more modern, safer language (Rust).
|
||||||
|
|
||||||
Note that, while similar, Oxipng is not a drop-in replacement for OptiPNG.
|
However, Oxipng has evolved considerably since then. While some of the options remain similar to
|
||||||
If you are migrating from OptiPNG, please check the [help](MANUAL.txt) before using.
|
OptiPNG, the architecture and capabilities are now quite different. It is not a drop-in
|
||||||
|
replacement - if you are migrating from OptiPNG, please check the [help](MANUAL.txt) before use.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
@ -151,42 +154,11 @@ to submit a fix with the bug report, it is preferred that you do so via pull req
|
||||||
however you do not need to be a Rust developer to contribute.
|
however you do not need to be a Rust developer to contribute.
|
||||||
Other contributions (such as improving documentation or translations) are also welcome via GitHub.
|
Other contributions (such as improving documentation or translations) are also welcome via GitHub.
|
||||||
|
|
||||||
|
## Benchmarks
|
||||||
|
|
||||||
|
An independent benchmark is linked here with permission by the author:\
|
||||||
|
[oxipng and friends: A comparison of PNG optimization tools](https://op111.net/posts/2025/09/png-compression-oxipng-optipng-fileoptimizer-cwebp/)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Oxipng is open-source software, distributed under the MIT license.
|
Oxipng is open-source software, distributed under the MIT license.
|
||||||
|
|
||||||
## Benchmarks
|
|
||||||
|
|
||||||
Tested Oxipng 9.0.0 (commit `c16519b38b0519988db625913be919d4f0e42f5d`, compiled
|
|
||||||
on `rustc 1.74.0-nightly (7b4d9e155 2023-09-28)`) against OptiPNG version 0.7.7,
|
|
||||||
as packaged by Debian unstable, on a Linux 6.5.0-2-amd64 kernel, Intel Core
|
|
||||||
i7-12700 CPU (8 performance cores, 4 efficiency cores, 20 threads), DDR5-5200
|
|
||||||
RAM in dual channel configuration.
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Benchmark 1: ./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png
|
|
||||||
Time (mean ± σ): 59.6 ms ± 7.7 ms [User: 77.4 ms, System: 3.6 ms]
|
|
||||||
Range (min … max): 53.3 ms … 89.9 ms 32 runs
|
|
||||||
|
|
||||||
Benchmark 2: optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
|
|
||||||
Time (mean ± σ): 132.4 ms ± 0.8 ms [User: 132.5 ms, System: 0.6 ms]
|
|
||||||
Range (min … max): 131.8 ms … 134.4 ms 22 runs
|
|
||||||
|
|
||||||
Summary
|
|
||||||
./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png ran
|
|
||||||
2.22 ± 0.29 times faster than optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
|
|
||||||
|
|
||||||
Benchmark 1: ./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png
|
|
||||||
Time (mean ± σ): 88.7 ms ± 4.3 ms [User: 270.3 ms, System: 11.0 ms]
|
|
||||||
Range (min … max): 86.8 ms … 109.4 ms 26 runs
|
|
||||||
|
|
||||||
Benchmark 2: optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
|
|
||||||
Time (mean ± σ): 444.9 ms ± 0.3 ms [User: 444.8 ms, System: 0.7 ms]
|
|
||||||
Range (min … max): 444.4 ms … 445.6 ms 10 runs
|
|
||||||
|
|
||||||
Summary
|
|
||||||
./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png ran
|
|
||||||
5.01 ± 0.25 times faster than optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
|
||||||
23
src/apng.rs
23
src/apng.rs
|
|
@ -1,5 +1,3 @@
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
PngResult,
|
PngResult,
|
||||||
error::PngError,
|
error::PngError,
|
||||||
|
|
@ -52,15 +50,14 @@ impl Frame {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn fctl_data(&self, sequence_number: u32) -> Vec<u8> {
|
pub fn fctl_data(&self, sequence_number: u32) -> Vec<u8> {
|
||||||
let mut byte_data = Vec::with_capacity(26);
|
let mut byte_data = Vec::with_capacity(26);
|
||||||
byte_data.write_all(&sequence_number.to_be_bytes()).unwrap();
|
byte_data.extend_from_slice(&sequence_number.to_be_bytes());
|
||||||
byte_data.write_all(&self.width.to_be_bytes()).unwrap();
|
byte_data.extend_from_slice(&self.width.to_be_bytes());
|
||||||
byte_data.write_all(&self.height.to_be_bytes()).unwrap();
|
byte_data.extend_from_slice(&self.height.to_be_bytes());
|
||||||
byte_data.write_all(&self.x_offset.to_be_bytes()).unwrap();
|
byte_data.extend_from_slice(&self.x_offset.to_be_bytes());
|
||||||
byte_data.write_all(&self.y_offset.to_be_bytes()).unwrap();
|
byte_data.extend_from_slice(&self.y_offset.to_be_bytes());
|
||||||
byte_data.write_all(&self.delay_num.to_be_bytes()).unwrap();
|
byte_data.extend_from_slice(&self.delay_num.to_be_bytes());
|
||||||
byte_data.write_all(&self.delay_den.to_be_bytes()).unwrap();
|
byte_data.extend_from_slice(&self.delay_den.to_be_bytes());
|
||||||
byte_data.push(self.dispose_op);
|
byte_data.extend_from_slice(&[self.dispose_op, self.blend_op]);
|
||||||
byte_data.push(self.blend_op);
|
|
||||||
byte_data
|
byte_data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,8 +65,8 @@ impl Frame {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn fdat_data(&self, sequence_number: u32) -> Vec<u8> {
|
pub fn fdat_data(&self, sequence_number: u32) -> Vec<u8> {
|
||||||
let mut byte_data = Vec::with_capacity(4 + self.data.len());
|
let mut byte_data = Vec::with_capacity(4 + self.data.len());
|
||||||
byte_data.write_all(&sequence_number.to_be_bytes()).unwrap();
|
byte_data.extend_from_slice(&sequence_number.to_be_bytes());
|
||||||
byte_data.write_all(&self.data).unwrap();
|
byte_data.extend_from_slice(&self.data);
|
||||||
byte_data
|
byte_data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
src/cli.rs
14
src/cli.rs
|
|
@ -212,7 +212,7 @@ losslessly.")
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("verbose")
|
Arg::new("verbose")
|
||||||
.help("Run in verbose mode (use twice to increase verbosity)")
|
.help("Show per-file info (use multiple times for more detail)")
|
||||||
.short('v')
|
.short('v')
|
||||||
.long("verbose")
|
.long("verbose")
|
||||||
.action(ArgAction::Count)
|
.action(ArgAction::Count)
|
||||||
|
|
@ -220,12 +220,20 @@ losslessly.")
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("quiet")
|
Arg::new("quiet")
|
||||||
.help("Run in quiet mode")
|
.help("Suppress all output messages")
|
||||||
.short('q')
|
.short('q')
|
||||||
.long("quiet")
|
.long("quiet")
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.conflicts_with("verbose"),
|
.conflicts_with("verbose"),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("json")
|
||||||
|
.help("Print results as JSON")
|
||||||
|
.short('j')
|
||||||
|
.long("json")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.conflicts_with("stdout"),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("filters")
|
Arg::new("filters")
|
||||||
.help("Filters to try (0-9; see '--help' for details)")
|
.help("Filters to try (0-9; see '--help' for details)")
|
||||||
|
|
@ -290,6 +298,8 @@ The default value depends on the optimization level preset.")
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("no-palette-reduction")
|
Arg::new("no-palette-reduction")
|
||||||
.help("Do not change color palette")
|
.help("Do not change color palette")
|
||||||
|
.long_help("\
|
||||||
|
Do not convert to indexed and do not modify an existing color palette.")
|
||||||
.long("np")
|
.long("np")
|
||||||
.action(ArgAction::SetTrue),
|
.action(ArgAction::SetTrue),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,12 @@ use std::sync::{
|
||||||
atomic::{AtomicUsize, Ordering::*},
|
atomic::{AtomicUsize, Ordering::*},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "parallel")]
|
|
||||||
use crossbeam_channel::{Receiver, Sender, unbounded};
|
|
||||||
use deflate::Deflater;
|
use deflate::Deflater;
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use log::trace;
|
use log::trace;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
#[cfg(feature = "parallel")]
|
||||||
|
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||||
|
|
||||||
#[cfg(not(feature = "parallel"))]
|
#[cfg(not(feature = "parallel"))]
|
||||||
use crate::rayon;
|
use crate::rayon;
|
||||||
|
|
@ -73,7 +73,7 @@ impl Evaluator {
|
||||||
final_round: bool,
|
final_round: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
#[cfg(feature = "parallel")]
|
#[cfg(feature = "parallel")]
|
||||||
let eval_channel = unbounded();
|
let eval_channel = channel();
|
||||||
Self {
|
Self {
|
||||||
deadline,
|
deadline,
|
||||||
filters,
|
filters,
|
||||||
|
|
@ -118,7 +118,7 @@ impl Evaluator {
|
||||||
|
|
||||||
/// Check if the image is smaller than others
|
/// Check if the image is smaller than others
|
||||||
pub fn try_image(&self, image: Arc<PngImage>) {
|
pub fn try_image(&self, image: Arc<PngImage>) {
|
||||||
let description = format!("{}", image.ihdr.color_type);
|
let description = image.ihdr.color_type.to_string();
|
||||||
self.try_image_with_description(image, &description);
|
self.try_image_with_description(image, &description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,6 +154,19 @@ impl Evaluator {
|
||||||
let idat_data = deflater.deflate(&filtered, best_candidate_size.get());
|
let idat_data = deflater.deflate(&filtered, best_candidate_size.get());
|
||||||
if let Ok(idat_data) = idat_data {
|
if let Ok(idat_data) = idat_data {
|
||||||
let estimated_output_size = image.estimated_output_size(&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
|
// We only need to retain the IDAT data in the final round
|
||||||
let new = Candidate {
|
let new = Candidate {
|
||||||
image: image.clone(),
|
image: image.clone(),
|
||||||
|
|
@ -164,10 +177,6 @@ impl Evaluator {
|
||||||
nth,
|
nth,
|
||||||
};
|
};
|
||||||
best_candidate_size.set_min(estimated_output_size);
|
best_candidate_size.set_min(estimated_output_size);
|
||||||
trace!(
|
|
||||||
"Eval: {}-bit {:23} {:8} {} bytes",
|
|
||||||
image.ihdr.bit_depth, description, filter, estimated_output_size
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(feature = "parallel")]
|
#[cfg(feature = "parallel")]
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -263,11 +263,11 @@ pub fn extract_icc(iccp: &Chunk, max_size: Option<usize>) -> Option<Vec<u8>> {
|
||||||
if compression_method != 0 {
|
if compression_method != 0 {
|
||||||
return None; // The profile is supposed to be compressed (method 0)
|
return None; // The profile is supposed to be compressed (method 0)
|
||||||
}
|
}
|
||||||
// The decompressed size is unknown so we have to guess the required buffer size
|
// Libdeflate works with a fixed size buffer. Since the decompressed size is unknown we have to
|
||||||
let mut out_size = compressed_data.len() * 2 + 1000;
|
// guess the required buffer size. We allow a fairly generous 10x factor with a minimum of 1000.
|
||||||
if let Some(max) = max_size {
|
let mut out_size = (compressed_data.len() * 10).max(1000);
|
||||||
out_size = out_size.min(max);
|
// 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) {
|
match inflate(compressed_data, out_size) {
|
||||||
Ok(icc) => Some(icc),
|
Ok(icc) => Some(icc),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
||||||
104
src/lib.rs
104
src/lib.rs
|
|
@ -7,9 +7,9 @@ extern crate rayon;
|
||||||
mod rayon;
|
mod rayon;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fs::{File, Metadata},
|
fs::File,
|
||||||
io::{BufWriter, Read, Write, stdin, stdout},
|
io::{BufWriter, Read, Write, stdin, stdout},
|
||||||
path::{Path, PathBuf},
|
path::PathBuf,
|
||||||
sync::{
|
sync::{
|
||||||
Arc,
|
Arc,
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
|
|
@ -64,6 +64,7 @@ pub mod internal_tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type PngResult<T> = Result<T, PngError>;
|
pub type PngResult<T> = Result<T, PngError>;
|
||||||
|
pub type OptimizationResult = PngResult<(usize, usize)>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
/// A raw image definition which can be used to create an optimized png
|
/// A raw image definition which can be used to create an optimized png
|
||||||
|
|
@ -86,7 +87,7 @@ impl RawImage {
|
||||||
color_type: ColorType,
|
color_type: ColorType,
|
||||||
bit_depth: BitDepth,
|
bit_depth: BitDepth,
|
||||||
data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
) -> Result<Self, PngError> {
|
) -> PngResult<Self> {
|
||||||
// Validate bit depth
|
// Validate bit depth
|
||||||
let valid_depth = match color_type {
|
let valid_depth = match color_type {
|
||||||
ColorType::Grayscale { .. } => true,
|
ColorType::Grayscale { .. } => true,
|
||||||
|
|
@ -165,40 +166,15 @@ impl RawImage {
|
||||||
/// Perform optimization on the input file using the options provided
|
/// Perform optimization on the input file using the options provided
|
||||||
///
|
///
|
||||||
/// Returns the original and optimized file sizes
|
/// Returns the original and optimized file sizes
|
||||||
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(usize, usize)> {
|
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> OptimizationResult {
|
||||||
// Read in the file and try to decode as PNG.
|
// Read in the file and try to decode as PNG.
|
||||||
info!("Processing: {input}");
|
info!("Processing: {input}");
|
||||||
|
|
||||||
let deadline = Arc::new(Deadline::new(opts.timeout));
|
let deadline = Arc::new(Deadline::new(opts.timeout));
|
||||||
|
|
||||||
// grab metadata before even opening input file to preserve atime
|
|
||||||
let opt_metadata_preserved;
|
|
||||||
let in_data = match *input {
|
let in_data = match *input {
|
||||||
InFile::Path(ref input_path) => {
|
InFile::Path(ref input_path) => PngData::read_file(input_path)?,
|
||||||
if matches!(
|
|
||||||
output,
|
|
||||||
OutFile::Path {
|
|
||||||
preserve_attrs: true,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
opt_metadata_preserved = input_path
|
|
||||||
.metadata()
|
|
||||||
.map_err(|err| {
|
|
||||||
// Fail if metadata cannot be preserved
|
|
||||||
PngError::new(&format!(
|
|
||||||
"Unable to read metadata from input file {input_path:?}: {err}"
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.map(Some)?;
|
|
||||||
trace!("preserving metadata: {opt_metadata_preserved:?}");
|
|
||||||
} else {
|
|
||||||
opt_metadata_preserved = None;
|
|
||||||
}
|
|
||||||
PngData::read_file(input_path)?
|
|
||||||
}
|
|
||||||
InFile::StdIn => {
|
InFile::StdIn => {
|
||||||
opt_metadata_preserved = None;
|
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
stdin()
|
stdin()
|
||||||
.read_to_end(&mut data)
|
.read_to_end(&mut data)
|
||||||
|
|
@ -220,7 +196,7 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
|
||||||
(OutFile::Path { path, .. }, InFile::Path(input_path))
|
(OutFile::Path { path, .. }, InFile::Path(input_path))
|
||||||
if path.as_ref().is_none_or(|p| p == input_path) =>
|
if path.as_ref().is_none_or(|p| p == input_path) =>
|
||||||
{
|
{
|
||||||
info!("{input}: Could not optimize further, no change written");
|
info!("Could not optimize further, no change written: {input}");
|
||||||
return Ok((in_length, in_length));
|
return Ok((in_length, in_length));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|
@ -252,18 +228,34 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
|
||||||
buffer
|
buffer
|
||||||
.write_all(&optimized_output)
|
.write_all(&optimized_output)
|
||||||
.map_err(|e| PngError::WriteFailed("stdout".into(), e))?;
|
.map_err(|e| PngError::WriteFailed("stdout".into(), e))?;
|
||||||
|
info!("{savings}: stdout");
|
||||||
}
|
}
|
||||||
(OutFile::Path { path, .. }, _) => {
|
(
|
||||||
|
OutFile::Path {
|
||||||
|
path,
|
||||||
|
preserve_attrs,
|
||||||
|
},
|
||||||
|
_,
|
||||||
|
) => {
|
||||||
|
let input_metadata = if *preserve_attrs {
|
||||||
|
input.path().and_then(|in_path| {
|
||||||
|
let meta = in_path.metadata();
|
||||||
|
if let Err(e) = &meta {
|
||||||
|
warn!("Unable to read metadata from {in_path:?}: {e}");
|
||||||
|
}
|
||||||
|
meta.ok()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let output_path = path
|
let output_path = path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or_else(|| input.path().unwrap(), PathBuf::as_path);
|
.map_or_else(|| input.path().unwrap(), PathBuf::as_path);
|
||||||
let out_file = File::create(output_path)
|
let out_file = File::create(output_path)
|
||||||
.map_err(|err| PngError::WriteFailed(output_path.display().to_string(), err))?;
|
.map_err(|err| PngError::WriteFailed(output_path.display().to_string(), err))?;
|
||||||
if let Some(metadata_input) = &opt_metadata_preserved {
|
|
||||||
copy_permissions(metadata_input, &out_file)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut buffer = BufWriter::new(out_file);
|
let mut buffer = BufWriter::new(&out_file);
|
||||||
buffer
|
buffer
|
||||||
.write_all(&optimized_output)
|
.write_all(&optimized_output)
|
||||||
// flush BufWriter so IO errors don't get swallowed silently on close() by drop!
|
// flush BufWriter so IO errors don't get swallowed silently on close() by drop!
|
||||||
|
|
@ -271,8 +263,18 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
|
||||||
.map_err(|e| PngError::WriteFailed(output_path.display().to_string(), e))?;
|
.map_err(|e| PngError::WriteFailed(output_path.display().to_string(), e))?;
|
||||||
// force drop and thereby closing of file handle before modifying any timestamp
|
// force drop and thereby closing of file handle before modifying any timestamp
|
||||||
std::mem::drop(buffer);
|
std::mem::drop(buffer);
|
||||||
if let Some(metadata_input) = &opt_metadata_preserved {
|
|
||||||
copy_times(metadata_input, output_path)?;
|
if let Some(metadata_input) = &input_metadata {
|
||||||
|
let set_time = metadata_input
|
||||||
|
.modified()
|
||||||
|
.and_then(|m| out_file.set_modified(m));
|
||||||
|
if let Err(e) = set_time {
|
||||||
|
warn!("Unable to set modification time on {output_path:?}: {e}");
|
||||||
|
}
|
||||||
|
let set_perm = out_file.set_permissions(metadata_input.permissions());
|
||||||
|
if let Err(e) = set_perm {
|
||||||
|
warn!("Unable to set permissions on {output_path:?}: {e}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
info!("{}: {}", savings, output_path.display());
|
info!("{}: {}", savings, output_path.display());
|
||||||
}
|
}
|
||||||
|
|
@ -642,29 +644,3 @@ fn recompress_frames(
|
||||||
const fn is_fully_optimized(original_size: usize, optimized_size: usize, opts: &Options) -> bool {
|
const fn is_fully_optimized(original_size: usize, optimized_size: usize, opts: &Options) -> bool {
|
||||||
original_size <= optimized_size && !opts.force
|
original_size <= optimized_size && !opts.force
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy_permissions(metadata_input: &Metadata, out_file: &File) -> PngResult<()> {
|
|
||||||
out_file
|
|
||||||
.set_permissions(metadata_input.permissions())
|
|
||||||
.map_err(|err_io| {
|
|
||||||
PngError::new(&format!(
|
|
||||||
"Unable to set permissions for output file: {err_io}"
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "filetime"))]
|
|
||||||
const fn copy_times(_: &Metadata, _: &Path) -> PngResult<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "filetime")]
|
|
||||||
fn copy_times(input_path_meta: &Metadata, out_path: &Path) -> PngResult<()> {
|
|
||||||
let mtime = filetime::FileTime::from_last_modification_time(input_path_meta);
|
|
||||||
trace!("attempting to set file modification time: {mtime:?}");
|
|
||||||
filetime::set_file_mtime(out_path, mtime).map_err(|err_io| {
|
|
||||||
PngError::new(&format!(
|
|
||||||
"Unable to set file times on {out_path:?}: {err_io}"
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
||||||
327
src/main.rs
327
src/main.rs
|
|
@ -6,9 +6,10 @@ use std::num::NonZeroU64;
|
||||||
use std::{
|
use std::{
|
||||||
ffi::{OsStr, OsString},
|
ffi::{OsStr, OsString},
|
||||||
fs::DirBuilder,
|
fs::DirBuilder,
|
||||||
io::Write,
|
io::{IsTerminal, Write, stdout},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process::ExitCode,
|
process::ExitCode,
|
||||||
|
sync::atomic::{AtomicUsize, Ordering::AcqRel},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -18,7 +19,9 @@ use indexmap::IndexSet;
|
||||||
use log::{Level, LevelFilter, error, warn};
|
use log::{Level, LevelFilter, error, warn};
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
use oxipng::ZopfliOptions;
|
use oxipng::ZopfliOptions;
|
||||||
use oxipng::{Deflater, FilterStrategy, InFile, Options, OutFile, PngError, StripChunks};
|
use oxipng::{
|
||||||
|
Deflater, FilterStrategy, InFile, OptimizationResult, Options, OutFile, PngError, StripChunks,
|
||||||
|
};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
|
||||||
use crate::cli::DISPLAY_CHUNKS;
|
use crate::cli::DISPLAY_CHUNKS;
|
||||||
|
|
@ -35,7 +38,7 @@ fn main() -> ExitCode {
|
||||||
.after_long_help("")
|
.after_long_help("")
|
||||||
.get_matches_from(std::env::args());
|
.get_matches_from(std::env::args());
|
||||||
|
|
||||||
let (out_file, out_dir, opts) = match parse_opts_into_struct(&matches) {
|
let (mut out_file, out_dir, opts) = match parse_opts_into_struct(&matches) {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
Err(x) => {
|
Err(x) => {
|
||||||
error!("{x}");
|
error!("{x}");
|
||||||
|
|
@ -43,51 +46,128 @@ fn main() -> ExitCode {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let files = collect_files(
|
// Determine input and output
|
||||||
|
let file_args = matches.get_many::<PathBuf>("files").unwrap().cloned();
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
matches
|
let inputs: Vec<_> = file_args.flat_map(apply_glob_pattern).collect();
|
||||||
.get_many::<PathBuf>("files")
|
|
||||||
.unwrap()
|
|
||||||
.cloned()
|
|
||||||
.flat_map(apply_glob_pattern)
|
|
||||||
.collect(),
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
matches
|
let inputs: Vec<_> = file_args.collect();
|
||||||
.get_many::<PathBuf>("files")
|
let using_stdin = inputs.len() == 1 && inputs[0].to_str() == Some("-");
|
||||||
.unwrap()
|
if using_stdin && out_dir.is_some() {
|
||||||
.cloned()
|
error!("Cannot use --dir when reading from stdin.");
|
||||||
.collect(),
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
if using_stdin && matches!(out_file, OutFile::Path { path: None, .. }) {
|
||||||
|
out_file = OutFile::StdOut;
|
||||||
|
}
|
||||||
|
let using_stdout = matches!(out_file, OutFile::StdOut);
|
||||||
|
let json = matches.get_flag("json");
|
||||||
|
if using_stdout && json {
|
||||||
|
error!("Cannot use --json when writing to stdout.");
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
let files = if using_stdin {
|
||||||
|
vec![(InFile::StdIn, out_file)]
|
||||||
|
} else {
|
||||||
|
collect_files(
|
||||||
|
inputs,
|
||||||
&out_dir,
|
&out_dir,
|
||||||
&out_file,
|
&out_file,
|
||||||
matches.get_flag("recursive"),
|
matches.get_flag("recursive"),
|
||||||
true,
|
true,
|
||||||
);
|
)
|
||||||
|
|
||||||
let parallel_files = matches.get_flag("parallel-files");
|
|
||||||
let summary = if parallel_files {
|
|
||||||
files
|
|
||||||
.into_par_iter()
|
|
||||||
.map(|(input, output)| process_file(&input, &output, &opts))
|
|
||||||
.min()
|
|
||||||
} else {
|
|
||||||
files
|
|
||||||
.into_iter()
|
|
||||||
.map(|(input, output)| process_file(&input, &output, &opts))
|
|
||||||
.min()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match summary.unwrap_or(OptimizationResult::Skipped) {
|
let is_verbose = matches.get_count("verbose") > 0;
|
||||||
OptimizationResult::Ok => ExitCode::SUCCESS,
|
let print_summary = !matches.get_flag("quiet") && !using_stdout;
|
||||||
OptimizationResult::Failed => ExitCode::FAILURE,
|
let print_progress = print_summary && !is_verbose && stdout().is_terminal();
|
||||||
OptimizationResult::Skipped => ExitCode::from(3),
|
let total_files = files.len();
|
||||||
|
let num_processed = AtomicUsize::new(0);
|
||||||
|
if print_progress {
|
||||||
|
print!("Files processed: 0/{}...", total_files);
|
||||||
|
stdout().flush().ok();
|
||||||
}
|
}
|
||||||
}
|
let process = |(input, output): &(InFile, OutFile)| {
|
||||||
|
let result = process_file(input, output, &opts);
|
||||||
|
if print_progress && matches!(result, OptimizationResult::Ok(_)) {
|
||||||
|
let value = num_processed.fetch_add(1, AcqRel) + 1;
|
||||||
|
print!("\rFiles processed: {}/{}...", value, total_files);
|
||||||
|
stdout().flush().ok();
|
||||||
|
}
|
||||||
|
result
|
||||||
|
};
|
||||||
|
let results: Vec<OptimizationResult> = if matches.get_flag("parallel-files") {
|
||||||
|
files.par_iter().map(process).collect()
|
||||||
|
} else {
|
||||||
|
files.iter().map(process).collect()
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Ord, PartialOrd)]
|
// Collect stats
|
||||||
enum OptimizationResult {
|
let mut num_succeeded = 0;
|
||||||
Ok,
|
let mut num_not_optimized = 0;
|
||||||
Failed,
|
let mut num_failed = 0;
|
||||||
Skipped,
|
let mut total_in: i64 = 0;
|
||||||
|
let mut total_out: i64 = 0;
|
||||||
|
for result in &results {
|
||||||
|
match result {
|
||||||
|
Ok((insize, outsize)) => {
|
||||||
|
num_succeeded += 1;
|
||||||
|
total_in += *insize as i64;
|
||||||
|
total_out += *outsize as i64;
|
||||||
|
if !opts.force && insize == outsize {
|
||||||
|
num_not_optimized += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(PngError::C2PAMetadataPreventsChanges | PngError::InflatedDataTooLong(_)) => {}
|
||||||
|
Err(_) => num_failed += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print results
|
||||||
|
if json {
|
||||||
|
json_output(&files, &results);
|
||||||
|
} else if print_summary {
|
||||||
|
let in_bytes = format_bytes(total_in, true);
|
||||||
|
let out_bytes = format_bytes(total_out, true);
|
||||||
|
let saved = total_in - total_out;
|
||||||
|
let saved_bytes = format_bytes(saved, false);
|
||||||
|
let percent = if total_in > 0 {
|
||||||
|
saved as f64 / total_in as f64 * 100_f64
|
||||||
|
} else {
|
||||||
|
0_f64
|
||||||
|
};
|
||||||
|
if is_verbose {
|
||||||
|
println!("--------------------");
|
||||||
|
}
|
||||||
|
println!("\rFiles processed: {num_succeeded}/{total_files} ");
|
||||||
|
println!("Input size: {}", in_bytes);
|
||||||
|
println!("Output size: {}", out_bytes);
|
||||||
|
println!("Total saved: {} ({:.2}%)", saved_bytes, percent);
|
||||||
|
if num_not_optimized == 1 {
|
||||||
|
println!("({num_not_optimized} file could not be optimized further)");
|
||||||
|
} else if num_not_optimized > 0 {
|
||||||
|
println!("({num_not_optimized} files could not be optimized further)");
|
||||||
|
}
|
||||||
|
if matches.get_flag("dry-run") {
|
||||||
|
println!("Dry run, no changes saved");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For optimizing single files, this will return the correct exit code always.
|
||||||
|
// For recursive optimization, the correct choice is a bit subjective.
|
||||||
|
// We're choosing to return a 0 exit code if ANY file in the set
|
||||||
|
// runs correctly.
|
||||||
|
// The reason for this is that recursion may pick up files that are not
|
||||||
|
// PNG files, and return an error for them.
|
||||||
|
// We don't really want to return an error code for those files.
|
||||||
|
if num_succeeded > 0 {
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
} else if num_failed > 0 {
|
||||||
|
ExitCode::FAILURE
|
||||||
|
} else {
|
||||||
|
ExitCode::from(3)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_files(
|
fn collect_files(
|
||||||
|
|
@ -98,10 +178,8 @@ fn collect_files(
|
||||||
top_level: bool, //explicitly specify files
|
top_level: bool, //explicitly specify files
|
||||||
) -> Vec<(InFile, OutFile)> {
|
) -> Vec<(InFile, OutFile)> {
|
||||||
let mut in_out_pairs = Vec::new();
|
let mut in_out_pairs = Vec::new();
|
||||||
let allow_stdin = top_level && files.len() == 1;
|
|
||||||
for input in files {
|
for input in files {
|
||||||
let using_stdin = allow_stdin && input.to_str() == Some("-");
|
if input.is_dir() {
|
||||||
if !using_stdin && input.is_dir() {
|
|
||||||
if recursive {
|
if recursive {
|
||||||
match input.read_dir() {
|
match input.read_dir() {
|
||||||
Ok(dir) => {
|
Ok(dir) => {
|
||||||
|
|
@ -118,6 +196,15 @@ fn collect_files(
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip non png files if not given on top level
|
||||||
|
if !top_level && {
|
||||||
|
let extension = input.extension().map(OsStr::to_ascii_lowercase);
|
||||||
|
extension != Some(OsString::from("png")) && extension != Some(OsString::from("apng"))
|
||||||
|
} {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let out_file =
|
let out_file =
|
||||||
if let (Some(out_dir), &OutFile::Path { preserve_attrs, .. }) = (out_dir, out_file) {
|
if let (Some(out_dir), &OutFile::Path { preserve_attrs, .. }) = (out_dir, out_file) {
|
||||||
let path = Some(out_dir.join(input.file_name().unwrap()));
|
let path = Some(out_dir.join(input.file_name().unwrap()));
|
||||||
|
|
@ -128,19 +215,7 @@ fn collect_files(
|
||||||
} else {
|
} else {
|
||||||
(*out_file).clone()
|
(*out_file).clone()
|
||||||
};
|
};
|
||||||
let in_file = if using_stdin {
|
let in_file = InFile::Path(input);
|
||||||
InFile::StdIn
|
|
||||||
} else {
|
|
||||||
// Skip non png files if not given on top level
|
|
||||||
if !top_level && {
|
|
||||||
let extension = input.extension().map(OsStr::to_ascii_lowercase);
|
|
||||||
extension != Some(OsString::from("png"))
|
|
||||||
&& extension != Some(OsString::from("apng"))
|
|
||||||
} {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
InFile::Path(input)
|
|
||||||
};
|
|
||||||
in_out_pairs.push((in_file, out_file));
|
in_out_pairs.push((in_file, out_file));
|
||||||
}
|
}
|
||||||
in_out_pairs
|
in_out_pairs
|
||||||
|
|
@ -165,8 +240,9 @@ fn parse_opts_into_struct(
|
||||||
) -> Result<(OutFile, Option<PathBuf>, Options), String> {
|
) -> Result<(OutFile, Option<PathBuf>, Options), String> {
|
||||||
let log_level = match matches.get_count("verbose") {
|
let log_level = match matches.get_count("verbose") {
|
||||||
_ if matches.get_flag("quiet") => LevelFilter::Off,
|
_ if matches.get_flag("quiet") => LevelFilter::Off,
|
||||||
0 => LevelFilter::Info,
|
0 => LevelFilter::Warn,
|
||||||
1 => LevelFilter::Debug,
|
1 => LevelFilter::Info,
|
||||||
|
2 => LevelFilter::Debug,
|
||||||
_ => LevelFilter::Trace,
|
_ => LevelFilter::Trace,
|
||||||
};
|
};
|
||||||
env_logger::builder()
|
env_logger::builder()
|
||||||
|
|
@ -175,7 +251,8 @@ fn parse_opts_into_struct(
|
||||||
match record.level() {
|
match record.level() {
|
||||||
Level::Error | Level::Warn => {
|
Level::Error | Level::Warn => {
|
||||||
let style = buf.default_level_style(record.level());
|
let style = buf.default_level_style(record.level());
|
||||||
writeln!(buf, "{style}{}{style:#}", record.args())
|
// Prepend carriage return to clear progress line
|
||||||
|
writeln!(buf, "\r{style}{}{style:#}", record.args())
|
||||||
}
|
}
|
||||||
// Leave info, debug and trace unstyled
|
// Leave info, debug and trace unstyled
|
||||||
_ => writeln!(buf, "{}", record.args()),
|
_ => writeln!(buf, "{}", record.args()),
|
||||||
|
|
@ -353,6 +430,16 @@ fn parse_opts_into_struct(
|
||||||
let iterations_without_improvement = *matches
|
let iterations_without_improvement = *matches
|
||||||
.get_one::<NonZeroU64>("iterations-without-improvement")
|
.get_one::<NonZeroU64>("iterations-without-improvement")
|
||||||
.unwrap_or(&NonZeroU64::MAX);
|
.unwrap_or(&NonZeroU64::MAX);
|
||||||
|
|
||||||
|
if iterations_without_improvement > iteration_count
|
||||||
|
&& iterations_without_improvement != NonZeroU64::MAX
|
||||||
|
{
|
||||||
|
warn!(
|
||||||
|
"--ziwi ({}) is higher than --zi ({}) and will never be reached.",
|
||||||
|
iterations_without_improvement, iteration_count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
opts.deflater = Deflater::Zopfli(ZopfliOptions {
|
opts.deflater = Deflater::Zopfli(ZopfliOptions {
|
||||||
iteration_count,
|
iteration_count,
|
||||||
iterations_without_improvement,
|
iterations_without_improvement,
|
||||||
|
|
@ -440,26 +527,128 @@ fn process_file(input: &InFile, output: &OutFile, opts: &Options) -> Optimizatio
|
||||||
if let (Some(max_size), InFile::Path(path)) = (opts.max_decompressed_size, input) {
|
if let (Some(max_size), InFile::Path(path)) = (opts.max_decompressed_size, input) {
|
||||||
if path.metadata().is_ok_and(|m| m.len() > max_size as u64) {
|
if path.metadata().is_ok_and(|m| m.len() > max_size as u64) {
|
||||||
warn!("{input}: Skipped: File exceeds the maximum size ({max_size} bytes)");
|
warn!("{input}: Skipped: File exceeds the maximum size ({max_size} bytes)");
|
||||||
return OptimizationResult::Skipped;
|
return Err(PngError::InflatedDataTooLong(max_size));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match oxipng::optimize(input, output, opts) {
|
let result = oxipng::optimize(input, output, opts);
|
||||||
// For optimizing single files, this will return the correct exit code always.
|
match &result {
|
||||||
// For recursive optimization, the correct choice is a bit subjective.
|
Ok(_) => {}
|
||||||
// We're choosing to return a 0 exit code if ANY file in the set
|
|
||||||
// runs correctly.
|
|
||||||
// The reason for this is that recursion may pick up files that are not
|
|
||||||
// PNG files, and return an error for them.
|
|
||||||
// We don't really want to return an error code for those files.
|
|
||||||
Ok(_) => OptimizationResult::Ok,
|
|
||||||
Err(e @ PngError::C2PAMetadataPreventsChanges | e @ PngError::InflatedDataTooLong(_)) => {
|
Err(e @ PngError::C2PAMetadataPreventsChanges | e @ PngError::InflatedDataTooLong(_)) => {
|
||||||
warn!("{input}: Skipped: {e}");
|
warn!("{input}: Skipped: {e}");
|
||||||
OptimizationResult::Skipped
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{input}: {e}");
|
error!("{input}: {e}");
|
||||||
OptimizationResult::Failed
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write optimization results as json.
|
||||||
|
/// ```
|
||||||
|
/// {
|
||||||
|
/// "results": [
|
||||||
|
/// {
|
||||||
|
/// "input": string,
|
||||||
|
/// "status": "success",
|
||||||
|
/// "output": string|null,
|
||||||
|
/// "insize": number,
|
||||||
|
/// "outsize": number
|
||||||
|
/// },
|
||||||
|
/// {
|
||||||
|
/// "input": string,
|
||||||
|
/// "status": "error",
|
||||||
|
/// "error": string
|
||||||
|
/// }
|
||||||
|
/// ]
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
fn json_output(files: &[(InFile, OutFile)], results: &[OptimizationResult]) {
|
||||||
|
print!(r#"{{"results":["#);
|
||||||
|
let mut first = true;
|
||||||
|
results
|
||||||
|
.iter()
|
||||||
|
.zip(files)
|
||||||
|
.for_each(|(result, (input, output))| {
|
||||||
|
if !first {
|
||||||
|
print!(",");
|
||||||
|
}
|
||||||
|
print!(r#"{{"input":"{}","#, json_escape(&input.to_string()));
|
||||||
|
match result {
|
||||||
|
Ok((insize, outsize)) => {
|
||||||
|
let outpath = match output {
|
||||||
|
OutFile::None => "null".to_owned(),
|
||||||
|
OutFile::Path { path: None, .. } => {
|
||||||
|
format!(r#""{}""#, json_escape(&input.to_string()))
|
||||||
|
}
|
||||||
|
OutFile::Path { path: Some(p), .. } => {
|
||||||
|
format!(r#""{}""#, json_escape(&p.display().to_string()))
|
||||||
|
}
|
||||||
|
OutFile::StdOut => unreachable!(),
|
||||||
|
};
|
||||||
|
print!(
|
||||||
|
r#""status":"success","output":{},"insize":{},"outsize":{}}}"#,
|
||||||
|
outpath, insize, outsize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
print!(
|
||||||
|
r#""status":"error","error":"{}"}}"#,
|
||||||
|
json_escape(&e.to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
});
|
||||||
|
print!("]}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_escape(string: &str) -> String {
|
||||||
|
string
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("\"", "\\\"")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\t", "\\t")
|
||||||
|
.replace("\r", "\\r")
|
||||||
|
.replace("\x08", "\\b")
|
||||||
|
.replace("\x0c", "\\f")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format byte counts as IEC units to 3 significant figures.
|
||||||
|
fn format_bytes(count: i64, include_raw: bool) -> String {
|
||||||
|
const K: i64 = 1 << 10;
|
||||||
|
const M: i64 = 1 << 20;
|
||||||
|
const G: i64 = 1 << 30;
|
||||||
|
fn format_3sf(value: f64) -> String {
|
||||||
|
match value.abs() {
|
||||||
|
..9.995 => format!("{:.2}", value),
|
||||||
|
9.995..99.95 => format!("{:.1}", value),
|
||||||
|
_ => format!("{:.0}", value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let formatted = match count.abs() {
|
||||||
|
..K => format!("{} bytes", count),
|
||||||
|
K..M => format!("{} KiB", format_3sf(count as f64 / K as f64)),
|
||||||
|
M..G => format!("{} MiB", format_3sf(count as f64 / M as f64)),
|
||||||
|
_ => format!("{} GiB", format_3sf(count as f64 / G as f64)),
|
||||||
|
};
|
||||||
|
if include_raw && count.abs() >= K {
|
||||||
|
format!("{} ({} bytes)", formatted, count)
|
||||||
|
} else {
|
||||||
|
formatted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::format_bytes;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_bytes() {
|
||||||
|
assert_eq!(format_bytes(1023, false), "1023 bytes");
|
||||||
|
assert_eq!(format_bytes(800_000, false), "781 KiB");
|
||||||
|
assert_eq!(format_bytes(12_500_000, false), "11.9 MiB");
|
||||||
|
assert_eq!(format_bytes(2_000_000_000, false), "1.86 GiB");
|
||||||
|
assert_eq!(format_bytes(-1024, false), "-1.00 KiB");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
use std::{fs, io::Write, path::Path, sync::Arc};
|
use std::{fs, path::Path, sync::Arc};
|
||||||
|
|
||||||
use bitvec::bitarr;
|
|
||||||
use libdeflater::{CompressionLvl, Compressor};
|
use libdeflater::{CompressionLvl, Compressor};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use rgb::ComponentSlice;
|
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Options,
|
Options, PngResult,
|
||||||
apng::*,
|
apng::*,
|
||||||
colors::{BitDepth, ColorType},
|
colors::{BitDepth, ColorType},
|
||||||
deflate,
|
deflate,
|
||||||
|
|
@ -45,18 +43,18 @@ pub struct PngData {
|
||||||
impl PngData {
|
impl PngData {
|
||||||
/// Create a new `PngData` struct by opening a file
|
/// Create a new `PngData` struct by opening a file
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn new(filepath: &Path, opts: &Options) -> Result<Self, PngError> {
|
pub fn new(filepath: &Path, opts: &Options) -> PngResult<Self> {
|
||||||
let byte_data = Self::read_file(filepath)?;
|
let byte_data = Self::read_file(filepath)?;
|
||||||
|
|
||||||
Self::from_slice(&byte_data, opts)
|
Self::from_slice(&byte_data, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_file(filepath: &Path) -> Result<Vec<u8>, PngError> {
|
pub fn read_file(filepath: &Path) -> PngResult<Vec<u8>> {
|
||||||
fs::read(filepath).map_err(|e| PngError::ReadFailed(filepath.display().to_string(), e))
|
fs::read(filepath).map_err(|e| PngError::ReadFailed(filepath.display().to_string(), e))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new `PngData` struct by reading a slice
|
/// Create a new `PngData` struct by reading a slice
|
||||||
pub fn from_slice(byte_data: &[u8], opts: &Options) -> Result<Self, PngError> {
|
pub fn from_slice(byte_data: &[u8], opts: &Options) -> PngResult<Self> {
|
||||||
let mut byte_offset: usize = 0;
|
let mut byte_offset: usize = 0;
|
||||||
// Test that png header is valid
|
// Test that png header is valid
|
||||||
let header = byte_data.get(0..8).ok_or(PngError::TruncatedData)?;
|
let header = byte_data.get(0..8).ok_or(PngError::TruncatedData)?;
|
||||||
|
|
@ -165,17 +163,15 @@ impl PngData {
|
||||||
let mut output = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
|
let mut output = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
|
||||||
// IHDR
|
// IHDR
|
||||||
let mut ihdr_data = Vec::with_capacity(13);
|
let mut ihdr_data = Vec::with_capacity(13);
|
||||||
ihdr_data.write_all(&self.raw.ihdr.width.to_be_bytes()).ok();
|
ihdr_data.extend_from_slice(&self.raw.ihdr.width.to_be_bytes());
|
||||||
ihdr_data
|
ihdr_data.extend_from_slice(&self.raw.ihdr.height.to_be_bytes());
|
||||||
.write_all(&self.raw.ihdr.height.to_be_bytes())
|
ihdr_data.extend_from_slice(&[self.raw.ihdr.bit_depth as u8]);
|
||||||
.ok();
|
ihdr_data.extend_from_slice(&[
|
||||||
ihdr_data.write_all(&[self.raw.ihdr.bit_depth as u8]).ok();
|
self.raw.ihdr.color_type.png_header_code(),
|
||||||
ihdr_data
|
0, // Compression -- deflate
|
||||||
.write_all(&[self.raw.ihdr.color_type.png_header_code()])
|
0, // Filter method -- 5-way adaptive filtering
|
||||||
.ok();
|
self.raw.ihdr.interlaced as u8,
|
||||||
ihdr_data.write_all(&[0]).ok(); // Compression -- deflate
|
]);
|
||||||
ihdr_data.write_all(&[0]).ok(); // Filter method -- 5-way adaptive filtering
|
|
||||||
ihdr_data.write_all(&[self.raw.ihdr.interlaced as u8]).ok();
|
|
||||||
write_png_block(b"IHDR", &ihdr_data, &mut output);
|
write_png_block(b"IHDR", &ihdr_data, &mut output);
|
||||||
// Ancillary chunks - split into those that come before IDAT and those that come after
|
// Ancillary chunks - split into those that come before IDAT and those that come after
|
||||||
let mut aux_split = self.aux_chunks.split(|c| &c.name == b"IDAT");
|
let mut aux_split = self.aux_chunks.split(|c| &c.name == b"IDAT");
|
||||||
|
|
@ -194,7 +190,7 @@ impl PngData {
|
||||||
ColorType::Indexed { palette } => {
|
ColorType::Indexed { palette } => {
|
||||||
let mut palette_data = Vec::with_capacity(palette.len() * 3);
|
let mut palette_data = Vec::with_capacity(palette.len() * 3);
|
||||||
for px in palette {
|
for px in palette {
|
||||||
palette_data.extend_from_slice(px.rgb().as_slice());
|
palette_data.extend_from_slice(px.rgb().as_ref());
|
||||||
}
|
}
|
||||||
write_png_block(b"PLTE", &palette_data, &mut output);
|
write_png_block(b"PLTE", &palette_data, &mut output);
|
||||||
if let Some(last_trns) = palette.iter().rposition(|px| px.a != 255) {
|
if let Some(last_trns) = palette.iter().rposition(|px| px.a != 255) {
|
||||||
|
|
@ -250,7 +246,7 @@ impl PngData {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PngImage {
|
impl PngImage {
|
||||||
pub fn new(ihdr: IhdrData, compressed_data: &[u8]) -> Result<Self, PngError> {
|
pub fn new(ihdr: IhdrData, compressed_data: &[u8]) -> PngResult<Self> {
|
||||||
let raw_data = deflate::inflate(compressed_data, ihdr.raw_data_size())?;
|
let raw_data = deflate::inflate(compressed_data, ihdr.raw_data_size())?;
|
||||||
|
|
||||||
// Reject files with incorrect width/height or truncated data
|
// Reject files with incorrect width/height or truncated data
|
||||||
|
|
@ -334,7 +330,7 @@ impl PngImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reverse all filters applied on the image, returning an unfiltered IDAT bytestream
|
/// Reverse all filters applied on the image, returning an unfiltered IDAT bytestream
|
||||||
fn unfilter_image(&self) -> Result<Vec<u8>, PngError> {
|
fn unfilter_image(&self) -> PngResult<Vec<u8>> {
|
||||||
let mut unfiltered = Vec::with_capacity(self.data.len());
|
let mut unfiltered = Vec::with_capacity(self.data.len());
|
||||||
let bpp = self.bytes_per_channel() * self.channels_per_pixel();
|
let bpp = self.bytes_per_channel() * self.channels_per_pixel();
|
||||||
let mut last_line: Vec<u8> = Vec::new();
|
let mut last_line: Vec<u8> = Vec::new();
|
||||||
|
|
@ -376,6 +372,12 @@ impl PngImage {
|
||||||
let mut f_buf = Vec::new();
|
let mut f_buf = Vec::new();
|
||||||
// For heuristic strategies, keep track of the actual filter used for each line
|
// For heuristic strategies, keep track of the actual filter used for each line
|
||||||
let mut filters_used = Vec::new();
|
let mut filters_used = Vec::new();
|
||||||
|
// Pre-allocate buffers for the Bigrams strategy to avoid per-line allocations
|
||||||
|
let (mut bigram_seen, mut bigram_touched) = if matches!(strategy, FilterStrategy::Bigrams) {
|
||||||
|
(vec![false; 0x10000], Vec::<u16>::new())
|
||||||
|
} else {
|
||||||
|
(Vec::new(), Vec::new())
|
||||||
|
};
|
||||||
for (i, line) in self.scan_lines(false).enumerate() {
|
for (i, line) in self.scan_lines(false).enumerate() {
|
||||||
if prev_pass != line.pass || line.data.len() != prev_line.len() {
|
if prev_pass != line.pass || line.data.len() != prev_line.len() {
|
||||||
prev_line = vec![0; line.data.len()];
|
prev_line = vec![0; line.data.len()];
|
||||||
|
|
@ -467,18 +469,29 @@ impl PngImage {
|
||||||
let mut best_size = usize::MAX;
|
let mut best_size = usize::MAX;
|
||||||
for f in try_filters {
|
for f in try_filters {
|
||||||
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
|
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
|
||||||
let mut set = bitarr![0; 0x10000];
|
let mut count = 0;
|
||||||
for pair in f_buf.windows(2) {
|
for pair in f_buf.windows(2) {
|
||||||
let bigram = ((pair[0] as usize) << 8) | pair[1] as usize;
|
let bigram = ((pair[0] as usize) << 8) | pair[1] as usize;
|
||||||
set.set(bigram, true);
|
if !bigram_seen[bigram] {
|
||||||
|
count += 1;
|
||||||
|
if count >= best_size {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
let size = set.count_ones();
|
bigram_seen[bigram] = true;
|
||||||
if size < best_size {
|
bigram_touched.push(bigram as u16);
|
||||||
best_size = size;
|
}
|
||||||
|
}
|
||||||
|
if count < best_size {
|
||||||
|
best_size = count;
|
||||||
std::mem::swap(&mut best_line, &mut f_buf);
|
std::mem::swap(&mut best_line, &mut f_buf);
|
||||||
best_line_raw.clone_from(&line_data);
|
best_line_raw.clone_from(&line_data);
|
||||||
best_filter = *f;
|
best_filter = *f;
|
||||||
}
|
}
|
||||||
|
// Clear only the entries that were touched
|
||||||
|
for &idx in &bigram_touched {
|
||||||
|
bigram_seen[idx as usize] = false;
|
||||||
|
}
|
||||||
|
bigram_touched.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FilterStrategy::BigEnt => {
|
FilterStrategy::BigEnt => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::hash::{BuildHasherDefault, Hash};
|
use std::hash::{BuildHasherDefault, Hash};
|
||||||
|
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use rgb::{ComponentMap, ComponentSlice, FromSlice, RGB, RGBA, alt::Gray};
|
use rgb::{ComponentMap, FromSlice, RGB, RGBA, alt::Gray};
|
||||||
use rustc_hash::FxHasher;
|
use rustc_hash::FxHasher;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -193,7 +193,7 @@ pub fn indexed_to_channels(
|
||||||
let mut data = Vec::with_capacity(out_size);
|
let mut data = Vec::with_capacity(out_size);
|
||||||
for b in &png.data {
|
for b in &png.data {
|
||||||
let color = palette.get(*b as usize).unwrap_or(&black);
|
let color = palette.get(*b as usize).unwrap_or(&black);
|
||||||
data.extend_from_slice(&color.as_slice()[ch_start..=ch_end]);
|
data.extend_from_slice(&color.as_ref()[ch_start..=ch_end]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(PngImage {
|
Some(PngImage {
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ pub(crate) fn perform_reductions(
|
||||||
// Attempt to reduce to indexed
|
// Attempt to reduce to indexed
|
||||||
// Keep the existing `png` var in case it is grayscale - we can test both for depth reduction later
|
// Keep the existing `png` var in case it is grayscale - we can test both for depth reduction later
|
||||||
let mut indexed = None;
|
let mut indexed = None;
|
||||||
if opts.color_type_reduction && !deadline.passed() {
|
if opts.color_type_reduction && opts.palette_reduction && !deadline.passed() {
|
||||||
if let Some(reduced) = reduced_to_indexed(&png, opts.grayscale_reduction) {
|
if let Some(reduced) = reduced_to_indexed(&png, opts.grayscale_reduction) {
|
||||||
// Make sure the palette gets sorted (but don't bother evaluating both results)
|
// Make sure the palette gets sorted (but don't bother evaluating both results)
|
||||||
let new = Arc::new(sorted_palette(&reduced).unwrap_or(reduced));
|
let new = Arc::new(sorted_palette(&reduced).unwrap_or(reduced));
|
||||||
|
|
|
||||||
BIN
tests/files/json.png
Normal file
BIN
tests/files/json.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
|
|
@ -1,11 +1,12 @@
|
||||||
|
use indexmap::indexset;
|
||||||
|
use oxipng::{internal_tests::*, *};
|
||||||
|
use serde_json::Value;
|
||||||
use std::{
|
use std::{
|
||||||
fs::remove_file,
|
fs::remove_file,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
process::Command,
|
||||||
};
|
};
|
||||||
|
|
||||||
use indexmap::indexset;
|
|
||||||
use oxipng::{internal_tests::*, *};
|
|
||||||
|
|
||||||
const GRAY: u8 = 0;
|
const GRAY: u8 = 0;
|
||||||
const RGB: u8 = 2;
|
const RGB: u8 = 2;
|
||||||
const INDEXED: u8 = 3;
|
const INDEXED: u8 = 3;
|
||||||
|
|
@ -95,13 +96,9 @@ fn test_it_converts(
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn verbose_mode() {
|
fn verbose_mode() {
|
||||||
use std::cell::RefCell;
|
|
||||||
#[cfg(not(feature = "parallel"))]
|
|
||||||
use std::sync::mpsc::{Sender, channel as unbounded};
|
|
||||||
|
|
||||||
#[cfg(feature = "parallel")]
|
|
||||||
use crossbeam_channel::{Sender, unbounded};
|
|
||||||
use log::{Level, LevelFilter, Log, Metadata, Record, set_logger, set_max_level};
|
use log::{Level, LevelFilter, Log, Metadata, Record, set_logger, set_max_level};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::sync::mpsc::{Sender, channel};
|
||||||
|
|
||||||
// Rust runs tests in parallel by default.
|
// Rust runs tests in parallel by default.
|
||||||
// We want to make sure that we verify only logs from our test.
|
// We want to make sure that we verify only logs from our test.
|
||||||
|
|
@ -141,7 +138,7 @@ fn verbose_mode() {
|
||||||
let input = PathBuf::from("tests/files/verbose_mode.png");
|
let input = PathBuf::from("tests/files/verbose_mode.png");
|
||||||
let (output, opts) = get_opts(&input);
|
let (output, opts) = get_opts(&input);
|
||||||
|
|
||||||
let (sender, receiver) = unbounded();
|
let (sender, receiver) = channel();
|
||||||
|
|
||||||
let thread_init = move || {
|
let thread_init = move || {
|
||||||
// Initialise logs storage for all threads within our test.
|
// Initialise logs storage for all threads within our test.
|
||||||
|
|
@ -430,12 +427,9 @@ fn interlaced_0_to_1_other_filter_mode() {
|
||||||
fn preserve_attrs() {
|
fn preserve_attrs() {
|
||||||
let input = PathBuf::from("tests/files/preserve_attrs.png");
|
let input = PathBuf::from("tests/files/preserve_attrs.png");
|
||||||
|
|
||||||
#[cfg(feature = "filetime")]
|
|
||||||
let meta_input = input
|
let meta_input = input
|
||||||
.metadata()
|
.metadata()
|
||||||
.expect("unable to get file metadata for output file");
|
.expect("unable to get metadata for input file");
|
||||||
#[cfg(feature = "filetime")]
|
|
||||||
let mtime_canon = filetime::FileTime::from_last_modification_time(&meta_input);
|
|
||||||
|
|
||||||
let (mut output, opts) = get_opts(&input);
|
let (mut output, opts) = get_opts(&input);
|
||||||
if let OutFile::Path { preserve_attrs, .. } = &mut output {
|
if let OutFile::Path { preserve_attrs, .. } = &mut output {
|
||||||
|
|
@ -449,17 +443,6 @@ fn preserve_attrs() {
|
||||||
let output = output.path().unwrap();
|
let output = output.path().unwrap();
|
||||||
assert!(output.exists());
|
assert!(output.exists());
|
||||||
|
|
||||||
#[cfg(feature = "filetime")]
|
|
||||||
let meta_output = output
|
|
||||||
.metadata()
|
|
||||||
.expect("unable to get file metadata for output file");
|
|
||||||
#[cfg(feature = "filetime")]
|
|
||||||
assert_eq!(
|
|
||||||
&mtime_canon,
|
|
||||||
&filetime::FileTime::from_last_modification_time(&meta_output),
|
|
||||||
"expected modification time to be identical to that of input",
|
|
||||||
);
|
|
||||||
|
|
||||||
match PngData::new(output, &opts) {
|
match PngData::new(output, &opts) {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
Err(x) => {
|
Err(x) => {
|
||||||
|
|
@ -468,9 +451,21 @@ fn preserve_attrs() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
remove_file(output).ok();
|
let meta_output = output
|
||||||
|
.metadata()
|
||||||
|
.expect("unable to get metadata for output file");
|
||||||
|
assert_eq!(
|
||||||
|
meta_input.modified().ok(),
|
||||||
|
meta_output.modified().ok(),
|
||||||
|
"expected modification time to be identical to that of input",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
meta_input.permissions(),
|
||||||
|
meta_output.permissions(),
|
||||||
|
"expected permissions to be identical to that of input",
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: Actually check permissions
|
remove_file(output).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -664,3 +659,39 @@ fn zopfli_mode() {
|
||||||
BitDepth::Eight,
|
BitDepth::Eight,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
#[test]
|
||||||
|
fn json_success() {
|
||||||
|
let path = "tests/files/json.png";
|
||||||
|
let output = Command::new(env!("CARGO_BIN_EXE_oxipng"))
|
||||||
|
.args(["--json", "--nx", "--nz", path])
|
||||||
|
.output()
|
||||||
|
.expect("Failed to run executable");
|
||||||
|
assert!(output.status.success());
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let data: Value = serde_json::from_str(&stdout).expect("Failed to parse JSON");
|
||||||
|
let result = &data["results"][0];
|
||||||
|
assert_eq!(result["input"], path);
|
||||||
|
assert_eq!(result["status"], "success");
|
||||||
|
assert_eq!(result["output"], path);
|
||||||
|
assert_eq!(result["insize"], result["outsize"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_dry_run() {
|
||||||
|
let path = "tests/files/json.png";
|
||||||
|
let path2 = "tests/files/escape chars \\ \" \n \t \r \x08 \x0c.png";
|
||||||
|
let output = Command::new(env!("CARGO_BIN_EXE_oxipng"))
|
||||||
|
.args(["--json", "--dry-run", path, path2])
|
||||||
|
.output()
|
||||||
|
.expect("Failed to run executable");
|
||||||
|
assert!(output.status.success());
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let data: Value = serde_json::from_str(&stdout).expect("Failed to parse JSON");
|
||||||
|
let result = &data["results"][0];
|
||||||
|
assert_eq!(result["input"], path);
|
||||||
|
assert_eq!(result["status"], "success");
|
||||||
|
assert_eq!(result["output"], Value::Null);
|
||||||
|
let result2 = &data["results"][1];
|
||||||
|
assert_eq!(result2["input"], path2);
|
||||||
|
assert_eq!(result2["status"], "error");
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue