Compare commits

..

No commits in common. "master" and "v10.0.0" have entirely different histories.

21 changed files with 611 additions and 750 deletions

View file

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

View file

@ -3,12 +3,7 @@ name: deploy
on:
push:
tags:
- v*.*.*
concurrency:
# If we ever end up with two concurrent jobs for releasing the same tag, the former should be cancelled
group: release-${{ github.ref }}
cancel-in-progress: true
- "v*.*.*"
permissions:
actions: read
@ -59,15 +54,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@v21
uses: dawidd6/action-download-artifact@v11
with:
workflow: oxipng.yml
commit: ${{ github.sha }}
commit: ${{ env.GITHUB_SHA }}
name: Oxipng binary (${{ matrix.target }})
path: target
@ -128,7 +123,7 @@ jobs:
run: tail -n +3 CHANGELOG.md | sed -e '/^$/,$d' > RELEASE_NOTES.txt
- name: Create release
uses: softprops/action-gh-release@v3
uses: softprops/action-gh-release@v2
with:
name: v${{ steps.oxipngMeta.outputs.version }}
body_path: RELEASE_NOTES.txt

View file

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

View file

@ -8,11 +8,6 @@ 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
@ -60,6 +55,7 @@ jobs:
env:
CARGO_BUILD_TARGET: ${{ matrix.target }}
RUSTFLAGS: '-Zlocation-detail=none'
steps:
- name: Checkout source
@ -78,7 +74,7 @@ jobs:
with:
toolchain: nightly
target: ${{ env.CARGO_BUILD_TARGET }}
components: clippy, rustfmt, rust-src
components: clippy, rustfmt
cache-bin: false
cache-shared-key: cache
@ -129,12 +125,10 @@ jobs:
run: cargo doc --release --no-deps
- name: Build CLI binary
run: cargo build --release -Zbuild-std
env:
RUSTFLAGS: "-Zlocation-detail=none -Zunstable-options -Cpanic=immediate-abort"
run: cargo build --release
- name: Upload CLI binary as artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v5
with:
name: Oxipng binary (${{ matrix.target }})
path: |

8
.gitignore vendored
View file

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

View file

@ -1,21 +1,7 @@
## 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>`.

394
Cargo.lock generated
View file

@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "anstream"
version = "1.0.0"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
dependencies = [
"anstyle",
"anstyle-parse",
@ -25,37 +25,37 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.14"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
dependencies = [
"windows-sys",
"windows-sys 0.60.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
"windows-sys 0.60.2",
]
[[package]]
@ -66,9 +66,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
version = "2.11.1"
version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]]
name = "bitvec"
@ -84,15 +84,15 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.20.2"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "bytemuck"
version = "1.25.0"
version = "1.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677"
[[package]]
name = "byteorder-lite"
@ -102,9 +102,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "cc"
version = "1.2.60"
version = "1.2.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9"
dependencies = [
"find-msvc-tools",
"shlex",
@ -112,24 +112,24 @@ dependencies = [
[[package]]
name = "cfg-if"
version = "1.0.4"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "clap"
version = "4.6.1"
version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
dependencies = [
"anstream",
"anstyle",
@ -140,15 +140,15 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "1.1.0"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]]
name = "colorchoice"
version = "1.0.5"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "crc32fast"
@ -159,6 +159,15 @@ 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"
@ -192,18 +201,18 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "env_filter"
version = "1.0.1"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
]
[[package]]
name = "env_logger"
version = "0.11.10"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
@ -224,7 +233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
"windows-sys 0.61.0",
]
[[package]]
@ -237,16 +246,28 @@ dependencies = [
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
name = "filetime"
version = "0.2.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.60.2",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959"
[[package]]
name = "flate2"
version = "1.1.9"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [
"crc32fast",
"miniz_oxide",
@ -266,9 +287,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "hashbrown"
version = "0.17.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "image"
@ -285,9 +306,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.14.0"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
dependencies = [
"equivalent",
"hashbrown",
@ -296,27 +317,21 @@ dependencies = [
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "libc"
version = "0.2.185"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "libdeflate-sys"
version = "1.25.2"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72753e0008ea87963d2f0770042d0df7abe51fafbb8dcaf618ac440f2f1fec0a"
checksum = "23bd6304ebf75390d8a99b88bdf2a266f62647838140cb64af8e6702f6e3fddc"
dependencies = [
"cc",
"pkg-config",
@ -324,30 +339,35 @@ dependencies = [
[[package]]
name = "libdeflater"
version = "1.25.2"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1ee41cf6fb1bb6030dfb59ffb7bc01ab26aade44142084c87f0fc7a1658fe71"
checksum = "d5d4880e6d634d3d029d65fa016038e788cc728a17b782684726fb34ee140caf"
dependencies = [
"libdeflate-sys",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
name = "libredox"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags",
"libc",
"redox_syscall",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "log"
version = "0.4.30"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "miniz_oxide"
@ -361,9 +381,9 @@ dependencies = [
[[package]]
name = "moxcms"
version = "0.7.11"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08"
dependencies = [
"num-traits",
"pxfm",
@ -380,17 +400,19 @@ dependencies = [
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "oxipng"
version = "10.1.1"
version = "10.0.0"
dependencies = [
"bitvec",
"clap",
"crossbeam-channel",
"env_logger",
"filetime",
"glob",
"image",
"indexmap",
@ -400,7 +422,6 @@ dependencies = [
"rayon",
"rgb",
"rustc-hash",
"serde_json",
"zopfli",
]
@ -412,15 +433,15 @@ checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b"
[[package]]
name = "pkg-config"
version = "0.3.33"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "png"
version = "0.18.1"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
dependencies = [
"bitflags",
"crc32fast",
@ -429,28 +450,13 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pxfm"
version = "0.1.29"
version = "0.1.24"
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"
checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde"
dependencies = [
"proc-macro2",
"num-traits",
]
[[package]]
@ -461,9 +467,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rayon"
version = "1.12.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
@ -480,73 +486,40 @@ dependencies = [
]
[[package]]
name = "rgb"
version = "0.8.53"
name = "redox_syscall"
version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [
"bitflags",
]
[[package]]
name = "rgb"
version = "0.8.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
dependencies = [
"bytemuck",
]
[[package]]
name = "rustc-hash"
version = "2.1.2"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustix"
version = "1.1.4"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
"windows-sys 0.61.0",
]
[[package]]
@ -557,9 +530,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "simd-adler32"
version = "0.3.9"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "strsim"
@ -567,17 +540,6 @@ 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"
@ -586,20 +548,14 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "terminal_size"
version = "0.4.4"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
dependencies = [
"rustix",
"windows-sys",
"windows-sys 0.60.2",
]
[[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"
@ -608,19 +564,99 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-link"
version = "0.2.1"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-link"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-sys"
version = "0.61.2"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-link",
"windows-targets",
]
[[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"
@ -630,12 +666,6 @@ 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"

View file

@ -20,7 +20,7 @@ homepage = "https://github.com/oxipng/oxipng"
license = "MIT"
name = "oxipng"
repository = "https://github.com/oxipng/oxipng"
version = "10.1.1"
version = "10.0.0"
rust-version = "1.85.1"
[badges]
@ -38,32 +38,59 @@ name = "zopfli"
required-features = ["zopfli"]
[dependencies]
bitvec = "1.0.1"
clap = { version = "4.6.0", optional = true, features = ["wrap_help"] }
env_logger = { version = "0.11.10", optional = true, default-features = false, features = ["auto-color"] }
image = { version = "0.25.9", optional = true, default-features = false, features = ["png"] }
indexmap = "2.14.0"
libdeflater = "1.25.2"
log = "0.4.30"
parse-size = { version = "1.1.0", optional = true }
rayon = { version = "1.11.0", optional = true }
rgb = "0.8.53"
rustc-hash = "2.1.2"
zopfli = { version = "0.8.3", optional = true, default-features = false, features = ["std", "zlib"] }
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"
[target.'cfg(windows)'.dependencies]
glob = { version = "0.3.3", optional = true }
[dependencies.env_logger]
optional = true
default-features = false
features = ["auto-color"]
version = "0.11.8"
[dev-dependencies]
serde_json = "1.0.150"
[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"
[features]
binary = ["dep:clap", "dep:glob", "dep:env_logger", "dep:parse-size"]
default = ["binary", "parallel", "zopfli"]
parallel = ["dep:rayon", "indexmap/rayon"]
default = ["binary", "parallel", "zopfli", "filetime"]
parallel = ["dep:rayon", "indexmap/rayon", "dep:crossbeam-channel"]
freestanding = ["libdeflater/freestanding"]
sanity-checks = ["dep:image"]
zopfli = ["dep:zopfli"]
filetime = ["dep:filetime"]
system-libdeflate = ["libdeflater/dynamic"]
[lib]

View file

@ -1,4 +1,4 @@
oxipng 10.1.0
oxipng 10.0.0
Losslessly improve compression of PNG files
Usage: oxipng [OPTIONS] <files>...
@ -105,13 +105,10 @@ Options:
losslessly.
-v, --verbose...
Show per-file info (use multiple times for more detail)
Run in verbose mode (use twice to increase verbosity)
-q, --quiet
Suppress all output messages
-j, --json
Print results as JSON
Run in quiet mode
-f, --filters <list>
Perform compression trials with each of the given filter types. You can specify a

View file

@ -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. It is fast and highly effective.
interface or as a library in other Rust programs.
## Installing
@ -76,14 +76,7 @@ All options are case-sensitive.
\* Note that oxipng is not a brute-force optimizer. This means that while higher optimization levels
are almost always better or equal to lower levels, this is not guaranteed and it is possible in
rare circumstances that a lower level may give a marginally smaller output. Similarly, using Zopfli
compression (`-z`) is not guaranteed to always be better than default compression.
## APNG support
Oxipng currently only supports limited optimization of animated PNGs (APNGs). It can perform
alpha-optimization, refiltering and recompression of all frames, but all transformations will be
disabled. For best results, it is recommended to use another tool such as
[apngopt](https://sourceforge.net/projects/apng/files/APNG_Optimizer/) before running Oxipng.
compression (`-z`) is not guaranteed to always be better than without.
## Git integration via [pre-commit]
@ -138,13 +131,17 @@ and specifying the desired ones, for example:
## History
Oxipng began in 2015 as a rewrite of the OptiPNG project. The core goal was to implement
multithreading, which would have been very difficult to do within the existing C codebase of OptiPNG.
Oxipng began as a complete rewrite of the OptiPNG project,
which was assumed to be dead as no commit had been made to it since March 2014.
(OptiPNG has since released a new version, after Oxipng was first released.)
The name has been changed to avoid confusion and potential legal issues.
The core goal of rewriting OptiPNG was to implement multithreading,
which would be very difficult to do within the existing C codebase of OptiPNG.
This also served as an opportunity to choose a more modern, safer language (Rust).
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.
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.
## Contributing
@ -154,11 +151,42 @@ 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
```

View file

@ -1,3 +1,5 @@
use std::io::Write;
use crate::{
PngResult,
error::PngError,
@ -50,14 +52,15 @@ impl Frame {
#[must_use]
pub fn fctl_data(&self, sequence_number: u32) -> Vec<u8> {
let mut byte_data = Vec::with_capacity(26);
byte_data.extend_from_slice(&sequence_number.to_be_bytes());
byte_data.extend_from_slice(&self.width.to_be_bytes());
byte_data.extend_from_slice(&self.height.to_be_bytes());
byte_data.extend_from_slice(&self.x_offset.to_be_bytes());
byte_data.extend_from_slice(&self.y_offset.to_be_bytes());
byte_data.extend_from_slice(&self.delay_num.to_be_bytes());
byte_data.extend_from_slice(&self.delay_den.to_be_bytes());
byte_data.extend_from_slice(&[self.dispose_op, self.blend_op]);
byte_data.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
}
@ -65,8 +68,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.extend_from_slice(&sequence_number.to_be_bytes());
byte_data.extend_from_slice(&self.data);
byte_data.write_all(&sequence_number.to_be_bytes()).unwrap();
byte_data.write_all(&self.data).unwrap();
byte_data
}
}

View file

@ -212,7 +212,7 @@ losslessly.")
)
.arg(
Arg::new("verbose")
.help("Show per-file info (use multiple times for more detail)")
.help("Run in verbose mode (use twice to increase verbosity)")
.short('v')
.long("verbose")
.action(ArgAction::Count)
@ -220,20 +220,12 @@ losslessly.")
)
.arg(
Arg::new("quiet")
.help("Suppress all output messages")
.help("Run in quiet mode")
.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)")
@ -298,8 +290,6 @@ 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),
)

View file

@ -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 = channel();
let eval_channel = unbounded();
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 = image.ihdr.color_type.to_string();
let description = format!("{}", image.ihdr.color_type);
self.try_image_with_description(image, &description);
}
@ -154,19 +154,6 @@ 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(),
@ -177,6 +164,10 @@ 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")]
{

View file

@ -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)
}
// 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));
// 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);
}
match inflate(compressed_data, out_size) {
Ok(icc) => Some(icc),
Err(e) => {

View file

@ -7,9 +7,9 @@ extern crate rayon;
mod rayon;
use std::{
fs::File,
fs::{File, Metadata},
io::{BufWriter, Read, Write, stdin, stdout},
path::PathBuf,
path::{Path, PathBuf},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
@ -64,7 +64,6 @@ 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
@ -87,7 +86,7 @@ impl RawImage {
color_type: ColorType,
bit_depth: BitDepth,
data: Vec<u8>,
) -> PngResult<Self> {
) -> Result<Self, PngError> {
// Validate bit depth
let valid_depth = match color_type {
ColorType::Grayscale { .. } => true,
@ -166,15 +165,40 @@ 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) -> OptimizationResult {
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(usize, usize)> {
// 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) => PngData::read_file(input_path)?,
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::StdIn => {
opt_metadata_preserved = None;
let mut data = Vec::new();
stdin()
.read_to_end(&mut data)
@ -196,7 +220,7 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> Optimizatio
(OutFile::Path { path, .. }, InFile::Path(input_path))
if path.as_ref().is_none_or(|p| p == input_path) =>
{
info!("Could not optimize further, no change written: {input}");
info!("{input}: Could not optimize further, no change written");
return Ok((in_length, in_length));
}
_ => {
@ -228,34 +252,18 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> Optimizatio
buffer
.write_all(&optimized_output)
.map_err(|e| PngError::WriteFailed("stdout".into(), e))?;
info!("{savings}: stdout");
}
(
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
};
(OutFile::Path { path, .. }, _) => {
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!
@ -263,18 +271,8 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> Optimizatio
.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) = &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}");
}
if let Some(metadata_input) = &opt_metadata_preserved {
copy_times(metadata_input, output_path)?;
}
info!("{}: {}", savings, output_path.display());
}
@ -644,3 +642,29 @@ 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}"
))
})
}

View file

@ -6,10 +6,9 @@ use std::num::NonZeroU64;
use std::{
ffi::{OsStr, OsString},
fs::DirBuilder,
io::{IsTerminal, Write, stdout},
io::Write,
path::PathBuf,
process::ExitCode,
sync::atomic::{AtomicUsize, Ordering::AcqRel},
time::Duration,
};
@ -19,9 +18,7 @@ use indexmap::IndexSet;
use log::{Level, LevelFilter, error, warn};
#[cfg(feature = "zopfli")]
use oxipng::ZopfliOptions;
use oxipng::{
Deflater, FilterStrategy, InFile, OptimizationResult, Options, OutFile, PngError, StripChunks,
};
use oxipng::{Deflater, FilterStrategy, InFile, Options, OutFile, PngError, StripChunks};
use rayon::prelude::*;
use crate::cli::DISPLAY_CHUNKS;
@ -38,7 +35,7 @@ fn main() -> ExitCode {
.after_long_help("")
.get_matches_from(std::env::args());
let (mut out_file, out_dir, opts) = match parse_opts_into_struct(&matches) {
let (out_file, out_dir, opts) = match parse_opts_into_struct(&matches) {
Ok(x) => x,
Err(x) => {
error!("{x}");
@ -46,128 +43,51 @@ fn main() -> ExitCode {
}
};
// 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 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,
);
let files = if using_stdin {
vec![(InFile::StdIn, out_file)]
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 {
collect_files(
inputs,
&out_dir,
&out_file,
matches.get_flag("recursive"),
true,
)
files
.into_iter()
.map(|(input, output)| process_file(&input, &output, &opts))
.min()
};
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();
match summary.unwrap_or(OptimizationResult::Skipped) {
OptimizationResult::Ok => ExitCode::SUCCESS,
OptimizationResult::Failed => ExitCode::FAILURE,
OptimizationResult::Skipped => ExitCode::from(3),
}
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()
};
}
// 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)
}
#[derive(Eq, PartialEq, Ord, PartialOrd)]
enum OptimizationResult {
Ok,
Failed,
Skipped,
}
fn collect_files(
@ -178,8 +98,10 @@ 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 {
if input.is_dir() {
let using_stdin = allow_stdin && input.to_str() == Some("-");
if !using_stdin && input.is_dir() {
if recursive {
match input.read_dir() {
Ok(dir) => {
@ -196,15 +118,6 @@ 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()));
@ -215,7 +128,19 @@ fn collect_files(
} else {
(*out_file).clone()
};
let in_file = InFile::Path(input);
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)
};
in_out_pairs.push((in_file, out_file));
}
in_out_pairs
@ -240,9 +165,8 @@ 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::Warn,
1 => LevelFilter::Info,
2 => LevelFilter::Debug,
0 => LevelFilter::Info,
1 => LevelFilter::Debug,
_ => LevelFilter::Trace,
};
env_logger::builder()
@ -251,8 +175,7 @@ fn parse_opts_into_struct(
match record.level() {
Level::Error | Level::Warn => {
let style = buf.default_level_style(record.level());
// Prepend carriage return to clear progress line
writeln!(buf, "\r{style}{}{style:#}", record.args())
writeln!(buf, "{style}{}{style:#}", record.args())
}
// Leave info, debug and trace unstyled
_ => writeln!(buf, "{}", record.args()),
@ -430,16 +353,6 @@ 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,
@ -527,128 +440,26 @@ 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 Err(PngError::InflatedDataTooLong(max_size));
return OptimizationResult::Skipped;
}
}
let result = oxipng::optimize(input, output, opts);
match &result {
Ok(_) => {}
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,
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");
}
}

View file

@ -1,11 +1,13 @@
use std::{fs, path::Path, sync::Arc};
use std::{fs, io::Write, path::Path, sync::Arc};
use bitvec::bitarr;
use libdeflater::{CompressionLvl, Compressor};
use log::warn;
use rgb::ComponentSlice;
use rustc_hash::FxHashMap;
use crate::{
Options, PngResult,
Options,
apng::*,
colors::{BitDepth, ColorType},
deflate,
@ -43,18 +45,18 @@ pub struct PngData {
impl PngData {
/// Create a new `PngData` struct by opening a file
#[inline]
pub fn new(filepath: &Path, opts: &Options) -> PngResult<Self> {
pub fn new(filepath: &Path, opts: &Options) -> Result<Self, PngError> {
let byte_data = Self::read_file(filepath)?;
Self::from_slice(&byte_data, opts)
}
pub fn read_file(filepath: &Path) -> PngResult<Vec<u8>> {
pub fn read_file(filepath: &Path) -> Result<Vec<u8>, PngError> {
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) -> PngResult<Self> {
pub fn from_slice(byte_data: &[u8], opts: &Options) -> Result<Self, PngError> {
let mut byte_offset: usize = 0;
// Test that png header is valid
let header = byte_data.get(0..8).ok_or(PngError::TruncatedData)?;
@ -163,15 +165,17 @@ 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.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,
]);
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();
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");
@ -190,7 +194,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_ref());
palette_data.extend_from_slice(px.rgb().as_slice());
}
write_png_block(b"PLTE", &palette_data, &mut output);
if let Some(last_trns) = palette.iter().rposition(|px| px.a != 255) {
@ -246,7 +250,7 @@ impl PngData {
}
impl PngImage {
pub fn new(ihdr: IhdrData, compressed_data: &[u8]) -> PngResult<Self> {
pub fn new(ihdr: IhdrData, compressed_data: &[u8]) -> Result<Self, PngError> {
let raw_data = deflate::inflate(compressed_data, ihdr.raw_data_size())?;
// Reject files with incorrect width/height or truncated data
@ -330,7 +334,7 @@ impl PngImage {
}
/// Reverse all filters applied on the image, returning an unfiltered IDAT bytestream
fn unfilter_image(&self) -> PngResult<Vec<u8>> {
fn unfilter_image(&self) -> Result<Vec<u8>, PngError> {
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();
@ -372,12 +376,6 @@ 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()];
@ -469,29 +467,18 @@ 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 count = 0;
let mut set = bitarr![0; 0x10000];
for pair in f_buf.windows(2) {
let bigram = ((pair[0] as usize) << 8) | pair[1] as usize;
if !bigram_seen[bigram] {
count += 1;
if count >= best_size {
break;
}
bigram_seen[bigram] = true;
bigram_touched.push(bigram as u16);
}
set.set(bigram, true);
}
if count < best_size {
best_size = count;
let size = set.count_ones();
if size < best_size {
best_size = size;
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 => {

View file

@ -1,7 +1,7 @@
use std::hash::{BuildHasherDefault, Hash};
use indexmap::IndexSet;
use rgb::{ComponentMap, FromSlice, RGB, RGBA, alt::Gray};
use rgb::{ComponentMap, ComponentSlice, 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_ref()[ch_start..=ch_end]);
data.extend_from_slice(&color.as_slice()[ch_start..=ch_end]);
}
Some(PngImage {

View file

@ -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 && opts.palette_reduction && !deadline.passed() {
if opts.color_type_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));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View file

@ -1,12 +1,11 @@
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;
@ -96,9 +95,13 @@ fn test_it_converts(
#[test]
fn verbose_mode() {
use log::{Level, LevelFilter, Log, Metadata, Record, set_logger, set_max_level};
use std::cell::RefCell;
use std::sync::mpsc::{Sender, channel};
#[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};
// Rust runs tests in parallel by default.
// We want to make sure that we verify only logs from our test.
@ -138,7 +141,7 @@ fn verbose_mode() {
let input = PathBuf::from("tests/files/verbose_mode.png");
let (output, opts) = get_opts(&input);
let (sender, receiver) = channel();
let (sender, receiver) = unbounded();
let thread_init = move || {
// Initialise logs storage for all threads within our test.
@ -427,9 +430,12 @@ 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 metadata for input file");
.expect("unable to get file metadata for output file");
#[cfg(feature = "filetime")]
let mtime_canon = filetime::FileTime::from_last_modification_time(&meta_input);
let (mut output, opts) = get_opts(&input);
if let OutFile::Path { preserve_attrs, .. } = &mut output {
@ -443,6 +449,17 @@ 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) => {
@ -451,21 +468,9 @@ fn preserve_attrs() {
}
};
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",
);
remove_file(output).ok();
// TODO: Actually check permissions
}
#[test]
@ -659,39 +664,3 @@ 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");
}