Compare commits

...

47 commits

Author SHA1 Message Date
dependabot[bot]
0466546981
chore(deps): bump the rust-dependencies group with 2 updates (#818)
Some checks failed
docker / build (push) Has been cancelled
oxipng / CI (push) Has been cancelled
oxipng / MSRV check (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-27 17:18:56 +00:00
Alejandro González
4e80dd009a
ci: cherry-pick changes from PR #815
That PR got hard to review and confidently merge, so I'm cherry-picking
some of the good ideas from it in a format that feels more comfortable
to me. Thanks @vlakoff for the original PR!

Co-authored-by: vlakoff <544424+vlakoff@users.noreply.github.com>
2026-05-08 23:37:53 +02:00
dependabot[bot]
028294cc8c
chore(deps): bump dawidd6/action-download-artifact from 20 to 21 (#816)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 20:25:30 +00:00
andrews05
628e241e23
v10.1.1 (#813)
Some checks failed
docker / build (push) Has been cancelled
oxipng / CI (push) Has been cancelled
oxipng / MSRV check (push) Has been cancelled
deploy / Deploy release (push) Has been cancelled
2026-04-22 20:29:48 +00:00
vlakoff
e167b04b85
Warn when Zopfli --ziwi exceeds --zi (#812) 2026-04-20 02:48:32 +00:00
dependabot[bot]
fbae35922d
chore(deps): bump dawidd6/action-download-artifact from 19 to 20 (#808)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-15 18:49:43 +00:00
dependabot[bot]
55b9758575
chore(deps): bump indexmap from 2.13.0 to 2.13.1 in the rust-dependencies group (#809)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-15 18:49:21 +00:00
dependabot[bot]
010b76eada
chore(deps): bump softprops/action-gh-release from 2 to 3 (#811)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-15 18:48:59 +00:00
AFCMS
d239592b58
Migrate Docker CI to newer docker/github-builder (#810) 2026-04-15 18:46:51 +00:00
andrews05
0cf8b782ff
Use build-std to reduce binary size (#807) 2026-04-06 21:18:00 +00:00
andrews05
25e147f6a0
Prevent conversion to indexed when palette reduction is off (#802) 2026-04-06 21:14:22 +00:00
andrews05
a75343800c
Increase icc decompression buffer size (#803) 2026-04-06 20:50:47 +00:00
andrews05
7d52fd66d0
Improve bigrams performance (#804) 2026-03-31 15:57:18 +00:00
dependabot[bot]
5bbd826ae3
chore(deps): bump the rust-dependencies group with 2 updates (#805)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 12:39:01 +00:00
dependabot[bot]
e4a7d0fd99
chore(deps): bump clap from 4.5.60 to 4.6.0 in the rust-dependencies group (#797)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 15:56:34 +00:00
dependabot[bot]
a1a18ffd21
chore(deps): bump dawidd6/action-download-artifact from 16 to 19 (#800)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 11:13:31 +00:00
dependabot[bot]
587fd03d0e
chore(deps): bump docker/setup-buildx-action from 3 to 4 (#795)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 23:42:42 +00:00
dependabot[bot]
ce52ab8748
chore(deps): bump docker/metadata-action from 5 to 6 (#794)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 23:42:28 +00:00
dependabot[bot]
494edba245
chore(deps): bump docker/login-action from 3 to 4 (#793)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 23:42:18 +00:00
dependabot[bot]
2e861b95db
chore(deps): bump docker/setup-qemu-action from 3 to 4 (#792)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 23:42:08 +00:00
dependabot[bot]
7834e4709e
chore(deps): bump docker/build-push-action from 6 to 7 (#791)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 23:41:42 +00:00
dependabot[bot]
8284676f50
chore(deps): bump rgb from 0.8.52 to 0.8.53 in the rust-dependencies group (#789)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alejandro González <me@alegon.dev>
2026-03-03 20:33:52 +00:00
dependabot[bot]
2a79fa4c6c
chore(deps): bump dawidd6/action-download-artifact from 15 to 16 (#788)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 19:19:33 +00:00
dependabot[bot]
00b9e33508
chore(deps): bump actions/upload-artifact from 6 to 7 (#787)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 16:15:36 +00:00
dependabot[bot]
2585529089
chore(deps): bump actions/attest-build-provenance from 3 to 4 (#786)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 16:14:54 +00:00
Demetris
6a99181fec
Make five small changes to the updated README.md (#785) 2026-02-26 20:59:09 +00:00
dependabot[bot]
ea9caac249
chore(deps): bump dawidd6/action-download-artifact from 14 to 15 (#779)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 16:51:30 +00:00
dependabot[bot]
764d057c32
chore(deps): bump clap from 4.5.58 to 4.5.60 in the rust-dependencies group (#780)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 16:51:09 +00:00
andrews05
4a537c087d
Update Readme (#767) 2026-02-22 13:39:54 +00:00
Jakub Onderka
015d31e6a1
Remove crossbeam-channel dependency (#777) 2026-02-22 13:37:38 +00:00
dependabot[bot]
854510a864
chore(deps): bump the rust-dependencies group with 2 updates (#776)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 15:27:09 +00:00
dependabot[bot]
8466e390b8
chore(deps): bump the rust-dependencies group with 2 updates (#774)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 23:10:41 +00:00
dependabot[bot]
baaafee4e4
chore(deps): bump clap from 4.5.54 to 4.5.56 in the rust-dependencies group (#772)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 13:04:45 +00:00
dependabot[bot]
9169f99674
chore(deps): bump dawidd6/action-download-artifact from 12 to 14 (#771)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 13:04:16 +00:00
dependabot[bot]
47d5628a68
chore(deps): bump serde_json from 1.0.147 to 1.0.149 in the rust-dependencies group (#770)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 21:15:59 +00:00
andrews05
d54e7c04ea
v10.1 (#769)
Some checks failed
oxipng / CI (push) Has been cancelled
oxipng / MSRV check (push) Has been cancelled
docker / build (push) Has been cancelled
deploy / Deploy release (push) Has been cancelled
2026-01-25 21:45:00 +00:00
andrews05
ff168f5d2d
Add json output option (#761) 2026-01-24 11:35:55 +00:00
andrews05
a8edb2871c
Show summary of results (#758) 2026-01-20 20:55:26 +00:00
andrews05
d48b147b3d
Tweak output when not optimised (#765) 2026-01-12 18:48:10 +00:00
dependabot[bot]
155eeb3637
chore(deps): bump dawidd6/action-download-artifact from 11 to 12 (#760)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-12 18:41:12 +00:00
dependabot[bot]
8665b2da8c
chore(deps): bump the rust-dependencies group across 1 directory with 2 updates (#768)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-12 18:40:20 +00:00
andrews05
e5e1568196
Rework preserving attributes (#759) 2026-01-12 19:11:45 +01:00
dependabot[bot]
8754013cbc
chore(deps): bump actions/upload-artifact from 5 to 6 (#756)
Bumps
[actions/upload-artifact](https://github.com/actions/upload-artifact)
from 5 to 6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/upload-artifact/releases">actions/upload-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>v6 - What's new</h2>
<blockquote>
<p>[!IMPORTANT]
actions/upload-artifact@v6 now runs on Node.js 24 (<code>runs.using:
node24</code>) and requires a minimum Actions Runner version of 2.327.1.
If you are using self-hosted runners, ensure they are updated before
upgrading.</p>
</blockquote>
<h3>Node.js 24</h3>
<p>This release updates the runtime to Node.js 24. v5 had preliminary
support for Node.js 24, however this action was by default still running
on Node.js 20. Now this action by default will run on Node.js 24.</p>
<h2>What's Changed</h2>
<ul>
<li>Upload Artifact Node 24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/719">actions/upload-artifact#719</a></li>
<li>fix: update <code>@​actions/artifact</code> for Node.js 24 punycode
deprecation by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/744">actions/upload-artifact#744</a></li>
<li>prepare release v6.0.0 for Node.js 24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/745">actions/upload-artifact#745</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v5.0.0...v6.0.0">https://github.com/actions/upload-artifact/compare/v5.0.0...v6.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b7c566a772"><code>b7c566a</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/745">#745</a>
from actions/upload-artifact-v6-release</li>
<li><a
href="e516bc8500"><code>e516bc8</code></a>
docs: correct description of Node.js 24 support in README</li>
<li><a
href="ddc45ed9bc"><code>ddc45ed</code></a>
docs: update README to correct action name for Node.js 24 support</li>
<li><a
href="615b319bd2"><code>615b319</code></a>
chore: release v6.0.0 for Node.js 24 support</li>
<li><a
href="017748b48f"><code>017748b</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/744">#744</a>
from actions/fix-storage-blob</li>
<li><a
href="38d4c7997f"><code>38d4c79</code></a>
chore: rebuild dist</li>
<li><a
href="7d27270e0c"><code>7d27270</code></a>
chore: add missing license cache files for <code>@​actions/core</code>,
<code>@​actions/io</code>, and mi...</li>
<li><a
href="5f643d3c94"><code>5f643d3</code></a>
chore: update license files for <code>@​actions/artifact</code><a
href="https://github.com/5"><code>@​5</code></a>.0.1 dependencies</li>
<li><a
href="1df1684032"><code>1df1684</code></a>
chore: update package-lock.json with <code>@​actions/artifact</code><a
href="https://github.com/5"><code>@​5</code></a>.0.1</li>
<li><a
href="b5b1a91840"><code>b5b1a91</code></a>
fix: update <code>@​actions/artifact</code> to ^5.0.0 for Node.js 24
punycode fix</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/upload-artifact/compare/v5...v6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-18 00:58:13 +01:00
dependabot[bot]
a0f4e751bf
chore(deps): bump log from 0.4.28 to 0.4.29 in the rust-dependencies group (#751)
Bumps the rust-dependencies group with 1 update:
[log](https://github.com/rust-lang/log).

Updates `log` from 0.4.28 to 0.4.29
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/log/releases">log's
releases</a>.</em></p>
<blockquote>
<h2>0.4.29</h2>
<h2>MSRV</h2>
<p>This release increases <code>log</code>'s MSRV from
<code>1.61.0</code> to <code>1.68.0</code>.</p>
<h2>What's Changed</h2>
<ul>
<li>docs: Add missing impls from README.md by <a
href="https://github.com/AldaronLau"><code>@​AldaronLau</code></a> in <a
href="https://redirect.github.com/rust-lang/log/pull/703">rust-lang/log#703</a></li>
<li>Point to new URLs for favicon and logo by <a
href="https://github.com/AldaronLau"><code>@​AldaronLau</code></a> in <a
href="https://redirect.github.com/rust-lang/log/pull/704">rust-lang/log#704</a></li>
<li>perf: reduce llvm-lines of FromStr for <code>Level</code> and
<code>LevelFilter</code> by <a
href="https://github.com/dishmaker"><code>@​dishmaker</code></a> in <a
href="https://redirect.github.com/rust-lang/log/pull/709">rust-lang/log#709</a></li>
<li>Replace serde with serde_core by <a
href="https://github.com/Thomasdezeeuw"><code>@​Thomasdezeeuw</code></a>
in <a
href="https://redirect.github.com/rust-lang/log/pull/712">rust-lang/log#712</a></li>
<li>Fix clippy lints by <a
href="https://github.com/Thomasdezeeuw"><code>@​Thomasdezeeuw</code></a>
in <a
href="https://redirect.github.com/rust-lang/log/pull/713">rust-lang/log#713</a></li>
<li>Use GitHub Actions to install Rust and cargo-hack by <a
href="https://github.com/Thomasdezeeuw"><code>@​Thomasdezeeuw</code></a>
in <a
href="https://redirect.github.com/rust-lang/log/pull/715">rust-lang/log#715</a></li>
<li>Exclude old unstable_kv features from testing matrix by <a
href="https://github.com/Thomasdezeeuw"><code>@​Thomasdezeeuw</code></a>
in <a
href="https://redirect.github.com/rust-lang/log/pull/716">rust-lang/log#716</a></li>
<li>Fix up CI by <a
href="https://github.com/KodrAus"><code>@​KodrAus</code></a> in <a
href="https://redirect.github.com/rust-lang/log/pull/718">rust-lang/log#718</a></li>
<li>Prepare for 0.4.29 release by <a
href="https://github.com/KodrAus"><code>@​KodrAus</code></a> in <a
href="https://redirect.github.com/rust-lang/log/pull/719">rust-lang/log#719</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/AldaronLau"><code>@​AldaronLau</code></a> made
their first contribution in <a
href="https://redirect.github.com/rust-lang/log/pull/703">rust-lang/log#703</a></li>
<li><a href="https://github.com/dishmaker"><code>@​dishmaker</code></a>
made their first contribution in <a
href="https://redirect.github.com/rust-lang/log/pull/709">rust-lang/log#709</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/rust-lang/log/compare/0.4.28...0.4.29">https://github.com/rust-lang/log/compare/0.4.28...0.4.29</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/log/blob/master/CHANGELOG.md">log's
changelog</a>.</em></p>
<blockquote>
<h2>[0.4.29] - 2025-12-02</h2>
<h2>What's Changed</h2>
<ul>
<li>perf: reduce llvm-lines of FromStr for <code>Level</code> and
<code>LevelFilter</code> by <a
href="https://github.com/dishmaker"><code>@​dishmaker</code></a> in <a
href="https://redirect.github.com/rust-lang/log/pull/709">rust-lang/log#709</a></li>
<li>Replace serde with serde_core by <a
href="https://github.com/Thomasdezeeuw"><code>@​Thomasdezeeuw</code></a>
in <a
href="https://redirect.github.com/rust-lang/log/pull/712">rust-lang/log#712</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/AldaronLau"><code>@​AldaronLau</code></a> made
their first contribution in <a
href="https://redirect.github.com/rust-lang/log/pull/703">rust-lang/log#703</a></li>
<li><a href="https://github.com/dishmaker"><code>@​dishmaker</code></a>
made their first contribution in <a
href="https://redirect.github.com/rust-lang/log/pull/709">rust-lang/log#709</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/rust-lang/log/compare/0.4.28...0.4.29">https://github.com/rust-lang/log/compare/0.4.28...0.4.29</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b1e2df7bce"><code>b1e2df7</code></a>
Merge pull request <a
href="https://redirect.github.com/rust-lang/log/issues/719">#719</a>
from rust-lang/cargo/0.4.29</li>
<li><a
href="3fe1a546dc"><code>3fe1a54</code></a>
prepare for 0.4.29 release</li>
<li><a
href="7a432d9ab5"><code>7a432d9</code></a>
Merge pull request <a
href="https://redirect.github.com/rust-lang/log/issues/718">#718</a>
from rust-lang/ci/msrv</li>
<li><a
href="0689d56847"><code>0689d56</code></a>
rebump msrv to 1.68.0</li>
<li><a
href="46b448e2a7"><code>46b448e</code></a>
try drop msrv back to 1.61.0</li>
<li><a
href="929ab3812e"><code>929ab38</code></a>
fix up doc test feature gate</li>
<li><a
href="957cece478"><code>957cece</code></a>
bump serde-dependent crates</li>
<li><a
href="bea40c847c"><code>bea40c8</code></a>
bump msrv to 1.68.0</li>
<li><a
href="c540184ee9"><code>c540184</code></a>
Merge pull request <a
href="https://redirect.github.com/rust-lang/log/issues/716">#716</a>
from rust-lang/ci-smaller-matrix2</li>
<li><a
href="c971e636c4"><code>c971e63</code></a>
Merge branch 'master' into ci-smaller-matrix2</li>
<li>Additional commits viewable in <a
href="https://github.com/rust-lang/log/compare/0.4.28...0.4.29">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=log&package-manager=cargo&previous-version=0.4.28&new-version=0.4.29)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-18 00:50:59 +01:00
andrews05
fa2c69071c
Ensure evaluator returns no result when all candidates are too large (#754)
There was a small crack in the fast evaluation logic where a phase 2
candidate's IDAT could be compressed below the best size from phase 1,
but the estimated output size (which includes chunks like PLTE) was
above. This candidate could end up getting returned from the evaluator
if no other filter actually did compress better, and would then be used
for the final compression even though the phase 1 candidate was actually
better.

The issue could potentially affect small, indexed images in `--fast`
mode (o2 and below).

Fixes #753.
2025-12-18 00:22:36 +01:00
Luracasmus
f99ce277da
Use PngResult where applicable (#752) 2025-12-11 11:34:46 +13:00
Luracasmus
0b68c08852
Use extend_from_slice in fctl_data & output 2025-12-11 10:52:10 +13:00
21 changed files with 748 additions and 609 deletions

View file

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

View file

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

View file

@ -2,9 +2,9 @@ name: docker
on: on:
push: push:
branches: branches:
- 'master' - master
tags: tags:
- 'v*.*.*' - v*.*.*
pull_request: pull_request:
types: types:
- opened - opened
@ -17,76 +17,44 @@ on:
type: boolean type: boolean
default: true default: true
env: concurrency:
REGISTRY: ghcr.io # If we ever end up with two concurrent Docker build jobs for the same commit
# ghcr.io/OWNER/REPO # and same versioning context, the former should be cancelled
IMAGE_NAME: ${{ github.repository }} group: docker-${{ github.sha }}-${{ github.ref_type }}-${{ github.head_ref }}
cancel-in-progress: true
jobs: jobs:
build: build:
runs-on: ubuntu-latest uses: docker/github-builder/.github/workflows/build.yml@v1
permissions: permissions:
contents: read contents: read
packages: write packages: write
id-token: write id-token: write
attestations: write attestations: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} output: image
username: ${{ github.actor }} platforms: linux/amd64,linux/arm64
password: ${{ secrets.GITHUB_TOKEN }} push: ${{ github.event_name != 'pull_request' }}
cache: ${{ github.event_name != 'workflow_dispatch' || inputs.use_cache }}
# Extract metadata (tags, labels) for Docker # We don't sign the image because ghcr.io doesn't support OCI Referrers API yet, and the fallback relies on pushing new tags on each build
# For some reason the title have to be set manually # https://github.com/docker/github-builder/issues/109#issuecomment-3885082486
# https://github.com/docker/metadata-action # https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#backwards-compatibility
- name: Extract Docker metadata sign: false
id: meta sbom: true
uses: docker/metadata-action@v5 meta-images: ghcr.io/${{ github.repository }}
with: meta-tags: |
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{raw}}
set-meta-annotations: true
set-meta-labels: true
labels: | labels: |
org.opencontainers.image.title=Oxipng org.opencontainers.image.title=Oxipng
annotations: | annotations: |
org.opencontainers.image.title=Oxipng org.opencontainers.image.title=Oxipng
# Build and push Docker image with Buildx (don't push on PR) secrets:
# Cache isn't used for tags and on workflow_dispatch if specified registry-auths: |
# https://github.com/docker/build-push-action - registry: ghcr.io
- name: Build and push Docker image username: ${{ github.actor }}
id: build-and-push password: ${{ secrets.GITHUB_TOKEN }}
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
no-cache: ${{ (github.event_name == 'workflow_dispatch' && !inputs.use_cache) || startsWith(github.ref, 'refs/tags/') }}
# Attest the build provenance
# TODO: enable push to registry when referrers API will be supported by ghcr.io
- name: Attest Build Provenance
if: github.event_name != 'pull_request'
uses: actions/attest-build-provenance@v3
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build-and-push.outputs.digest }}
push-to-registry: false

View file

@ -8,6 +8,11 @@ on:
- synchronize - synchronize
workflow_dispatch: workflow_dispatch:
concurrency:
# If we ever end up with two concurrent CI jobs for the same commit, the former should be cancelled
group: ci-${{ github.sha }}
cancel-in-progress: true
jobs: jobs:
ci: ci:
name: CI name: CI
@ -55,7 +60,6 @@ jobs:
env: env:
CARGO_BUILD_TARGET: ${{ matrix.target }} CARGO_BUILD_TARGET: ${{ matrix.target }}
RUSTFLAGS: '-Zlocation-detail=none'
steps: steps:
- name: Checkout source - name: Checkout source
@ -74,7 +78,7 @@ jobs:
with: with:
toolchain: nightly toolchain: nightly
target: ${{ env.CARGO_BUILD_TARGET }} target: ${{ env.CARGO_BUILD_TARGET }}
components: clippy, rustfmt components: clippy, rustfmt, rust-src
cache-bin: false cache-bin: false
cache-shared-key: cache cache-shared-key: cache
@ -125,10 +129,12 @@ jobs:
run: cargo doc --release --no-deps run: cargo doc --release --no-deps
- name: Build CLI binary - name: Build CLI binary
run: cargo build --release run: cargo build --release -Zbuild-std
env:
RUSTFLAGS: "-Zlocation-detail=none -Zunstable-options -Cpanic=immediate-abort"
- name: Upload CLI binary as artifact - name: Upload CLI binary as artifact
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v7
with: with:
name: Oxipng binary (${{ matrix.target }}) name: Oxipng binary (${{ matrix.target }})
path: | path: |

8
.gitignore vendored
View file

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

View file

@ -1,7 +1,21 @@
## Version 10.1.1
- [Performance] Improve Bigrams performance, giving notably faster results at lower levels.
- [Improvement] Change `--np` flag to also prevent conversion to indexed from other color types.
- [Improvement] Improve support for recompressing ICC profiles with high compression ratios.
- [Misc] Add warning when `--ziwi` exceeds `--zi`.
- [Build] Further reduce size of binaries.
## Version 10.1.0
- [Feature] Add `--json` option for machine-readable output.
- [Improvement] New default output with file counter and summary. (Use an extra `-v` flag to get the same output as before.)
- [Bugfix] Fix fast mode sometimes giving suboptimal results for small, indexed images.
## Version 10.0.0 ## Version 10.0.0
- [Breaking] CLI: Change short zopfli flag from `-Z` to `-z`. - [Breaking] CLI: Change short zopfli flag from `-Z` to `-z`.
- [Breaking] CLI: Change `--pretend`/`-P` to `--dry-run`/`-D`. - [Breaking] CLI: Change `--pretend`/`-P` to `--dry-run`/`-d`.
- [Breaking] CLI: Change `--interlace` options from `1`/`0` to `on`/`off`. - [Breaking] CLI: Change `--interlace` options from `1`/`0` to `on`/`off`.
- [Breaking] API: Change `Options.interlace: Option<Interlacing>` to `Options.interlace: Option<bool>`. - [Breaking] API: Change `Options.interlace: Option<Interlacing>` to `Options.interlace: Option<bool>`.
- [Breaking] API: Change `Options.filter: IndexSet<RowFilter>` to `Options.filters: IndexSet<FilterStrategy>`. - [Breaking] API: Change `Options.filter: IndexSet<RowFilter>` to `Options.filters: IndexSet<FilterStrategy>`.

390
Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@
## Overview ## Overview
Oxipng is a multithreaded lossless PNG/APNG compression optimizer. It can be used via a command-line Oxipng is a multithreaded lossless PNG/APNG compression optimizer. It can be used via a command-line
interface or as a library in other Rust programs. interface or as a library in other Rust programs. It is fast and highly effective.
## Installing ## Installing
@ -76,7 +76,14 @@ All options are case-sensitive.
\* Note that oxipng is not a brute-force optimizer. This means that while higher optimization levels \* Note that oxipng is not a brute-force optimizer. This means that while higher optimization levels
are almost always better or equal to lower levels, this is not guaranteed and it is possible in are almost always better or equal to lower levels, this is not guaranteed and it is possible in
rare circumstances that a lower level may give a marginally smaller output. Similarly, using Zopfli rare circumstances that a lower level may give a marginally smaller output. Similarly, using Zopfli
compression (`-z`) is not guaranteed to always be better than without. compression (`-z`) is not guaranteed to always be better than default compression.
## APNG support
Oxipng currently only supports limited optimization of animated PNGs (APNGs). It can perform
alpha-optimization, refiltering and recompression of all frames, but all transformations will be
disabled. For best results, it is recommended to use another tool such as
[apngopt](https://sourceforge.net/projects/apng/files/APNG_Optimizer/) before running Oxipng.
## Git integration via [pre-commit] ## Git integration via [pre-commit]
@ -131,17 +138,13 @@ and specifying the desired ones, for example:
## History ## History
Oxipng began as a complete rewrite of the OptiPNG project, Oxipng began in 2015 as a rewrite of the OptiPNG project. The core goal was to implement
which was assumed to be dead as no commit had been made to it since March 2014. multithreading, which would have been very difficult to do within the existing C codebase of OptiPNG.
(OptiPNG has since released a new version, after Oxipng was first released.)
The name has been changed to avoid confusion and potential legal issues.
The core goal of rewriting OptiPNG was to implement multithreading,
which would be very difficult to do within the existing C codebase of OptiPNG.
This also served as an opportunity to choose a more modern, safer language (Rust). This also served as an opportunity to choose a more modern, safer language (Rust).
Note that, while similar, Oxipng is not a drop-in replacement for OptiPNG. However, Oxipng has evolved considerably since then. While some of the options remain similar to
If you are migrating from OptiPNG, please check the [help](MANUAL.txt) before using. OptiPNG, the architecture and capabilities are now quite different. It is not a drop-in
replacement - if you are migrating from OptiPNG, please check the [help](MANUAL.txt) before use.
## Contributing ## Contributing
@ -151,42 +154,11 @@ to submit a fix with the bug report, it is preferred that you do so via pull req
however you do not need to be a Rust developer to contribute. however you do not need to be a Rust developer to contribute.
Other contributions (such as improving documentation or translations) are also welcome via GitHub. Other contributions (such as improving documentation or translations) are also welcome via GitHub.
## Benchmarks
An independent benchmark is linked here with permission by the author:\
[oxipng and friends: A comparison of PNG optimization tools](https://op111.net/posts/2025/09/png-compression-oxipng-optipng-fileoptimizer-cwebp/)
## License ## License
Oxipng is open-source software, distributed under the MIT license. Oxipng is open-source software, distributed under the MIT license.
## Benchmarks
Tested Oxipng 9.0.0 (commit `c16519b38b0519988db625913be919d4f0e42f5d`, compiled
on `rustc 1.74.0-nightly (7b4d9e155 2023-09-28)`) against OptiPNG version 0.7.7,
as packaged by Debian unstable, on a Linux 6.5.0-2-amd64 kernel, Intel Core
i7-12700 CPU (8 performance cores, 4 efficiency cores, 20 threads), DDR5-5200
RAM in dual channel configuration.
```
Benchmark 1: ./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 59.6 ms ± 7.7 ms [User: 77.4 ms, System: 3.6 ms]
Range (min … max): 53.3 ms … 89.9 ms 32 runs
Benchmark 2: optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 132.4 ms ± 0.8 ms [User: 132.5 ms, System: 0.6 ms]
Range (min … max): 131.8 ms … 134.4 ms 22 runs
Summary
./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png ran
2.22 ± 0.29 times faster than optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
Benchmark 1: ./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 88.7 ms ± 4.3 ms [User: 270.3 ms, System: 11.0 ms]
Range (min … max): 86.8 ms … 109.4 ms 26 runs
Benchmark 2: optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 444.9 ms ± 0.3 ms [User: 444.8 ms, System: 0.7 ms]
Range (min … max): 444.4 ms … 445.6 ms 10 runs
Summary
./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png ran
5.01 ± 0.25 times faster than optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
```

View file

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

View file

@ -212,7 +212,7 @@ losslessly.")
) )
.arg( .arg(
Arg::new("verbose") Arg::new("verbose")
.help("Run in verbose mode (use twice to increase verbosity)") .help("Show per-file info (use multiple times for more detail)")
.short('v') .short('v')
.long("verbose") .long("verbose")
.action(ArgAction::Count) .action(ArgAction::Count)
@ -220,12 +220,20 @@ losslessly.")
) )
.arg( .arg(
Arg::new("quiet") Arg::new("quiet")
.help("Run in quiet mode") .help("Suppress all output messages")
.short('q') .short('q')
.long("quiet") .long("quiet")
.action(ArgAction::SetTrue) .action(ArgAction::SetTrue)
.conflicts_with("verbose"), .conflicts_with("verbose"),
) )
.arg(
Arg::new("json")
.help("Print results as JSON")
.short('j')
.long("json")
.action(ArgAction::SetTrue)
.conflicts_with("stdout"),
)
.arg( .arg(
Arg::new("filters") Arg::new("filters")
.help("Filters to try (0-9; see '--help' for details)") .help("Filters to try (0-9; see '--help' for details)")
@ -290,6 +298,8 @@ The default value depends on the optimization level preset.")
.arg( .arg(
Arg::new("no-palette-reduction") Arg::new("no-palette-reduction")
.help("Do not change color palette") .help("Do not change color palette")
.long_help("\
Do not convert to indexed and do not modify an existing color palette.")
.long("np") .long("np")
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )

View file

@ -8,12 +8,12 @@ use std::sync::{
atomic::{AtomicUsize, Ordering::*}, atomic::{AtomicUsize, Ordering::*},
}; };
#[cfg(feature = "parallel")]
use crossbeam_channel::{Receiver, Sender, unbounded};
use deflate::Deflater; use deflate::Deflater;
use indexmap::IndexSet; use indexmap::IndexSet;
use log::trace; use log::trace;
use rayon::prelude::*; use rayon::prelude::*;
#[cfg(feature = "parallel")]
use std::sync::mpsc::{Receiver, Sender, channel};
#[cfg(not(feature = "parallel"))] #[cfg(not(feature = "parallel"))]
use crate::rayon; use crate::rayon;
@ -73,7 +73,7 @@ impl Evaluator {
final_round: bool, final_round: bool,
) -> Self { ) -> Self {
#[cfg(feature = "parallel")] #[cfg(feature = "parallel")]
let eval_channel = unbounded(); let eval_channel = channel();
Self { Self {
deadline, deadline,
filters, filters,
@ -118,7 +118,7 @@ impl Evaluator {
/// Check if the image is smaller than others /// Check if the image is smaller than others
pub fn try_image(&self, image: Arc<PngImage>) { pub fn try_image(&self, image: Arc<PngImage>) {
let description = format!("{}", image.ihdr.color_type); let description = image.ihdr.color_type.to_string();
self.try_image_with_description(image, &description); self.try_image_with_description(image, &description);
} }
@ -154,6 +154,19 @@ impl Evaluator {
let idat_data = deflater.deflate(&filtered, best_candidate_size.get()); let idat_data = deflater.deflate(&filtered, best_candidate_size.get());
if let Ok(idat_data) = idat_data { if let Ok(idat_data) = idat_data {
let estimated_output_size = image.estimated_output_size(&idat_data); let estimated_output_size = image.estimated_output_size(&idat_data);
trace!(
"Eval: {}-bit {:23} {:8} {} bytes",
image.ihdr.bit_depth, description, filter, estimated_output_size
);
// Skip if it exceeds best known size. (This is important to ensure
// the evaluator returns no result when all candidates are too large.)
if let Some(max) = best_candidate_size.get() {
if estimated_output_size > max {
return;
}
}
// We only need to retain the IDAT data in the final round // We only need to retain the IDAT data in the final round
let new = Candidate { let new = Candidate {
image: image.clone(), image: image.clone(),
@ -164,10 +177,6 @@ impl Evaluator {
nth, nth,
}; };
best_candidate_size.set_min(estimated_output_size); best_candidate_size.set_min(estimated_output_size);
trace!(
"Eval: {}-bit {:23} {:8} {} bytes",
image.ihdr.bit_depth, description, filter, estimated_output_size
);
#[cfg(feature = "parallel")] #[cfg(feature = "parallel")]
{ {

View file

@ -263,11 +263,11 @@ pub fn extract_icc(iccp: &Chunk, max_size: Option<usize>) -> Option<Vec<u8>> {
if compression_method != 0 { if compression_method != 0 {
return None; // The profile is supposed to be compressed (method 0) return None; // The profile is supposed to be compressed (method 0)
} }
// The decompressed size is unknown so we have to guess the required buffer size // Libdeflate works with a fixed size buffer. Since the decompressed size is unknown we have to
let mut out_size = compressed_data.len() * 2 + 1000; // guess the required buffer size. We allow a fairly generous 10x factor with a minimum of 1000.
if let Some(max) = max_size { let mut out_size = (compressed_data.len() * 10).max(1000);
out_size = out_size.min(max); // For sanity, impose a default limit of 1MB.
} out_size = out_size.min(max_size.unwrap_or(1_000_000));
match inflate(compressed_data, out_size) { match inflate(compressed_data, out_size) {
Ok(icc) => Some(icc), Ok(icc) => Some(icc),
Err(e) => { Err(e) => {

View file

@ -7,9 +7,9 @@ extern crate rayon;
mod rayon; mod rayon;
use std::{ use std::{
fs::{File, Metadata}, fs::File,
io::{BufWriter, Read, Write, stdin, stdout}, io::{BufWriter, Read, Write, stdin, stdout},
path::{Path, PathBuf}, path::PathBuf,
sync::{ sync::{
Arc, Arc,
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
@ -64,6 +64,7 @@ pub mod internal_tests {
} }
pub type PngResult<T> = Result<T, PngError>; pub type PngResult<T> = Result<T, PngError>;
pub type OptimizationResult = PngResult<(usize, usize)>;
#[derive(Debug)] #[derive(Debug)]
/// A raw image definition which can be used to create an optimized png /// A raw image definition which can be used to create an optimized png
@ -86,7 +87,7 @@ impl RawImage {
color_type: ColorType, color_type: ColorType,
bit_depth: BitDepth, bit_depth: BitDepth,
data: Vec<u8>, data: Vec<u8>,
) -> Result<Self, PngError> { ) -> PngResult<Self> {
// Validate bit depth // Validate bit depth
let valid_depth = match color_type { let valid_depth = match color_type {
ColorType::Grayscale { .. } => true, ColorType::Grayscale { .. } => true,
@ -165,40 +166,15 @@ impl RawImage {
/// Perform optimization on the input file using the options provided /// Perform optimization on the input file using the options provided
/// ///
/// Returns the original and optimized file sizes /// Returns the original and optimized file sizes
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(usize, usize)> { pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> OptimizationResult {
// Read in the file and try to decode as PNG. // Read in the file and try to decode as PNG.
info!("Processing: {input}"); info!("Processing: {input}");
let deadline = Arc::new(Deadline::new(opts.timeout)); let deadline = Arc::new(Deadline::new(opts.timeout));
// grab metadata before even opening input file to preserve atime
let opt_metadata_preserved;
let in_data = match *input { let in_data = match *input {
InFile::Path(ref input_path) => { InFile::Path(ref input_path) => PngData::read_file(input_path)?,
if matches!(
output,
OutFile::Path {
preserve_attrs: true,
..
}
) {
opt_metadata_preserved = input_path
.metadata()
.map_err(|err| {
// Fail if metadata cannot be preserved
PngError::new(&format!(
"Unable to read metadata from input file {input_path:?}: {err}"
))
})
.map(Some)?;
trace!("preserving metadata: {opt_metadata_preserved:?}");
} else {
opt_metadata_preserved = None;
}
PngData::read_file(input_path)?
}
InFile::StdIn => { InFile::StdIn => {
opt_metadata_preserved = None;
let mut data = Vec::new(); let mut data = Vec::new();
stdin() stdin()
.read_to_end(&mut data) .read_to_end(&mut data)
@ -220,7 +196,7 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
(OutFile::Path { path, .. }, InFile::Path(input_path)) (OutFile::Path { path, .. }, InFile::Path(input_path))
if path.as_ref().is_none_or(|p| p == input_path) => if path.as_ref().is_none_or(|p| p == input_path) =>
{ {
info!("{input}: Could not optimize further, no change written"); info!("Could not optimize further, no change written: {input}");
return Ok((in_length, in_length)); return Ok((in_length, in_length));
} }
_ => { _ => {
@ -252,18 +228,34 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
buffer buffer
.write_all(&optimized_output) .write_all(&optimized_output)
.map_err(|e| PngError::WriteFailed("stdout".into(), e))?; .map_err(|e| PngError::WriteFailed("stdout".into(), e))?;
info!("{savings}: stdout");
} }
(OutFile::Path { path, .. }, _) => { (
OutFile::Path {
path,
preserve_attrs,
},
_,
) => {
let input_metadata = if *preserve_attrs {
input.path().and_then(|in_path| {
let meta = in_path.metadata();
if let Err(e) = &meta {
warn!("Unable to read metadata from {in_path:?}: {e}");
}
meta.ok()
})
} else {
None
};
let output_path = path let output_path = path
.as_ref() .as_ref()
.map_or_else(|| input.path().unwrap(), PathBuf::as_path); .map_or_else(|| input.path().unwrap(), PathBuf::as_path);
let out_file = File::create(output_path) let out_file = File::create(output_path)
.map_err(|err| PngError::WriteFailed(output_path.display().to_string(), err))?; .map_err(|err| PngError::WriteFailed(output_path.display().to_string(), err))?;
if let Some(metadata_input) = &opt_metadata_preserved {
copy_permissions(metadata_input, &out_file)?;
}
let mut buffer = BufWriter::new(out_file); let mut buffer = BufWriter::new(&out_file);
buffer buffer
.write_all(&optimized_output) .write_all(&optimized_output)
// flush BufWriter so IO errors don't get swallowed silently on close() by drop! // flush BufWriter so IO errors don't get swallowed silently on close() by drop!
@ -271,8 +263,18 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
.map_err(|e| PngError::WriteFailed(output_path.display().to_string(), e))?; .map_err(|e| PngError::WriteFailed(output_path.display().to_string(), e))?;
// force drop and thereby closing of file handle before modifying any timestamp // force drop and thereby closing of file handle before modifying any timestamp
std::mem::drop(buffer); std::mem::drop(buffer);
if let Some(metadata_input) = &opt_metadata_preserved {
copy_times(metadata_input, output_path)?; if let Some(metadata_input) = &input_metadata {
let set_time = metadata_input
.modified()
.and_then(|m| out_file.set_modified(m));
if let Err(e) = set_time {
warn!("Unable to set modification time on {output_path:?}: {e}");
}
let set_perm = out_file.set_permissions(metadata_input.permissions());
if let Err(e) = set_perm {
warn!("Unable to set permissions on {output_path:?}: {e}");
}
} }
info!("{}: {}", savings, output_path.display()); info!("{}: {}", savings, output_path.display());
} }
@ -642,29 +644,3 @@ fn recompress_frames(
const fn is_fully_optimized(original_size: usize, optimized_size: usize, opts: &Options) -> bool { const fn is_fully_optimized(original_size: usize, optimized_size: usize, opts: &Options) -> bool {
original_size <= optimized_size && !opts.force original_size <= optimized_size && !opts.force
} }
fn copy_permissions(metadata_input: &Metadata, out_file: &File) -> PngResult<()> {
out_file
.set_permissions(metadata_input.permissions())
.map_err(|err_io| {
PngError::new(&format!(
"Unable to set permissions for output file: {err_io}"
))
})
}
#[cfg(not(feature = "filetime"))]
const fn copy_times(_: &Metadata, _: &Path) -> PngResult<()> {
Ok(())
}
#[cfg(feature = "filetime")]
fn copy_times(input_path_meta: &Metadata, out_path: &Path) -> PngResult<()> {
let mtime = filetime::FileTime::from_last_modification_time(input_path_meta);
trace!("attempting to set file modification time: {mtime:?}");
filetime::set_file_mtime(out_path, mtime).map_err(|err_io| {
PngError::new(&format!(
"Unable to set file times on {out_path:?}: {err_io}"
))
})
}

View file

@ -6,9 +6,10 @@ use std::num::NonZeroU64;
use std::{ use std::{
ffi::{OsStr, OsString}, ffi::{OsStr, OsString},
fs::DirBuilder, fs::DirBuilder,
io::Write, io::{IsTerminal, Write, stdout},
path::PathBuf, path::PathBuf,
process::ExitCode, process::ExitCode,
sync::atomic::{AtomicUsize, Ordering::AcqRel},
time::Duration, time::Duration,
}; };
@ -18,7 +19,9 @@ use indexmap::IndexSet;
use log::{Level, LevelFilter, error, warn}; use log::{Level, LevelFilter, error, warn};
#[cfg(feature = "zopfli")] #[cfg(feature = "zopfli")]
use oxipng::ZopfliOptions; use oxipng::ZopfliOptions;
use oxipng::{Deflater, FilterStrategy, InFile, Options, OutFile, PngError, StripChunks}; use oxipng::{
Deflater, FilterStrategy, InFile, OptimizationResult, Options, OutFile, PngError, StripChunks,
};
use rayon::prelude::*; use rayon::prelude::*;
use crate::cli::DISPLAY_CHUNKS; use crate::cli::DISPLAY_CHUNKS;
@ -35,7 +38,7 @@ fn main() -> ExitCode {
.after_long_help("") .after_long_help("")
.get_matches_from(std::env::args()); .get_matches_from(std::env::args());
let (out_file, out_dir, opts) = match parse_opts_into_struct(&matches) { let (mut out_file, out_dir, opts) = match parse_opts_into_struct(&matches) {
Ok(x) => x, Ok(x) => x,
Err(x) => { Err(x) => {
error!("{x}"); error!("{x}");
@ -43,51 +46,128 @@ fn main() -> ExitCode {
} }
}; };
let files = collect_files( // Determine input and output
let file_args = matches.get_many::<PathBuf>("files").unwrap().cloned();
#[cfg(windows)] #[cfg(windows)]
matches let inputs: Vec<_> = file_args.flat_map(apply_glob_pattern).collect();
.get_many::<PathBuf>("files")
.unwrap()
.cloned()
.flat_map(apply_glob_pattern)
.collect(),
#[cfg(not(windows))] #[cfg(not(windows))]
matches let inputs: Vec<_> = file_args.collect();
.get_many::<PathBuf>("files") let using_stdin = inputs.len() == 1 && inputs[0].to_str() == Some("-");
.unwrap() if using_stdin && out_dir.is_some() {
.cloned() error!("Cannot use --dir when reading from stdin.");
.collect(), return ExitCode::FAILURE;
}
if using_stdin && matches!(out_file, OutFile::Path { path: None, .. }) {
out_file = OutFile::StdOut;
}
let using_stdout = matches!(out_file, OutFile::StdOut);
let json = matches.get_flag("json");
if using_stdout && json {
error!("Cannot use --json when writing to stdout.");
return ExitCode::FAILURE;
}
let files = if using_stdin {
vec![(InFile::StdIn, out_file)]
} else {
collect_files(
inputs,
&out_dir, &out_dir,
&out_file, &out_file,
matches.get_flag("recursive"), matches.get_flag("recursive"),
true, true,
); )
let parallel_files = matches.get_flag("parallel-files");
let summary = if parallel_files {
files
.into_par_iter()
.map(|(input, output)| process_file(&input, &output, &opts))
.min()
} else {
files
.into_iter()
.map(|(input, output)| process_file(&input, &output, &opts))
.min()
}; };
match summary.unwrap_or(OptimizationResult::Skipped) { let is_verbose = matches.get_count("verbose") > 0;
OptimizationResult::Ok => ExitCode::SUCCESS, let print_summary = !matches.get_flag("quiet") && !using_stdout;
OptimizationResult::Failed => ExitCode::FAILURE, let print_progress = print_summary && !is_verbose && stdout().is_terminal();
OptimizationResult::Skipped => ExitCode::from(3), let total_files = files.len();
let num_processed = AtomicUsize::new(0);
if print_progress {
print!("Files processed: 0/{}...", total_files);
stdout().flush().ok();
}
let process = |(input, output): &(InFile, OutFile)| {
let result = process_file(input, output, &opts);
if print_progress && matches!(result, OptimizationResult::Ok(_)) {
let value = num_processed.fetch_add(1, AcqRel) + 1;
print!("\rFiles processed: {}/{}...", value, total_files);
stdout().flush().ok();
}
result
};
let results: Vec<OptimizationResult> = if matches.get_flag("parallel-files") {
files.par_iter().map(process).collect()
} else {
files.iter().map(process).collect()
};
// 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,
} }
} }
#[derive(Eq, PartialEq, Ord, PartialOrd)] // Print results
enum OptimizationResult { if json {
Ok, json_output(&files, &results);
Failed, } else if print_summary {
Skipped, let in_bytes = format_bytes(total_in, true);
let out_bytes = format_bytes(total_out, true);
let saved = total_in - total_out;
let saved_bytes = format_bytes(saved, false);
let percent = if total_in > 0 {
saved as f64 / total_in as f64 * 100_f64
} else {
0_f64
};
if is_verbose {
println!("--------------------");
}
println!("\rFiles processed: {num_succeeded}/{total_files} ");
println!("Input size: {}", in_bytes);
println!("Output size: {}", out_bytes);
println!("Total saved: {} ({:.2}%)", saved_bytes, percent);
if num_not_optimized == 1 {
println!("({num_not_optimized} file could not be optimized further)");
} else if num_not_optimized > 0 {
println!("({num_not_optimized} files could not be optimized further)");
}
if matches.get_flag("dry-run") {
println!("Dry run, no changes saved");
}
}
// For optimizing single files, this will return the correct exit code always.
// For recursive optimization, the correct choice is a bit subjective.
// We're choosing to return a 0 exit code if ANY file in the set
// runs correctly.
// The reason for this is that recursion may pick up files that are not
// PNG files, and return an error for them.
// We don't really want to return an error code for those files.
if num_succeeded > 0 {
ExitCode::SUCCESS
} else if num_failed > 0 {
ExitCode::FAILURE
} else {
ExitCode::from(3)
}
} }
fn collect_files( fn collect_files(
@ -98,10 +178,8 @@ fn collect_files(
top_level: bool, //explicitly specify files top_level: bool, //explicitly specify files
) -> Vec<(InFile, OutFile)> { ) -> Vec<(InFile, OutFile)> {
let mut in_out_pairs = Vec::new(); let mut in_out_pairs = Vec::new();
let allow_stdin = top_level && files.len() == 1;
for input in files { for input in files {
let using_stdin = allow_stdin && input.to_str() == Some("-"); if input.is_dir() {
if !using_stdin && input.is_dir() {
if recursive { if recursive {
match input.read_dir() { match input.read_dir() {
Ok(dir) => { Ok(dir) => {
@ -118,6 +196,15 @@ fn collect_files(
} }
continue; continue;
} }
// Skip non png files if not given on top level
if !top_level && {
let extension = input.extension().map(OsStr::to_ascii_lowercase);
extension != Some(OsString::from("png")) && extension != Some(OsString::from("apng"))
} {
continue;
}
let out_file = let out_file =
if let (Some(out_dir), &OutFile::Path { preserve_attrs, .. }) = (out_dir, out_file) { if let (Some(out_dir), &OutFile::Path { preserve_attrs, .. }) = (out_dir, out_file) {
let path = Some(out_dir.join(input.file_name().unwrap())); let path = Some(out_dir.join(input.file_name().unwrap()));
@ -128,19 +215,7 @@ fn collect_files(
} else { } else {
(*out_file).clone() (*out_file).clone()
}; };
let in_file = if using_stdin { let in_file = InFile::Path(input);
InFile::StdIn
} else {
// Skip non png files if not given on top level
if !top_level && {
let extension = input.extension().map(OsStr::to_ascii_lowercase);
extension != Some(OsString::from("png"))
&& extension != Some(OsString::from("apng"))
} {
continue;
}
InFile::Path(input)
};
in_out_pairs.push((in_file, out_file)); in_out_pairs.push((in_file, out_file));
} }
in_out_pairs in_out_pairs
@ -165,8 +240,9 @@ fn parse_opts_into_struct(
) -> Result<(OutFile, Option<PathBuf>, Options), String> { ) -> Result<(OutFile, Option<PathBuf>, Options), String> {
let log_level = match matches.get_count("verbose") { let log_level = match matches.get_count("verbose") {
_ if matches.get_flag("quiet") => LevelFilter::Off, _ if matches.get_flag("quiet") => LevelFilter::Off,
0 => LevelFilter::Info, 0 => LevelFilter::Warn,
1 => LevelFilter::Debug, 1 => LevelFilter::Info,
2 => LevelFilter::Debug,
_ => LevelFilter::Trace, _ => LevelFilter::Trace,
}; };
env_logger::builder() env_logger::builder()
@ -175,7 +251,8 @@ fn parse_opts_into_struct(
match record.level() { match record.level() {
Level::Error | Level::Warn => { Level::Error | Level::Warn => {
let style = buf.default_level_style(record.level()); let style = buf.default_level_style(record.level());
writeln!(buf, "{style}{}{style:#}", record.args()) // Prepend carriage return to clear progress line
writeln!(buf, "\r{style}{}{style:#}", record.args())
} }
// Leave info, debug and trace unstyled // Leave info, debug and trace unstyled
_ => writeln!(buf, "{}", record.args()), _ => writeln!(buf, "{}", record.args()),
@ -353,6 +430,16 @@ fn parse_opts_into_struct(
let iterations_without_improvement = *matches let iterations_without_improvement = *matches
.get_one::<NonZeroU64>("iterations-without-improvement") .get_one::<NonZeroU64>("iterations-without-improvement")
.unwrap_or(&NonZeroU64::MAX); .unwrap_or(&NonZeroU64::MAX);
if iterations_without_improvement > iteration_count
&& iterations_without_improvement != NonZeroU64::MAX
{
warn!(
"--ziwi ({}) is higher than --zi ({}) and will never be reached.",
iterations_without_improvement, iteration_count
);
}
opts.deflater = Deflater::Zopfli(ZopfliOptions { opts.deflater = Deflater::Zopfli(ZopfliOptions {
iteration_count, iteration_count,
iterations_without_improvement, iterations_without_improvement,
@ -440,26 +527,128 @@ fn process_file(input: &InFile, output: &OutFile, opts: &Options) -> Optimizatio
if let (Some(max_size), InFile::Path(path)) = (opts.max_decompressed_size, input) { if let (Some(max_size), InFile::Path(path)) = (opts.max_decompressed_size, input) {
if path.metadata().is_ok_and(|m| m.len() > max_size as u64) { if path.metadata().is_ok_and(|m| m.len() > max_size as u64) {
warn!("{input}: Skipped: File exceeds the maximum size ({max_size} bytes)"); warn!("{input}: Skipped: File exceeds the maximum size ({max_size} bytes)");
return OptimizationResult::Skipped; return Err(PngError::InflatedDataTooLong(max_size));
} }
} }
match oxipng::optimize(input, output, opts) { let result = oxipng::optimize(input, output, opts);
// For optimizing single files, this will return the correct exit code always. match &result {
// For recursive optimization, the correct choice is a bit subjective. Ok(_) => {}
// We're choosing to return a 0 exit code if ANY file in the set
// runs correctly.
// The reason for this is that recursion may pick up files that are not
// PNG files, and return an error for them.
// We don't really want to return an error code for those files.
Ok(_) => OptimizationResult::Ok,
Err(e @ PngError::C2PAMetadataPreventsChanges | e @ PngError::InflatedDataTooLong(_)) => { Err(e @ PngError::C2PAMetadataPreventsChanges | e @ PngError::InflatedDataTooLong(_)) => {
warn!("{input}: Skipped: {e}"); warn!("{input}: Skipped: {e}");
OptimizationResult::Skipped
} }
Err(e) => { Err(e) => {
error!("{input}: {e}"); error!("{input}: {e}");
OptimizationResult::Failed
} }
} }
result
}
/// Write optimization results as json.
/// ```
/// {
/// "results": [
/// {
/// "input": string,
/// "status": "success",
/// "output": string|null,
/// "insize": number,
/// "outsize": number
/// },
/// {
/// "input": string,
/// "status": "error",
/// "error": string
/// }
/// ]
/// }
/// ```
fn json_output(files: &[(InFile, OutFile)], results: &[OptimizationResult]) {
print!(r#"{{"results":["#);
let mut first = true;
results
.iter()
.zip(files)
.for_each(|(result, (input, output))| {
if !first {
print!(",");
}
print!(r#"{{"input":"{}","#, json_escape(&input.to_string()));
match result {
Ok((insize, outsize)) => {
let outpath = match output {
OutFile::None => "null".to_owned(),
OutFile::Path { path: None, .. } => {
format!(r#""{}""#, json_escape(&input.to_string()))
}
OutFile::Path { path: Some(p), .. } => {
format!(r#""{}""#, json_escape(&p.display().to_string()))
}
OutFile::StdOut => unreachable!(),
};
print!(
r#""status":"success","output":{},"insize":{},"outsize":{}}}"#,
outpath, insize, outsize
);
}
Err(e) => {
print!(
r#""status":"error","error":"{}"}}"#,
json_escape(&e.to_string())
);
}
}
first = false;
});
print!("]}}");
}
fn json_escape(string: &str) -> String {
string
.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\t", "\\t")
.replace("\r", "\\r")
.replace("\x08", "\\b")
.replace("\x0c", "\\f")
}
/// Format byte counts as IEC units to 3 significant figures.
fn format_bytes(count: i64, include_raw: bool) -> String {
const K: i64 = 1 << 10;
const M: i64 = 1 << 20;
const G: i64 = 1 << 30;
fn format_3sf(value: f64) -> String {
match value.abs() {
..9.995 => format!("{:.2}", value),
9.995..99.95 => format!("{:.1}", value),
_ => format!("{:.0}", value),
}
}
let formatted = match count.abs() {
..K => format!("{} bytes", count),
K..M => format!("{} KiB", format_3sf(count as f64 / K as f64)),
M..G => format!("{} MiB", format_3sf(count as f64 / M as f64)),
_ => format!("{} GiB", format_3sf(count as f64 / G as f64)),
};
if include_raw && count.abs() >= K {
format!("{} ({} bytes)", formatted, count)
} else {
formatted
}
}
#[cfg(test)]
mod tests {
use super::format_bytes;
#[test]
fn test_format_bytes() {
assert_eq!(format_bytes(1023, false), "1023 bytes");
assert_eq!(format_bytes(800_000, false), "781 KiB");
assert_eq!(format_bytes(12_500_000, false), "11.9 MiB");
assert_eq!(format_bytes(2_000_000_000, false), "1.86 GiB");
assert_eq!(format_bytes(-1024, false), "-1.00 KiB");
}
} }

View file

@ -1,13 +1,11 @@
use std::{fs, io::Write, path::Path, sync::Arc}; use std::{fs, path::Path, sync::Arc};
use bitvec::bitarr;
use libdeflater::{CompressionLvl, Compressor}; use libdeflater::{CompressionLvl, Compressor};
use log::warn; use log::warn;
use rgb::ComponentSlice;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use crate::{ use crate::{
Options, Options, PngResult,
apng::*, apng::*,
colors::{BitDepth, ColorType}, colors::{BitDepth, ColorType},
deflate, deflate,
@ -45,18 +43,18 @@ pub struct PngData {
impl PngData { impl PngData {
/// Create a new `PngData` struct by opening a file /// Create a new `PngData` struct by opening a file
#[inline] #[inline]
pub fn new(filepath: &Path, opts: &Options) -> Result<Self, PngError> { pub fn new(filepath: &Path, opts: &Options) -> PngResult<Self> {
let byte_data = Self::read_file(filepath)?; let byte_data = Self::read_file(filepath)?;
Self::from_slice(&byte_data, opts) Self::from_slice(&byte_data, opts)
} }
pub fn read_file(filepath: &Path) -> Result<Vec<u8>, PngError> { pub fn read_file(filepath: &Path) -> PngResult<Vec<u8>> {
fs::read(filepath).map_err(|e| PngError::ReadFailed(filepath.display().to_string(), e)) fs::read(filepath).map_err(|e| PngError::ReadFailed(filepath.display().to_string(), e))
} }
/// Create a new `PngData` struct by reading a slice /// Create a new `PngData` struct by reading a slice
pub fn from_slice(byte_data: &[u8], opts: &Options) -> Result<Self, PngError> { pub fn from_slice(byte_data: &[u8], opts: &Options) -> PngResult<Self> {
let mut byte_offset: usize = 0; let mut byte_offset: usize = 0;
// Test that png header is valid // Test that png header is valid
let header = byte_data.get(0..8).ok_or(PngError::TruncatedData)?; let header = byte_data.get(0..8).ok_or(PngError::TruncatedData)?;
@ -165,17 +163,15 @@ impl PngData {
let mut output = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; let mut output = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
// IHDR // IHDR
let mut ihdr_data = Vec::with_capacity(13); let mut ihdr_data = Vec::with_capacity(13);
ihdr_data.write_all(&self.raw.ihdr.width.to_be_bytes()).ok(); ihdr_data.extend_from_slice(&self.raw.ihdr.width.to_be_bytes());
ihdr_data ihdr_data.extend_from_slice(&self.raw.ihdr.height.to_be_bytes());
.write_all(&self.raw.ihdr.height.to_be_bytes()) ihdr_data.extend_from_slice(&[self.raw.ihdr.bit_depth as u8]);
.ok(); ihdr_data.extend_from_slice(&[
ihdr_data.write_all(&[self.raw.ihdr.bit_depth as u8]).ok(); self.raw.ihdr.color_type.png_header_code(),
ihdr_data 0, // Compression -- deflate
.write_all(&[self.raw.ihdr.color_type.png_header_code()]) 0, // Filter method -- 5-way adaptive filtering
.ok(); self.raw.ihdr.interlaced as u8,
ihdr_data.write_all(&[0]).ok(); // Compression -- deflate ]);
ihdr_data.write_all(&[0]).ok(); // Filter method -- 5-way adaptive filtering
ihdr_data.write_all(&[self.raw.ihdr.interlaced as u8]).ok();
write_png_block(b"IHDR", &ihdr_data, &mut output); write_png_block(b"IHDR", &ihdr_data, &mut output);
// Ancillary chunks - split into those that come before IDAT and those that come after // Ancillary chunks - split into those that come before IDAT and those that come after
let mut aux_split = self.aux_chunks.split(|c| &c.name == b"IDAT"); let mut aux_split = self.aux_chunks.split(|c| &c.name == b"IDAT");
@ -194,7 +190,7 @@ impl PngData {
ColorType::Indexed { palette } => { ColorType::Indexed { palette } => {
let mut palette_data = Vec::with_capacity(palette.len() * 3); let mut palette_data = Vec::with_capacity(palette.len() * 3);
for px in palette { for px in palette {
palette_data.extend_from_slice(px.rgb().as_slice()); palette_data.extend_from_slice(px.rgb().as_ref());
} }
write_png_block(b"PLTE", &palette_data, &mut output); write_png_block(b"PLTE", &palette_data, &mut output);
if let Some(last_trns) = palette.iter().rposition(|px| px.a != 255) { if let Some(last_trns) = palette.iter().rposition(|px| px.a != 255) {
@ -250,7 +246,7 @@ impl PngData {
} }
impl PngImage { impl PngImage {
pub fn new(ihdr: IhdrData, compressed_data: &[u8]) -> Result<Self, PngError> { pub fn new(ihdr: IhdrData, compressed_data: &[u8]) -> PngResult<Self> {
let raw_data = deflate::inflate(compressed_data, ihdr.raw_data_size())?; let raw_data = deflate::inflate(compressed_data, ihdr.raw_data_size())?;
// Reject files with incorrect width/height or truncated data // Reject files with incorrect width/height or truncated data
@ -334,7 +330,7 @@ impl PngImage {
} }
/// Reverse all filters applied on the image, returning an unfiltered IDAT bytestream /// Reverse all filters applied on the image, returning an unfiltered IDAT bytestream
fn unfilter_image(&self) -> Result<Vec<u8>, PngError> { fn unfilter_image(&self) -> PngResult<Vec<u8>> {
let mut unfiltered = Vec::with_capacity(self.data.len()); let mut unfiltered = Vec::with_capacity(self.data.len());
let bpp = self.bytes_per_channel() * self.channels_per_pixel(); let bpp = self.bytes_per_channel() * self.channels_per_pixel();
let mut last_line: Vec<u8> = Vec::new(); let mut last_line: Vec<u8> = Vec::new();
@ -376,6 +372,12 @@ impl PngImage {
let mut f_buf = Vec::new(); let mut f_buf = Vec::new();
// For heuristic strategies, keep track of the actual filter used for each line // For heuristic strategies, keep track of the actual filter used for each line
let mut filters_used = Vec::new(); let mut filters_used = Vec::new();
// Pre-allocate buffers for the Bigrams strategy to avoid per-line allocations
let (mut bigram_seen, mut bigram_touched) = if matches!(strategy, FilterStrategy::Bigrams) {
(vec![false; 0x10000], Vec::<u16>::new())
} else {
(Vec::new(), Vec::new())
};
for (i, line) in self.scan_lines(false).enumerate() { for (i, line) in self.scan_lines(false).enumerate() {
if prev_pass != line.pass || line.data.len() != prev_line.len() { if prev_pass != line.pass || line.data.len() != prev_line.len() {
prev_line = vec![0; line.data.len()]; prev_line = vec![0; line.data.len()];
@ -467,18 +469,29 @@ impl PngImage {
let mut best_size = usize::MAX; let mut best_size = usize::MAX;
for f in try_filters { for f in try_filters {
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes); f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
let mut set = bitarr![0; 0x10000]; let mut count = 0;
for pair in f_buf.windows(2) { for pair in f_buf.windows(2) {
let bigram = ((pair[0] as usize) << 8) | pair[1] as usize; let bigram = ((pair[0] as usize) << 8) | pair[1] as usize;
set.set(bigram, true); if !bigram_seen[bigram] {
count += 1;
if count >= best_size {
break;
} }
let size = set.count_ones(); bigram_seen[bigram] = true;
if size < best_size { bigram_touched.push(bigram as u16);
best_size = size; }
}
if count < best_size {
best_size = count;
std::mem::swap(&mut best_line, &mut f_buf); std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data); best_line_raw.clone_from(&line_data);
best_filter = *f; best_filter = *f;
} }
// Clear only the entries that were touched
for &idx in &bigram_touched {
bigram_seen[idx as usize] = false;
}
bigram_touched.clear();
} }
} }
FilterStrategy::BigEnt => { FilterStrategy::BigEnt => {

View file

@ -1,7 +1,7 @@
use std::hash::{BuildHasherDefault, Hash}; use std::hash::{BuildHasherDefault, Hash};
use indexmap::IndexSet; use indexmap::IndexSet;
use rgb::{ComponentMap, ComponentSlice, FromSlice, RGB, RGBA, alt::Gray}; use rgb::{ComponentMap, FromSlice, RGB, RGBA, alt::Gray};
use rustc_hash::FxHasher; use rustc_hash::FxHasher;
use crate::{ use crate::{
@ -193,7 +193,7 @@ pub fn indexed_to_channels(
let mut data = Vec::with_capacity(out_size); let mut data = Vec::with_capacity(out_size);
for b in &png.data { for b in &png.data {
let color = palette.get(*b as usize).unwrap_or(&black); let color = palette.get(*b as usize).unwrap_or(&black);
data.extend_from_slice(&color.as_slice()[ch_start..=ch_end]); data.extend_from_slice(&color.as_ref()[ch_start..=ch_end]);
} }
Some(PngImage { Some(PngImage {

View file

@ -118,7 +118,7 @@ pub(crate) fn perform_reductions(
// Attempt to reduce to indexed // Attempt to reduce to indexed
// Keep the existing `png` var in case it is grayscale - we can test both for depth reduction later // Keep the existing `png` var in case it is grayscale - we can test both for depth reduction later
let mut indexed = None; let mut indexed = None;
if opts.color_type_reduction && !deadline.passed() { if opts.color_type_reduction && opts.palette_reduction && !deadline.passed() {
if let Some(reduced) = reduced_to_indexed(&png, opts.grayscale_reduction) { if let Some(reduced) = reduced_to_indexed(&png, opts.grayscale_reduction) {
// Make sure the palette gets sorted (but don't bother evaluating both results) // Make sure the palette gets sorted (but don't bother evaluating both results)
let new = Arc::new(sorted_palette(&reduced).unwrap_or(reduced)); let new = Arc::new(sorted_palette(&reduced).unwrap_or(reduced));

BIN
tests/files/json.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -1,11 +1,12 @@
use indexmap::indexset;
use oxipng::{internal_tests::*, *};
use serde_json::Value;
use std::{ use std::{
fs::remove_file, fs::remove_file,
path::{Path, PathBuf}, path::{Path, PathBuf},
process::Command,
}; };
use indexmap::indexset;
use oxipng::{internal_tests::*, *};
const GRAY: u8 = 0; const GRAY: u8 = 0;
const RGB: u8 = 2; const RGB: u8 = 2;
const INDEXED: u8 = 3; const INDEXED: u8 = 3;
@ -95,13 +96,9 @@ fn test_it_converts(
#[test] #[test]
fn verbose_mode() { fn verbose_mode() {
use std::cell::RefCell;
#[cfg(not(feature = "parallel"))]
use std::sync::mpsc::{Sender, channel as unbounded};
#[cfg(feature = "parallel")]
use crossbeam_channel::{Sender, unbounded};
use log::{Level, LevelFilter, Log, Metadata, Record, set_logger, set_max_level}; use log::{Level, LevelFilter, Log, Metadata, Record, set_logger, set_max_level};
use std::cell::RefCell;
use std::sync::mpsc::{Sender, channel};
// Rust runs tests in parallel by default. // Rust runs tests in parallel by default.
// We want to make sure that we verify only logs from our test. // We want to make sure that we verify only logs from our test.
@ -141,7 +138,7 @@ fn verbose_mode() {
let input = PathBuf::from("tests/files/verbose_mode.png"); let input = PathBuf::from("tests/files/verbose_mode.png");
let (output, opts) = get_opts(&input); let (output, opts) = get_opts(&input);
let (sender, receiver) = unbounded(); let (sender, receiver) = channel();
let thread_init = move || { let thread_init = move || {
// Initialise logs storage for all threads within our test. // Initialise logs storage for all threads within our test.
@ -430,12 +427,9 @@ fn interlaced_0_to_1_other_filter_mode() {
fn preserve_attrs() { fn preserve_attrs() {
let input = PathBuf::from("tests/files/preserve_attrs.png"); let input = PathBuf::from("tests/files/preserve_attrs.png");
#[cfg(feature = "filetime")]
let meta_input = input let meta_input = input
.metadata() .metadata()
.expect("unable to get file metadata for output file"); .expect("unable to get metadata for input file");
#[cfg(feature = "filetime")]
let mtime_canon = filetime::FileTime::from_last_modification_time(&meta_input);
let (mut output, opts) = get_opts(&input); let (mut output, opts) = get_opts(&input);
if let OutFile::Path { preserve_attrs, .. } = &mut output { if let OutFile::Path { preserve_attrs, .. } = &mut output {
@ -449,17 +443,6 @@ fn preserve_attrs() {
let output = output.path().unwrap(); let output = output.path().unwrap();
assert!(output.exists()); assert!(output.exists());
#[cfg(feature = "filetime")]
let meta_output = output
.metadata()
.expect("unable to get file metadata for output file");
#[cfg(feature = "filetime")]
assert_eq!(
&mtime_canon,
&filetime::FileTime::from_last_modification_time(&meta_output),
"expected modification time to be identical to that of input",
);
match PngData::new(output, &opts) { match PngData::new(output, &opts) {
Ok(x) => x, Ok(x) => x,
Err(x) => { Err(x) => {
@ -468,9 +451,21 @@ fn preserve_attrs() {
} }
}; };
remove_file(output).ok(); let meta_output = output
.metadata()
.expect("unable to get metadata for output file");
assert_eq!(
meta_input.modified().ok(),
meta_output.modified().ok(),
"expected modification time to be identical to that of input",
);
assert_eq!(
meta_input.permissions(),
meta_output.permissions(),
"expected permissions to be identical to that of input",
);
// TODO: Actually check permissions remove_file(output).ok();
} }
#[test] #[test]
@ -664,3 +659,39 @@ fn zopfli_mode() {
BitDepth::Eight, BitDepth::Eight,
); );
} }
#[test]
fn json_success() {
let path = "tests/files/json.png";
let output = Command::new(env!("CARGO_BIN_EXE_oxipng"))
.args(["--json", "--nx", "--nz", path])
.output()
.expect("Failed to run executable");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let data: Value = serde_json::from_str(&stdout).expect("Failed to parse JSON");
let result = &data["results"][0];
assert_eq!(result["input"], path);
assert_eq!(result["status"], "success");
assert_eq!(result["output"], path);
assert_eq!(result["insize"], result["outsize"]);
}
#[test]
fn json_dry_run() {
let path = "tests/files/json.png";
let path2 = "tests/files/escape chars \\ \" \n \t \r \x08 \x0c.png";
let output = Command::new(env!("CARGO_BIN_EXE_oxipng"))
.args(["--json", "--dry-run", path, path2])
.output()
.expect("Failed to run executable");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let data: Value = serde_json::from_str(&stdout).expect("Failed to parse JSON");
let result = &data["results"][0];
assert_eq!(result["input"], path);
assert_eq!(result["status"], "success");
assert_eq!(result["output"], Value::Null);
let result2 = &data["results"][1];
assert_eq!(result2["input"], path2);
assert_eq!(result2["status"], "error");
}