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