Compare commits

..

157 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
andrews05
58eec39cc8
v10 (#748)
Some checks failed
deploy / Deploy release (push) Has been cancelled
docker / build (push) Has been cancelled
oxipng / CI (push) Has been cancelled
oxipng / MSRV check (push) Has been cancelled
2025-12-06 23:04:29 +01:00
Filip Czaplicki
6a2379ce78
Add colors to --help/-h (#745)
~I used [clap-cargo](https://github.com/crate-ci/clap-cargo) as it's
easy to use and provides sensible default styles.~
User can disable colors via `NO_COLOR=1` environment variable

Output of `oxipng --help`:
| before | after | `NO_COLOR=1` |
| - | - | - |
| <img width="1836" height="3667" alt="oxipng-before"
src="https://github.com/user-attachments/assets/c23ac0ac-6333-4944-8f8f-9e3ae05d6c4c"
/> | <img width="1836" height="3667" alt="oxipng-colors"
src="https://github.com/user-attachments/assets/a4ee13d1-5920-4253-acf6-23481ac1c80f"
/> | <img width="1836" height="3667" alt="oxipng-no-color"
src="https://github.com/user-attachments/assets/4143bdd5-019f-4b2e-b93a-a176b58f3f54"
/> |
2025-12-06 16:42:41 +01:00
Luracasmus
33cea5603e
Apply some Clippy lints (#744)
Let me know if I should add these to the `Cargo.toml`.

All `cargo test` tests pass on the latest x86_64-unknown-linux-gnu
nightly.

---------

Co-authored-by: Alejandro González <me@alegon.dev>
2025-12-06 16:06:35 +01:00
dependabot[bot]
285b0aae10 chore(deps): bump the rust-dependencies group with 2 updates
Bumps the rust-dependencies group with 2 updates: [indexmap](https://github.com/indexmap-rs/indexmap) and [clap](https://github.com/clap-rs/clap).


Updates `indexmap` from 2.12.0 to 2.12.1
- [Changelog](https://github.com/indexmap-rs/indexmap/blob/main/RELEASES.md)
- [Commits](https://github.com/indexmap-rs/indexmap/compare/2.12.0...2.12.1)

Updates `clap` from 4.5.51 to 4.5.53
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.51...clap_complete-v4.5.53)

---
updated-dependencies:
- dependency-name: indexmap
  dependency-version: 2.12.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: clap
  dependency-version: 4.5.53
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-26 21:10:35 +13:00
dependabot[bot]
e868af9106 chore(deps): bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-26 20:57:40 +13:00
dependabot[bot]
1a9e0d79e5
chore(deps): bump image from 0.25.8 to 0.25.9 in the rust-dependencies group (#743)
Bumps the rust-dependencies group with 1 update:
[image](https://github.com/image-rs/image).

Updates `image` from 0.25.8 to 0.25.9
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/image-rs/image/blob/main/CHANGES.md">image's
changelog</a>.</em></p>
<blockquote>
<h3>Version 0.25.9</h3>
<p>Features:</p>
<ul>
<li>Support extracting XMP metadata from PNG, JPEG, GIF, WebP and TIFF
files (<a
href="https://redirect.github.com/image-rs/image/issues/2567">#2567</a>,
<a
href="https://redirect.github.com/image-rs/image/issues/2634">#2634</a>,
<a
href="https://redirect.github.com/image-rs/image/issues/2644">#2644</a>)</li>
<li>Support reading IPTC metadata from PNG and JPG files (<a
href="https://redirect.github.com/image-rs/image/issues/2611">#2611</a>)</li>
<li>Support reading ICC profile from GIF files (<a
href="https://redirect.github.com/image-rs/image/issues/2644">#2644</a>)</li>
<li>Allow setting a specific DEFLATE compression level when writing PNG
(<a
href="https://redirect.github.com/image-rs/image/issues/2583">#2583</a>)</li>
<li>Initial support for 16-bit CMYK TIFF files (<a
href="https://redirect.github.com/image-rs/image/issues/2588">#2588</a>)</li>
<li>Allow extracting the alpha channel of a <code>Pixel</code> in a
generic way (<a
href="https://redirect.github.com/image-rs/image/issues/2638">#2638</a>)</li>
</ul>
<p>Structural changes:</p>
<ul>
<li>EXR format decoding now only uses multi-threading via Rayon when the
<code>rayon</code> feature is enabled (<a
href="https://redirect.github.com/image-rs/image/issues/2643">#2643</a>)</li>
<li>Upgraded zune-jpeg to 0.5.x, ravif to 0.12.x, gif to 0.14.x</li>
<li>pnm: parse integers in PBM/PGM/PPM headers without allocations (<a
href="https://redirect.github.com/image-rs/image/issues/2620">#2620</a>)</li>
<li>Replace <code>doc_auto_cfg</code> with <code>doc_cfg</code> (<a
href="https://redirect.github.com/image-rs/image/issues/2637">#2637</a>)</li>
</ul>
<p>Bug fixes:</p>
<ul>
<li>Do not encode empty JPEG images (<a
href="https://redirect.github.com/image-rs/image/issues/2624">#2624</a>)</li>
<li>tga: reject empty images (<a
href="https://redirect.github.com/image-rs/image/issues/2614">#2614</a>)</li>
<li>tga: fix orientation flip for color mapped images (<a
href="https://redirect.github.com/image-rs/image/issues/2607">#2607</a>)</li>
<li>tga: adjust colormap lookup to match tga 2.0 spec (<a
href="https://redirect.github.com/image-rs/image/issues/2608">#2608</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="5ceb6af6c2"><code>5ceb6af</code></a>
Merge pull request <a
href="https://redirect.github.com/image-rs/image/issues/2640">#2640</a>
from Shnatsel/release-v0.25.9</li>
<li><a
href="282d7b345c"><code>282d7b3</code></a>
Merge pull request <a
href="https://redirect.github.com/image-rs/image/issues/2646">#2646</a>
from oligamiq/main</li>
<li><a
href="5412aeee5a"><code>5412aee</code></a>
Amend the note in accordance with the advice of 197g.</li>
<li><a
href="4e8a4ed2e8"><code>4e8a4ed</code></a>
Clarify default features in README and add usage note</li>
<li><a
href="ca8fa528ff"><code>ca8fa52</code></a>
Merge pull request <a
href="https://redirect.github.com/image-rs/image/issues/2644">#2644</a>
from image-rs/gif-0.14</li>
<li><a
href="d9bc8fe790"><code>d9bc8fe</code></a>
mention GIF 0.14 changes</li>
<li><a
href="053220a0b1"><code>053220a</code></a>
Provide gif's XMP and ICC metadata</li>
<li><a
href="2ec20b3b3b"><code>2ec20b3</code></a>
Prepare codec with gif@0.14</li>
<li><a
href="31939facce"><code>31939fa</code></a>
Mention EXR rayon change</li>
<li><a
href="c7f68be265"><code>c7f68be</code></a>
Merge pull request <a
href="https://redirect.github.com/image-rs/image/issues/2643">#2643</a>
from Shnatsel/really-optional-rayon</li>
<li>Additional commits viewable in <a
href="https://github.com/image-rs/image/compare/v0.25.8...v0.25.9">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=image&package-manager=cargo&previous-version=0.25.8&new-version=0.25.9)](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-11-20 20:25:40 +01:00
andrews05
cb132b1cc4
Always remove iDOT (#741)
Closes #740
2025-11-20 20:04:58 +01:00
dependabot[bot]
99eccf3f30
chore(deps): bump actions/upload-artifact from 4 to 5 (#737)
Bumps
[actions/upload-artifact](https://github.com/actions/upload-artifact)
from 4 to 5.
<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>v5.0.0</h2>
<h2>What's Changed</h2>
<p><strong>BREAKING CHANGE:</strong> this update supports Node
<code>v24.x</code>. This is not a breaking change per-se but we're
treating it as such.</p>
<ul>
<li>Update README.md by <a
href="https://github.com/GhadimiR"><code>@​GhadimiR</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/681">actions/upload-artifact#681</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/712">actions/upload-artifact#712</a></li>
<li>Readme: spell out the first use of GHES by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/727">actions/upload-artifact#727</a></li>
<li>Update GHES guidance to include reference to Node 20 version by <a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
in <a
href="https://redirect.github.com/actions/upload-artifact/pull/725">actions/upload-artifact#725</a></li>
<li>Bump <code>@actions/artifact</code> to <code>v4.0.0</code></li>
<li>Prepare <code>v5.0.0</code> by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/734">actions/upload-artifact#734</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/GhadimiR"><code>@​GhadimiR</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/681">actions/upload-artifact#681</a></li>
<li><a href="https://github.com/nebuk89"><code>@​nebuk89</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/712">actions/upload-artifact#712</a></li>
<li><a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/727">actions/upload-artifact#727</a></li>
<li><a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/725">actions/upload-artifact#725</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v5.0.0">https://github.com/actions/upload-artifact/compare/v4...v5.0.0</a></p>
<h2>v4.6.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Update to use artifact 2.3.2 package &amp; prepare for new
upload-artifact release by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/685">actions/upload-artifact#685</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/685">actions/upload-artifact#685</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v4.6.2">https://github.com/actions/upload-artifact/compare/v4...v4.6.2</a></p>
<h2>v4.6.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Update to use artifact 2.2.2 package by <a
href="https://github.com/yacaovsnc"><code>@​yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/673">actions/upload-artifact#673</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v4.6.1">https://github.com/actions/upload-artifact/compare/v4...v4.6.1</a></p>
<h2>v4.6.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Expose env vars to control concurrency and timeout by <a
href="https://github.com/yacaovsnc"><code>@​yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/662">actions/upload-artifact#662</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v4.6.0">https://github.com/actions/upload-artifact/compare/v4...v4.6.0</a></p>
<h2>v4.5.0</h2>
<h2>What's Changed</h2>
<ul>
<li>fix: deprecated <code>Node.js</code> version in action by <a
href="https://github.com/hamirmahal"><code>@​hamirmahal</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/578">actions/upload-artifact#578</a></li>
<li>Add new <code>artifact-digest</code> output by <a
href="https://github.com/bdehamer"><code>@​bdehamer</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/656">actions/upload-artifact#656</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/hamirmahal"><code>@​hamirmahal</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/578">actions/upload-artifact#578</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="330a01c490"><code>330a01c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/734">#734</a>
from actions/danwkennedy/prepare-5.0.0</li>
<li><a
href="03f2824452"><code>03f2824</code></a>
Update <code>github.dep.yml</code></li>
<li><a
href="905a1ecb59"><code>905a1ec</code></a>
Prepare <code>v5.0.0</code></li>
<li><a
href="2d9f9cdfa9"><code>2d9f9cd</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/725">#725</a>
from patrikpolyak/patch-1</li>
<li><a
href="9687587dec"><code>9687587</code></a>
Merge branch 'main' into patch-1</li>
<li><a
href="2848b2cda0"><code>2848b2c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/727">#727</a>
from danwkennedy/patch-1</li>
<li><a
href="9b511775fd"><code>9b51177</code></a>
Spell out the first use of GHES</li>
<li><a
href="cd231ca1ed"><code>cd231ca</code></a>
Update GHES guidance to include reference to Node 20 version</li>
<li><a
href="de65e23aa2"><code>de65e23</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/712">#712</a>
from actions/nebuk89-patch-1</li>
<li><a
href="8747d8cd76"><code>8747d8c</code></a>
Update README.md</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/upload-artifact/compare/v4...v5">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=4&new-version=5)](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-11-03 18:16:12 +01:00
dependabot[bot]
fb9be2f5d2
chore(deps): bump the rust-dependencies group across 1 directory with 4 updates (#738)
Bumps the rust-dependencies group with 4 updates in the / directory:
[zopfli](https://github.com/zopfli-rs/zopfli),
[indexmap](https://github.com/indexmap-rs/indexmap),
[libdeflater](https://github.com/libdeflater/libdeflater) and
[clap](https://github.com/clap-rs/clap).

Updates `zopfli` from 0.8.2 to 0.8.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/zopfli-rs/zopfli/releases">zopfli's
releases</a>.</em></p>
<blockquote>
<h2>v0.8.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Add DeflateEncoder::get_ref/get_mut by <a
href="https://github.com/roblabla"><code>@​roblabla</code></a> in <a
href="https://redirect.github.com/zopfli-rs/zopfli/pull/69">zopfli-rs/zopfli#69</a>.</li>
<li>Updated dependencies.</li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/roblabla"><code>@​roblabla</code></a>
made their first contribution in <a
href="https://redirect.github.com/zopfli-rs/zopfli/pull/69">zopfli-rs/zopfli#69</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/zopfli-rs/zopfli/compare/v0.8.2...v0.8.3">https://github.com/zopfli-rs/zopfli/compare/v0.8.2...v0.8.3</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="91a34e5d2c"><code>91a34e5</code></a>
Bump Zopfli version to 0.8.3 🎉</li>
<li><a
href="684d9da6bd"><code>684d9da</code></a>
docs: better describe what the nightly feature does</li>
<li><a
href="5b539d0df1"><code>5b539d0</code></a>
chore: update dependencies</li>
<li><a
href="8106f7e415"><code>8106f7e</code></a>
Add DeflateEncoder::get_ref/get_mut (<a
href="https://redirect.github.com/zopfli-rs/zopfli/issues/69">#69</a>)</li>
<li><a
href="c6c7a42fc3"><code>c6c7a42</code></a>
chore(deps): update actions/checkout action to v5 (<a
href="https://redirect.github.com/zopfli-rs/zopfli/issues/68">#68</a>)</li>
<li><a
href="0b5a4c92de"><code>0b5a4c9</code></a>
chore: remove dependency on removed nightly feature</li>
<li><a
href="aacd1aabf8"><code>aacd1aa</code></a>
chore: fix new Clippy lint</li>
<li><a
href="1652084abb"><code>1652084</code></a>
chore: update dependencies</li>
<li><a
href="b8d149ab75"><code>b8d149a</code></a>
ci: drop usage of <code>cargo nono</code></li>
<li><a
href="812da6d545"><code>812da6d</code></a>
chore(deps): update rust crate proptest-derive to 0.6.0 (<a
href="https://redirect.github.com/zopfli-rs/zopfli/issues/66">#66</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/zopfli-rs/zopfli/compare/v0.8.2...v0.8.3">compare
view</a></li>
</ul>
</details>
<br />

Updates `indexmap` from 2.11.4 to 2.12.0
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/indexmap-rs/indexmap/blob/main/RELEASES.md">indexmap's
changelog</a>.</em></p>
<blockquote>
<h2>2.12.0 (2025-10-17)</h2>
<ul>
<li><strong>MSRV</strong>: Rust 1.82.0 or later is now required.</li>
<li>Updated the <code>hashbrown</code> dependency to 0.16 alone.</li>
<li>Error types now implement <code>core::error::Error</code>.</li>
<li>Added <code>pop_if</code> methods to <code>IndexMap</code> and
<code>IndexSet</code>, similar to the
method for <code>Vec</code> added in Rust 1.86.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="0e68f8a360"><code>0e68f8a</code></a>
Merge pull request <a
href="https://redirect.github.com/indexmap-rs/indexmap/issues/422">#422</a>
from cuviper/msrv-1.82</li>
<li><a
href="61c9c94672"><code>61c9c94</code></a>
ci: only run full miri in the merge queue</li>
<li><a
href="db43f1945a"><code>db43f19</code></a>
Release 2.12.0</li>
<li><a
href="b46a32a585"><code>b46a32a</code></a>
Move more to the lints table</li>
<li><a
href="4849b1679f"><code>4849b16</code></a>
Make use of RFC2145 type privacy for sealed traits</li>
<li><a
href="cfff4b7d03"><code>cfff4b7</code></a>
Use bounds in associated type position</li>
<li><a
href="c7178d73c4"><code>c7178d7</code></a>
Use <code>core::error::Error</code></li>
<li><a
href="76b459b82c"><code>76b459b</code></a>
Use more precise capturing for some <code>impl Trait</code></li>
<li><a
href="b3d9cc355e"><code>b3d9cc3</code></a>
Use the primitive slice's <code>is_sorted</code> methods</li>
<li><a
href="09db3cce19"><code>09db3cc</code></a>
Use inherent <code>usize::div_ceil</code></li>
<li>Additional commits viewable in <a
href="https://github.com/indexmap-rs/indexmap/compare/2.11.4...2.12.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `libdeflater` from 1.24.0 to 1.25.0
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/libdeflater/libdeflater/blob/master/CHANGELOG.md">libdeflater's
changelog</a>.</em></p>
<blockquote>
<h2>[1.25.0]</h2>
<ul>
<li>Updated libdeflate to v1.25 (<a
href="https://redirect.github.com/libdeflater/libdeflater/issues/50">#50</a>,
thanks <a
href="https://github.com/musicinmybrain"><code>@​musicinmybrain</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="2acaaa9afa"><code>2acaaa9</code></a>
Link v1.25.0 to <a
href="https://redirect.github.com/libdeflater/libdeflater/issues/50">#50</a>,
credit <a
href="https://github.com/musicinmybrain"><code>@​musicinmybrain</code></a></li>
<li><a
href="429baa4557"><code>429baa4</code></a>
Merge pull request <a
href="https://redirect.github.com/libdeflater/libdeflater/issues/50">#50</a>
from musicinmybrain/v1.25.0</li>
<li><a
href="96156b7cd4"><code>96156b7</code></a>
Fix unnecessary parentheses that produced a warning</li>
<li><a
href="36e84d153a"><code>36e84d1</code></a>
Update libdeflate to v1.25</li>
<li><a
href="0dccb7831c"><code>0dccb78</code></a>
Replace mentions of adamkewley/lideflater with libdeflater/libdeflater
(<a
href="https://redirect.github.com/libdeflater/libdeflater/issues/48">#48</a>)</li>
<li>See full diff in <a
href="https://github.com/libdeflater/libdeflater/compare/1.24.0...1.25.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `clap` from 4.5.48 to 4.5.51
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/clap-rs/clap/releases">clap's
releases</a>.</em></p>
<blockquote>
<h2>v4.5.51</h2>
<h2>[4.5.51] - 2025-10-29</h2>
<h3>Fixes</h3>
<ul>
<li><em>(help)</em> Correctly calculate padding for short flags that
take a value</li>
<li><em>(help)</em> Don't panic on short flags using
<code>ArgAction::Count</code></li>
</ul>
<h2>v4.5.50</h2>
<h2>[4.5.50] - 2025-10-20</h2>
<h3>Features</h3>
<ul>
<li>Accept <code>Cow</code> where <code>String</code> and
<code>&amp;str</code> are accepted</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/clap-rs/clap/blob/master/CHANGELOG.md">clap's
changelog</a>.</em></p>
<blockquote>
<h2>[4.5.51] - 2025-10-29</h2>
<h3>Fixes</h3>
<ul>
<li><em>(help)</em> Correctly calculate padding for short flags that
take a value</li>
<li><em>(help)</em> Don't panic on short flags using
<code>ArgAction::Count</code></li>
</ul>
<h2>[4.5.50] - 2025-10-20</h2>
<h3>Features</h3>
<ul>
<li>Accept <code>Cow</code> where <code>String</code> and
<code>&amp;str</code> are accepted</li>
</ul>
<h2>[4.5.49] - 2025-10-13</h2>
<h3>Fixes</h3>
<ul>
<li><em>(help)</em> Correctly wrap when ANSI escape codes are
present</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b49dae25ec"><code>b49dae2</code></a>
chore: Release</li>
<li><a
href="d37b0b57ab"><code>d37b0b5</code></a>
docs: Update changelog</li>
<li><a
href="3398b6ab10"><code>3398b6a</code></a>
Merge pull request <a
href="https://redirect.github.com/clap-rs/clap/issues/6009">#6009</a>
from gtema/complete_try_generate</li>
<li><a
href="21fc9e46d4"><code>21fc9e4</code></a>
feat(clap-complete): Introduce fallible generator</li>
<li><a
href="bd01bdc0ff"><code>bd01bdc</code></a>
Merge pull request <a
href="https://redirect.github.com/clap-rs/clap/issues/6012">#6012</a>
from epage/sub</li>
<li><a
href="ed0c63deba"><code>ed0c63d</code></a>
docs(derive): Specify arg_required_else_help is set for users</li>
<li><a
href="e2188d9af3"><code>e2188d9</code></a>
chore(deps): Update Rust Stable to v1.87 (<a
href="https://redirect.github.com/clap-rs/clap/issues/6004">#6004</a>)</li>
<li><a
href="e01f2b7640"><code>e01f2b7</code></a>
docs: Cleanup unused links</li>
<li><a
href="6b12a81baf"><code>6b12a81</code></a>
chore: Release</li>
<li><a
href="8dd92a792d"><code>8dd92a7</code></a>
docs: Update changelog</li>
<li>Additional commits viewable in <a
href="https://github.com/clap-rs/clap/compare/clap_complete-v4.5.48...clap_complete-v4.5.51">compare
view</a></li>
</ul>
</details>
<br />


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-11-03 18:05:10 +01:00
dependabot[bot]
779336f60c
chore(deps): bump github/codeql-action from 3 to 4 (#735)
Bumps [github/codeql-action](https://github.com/github/codeql-action)
from 3 to 4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/releases">github/codeql-action's
releases</a>.</em></p>
<blockquote>
<h2>v3.30.8</h2>
<h1>CodeQL Action Changelog</h1>
<p>See the <a
href="https://github.com/github/codeql-action/releases">releases
page</a> for the relevant changes to the CodeQL CLI and language
packs.</p>
<h2>3.30.8 - 10 Oct 2025</h2>
<p>No user facing changes.</p>
<p>See the full <a
href="https://github.com/github/codeql-action/blob/v3.30.8/CHANGELOG.md">CHANGELOG.md</a>
for more information.</p>
<h2>v3.30.7</h2>
<h1>CodeQL Action Changelog</h1>
<p>See the <a
href="https://github.com/github/codeql-action/releases">releases
page</a> for the relevant changes to the CodeQL CLI and language
packs.</p>
<h2>3.30.7 - 06 Oct 2025</h2>
<p>No user facing changes.</p>
<p>See the full <a
href="https://github.com/github/codeql-action/blob/v3.30.7/CHANGELOG.md">CHANGELOG.md</a>
for more information.</p>
<h2>v3.30.6</h2>
<h1>CodeQL Action Changelog</h1>
<p>See the <a
href="https://github.com/github/codeql-action/releases">releases
page</a> for the relevant changes to the CodeQL CLI and language
packs.</p>
<h2>3.30.6 - 02 Oct 2025</h2>
<ul>
<li>Update default CodeQL bundle version to 2.23.2. <a
href="https://redirect.github.com/github/codeql-action/pull/3168">#3168</a></li>
</ul>
<p>See the full <a
href="https://github.com/github/codeql-action/blob/v3.30.6/CHANGELOG.md">CHANGELOG.md</a>
for more information.</p>
<h2>v3.30.5</h2>
<h1>CodeQL Action Changelog</h1>
<p>See the <a
href="https://github.com/github/codeql-action/releases">releases
page</a> for the relevant changes to the CodeQL CLI and language
packs.</p>
<h2>3.30.5 - 26 Sep 2025</h2>
<ul>
<li>We fixed a bug that was introduced in <code>3.30.4</code> with
<code>upload-sarif</code> which resulted in files without a
<code>.sarif</code> extension not getting uploaded. <a
href="https://redirect.github.com/github/codeql-action/pull/3160">#3160</a></li>
</ul>
<p>See the full <a
href="https://github.com/github/codeql-action/blob/v3.30.5/CHANGELOG.md">CHANGELOG.md</a>
for more information.</p>
<h2>v3.30.4</h2>
<h1>CodeQL Action Changelog</h1>
<p>See the <a
href="https://github.com/github/codeql-action/releases">releases
page</a> for the relevant changes to the CodeQL CLI and language
packs.</p>
<h2>3.30.4 - 25 Sep 2025</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/blob/main/CHANGELOG.md">github/codeql-action's
changelog</a>.</em></p>
<blockquote>
<h2>3.29.4 - 23 Jul 2025</h2>
<p>No user facing changes.</p>
<h2>3.29.3 - 21 Jul 2025</h2>
<p>No user facing changes.</p>
<h2>3.29.2 - 30 Jun 2025</h2>
<ul>
<li>Experimental: When the <code>quality-queries</code> input for the
<code>init</code> action is provided with an argument, separate
<code>.quality.sarif</code> files are produced and uploaded for each
language with the results of the specified queries. Do not use this in
production as it is part of an internal experiment and subject to change
at any time. <a
href="https://redirect.github.com/github/codeql-action/pull/2935">#2935</a></li>
</ul>
<h2>3.29.1 - 27 Jun 2025</h2>
<ul>
<li>Fix bug in PR analysis where user-provided <code>include</code>
query filter fails to exclude non-included queries. <a
href="https://redirect.github.com/github/codeql-action/pull/2938">#2938</a></li>
<li>Update default CodeQL bundle version to 2.22.1. <a
href="https://redirect.github.com/github/codeql-action/pull/2950">#2950</a></li>
</ul>
<h2>3.29.0 - 11 Jun 2025</h2>
<ul>
<li>Update default CodeQL bundle version to 2.22.0. <a
href="https://redirect.github.com/github/codeql-action/pull/2925">#2925</a></li>
<li>Bump minimum CodeQL bundle version to 2.16.6. <a
href="https://redirect.github.com/github/codeql-action/pull/2912">#2912</a></li>
</ul>
<h2>3.28.21 - 28 July 2025</h2>
<p>No user facing changes.</p>
<h2>3.28.20 - 21 July 2025</h2>
<ul>
<li>Remove support for combining SARIF files from a single upload for
GHES 3.18, see <a
href="https://github.blog/changelog/2024-05-06-code-scanning-will-stop-combining-runs-from-a-single-upload/">the
changelog post</a>. <a
href="https://redirect.github.com/github/codeql-action/pull/2959">#2959</a></li>
</ul>
<h2>3.28.19 - 03 Jun 2025</h2>
<ul>
<li>The CodeQL Action no longer includes its own copy of the extractor
for the <code>actions</code> language, which is currently in public
preview.
The <code>actions</code> extractor has been included in the CodeQL CLI
since v2.20.6. If your workflow has enabled the <code>actions</code>
language <em>and</em> you have pinned
your <code>tools:</code> property to a specific version of the CodeQL
CLI earlier than v2.20.6, you will need to update to at least CodeQL
v2.20.6 or disable
<code>actions</code> analysis.</li>
<li>Update default CodeQL bundle version to 2.21.4. <a
href="https://redirect.github.com/github/codeql-action/pull/2910">#2910</a></li>
</ul>
<h2>3.28.18 - 16 May 2025</h2>
<ul>
<li>Update default CodeQL bundle version to 2.21.3. <a
href="https://redirect.github.com/github/codeql-action/pull/2893">#2893</a></li>
<li>Skip validating SARIF produced by CodeQL for improved performance.
<a
href="https://redirect.github.com/github/codeql-action/pull/2894">#2894</a></li>
<li>The number of threads and amount of RAM used by CodeQL can now be
set via the <code>CODEQL_THREADS</code> and <code>CODEQL_RAM</code>
runner environment variables. If set, these environment variables
override the <code>threads</code> and <code>ram</code> inputs
respectively. <a
href="https://redirect.github.com/github/codeql-action/pull/2891">#2891</a></li>
</ul>
<h2>3.28.17 - 02 May 2025</h2>
<ul>
<li>Update default CodeQL bundle version to 2.21.2. <a
href="https://redirect.github.com/github/codeql-action/pull/2872">#2872</a></li>
</ul>
<h2>3.28.16 - 23 Apr 2025</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a841c540b7"><code>a841c54</code></a>
Scratch <code>uploadSpecifiedFiles</code> tests, make
<code>uploadPayload</code> tests instead</li>
<li><a
href="aeb12f6eaa"><code>aeb12f6</code></a>
Merge branch 'main' into redsun82/skip-sarif-upload-tests</li>
<li><a
href="6fd4ceb7bb"><code>6fd4ceb</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3189">#3189</a>
from github/henrymercer/download-codeql-rate-limit</li>
<li><a
href="196a3e577b"><code>196a3e5</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3188">#3188</a>
from github/mbg/telemetry/partial-config</li>
<li><a
href="98abb870dc"><code>98abb87</code></a>
Add configuration error for rate limited CodeQL download</li>
<li><a
href="bdd2cdf891"><code>bdd2cdf</code></a>
Also include <code>language</code> in error status report for
<code>start-proxy</code>, if available</li>
<li><a
href="fb148789ab"><code>fb14878</code></a>
Include <code>languages</code> in <code>start-proxy</code>
telemetry</li>
<li><a
href="2ff418f28a"><code>2ff418f</code></a>
Parse <code>language</code> before calling
<code>getCredentials</code></li>
<li>See full diff in <a
href="https://github.com/github/codeql-action/compare/v3...v4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github/codeql-action&package-manager=github_actions&previous-version=3&new-version=4)](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-10-23 18:40:01 +02:00
andrews05
e091767278
Small tweaks (#734)
- Fix docs for entropy filter (fixes #733)
- Improve docs for `--fix` (fixes #728)
- Add tests for alpha optimisation
- Following a recent change to the code here, I realised we don't
actually have any tests covering alpha optimisation. I've adjusted some
of the test images to include lines with both leading transparent and
all transparent pixels, and added dedicated tests for them.
- Change x86_64 macOS build to use the macos-15 runner
- There is a new macos-15-intel runner available but we don't actually
need an intel runner to build x86_64. (As support for x86_64 winds down,
we can probably drop these builds eventually - homebrew is the preferred
installation method for macOS anyway)
2025-10-04 11:46:47 +02:00
andrews05
aebddf443a
Allow brute filter configuration (#720)
This PR allows the Brute filter parameters to be configured rather than
using fixed constants. It also tweaks the presets to take advantage of
these new options:
o3: level=1, lines=3 (fractionally worse/faster)
o4: level=1, lines=4 (same as before)
o5: level=4, lines=4 (fractionally better/slower)
o6: level=5, lines=8 (slightly better/slower)

The parameters can be explicitly set in both the API and the CLI.
2025-09-25 23:20:29 +02:00
andrews05
7cc91b1308
Add max_uncompressed_size (#732)
Adds an option to skip files when the uncompressed IDAT (or the file
size itself) exceeds a certain size. This provides an effective way to
limit resource usage in constrained environments.

Closes #411.
2025-09-25 22:46:35 +02:00
andrews05
14532e8bf5
BC breaks for v10 (#715)
This is a collection of all the BC breaks mentioned in #714, except for
11 which I'm not including for now.

Fixes #658.
Fixes #660.

It might be best to review each commit individually, referencing the
notes in #714 and #660 (I just didn't want to create a dozen separate
PRs).
2025-09-20 17:16:22 +02:00
Alejandro González
2a3aa83928
chore: fix Clippy lint, very slightly optimize optimize_alpha function 2025-09-20 13:35:13 +02:00
Alejandro González
82cae58794
chore: update dependencies 2025-09-20 13:35:13 +02:00
andrews05
71290acc62
Reduce memory usage of fast evaluation (#713)
This is an experiment I started a while ago before life happened. It
reduces memory usage of fast evaluation (-o2 and lower), bringing it
inline with normal (slow) evaluation. It does this by not retaining the
filtered image data of the evaluations, but instead retaining the row
filters that were used in each line so it can be quickly re-filtered
when required. This does incur a tiny performance penalty, but it's
negligible even at `-o0`.
Although this is fully functional, there are a few rough spots in the
code so I'm just opening it as a draft for now.

```
PR -sao6
       70.08 real       614.36 user         2.66 sys
          2024243200  maximum resident set size
          1839339520  peak memory footprint

PR -sao2
       11.16 real        60.85 user         1.33 sys
          1982562304  maximum resident set size
          1842173824  peak memory footprint

PR -sao2 -t1
       55.11 real        53.85 user         0.76 sys
           429457408  maximum resident set size
           245008064  peak memory footprint

master -sao6
       67.70 real       616.07 user         2.72 sys
          2043379712  maximum resident set size
          1838340416  peak memory footprint

master -sao2
       11.53 real        60.63 user         1.25 sys
          2753396736  maximum resident set size
          2283741440  peak memory footprint

master -sao2 -t1
       54.29 real        53.55 user         0.72 sys
           626311168  maximum resident set size
           305252544  peak memory footprint
```

Note that this involves some refactoring of `RowFilter` and the new
`FilterStrategy`. These are breaking changes so it will ultimately be
destined for v10. One advantage to this new structure is it opens the
door for future changes such as allowing the Brute strategy to take a
parameter for the number of lines.
2025-09-19 20:36:17 +02:00
dependabot[bot]
ff9beb00fe chore(deps): bump actions/attest-build-provenance from 2 to 3
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2 to 3.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 18:47:53 +12:00
andrews05
c60dbd12a5
Update runners, rust, and dependencies (#724)
#719 is failing tests due to requiring a newer version of rust than we
currently specify. This PR updates to 1.85.1 and sets the edition to
2024.
I've also updated dependencies and runner images, using the ubuntu arm
runner which removes the need for qemu and other hacks.

Closes #719.
2025-08-25 14:49:59 +02:00
dependabot[bot]
54c49a1644 chore(deps): bump the rust-dependencies group with 2 updates
Bumps the rust-dependencies group with 2 updates: [clap](https://github.com/clap-rs/clap) and [glob](https://github.com/rust-lang/glob).


Updates `clap` from 4.5.41 to 4.5.43
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.41...clap_complete-v4.5.43)

Updates `glob` from 0.3.2 to 0.3.3
- [Release notes](https://github.com/rust-lang/glob/releases)
- [Changelog](https://github.com/rust-lang/glob/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/glob/compare/v0.3.2...v0.3.3)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.43
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: glob
  dependency-version: 0.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-14 12:49:04 +12:00
dependabot[bot]
9d3e6e6f33 chore(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-14 12:37:46 +12:00
dependabot[bot]
5ee3c2afa0 chore(deps): bump the rust-dependencies group with 2 updates
Bumps the rust-dependencies group with 2 updates: [rgb](https://github.com/kornelski/rust-rgb) and [clap](https://github.com/clap-rs/clap).


Updates `rgb` from 0.8.51 to 0.8.52
- [Changelog](https://github.com/kornelski/rust-rgb/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kornelski/rust-rgb/commits)

Updates `clap` from 4.5.40 to 4.5.41
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.40...clap_complete-v4.5.41)

---
updated-dependencies:
- dependency-name: rgb
  dependency-version: 0.8.52
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: clap
  dependency-version: 4.5.41
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-09 11:42:08 +12:00
John Bampton
1c02ee9299
README: use version v9.1.5 for the pre-commit config example (#711)
Use latest version
2025-07-09 20:42:03 +02:00
dependabot[bot]
0ff97448c2
chore(deps): bump rgb from 0.8.50 to 0.8.51 in the rust-dependencies group (#710)
Bumps the rust-dependencies group with 1 update:
[rgb](https://github.com/kornelski/rust-rgb).

Updates `rgb` from 0.8.50 to 0.8.51
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/kornelski/rust-rgb/commits">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=rgb&package-manager=cargo&previous-version=0.8.50&new-version=0.8.51)](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-07-07 17:48:17 +02:00
dependabot[bot]
420ea940bc
chore(deps): bump indexmap from 2.9.0 to 2.10.0 in the rust-dependencies group (#709)
Bumps the rust-dependencies group with 1 update:
[indexmap](https://github.com/indexmap-rs/indexmap).

Updates `indexmap` from 2.9.0 to 2.10.0
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/indexmap-rs/indexmap/blob/main/RELEASES.md">indexmap's
changelog</a>.</em></p>
<blockquote>
<h2>2.10.0 (2025-06-26)</h2>
<ul>
<li>Added <code>extract_if</code> methods to <code>IndexMap</code> and
<code>IndexSet</code>, similar to the
methods for <code>HashMap</code> and <code>HashSet</code> with ranges
like <code>Vec::extract_if</code>.</li>
<li>Added more <code>#[track_caller]</code> annotations to functions
that may panic.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="91dbcc55d2"><code>91dbcc5</code></a>
Merge pull request <a
href="https://redirect.github.com/indexmap-rs/indexmap/issues/399">#399</a>
from cuviper/release-2.10.0</li>
<li><a
href="67a5a712fc"><code>67a5a71</code></a>
Release 2.10.0</li>
<li><a
href="37e519a3be"><code>37e519a</code></a>
Merge pull request <a
href="https://redirect.github.com/indexmap-rs/indexmap/issues/308">#308</a>
from cuviper/extract_if</li>
<li><a
href="4d7618fbc9"><code>4d7618f</code></a>
Merge pull request <a
href="https://redirect.github.com/indexmap-rs/indexmap/issues/398">#398</a>
from cuviper/bench-deps</li>
<li><a
href="68201eb0a3"><code>68201eb</code></a>
Drop lazy_static for LazyLock</li>
<li><a
href="eaaaa56e71"><code>eaaaa56</code></a>
Switch to fastrand for bench shuffling</li>
<li><a
href="b19d84edaf"><code>b19d84e</code></a>
Fix <code>clippy::needless_lifetimes</code></li>
<li><a
href="4f62778c86"><code>4f62778</code></a>
<code>impl FusedIterator for ExtractIf</code></li>
<li><a
href="e09eaaf8e5"><code>e09eaaf</code></a>
Document and track <code>extract_if</code> panics</li>
<li><a
href="a8d7dc50a2"><code>a8d7dc5</code></a>
Add range support to <code>extract_if</code></li>
<li>Additional commits viewable in <a
href="https://github.com/indexmap-rs/indexmap/compare/2.9.0...2.10.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=indexmap&package-manager=cargo&previous-version=2.9.0&new-version=2.10.0)](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-06-30 20:39:02 +02:00
AFCMS
fe1f5a0889
Change repository name reference + Docker registry URL 2025-06-23 19:38:43 -04:00
dependabot[bot]
fd5b7a4aa8
chore(deps): bump dawidd6/action-download-artifact from 10 to 11 (#705)
Bumps
[dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact)
from 10 to 11.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dawidd6/action-download-artifact/releases">dawidd6/action-download-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v11</h2>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/dawidd6/action-download-artifact/compare/v10...v11">https://github.com/dawidd6/action-download-artifact/compare/v10...v11</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="ac66b43f0e"><code>ac66b43</code></a>
node_modules: upgrade</li>
<li><a
href="9b54a0a70c"><code>9b54a0a</code></a>
Update README.md</li>
<li>See full diff in <a
href="https://github.com/dawidd6/action-download-artifact/compare/v10...v11">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dawidd6/action-download-artifact&package-manager=github_actions&previous-version=10&new-version=11)](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-06-24 00:42:19 +02:00
dependabot[bot]
8631eb1c79
chore(deps): bump clap from 4.5.39 to 4.5.40 in the rust-dependencies group (#706)
Bumps the rust-dependencies group with 1 update:
[clap](https://github.com/clap-rs/clap).

Updates `clap` from 4.5.39 to 4.5.40
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/clap-rs/clap/blob/master/CHANGELOG.md">clap's
changelog</a>.</em></p>
<blockquote>
<h2>[4.5.40] - 2025-06-09</h2>
<h3>Features</h3>
<ul>
<li>Support quoted ids in <code>arg!()</code> macro (e.g.
<code>arg!(&quot;check-config&quot;: ...)</code>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="cff27dbf57"><code>cff27db</code></a>
chore: Release</li>
<li><a
href="4ef41249f1"><code>4ef4124</code></a>
docs: Update changelog</li>
<li><a
href="ca896175c1"><code>ca89617</code></a>
Merge pull request <a
href="https://redirect.github.com/clap-rs/clap/issues/5848">#5848</a>
from jennings/jennings/push-xolwzyoornps</li>
<li><a
href="99b6391ee9"><code>99b6391</code></a>
fix(complete): Fix PowerShell dynamic completion</li>
<li>See full diff in <a
href="https://github.com/clap-rs/clap/compare/clap_complete-v4.5.39...clap_complete-v4.5.40">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=clap&package-manager=cargo&previous-version=4.5.39&new-version=4.5.40)](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-06-16 17:38:20 +02:00
dependabot[bot]
d57d880770 chore(deps): bump the rust-dependencies group with 2 updates
Bumps the rust-dependencies group with 2 updates: [libdeflater](https://github.com/adamkewley/libdeflater) and [clap](https://github.com/clap-rs/clap).


Updates `libdeflater` from 1.23.1 to 1.24.0
- [Changelog](https://github.com/libdeflater/libdeflater/blob/master/CHANGELOG.md)
- [Commits](https://github.com/adamkewley/libdeflater/compare/1.23.1...1.24.0)

Updates `clap` from 4.5.36 to 4.5.39
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.36...clap_complete-v4.5.39)

---
updated-dependencies:
- dependency-name: libdeflater
  dependency-version: 1.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: clap
  dependency-version: 4.5.39
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-05 11:02:11 -04:00
Josh Holmer
bf276f29b8
Include Cargo deps in Dependabot checks 2025-06-05 10:09:52 -04:00
dependabot[bot]
54a31a65a7 Bump dawidd6/action-download-artifact from 9 to 10
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 9 to 10.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](https://github.com/dawidd6/action-download-artifact/compare/v9...v10)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  dependency-version: '10'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-01 13:30:35 -04:00
Demetris Kikizas
788997c437 Fix letter transposition in README.md 2025-05-05 17:20:16 +12:00
Alejandro González
a451711e96
Fix ARM64 Debian packages lacking dependencies
It looks like cargo-deb v2.5.0 silently broke this for our use case, and
other use cases which have been reported upstream: https://github.com/kornelski/cargo-deb/issues/21

To make dpkg-shlibdeps work more reliably until this inconsistent choice
of multiarch and cross-build lib directories is sorted out, let's pass
the documented `LD_LIBRARY_PATH` environment variable to ensure such
libraries are always found. Interestingly, setting `DEB_HOST_ARCH` to a
different value did not have any effect.
2025-04-26 16:29:22 +02:00
Alejandro González
ef1d87427d
Simplify and optimize CI workflows 2025-04-26 16:29:22 +02:00
Alejandro González
fa7d3d9067
Tentative fix for deploy job flakiness
The failure described in #967 is suspected to stem from race conditions
with the GH API, as described in
https://github.com/softprops/action-gh-release/issues/445#issuecomment-2407940052.
2025-04-26 13:47:25 +02:00
Alejandro González
8c6532ca1b
Reformat deploy.yml file 2025-04-26 13:47:04 +02:00
andrews05
c7d462f909
Bump version to 9.1.5 (#697)
Some checks failed
deploy / Deploy release (push) Has been cancelled
docker / build (push) Has been cancelled
oxipng / CI (push) Has been cancelled
oxipng / MSRV check (push) Has been cancelled
2025-04-26 01:19:57 +02:00
andrews05
a44ba5f88b
Add option to disable parallel file processing (#695)
Fixes #685.
Fixes #572.
Also fixes #690 by using "logical CPUs".
2025-04-23 11:09:15 +02:00
Josh Holmer
d168fffb12 Create security policies document
Github recommends to create a SECURITY.md file containing security policies for the project. I've also enabled in-project vulnerability reporting, and linked to it in this file.
2025-04-22 01:07:32 -04:00
Alejandro González
748d5987a5 Noteworthy dependency updates, including zopfli 2025-04-19 08:44:46 +12:00
AFCMS
26b084c161
Switch Docker image from alpine to scratch (#692)
Following https://github.com/shssoichiro/oxipng/pull/642

- [x] Fail to build on linting errors
- [x] Switch Docker image from `alpine` to `scratch`

Reduce the image size roughly from 15Mb to 2Mb.


![image](https://github.com/user-attachments/assets/a0fee179-42d4-4558-b5f6-e469a769959a)

It doesn't break the usecase mentioned in the README, as well as people
copying the binary in a Docker step.

I have considered making the image rootless but since it's doing file
modifications it requires overwriting the user to bring back root in the
README's usecase. The image would benefit of if using it only with
stdin/stdout. Not sure it's worth it.

I have been working on enabling cross compilation for non-Tier 1
platforms, but no ETA yet.
2025-04-15 22:17:46 +02:00
dependabot[bot]
f7a66f2075 Bump crossbeam-channel from 0.5.14 to 0.5.15
Bumps [crossbeam-channel](https://github.com/crossbeam-rs/crossbeam) from 0.5.14 to 0.5.15.
- [Release notes](https://github.com/crossbeam-rs/crossbeam/releases)
- [Changelog](https://github.com/crossbeam-rs/crossbeam/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crossbeam-rs/crossbeam/compare/crossbeam-channel-0.5.14...crossbeam-channel-0.5.15)

---
updated-dependencies:
- dependency-name: crossbeam-channel
  dependency-version: 0.5.15
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-10 14:35:53 -04:00
andrews05
7650953faf
Improve memory usage of evaluator (#688)
This adds a new `final_round` parameter to the evaluator, which allows
us to determine when to keep the `filtered` data and when to keep the
compressed `idat_data`. By only keeping one of these we can
significantly improve memory usage.

Ref #683
2025-03-08 12:01:02 +01:00
dependabot[bot]
6cf42fbefa
Bump dawidd6/action-download-artifact from 8 to 9 (#686)
Bumps
[dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact)
from 8 to 9.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dawidd6/action-download-artifact/releases">dawidd6/action-download-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v9</h2>
<h2>What's Changed</h2>
<ul>
<li>add merge_multiple option by <a
href="https://github.com/timostroehlein"><code>@​timostroehlein</code></a>
in <a
href="https://redirect.github.com/dawidd6/action-download-artifact/pull/327">dawidd6/action-download-artifact#327</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/timostroehlein"><code>@​timostroehlein</code></a>
made their first contribution in <a
href="https://redirect.github.com/dawidd6/action-download-artifact/pull/327">dawidd6/action-download-artifact#327</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/dawidd6/action-download-artifact/compare/v8...v9">https://github.com/dawidd6/action-download-artifact/compare/v8...v9</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="07ab29fd4a"><code>07ab29f</code></a>
add merge_multiple option (<a
href="https://redirect.github.com/dawidd6/action-download-artifact/issues/327">#327</a>)</li>
<li>See full diff in <a
href="https://github.com/dawidd6/action-download-artifact/compare/v8...v9">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dawidd6/action-download-artifact&package-manager=github_actions&previous-version=8&new-version=9)](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-03-01 16:14:09 +01:00
andrews05
1e3f4520ef
Disallow grayscale transformation if non-sRGB profile (#682)
Fixes #677
2025-02-26 22:39:20 +01:00
Jakub Marcowski
d649beacac
Use current version in pre-commit example (#681)
Simple enough change, use the current version in the `README.md`'s
`pre-commit` example.
2025-02-14 22:39:47 +01:00
andrews05
bc8c364568
Bump version to 9.1.4 (#680)
Some checks failed
docker / build (push) Has been cancelled
oxipng / CI (push) Has been cancelled
deploy / Deploy release (push) Has been cancelled
oxipng / MSRV check (push) Has been cancelled
2025-02-14 19:11:32 +01:00
andrews05
30387b26d9
Bump dependencies (#679) 2025-02-14 11:51:36 +01:00
andrews05
1188b9a91e
Reduction performance tweaks (#676)
Two key changes here:
- Removed some defunct reduction benches and added some more alpha
reduction benches.
- Changed use of `chunks()` to `chunks_exact()` where appropriate to
improve performance. (As a byproduct, this also fixes a potential crash
if a malformed file has a PLTE chunk that isn't a multiple of 3.)
2025-02-08 18:49:04 +01:00
Alejandro González
5a4cfa9505
Exclude system-libdeflate feature from Clippy feature combinations 2025-02-02 18:31:20 +01:00
andrews05
174f36ad8f
Improve indexed to grayscale conversion (#674)
If no palette reduction occurred (or palette reductions were off), a
fully transparent palette entry may not have been zeroed to black. By
ensuring this gets cleaned in the `indexed_to_channels` transformation,
we may able to achieve a grayscale conversion that would otherwise have
been RGB.

This is the final piece of the puzzle in #649 to achieve better file
sizes on the first run and avoid further changes on a second run.

Results from the images in #649 (combining this PR and #673):
9.1.3 1st run: 2,191,773
9.1.3 2nd run: 2,191,629
PR 1st run: 2,191,380
PR 2nd run: 2,191,380
2025-02-02 18:26:18 +01:00
andrews05
e6f84264e5
Don't use most popular edge colour if tied (#673)
If multiple edge colours were tied for most popular, the one chosen
would depend on the current palette order, which could cause it to
change if run a second time.

This further improves determinism for #649. There is still one more
issue I've found affecting the images there which I'll try to address in
another PR.
2025-02-02 18:15:55 +01:00
dependabot[bot]
8f298be7fb
Bump dawidd6/action-download-artifact from 7 to 8 (#675)
Bumps
[dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact)
from 7 to 8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dawidd6/action-download-artifact/releases">dawidd6/action-download-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v8</h2>
<h2>New features</h2>
<ul>
<li><code>use_unzip</code> boolean input (defaulting to false) - if set
to true, the action will use system provided <code>unzip</code> utility
for unpacking downloaded artifact(s) (note that the action will first
download the .zip artifact file, then unpack it and remove the .zip
file)</li>
</ul>
<h2>What's Changed</h2>
<ul>
<li>README: v7 by <a
href="https://github.com/haines"><code>@​haines</code></a> in <a
href="https://redirect.github.com/dawidd6/action-download-artifact/pull/318">dawidd6/action-download-artifact#318</a></li>
<li>Unzip by <a
href="https://github.com/dawidd6"><code>@​dawidd6</code></a> in <a
href="https://redirect.github.com/dawidd6/action-download-artifact/pull/325">dawidd6/action-download-artifact#325</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/haines"><code>@​haines</code></a> made
their first contribution in <a
href="https://redirect.github.com/dawidd6/action-download-artifact/pull/318">dawidd6/action-download-artifact#318</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/dawidd6/action-download-artifact/compare/v7...v8">https://github.com/dawidd6/action-download-artifact/compare/v7...v8</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="20319c5641"><code>20319c5</code></a>
README: v8</li>
<li><a
href="e58a9e5d14"><code>e58a9e5</code></a>
Unzip (<a
href="https://redirect.github.com/dawidd6/action-download-artifact/issues/325">#325</a>)</li>
<li><a
href="6d05268723"><code>6d05268</code></a>
node_modules: update</li>
<li><a
href="c03fb0c928"><code>c03fb0c</code></a>
README: v7 (<a
href="https://redirect.github.com/dawidd6/action-download-artifact/issues/318">#318</a>)</li>
<li>See full diff in <a
href="https://github.com/dawidd6/action-download-artifact/compare/v7...v8">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dawidd6/action-download-artifact&package-manager=github_actions&previous-version=7&new-version=8)](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-02-02 18:07:51 +01:00
Alejandro González
5f0a0d6938
Fix new Clippy lints 2025-01-30 18:39:23 +01:00
andrews05
2632be2034
Fix size passed to fast filter evaluator (#672)
The evaluator uses the `estimated_output_size` internally but we were
passing only the idat size in the fast filter evaluator. At level `o2`,
or higher levels when using `--fast`, this could cause a slight bias
towards the None and Bigrams filters for indexed images. The actual
impact to results is incredibly minor but it's definitely incorrect so
we should fix it.
2025-01-30 18:28:42 +01:00
andrews05
bbde68ddce
Refactor optimize_raw (#670)
Code always tends to get messy over time. I've found the `optimize_raw`
function increasingly harder to read, particularly after the addition of
fast mode, so I've taken some time to refactor and simplify it.

One change of note here is the main compression trials now use the
Evaluator. This means verbose output is a little different which is
shown below.

There is no change to performance or output size.

`-vvo2`: master
```
Processing: tests/files/rgba_8_should_be_palette_4.png
    500x400 pixels, PNG format
    8-bit RGB + Alpha, non-interlaced
    IDAT size = 2757 bytes
    File size = 18109 bytes
Eval: 4-bit Indexed (5 colors)      None       1837 bytes
Eval: 8-bit Indexed (5 colors)      None       1988 bytes
Eval: 4-bit Indexed (5 colors)      Bigrams   >1837 bytes
Eval: 8-bit Indexed (5 colors)      Bigrams   >1837 bytes
Transformed image to 4-bit Indexed (5 colors), non-interlaced
Evaluating: 2 filters
Eval: 4-bit Indexed (5 colors)      Sub       >1810 bytes
Eval: 4-bit Indexed (5 colors)      Entropy   >1810 bytes
Trying: None
    zc = 11  f = None      1583 bytes
Found better combination:
    zc = 11  f = None      1583 bytes
    IDAT size = 1583 bytes (1174 bytes decrease)
    file size = 16962 bytes (1147 bytes = 6.33% decrease)
16962 bytes (6.33% smaller): Running in pretend mode, no output
```

`-vvo2`: PR
```
Processing: tests/files/rgba_8_should_be_palette_4.png
    500x400 pixels, PNG format
    8-bit RGB + Alpha, non-interlaced
    IDAT size = 2757 bytes
    File size = 18109 bytes
Eval: 4-bit Indexed (5 colors)      None       1837 bytes
Eval: 8-bit Indexed (5 colors)      None       1988 bytes
Eval: 4-bit Indexed (5 colors)      Bigrams   >1837 bytes
Eval: 8-bit Indexed (5 colors)      Bigrams   >1837 bytes
Transformed image to 4-bit Indexed (5 colors), non-interlaced
Evaluating 2 filters
Eval: 4-bit Indexed (5 colors)      Sub       >1810 bytes
Eval: 4-bit Indexed (5 colors)      Entropy   >1810 bytes
Trying filter None with zc = 11
1610 bytes
Found better result:
    zc = 11, f = None
    IDAT size = 1583 bytes (1174 bytes decrease)
    file size = 16962 bytes (1147 bytes = 6.33% decrease)
16962 bytes (6.33% smaller): Running in pretend mode, no output
```

`-vvZo5`: master
```
Processing: tests/files/rgba_8_should_be_palette_4.png
    500x400 pixels, PNG format
    8-bit RGB + Alpha, non-interlaced
    IDAT size = 2757 bytes
    File size = 18109 bytes
Eval: 8-bit Indexed (battiato sort) None       1821 bytes
Eval: 4-bit Indexed (5 colors)      None       1657 bytes
Eval: 8-bit Indexed (mzeng sort)    None       1821 bytes
Eval: 8-bit Indexed (5 colors)      None       1821 bytes
Eval: 8-bit Indexed (battiato sort) Bigrams   >1821 bytes
Eval: 4-bit Indexed (5 colors)      Bigrams   >1657 bytes
Eval: 8-bit Indexed (mzeng sort)    Bigrams   >1657 bytes
Eval: 8-bit Indexed (5 colors)      Bigrams   >1657 bytes
Transformed image to 4-bit Indexed (5 colors), non-interlaced
Trying: 8 filters
    zc = zopfli  f = Brute     1562 bytes
    zc = zopfli  f = Sub      >1562 bytes
    zc = zopfli  f = Bigrams  >1562 bytes
    zc = zopfli  f = None      1407 bytes
    zc = zopfli  f = Up       >1407 bytes
    zc = zopfli  f = MinSum   >1407 bytes
    zc = zopfli  f = BigEnt   >1407 bytes
    zc = zopfli  f = Entropy  >1407 bytes
Found better combination:
    zc = zopfli  f = None      1407 bytes
    IDAT size = 1407 bytes (1350 bytes decrease)
    file size = 16786 bytes (1323 bytes = 7.31% decrease)
16786 bytes (7.31% smaller): Running in pretend mode, no output
```

`-vvZo5`: PR
```
Processing: tests/files/rgba_8_should_be_palette_4.png
    500x400 pixels, PNG format
    8-bit RGB + Alpha, non-interlaced
    IDAT size = 2757 bytes
    File size = 18109 bytes
Eval: 8-bit Indexed (battiato sort) None       1821 bytes
Eval: 4-bit Indexed (5 colors)      None       1657 bytes
Eval: 8-bit Indexed (mzeng sort)    None       1821 bytes
Eval: 8-bit Indexed (5 colors)      None       1821 bytes
Eval: 8-bit Indexed (battiato sort) Bigrams   >1657 bytes
Eval: 4-bit Indexed (5 colors)      Bigrams   >1657 bytes
Eval: 8-bit Indexed (mzeng sort)    Bigrams   >1657 bytes
Eval: 8-bit Indexed (5 colors)      Bigrams   >1657 bytes
Transformed image to 4-bit Indexed (5 colors), non-interlaced
Trying 8 filters with zopfli, zi = 15
Eval: 4-bit Indexed (5 colors)      Brute      1589 bytes
Eval: 4-bit Indexed (5 colors)      Bigrams    1641 bytes
Eval: 4-bit Indexed (5 colors)      Sub        1711 bytes
Eval: 4-bit Indexed (5 colors)      None       1434 bytes
Eval: 4-bit Indexed (5 colors)      Up         1764 bytes
Eval: 4-bit Indexed (5 colors)      MinSum     1760 bytes
Eval: 4-bit Indexed (5 colors)      BigEnt     1742 bytes
Eval: 4-bit Indexed (5 colors)      Entropy    1748 bytes
Found better result:
    zopfli, zi = 15, f = None
    IDAT size = 1407 bytes (1350 bytes decrease)
    file size = 16786 bytes (1323 bytes = 7.31% decrease)
16786 bytes (7.31% smaller): Running in pretend mode, no output
```
2025-01-29 20:53:11 +01:00
andrews05
8a44cdbc84
Tweak eval cmp key for more consistent results (#671)
This changes the evaluator to prefer a later image instead of an earlier
one in the case of a tie. This gives more consistent results since the
baseline is always added last.

I also removed bit depth from the key since the uncompressed size should
cover that.

Fixes #649.
(Mostly. I did discover another possible but very rare issue: if
multiple colours tie for "most popular edge colour" in the luma sort,
the sort may affect which one is actually picked and multiple runs may
flip-flop back and forth)
2025-01-29 19:56:27 +01:00
Andrew
14b8b0e93a Error if fdAT invalid 2025-01-26 00:16:24 +01:00
Andrew
cca23b5479 Refilter frames 2025-01-26 00:16:24 +01:00
Andrew
09c4851118 Retain filter applied 2025-01-26 00:16:24 +01:00
Andrew
5fdaa12c96 Recompress frames 2025-01-26 00:16:24 +01:00
Andrew
fec6b2b9f3 Parse fcTL/fdAT into frames 2025-01-26 00:16:24 +01:00
Andrew
60911977f6 Add read_be_u16 and tidy parse_next_chunk 2025-01-26 00:16:24 +01:00
Andrew
6c7ab5d79f Add warning when interlacing was not enabled as requested 2025-01-16 12:37:33 +01:00
dependabot[bot]
3a6b792ca1 Bump actions/attest-build-provenance from 1 to 2
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 1 to 2.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/v1...v2)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-02 00:28:34 +01:00
Kornel
ffbd927d82 Remove obsolete flag warning 2025-01-02 09:40:52 +13:00
Kornel
a72bc945fc Clippy 2025-01-02 09:15:24 +13:00
Kornel
8270c6a034 Skip signed files or remove C2PA metadata 2024-12-26 19:03:03 +01:00
Kornel
910e779b09 Use ExitCode in main 2024-12-26 19:03:03 +01:00
Frank Elsinga
8740f475d7
docs: added the defaults for some Options (#661)
There are some parts of the `Options`-struct that miss the defaults.
I also made sure that `Interlacing` is a link
2024-12-20 17:08:13 +01:00
Josh Holmer
4df1464f39
Add feature flag to use system libdeflate (#659)
This need was raised in https://bugs.gentoo.org/944285. Can be enabled
with `--features system-libdeflate` to use the libdeflate located on the
user's system instead of building our own.
2024-12-03 20:02:03 +01:00
andrews05
8ebb5e6dfd
Cleanup tests (#656)
I've replaced a few tests with synthetic images and removed a number
that I don't believe are relevant anymore.

I've also removed the #553 test without replacement as I don't know how
to synthesise a replacement and the issue is still open anyway.

Going forward I think we should avoid adding any new "issue" tests and
instead synthesise something suitable in one of the other collections.

Closes #655.
2024-12-02 20:58:01 +01:00
dependabot[bot]
3b7467a5da
Bump dawidd6/action-download-artifact from 6 to 7 (#657)
Bumps
[dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact)
from 6 to 7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dawidd6/action-download-artifact/releases">dawidd6/action-download-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v7</h2>
<h2>What's Changed</h2>
<ul>
<li>build(deps): bump fast-xml-parser from 4.4.0 to 4.4.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/dawidd6/action-download-artifact/pull/299">dawidd6/action-download-artifact#299</a></li>
<li>build(deps): bump <code>@​actions/artifact</code> from 2.1.7 to
2.1.9 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/dawidd6/action-download-artifact/pull/300">dawidd6/action-download-artifact#300</a></li>
<li>build(deps): bump adm-zip from 0.5.14 to 0.5.15 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/dawidd6/action-download-artifact/pull/301">dawidd6/action-download-artifact#301</a></li>
<li>build(deps): bump adm-zip from 0.5.15 to 0.5.16 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/dawidd6/action-download-artifact/pull/306">dawidd6/action-download-artifact#306</a></li>
<li>build(deps): bump path-to-regexp from 6.2.2 to 6.3.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/dawidd6/action-download-artifact/pull/307">dawidd6/action-download-artifact#307</a></li>
<li>build(deps): bump <code>@​actions/artifact</code> from 2.1.9 to
2.1.10 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/dawidd6/action-download-artifact/pull/311">dawidd6/action-download-artifact#311</a></li>
<li>build(deps): bump <code>@​actions/core</code> from 1.10.1 to 1.11.0
by <a href="https://github.com/dependabot"><code>@​dependabot</code></a>
in <a
href="https://redirect.github.com/dawidd6/action-download-artifact/pull/310">dawidd6/action-download-artifact#310</a></li>
<li>build(deps): bump <code>@​actions/core</code> from 1.11.0 to 1.11.1
by <a href="https://github.com/dependabot"><code>@​dependabot</code></a>
in <a
href="https://redirect.github.com/dawidd6/action-download-artifact/pull/312">dawidd6/action-download-artifact#312</a></li>
<li>build(deps): bump <code>@​actions/artifact</code> from 2.1.10 to
2.1.11 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/dawidd6/action-download-artifact/pull/313">dawidd6/action-download-artifact#313</a></li>
<li>build(deps): Fix cross-spawn &gt;=7.0.0 &lt;= 7.0.5 vulnerability by
<a href="https://github.com/alexcouret"><code>@​alexcouret</code></a> in
<a
href="https://redirect.github.com/dawidd6/action-download-artifact/pull/317">dawidd6/action-download-artifact#317</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/alexcouret"><code>@​alexcouret</code></a> made
their first contribution in <a
href="https://redirect.github.com/dawidd6/action-download-artifact/pull/317">dawidd6/action-download-artifact#317</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/dawidd6/action-download-artifact/compare/v6...v7">https://github.com/dawidd6/action-download-artifact/compare/v6...v7</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="80620a5d27"><code>80620a5</code></a>
node_modules: update</li>
<li><a
href="b15e003f46"><code>b15e003</code></a>
node_modules: install</li>
<li><a
href="1ee9a455fd"><code>1ee9a45</code></a>
build(deps): Fix cross-spawn &gt;=7.0.0 &lt;= 7.0.5 vulnerability (<a
href="https://redirect.github.com/dawidd6/action-download-artifact/issues/317">#317</a>)</li>
<li><a
href="b2f2706ac4"><code>b2f2706</code></a>
build(deps): bump <code>@​actions/artifact</code> from 2.1.10 to 2.1.11
(<a
href="https://redirect.github.com/dawidd6/action-download-artifact/issues/313">#313</a>)</li>
<li><a
href="fdbeba027c"><code>fdbeba0</code></a>
build(deps): bump <code>@​actions/core</code> from 1.11.0 to 1.11.1 (<a
href="https://redirect.github.com/dawidd6/action-download-artifact/issues/312">#312</a>)</li>
<li><a
href="a74b42987a"><code>a74b429</code></a>
build(deps): bump <code>@​actions/core</code> from 1.10.1 to 1.11.0 (<a
href="https://redirect.github.com/dawidd6/action-download-artifact/issues/310">#310</a>)</li>
<li><a
href="24e807a70c"><code>24e807a</code></a>
build(deps): bump <code>@​actions/artifact</code> from 2.1.9 to 2.1.10
(<a
href="https://redirect.github.com/dawidd6/action-download-artifact/issues/311">#311</a>)</li>
<li><a
href="9592e3c4ab"><code>9592e3c</code></a>
build(deps): bump path-to-regexp from 6.2.2 to 6.3.0 (<a
href="https://redirect.github.com/dawidd6/action-download-artifact/issues/307">#307</a>)</li>
<li><a
href="5f966b63eb"><code>5f966b6</code></a>
build(deps): bump adm-zip from 0.5.15 to 0.5.16 (<a
href="https://redirect.github.com/dawidd6/action-download-artifact/issues/306">#306</a>)</li>
<li><a
href="db9477a3eb"><code>db9477a</code></a>
build(deps): bump adm-zip from 0.5.14 to 0.5.15 (<a
href="https://redirect.github.com/dawidd6/action-download-artifact/issues/301">#301</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/dawidd6/action-download-artifact/compare/v6...v7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dawidd6/action-download-artifact&package-manager=github_actions&previous-version=6&new-version=7)](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>
2024-12-02 20:32:44 +01:00
andrews05
262082f546
Fix manpage for --zi (#653)
I don't know if this is a bug in the mangen but it seems to require
explicitly setting the value name to display correctly.
Before:
```
       --zi [default: 15]
              Set the number of iterations to use for Zopfli compression.
```

After:
```
       --zi=iterations [default: 15]
              Set the number of iterations to use for Zopfli compression.
```

This does not affect MANUAL.txt or the built-in help.
2024-12-02 12:34:52 +01:00
Kornel
f8c84c258d API docs 2024-11-30 13:28:05 +13:00
andrews05
e8e8309c2d
Bump version to 9.1.3 (#647)
Some checks failed
deploy / Deploy release (push) Has been cancelled
docker / build (push) Has been cancelled
oxipng / CI (push) Has been cancelled
oxipng / MSRV check (push) Has been cancelled
Just as a point of interest, I observed a 3-4% performance improvement
from the BigEnt strategy benchmark as a result of the upgrade to
rustc-hash 2.0. This has zero impact on overall performance.
2024-11-29 17:36:50 +01:00
Alejandro González
e638807d33
Add keywords to Cargo package manifest
This was recommended by `lib.rs`'s maintainer dashboard, and it's
actually a good idea for making the crate more discoverable, I think.
2024-11-28 21:33:37 +01:00
andrews05
ca05d99be4
Assume None if the line is all zeros (#650)
#648 may have been a bit hasty - I realised afterward that there's a
simpler way to achieve the same thing, and include the Brute filter as
well.

This reverts #648 and instead just picks None up front if the line is
all zeros. This is guaranteed to be the chosen filter for MinSum,
Entropy, Bigrams and BigEnt. It's almost certainly true for Brute as
well but this is harder to prove. I've tested this across hundreds of
images and found no change in output.
2024-11-28 17:21:10 +01:00
andrews05
1efacace9f
Break when best possible filter result found 2024-11-27 00:52:21 +13:00
andrews05
1936861b91
Workaround weird regression with &[u8] and zopfli (#595)
Fixes #579. In lack of a proper understanding of what's going on here
and why the issue is happening, this workaround will do for now.
2024-11-26 12:41:18 +01:00
Alejandro González
f602e84fa6
Remove obsolete extern crate oxipng advice in lib usage instructions
This syntax hasn't been required since at least Rust's 2018 edition,
except in very specialized scenarios where you want to convince the
compiler that a crate is actually used to guarantee that the
side-effects of its build script (e.g., library linking) are visible,
which do not apply to Oxipng in any case.
2024-11-25 18:59:38 +01:00
Alejandro González
e367f23c01
Upgrade x64 macOS CI workflows from macOS 12 to macOS 13
macOS 12 runners are scheduled for removal by GitHub: https://github.com/actions/runner-images/issues/10721
2024-11-25 18:56:31 +01:00
andrews05
cb97e69074
Add list of software using oxipng (#646)
This adds a short list of software using Oxipng to the read me. It's not
intended to be an exhaustive list, just some ones that I'm aware of that
may have broad general interest.

I've also removed the long section about Trunk which I'm not sure is
appropriate to include in this read me. Trunk users can find
documentation on this from Trunk directly.

Lastly, I've removed the older benchmark which I'm sure has no relevance
any more.
2024-11-25 18:42:48 +01:00
Karl Horky
490a4691cb
Fix name capitalization (#630)
Hi, thanks for Oxipng!

I saw further down in the readme that Oxipng was being used in the
middle of a sentence with a capital O, so I adjusted the other lowercase
usages (other than the usages in backticks, which may instead refer to
the command line program itself) from "oxipng" to also use "Oxipng"
2024-11-24 14:17:53 +01:00
AFCMS
ef64dd0768
Improved Docker image + publish on ghcr.io (#642)
Fix #610

- [x] Native cross-compilation support in Dockerfile. Only `linux/amd64`
and `linux/arm64` supported and tested (Tier 1 platform support from
Rust)
- [x] Cache support for cargo downloads and compilation results in
Dockerfile
- [x] Open Container's
[annotations](https://github.com/opencontainers/image-spec/blob/main/annotations.md)
in Dockerfile
- [x] GitHub Actions workflow to build the image for both platforms,
publishing to ghcr.io on tags and master branch pushes.
- [x] Disable use of GitHub Actions cache for tags build, allow manually
triggering the workflow with or without cache.
- [x] [Attestation
artifacts](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds)
for builds
- [x] Add basic informations about the use of the Docker image in README

I also changed the WORKDIR from `/src` to `/work`, because if got me
confused with the use of the src folder for the project source in the
first stage of the Dockerfile. It doesn't impact anything anyways since
you can mount the file where you want and have the program options point
to it.

**How to test?**
_After checking out the branch and making sure you have QEMU installed
to build/test_
```shell
# Docker sadly doesn't have yet the way to have locally a tag with multiple platforms
# despite being able to pass multiple platforms to the build command, so we have to use two distinct tags.
docker build --platform=linux/amd64 --tag test-oxipng-amd:latest --load .
docker build --platform=linux/arm64 --tag test-oxipng-arm:latest --load .

docker run --rm test-oxipng-amd:latest --version
docker run --rm test-oxipng-arm:latest --version

# Run on some files
docker run --rm -it -v $(pwd):/work test-oxipng-amd:latest -a /work/tests/files/apng_file.png
docker run --rm -it -v $(pwd):/work test-oxipng-arm:latest -a /work/tests/files/apng_file.png

# Remove the images
docker image rm test-oxipng-amd:latest
docker image rm test-oxipng-arm:latest
```

For the workflow, see the GitHub Actions logs. If you want to test the
ghcr.io publishing you can merge the branch into a fork and see the
result.

---------

Co-authored-by: Alejandro González <me@alegon.dev>
2024-11-24 14:08:11 +01:00
Alejandro González
c81a863e69
Move manpages generation to an xtask (#645)
PR #596 brought forward automatic generation of Linux manual pages for
Oxipng, which is executed every time Oxipng is built. However, while
building manpages on every build is convenient for Oxipng development
and doing so didn't catch my attention initially, it introduces
noticeable inefficiencies for crates using Oxipng as a library: during
their build, Oxipng manpages are also built, even though most dependent
crates won't use such artifacts, as they are not considered part of the
public Oxipng crate API or even appropriate for non-human consumption.

Moreover, generating manpages depends on `clap`, which is a heavyweight
dependency: according to a fresh `cargo build --timings --release` on my
development workstation, its `clap_builder` dependency is the third most
time consuming unit to build, totalling 1.5 s (out of 11.7 s, or 12.8%).
And there is no way for dependent crates to turn this off:
[`build-dependencies` cannot be conditional on crate
features](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies).
Potentially using other `cfg` hacks to either enable or disable manpage
generation is unergonomic, if not outright disallowed. Besides reducing
their compilation time cost, dependent crates may also want to trim the
size of their dependency tree, avoiding unnecessary dependency downloads
in the process.

Therefore, a better solution to conditionally build manpages in a way
convenient for both Oxipng maintainers and downstream consumers is
needed. My proposal implemented in this PR is to leverage the
[`cargo-xtask`](https://github.com/matklad/cargo-xtask) convention to
define an auxiliary crate to move the manpage generation logic and
dependencies to, which is not part of the `oxipng` crate published on
`crates.io`. That way Oxipng maintainers and packagers can still
generate manpages at request with ease, without any automation being
noticeable to uninterested crate consumers. And as a side benefit,
Oxipng maintainers can also benefit from slightly faster iteration times
due to the lack of a build script for the main crate.

The new `mangen` xtask can be run at any time with `cargo xtask mangen`.
The generated manpages are now available at
`target/xtask/mangen/manpages`. Existing deployment scripts were updated
accordingly.
2024-11-24 13:57:29 +01:00
Alejandro González
e7f1c04488
Update dependency declarations and lockfile to the latest versions 2024-11-19 22:46:00 +01:00
Alejandro González
e83c6725dc
Tweak Cargo config comment placement 2024-11-19 22:40:26 +01:00
Alejandro González
95eea28c82
Tweak README wording on installer to mention Windows package managers 2024-11-19 22:03:27 +01:00
Alejandro González
ac943da875
Remove unused build dependency
It's been like that for a while!
2024-11-19 21:36:06 +01:00
Alejandro González
59e0509a58
Fix new Clippy lint 2024-11-19 21:36:06 +01:00
Alejandro González
44619fd24d
Fix new Clippy lint about manual div_ceil implementation
This numeric method was added on Rust 1.73, which is below our MSRV.
2024-09-28 15:42:02 +02:00
Alejandro González
7a29741f18
Run cargo fmt 2024-09-28 15:41:54 +02:00
Galaxy4594
0d60e8b7c6
Expose zc level 0 (uncompressed) 🚀 (#641)
Allow the creation of PNGs with uncompressed deflate streams via level 0
of libdeflate. If you want a glorified BMP with delta filters, this
change will make your dreams come true 🙂.
2024-09-05 00:32:28 +02:00
Matthew House
0f24120c9a
Add option for Zopfli iteration count (#640)
This PR extends the `--zopfli` argument with an optional iteration
count. In my case, I have a bunch of very small images (a few kB or
less), and I often like to use hundreds of iterations to squeeze off the
last several bytes. (I know that this crate isn't intended for
brute-force optimization, but I've found that some of its
transformations and filter strategies can be more creative than
`zopflipng`.) But this is also useful in the opposite direction, for
allowing Zopfli compression on large images where 15 iterations would be
prohibitive.
2024-08-07 21:09:47 +02:00
Alejandro González
1bb7109804
Another AArch64 Linux musl build fix attempt
After testing more thoroughly on my box, it turned out that the libgcc
linking workaround should indeed fix our build errors, but on CI that
did not happen because the `RUSTFLAGS` environment variable is set,
which takes precedence over any configuration at `.cargo/config.toml`.
Therefore, let's also add the workaround to `RUSTFLAGS` on the CI
workflow definition.

This should be a satisfactory enough stopgap solution that does not
require ourselves to be pinned to a specific nightly or drop these
builds until https://github.com/rust-lang/rust/issues/128401 is sorted
out.
2024-08-07 20:54:30 +02:00
Alejandro González
e2d58c112b
Fix CI AArch64 Linux musl builds
This is required to get PR #640 and further work on the repository
moving.
2024-08-07 16:12:06 +02:00
Alejandro González
61deab369b
Fix Clippy lints not causing CI workflow failure
It's easy to miss them out if CI is not aborted on a Clippy lint, and
this behavior change was an unintended regression when CodeQL
integration was added, so let's change it back.
2024-07-28 16:11:36 +02:00
andrews05
d30da6e8a0
Note that higher levels are not guaranteed to be better (#639)
Closes #638
2024-07-22 11:50:15 +02:00
Andrew
e8366882ad Bump version to 9.1.2
Some checks failed
oxipng / CI (push) Has been cancelled
deploy / Deploy release (push) Has been cancelled
oxipng / MSRV check (push) Has been cancelled
2024-07-13 09:39:25 +12:00
Alejandro González
664b27c4ca Do not fail workflow on static analysis results upload failures
When Clippy finds lints, it already aborts the workflow. No double
failure is needed.
2024-07-11 15:20:53 +12:00
Alejandro González
0506418157 Remove some now unnecessary Clippy lints allowances 2024-07-11 15:20:53 +12:00
Alejandro González
a8846b897d Make CI Clippy static analysis checks more robust
I have identified two potential improvements for how we perform static analysis
on our code in our CI pipeline:

- The `giraffate/clippy-action` we currently use has not been updated to Node
  20, and GitHub has repeatedly indicated that they will phase out actions that
  do not support the latest Node versions. Despite my efforts to help with the
  update by submitting a pull request upstream, it has been ignored for months
  despite its perceived ease of review, raising concerns about the ongoing
  maintenance of the action. This situation suggests we should explore
  alternative methods for integrating Clippy with GitHub's UI.
- As evidenced by PR 632, thoroughly testing Rust crates for every possible
  feature combination is often overlooked due to the tedious nature of the task.
  Our current CI setup only checks two feature combinations, which is far from
  comprehensive.

To address the first improvement, these changes drop `clippy-action` entirely in
favor of utilizing GitHub's native CodeQL SARIF (Static Analysis Results
Interchange Format) file integration. Since Clippy cannot directly output lints
in SARIF, `clippy-sarif` is used to convert Clippy's JSON output to SARIF.
Additionally, `sarif-fmt` is added to turn SARIF into a human-friendly display
format in the workflow run logs.

For the second improvement, let's use `cargo hack` with the `--feature-powerset`
flag to run Clippy for every possible feature combination. This approach strikes
a good balance between CI runtime and thoroughness, as the number of feature
combinations grows superlinearly with the number of features: running `cargo
nextest` for every powerset element would lead to excessively long CI times.
2024-07-11 15:20:53 +12:00
andrews05
f7b837af69
Allow sanity-checks without parallel (#634)
This adds the use of the rayon shim when parallel is not enabled. See
#632.
2024-07-10 21:38:34 +02:00
Kornel
f6e3614fe9 Use Rust 1.60 feature syntax 2024-07-10 12:47:26 +12:00
dependabot[bot]
f70fe61e05 Bump dawidd6/action-download-artifact from 3 to 6
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 3 to 6.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](https://github.com/dawidd6/action-download-artifact/compare/v3...v6)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-10 08:39:30 +12:00
Kornel
a2dedcfed2 Bump RGB crate 2024-07-10 08:31:47 +12:00
andrews05
3759ca85ec
Keep fcTL after PLTE (#626)
Fixes #625

---------

Co-authored-by: Alejandro González <me@alegon.dev>
2024-06-23 01:38:02 +02:00
andrews05
90ceef9796
Use MatchOptions::default() to disable case-sensitivity (#622)
Fixes #621
2024-06-04 18:08:07 +02:00
andrews05
e2c5b45f4b
Remove default value for interlace to ensure it is kept when nx is used (#620)
Fixes #619.

Interlacing still defaults to 0 when `--nx` is not specified and can
still be explicitly set even if it is (e.g. `-i 0 --nx` will still
deinterlace).
2024-06-02 18:46:44 +02:00
303 changed files with 3868 additions and 2874 deletions

View file

@ -1,9 +1,2 @@
# Remove this if targeting AArch64 from an AArch64 Linux box
[target.'cfg(all(target_os = "linux", target_arch = "aarch64"))']
runner = 'qemu-aarch64'
[target.aarch64-unknown-linux-gnu]
linker = 'aarch64-linux-gnu-gcc'
[target.aarch64-unknown-linux-musl]
linker = 'aarch64-linux-musl-gcc'
[alias]
xtask = "run --manifest-path xtask/Cargo.toml --"

7
.dockerignore Normal file
View file

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

View file

@ -1,6 +1,24 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
commit-message:
prefix: "chore"
include: "scope"
labels:
- "dependencies"
- "rust"
groups:
rust-dependencies:
patterns:
- "*"
update-types:
- "minor"
- "patch"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: monthly
interval: "weekly"

View file

@ -3,7 +3,12 @@ name: deploy
on:
push:
tags:
- 'v*.*.*'
- v*.*.*
concurrency:
# If we ever end up with two concurrent jobs for releasing the same tag, the former should be cancelled
group: release-${{ github.ref }}
cancel-in-progress: true
permissions:
actions: read
@ -20,6 +25,10 @@ jobs:
if: ${{ !github.event.repository.fork }}
strategy:
# Execute one job at a time to avoid potential race conditions in the
# GitHub release management APIs. See:
# https://github.com/softprops/action-gh-release/issues/445#issuecomment-2407940052
max-parallel: 1
matrix:
target:
- x86_64-unknown-linux-gnu
@ -33,18 +42,32 @@ jobs:
steps:
- name: Checkout source
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: nightly
cache-bin: false
cache-shared-key: cache
- name: Install cargo-deb
if: endsWith(matrix.target, '-linux-gnu')
uses: taiki-e/install-action@v2
with:
tool: cargo-deb
- name: Get the Oxipng version
id: oxipngMeta
run: echo "version=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "oxipng").version')"
>> "$GITHUB_OUTPUT"
run: >
echo "version=$(cargo metadata --format-version 1 --no-deps |
jq -r '.packages[] | select(.name == "oxipng").version')" >> "$GITHUB_OUTPUT"
- name: Retrieve ${{ matrix.target }} binary
uses: dawidd6/action-download-artifact@v3
uses: dawidd6/action-download-artifact@v21
with:
workflow: oxipng.yml
commit: ${{ env.GITHUB_SHA }}
commit: ${{ github.sha }}
name: Oxipng binary (${{ matrix.target }})
path: target
@ -71,12 +94,12 @@ jobs:
# so make the binary world-executable to meet user
# expectations set by preceding releases.
# Related issue:
# https://github.com/shssoichiro/oxipng/issues/575
# https://github.com/oxipng/oxipng/issues/575
chmod ugo+x "$ARCHIVE_NAME"/oxipng
tar -vczf "${ARCHIVE_NAME}.tar.gz" "$ARCHIVE_NAME"/*;;
esac
- name: Install QEMU and AArch64 cross compiler
- name: Install AArch64 libc components
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get -yq update
@ -85,20 +108,27 @@ jobs:
# properly
sudo apt-get -yq install libc6-arm64-cross libgcc-s1-arm64-cross
- name: Build deb archives
- name: Build Debian packages
if: endsWith(matrix.target, '-linux-gnu')
env:
# The *-arm64-cross packages above install AArch64 libraries in
# /usr/<arch>/lib instead of /usr/lib/<arch>, as expected by
# cargo-deb and dpkg-shlibdeps to find such shared libraries.
# Make both of them visible to such commands by adding that directory
# to the dynamic linker's library search path. See:
# - <https://man7.org/linux/man-pages/man1/dpkg-shlibdeps.1.html> ("Errors" section)
# - <https://github.com/kornelski/cargo-deb/issues/21>
LD_LIBRARY_PATH: /usr/aarch64-linux-gnu/lib
run: |
mkdir -p "target/${{ matrix.target }}/release"
mv target/oxipng "target/${{ matrix.target }}/release"
mv target/debug/assets "target/${{ matrix.target }}/release"
cargo install --locked cargo-deb
cargo deb --target "${{ matrix.target }}" --no-build --no-strip
- name: Create release notes
run: tail -n +3 CHANGELOG.md | sed -e '/^$/,$d' > RELEASE_NOTES.txt
- name: Create release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
name: v${{ steps.oxipngMeta.outputs.version }}
body_path: RELEASE_NOTES.txt

60
.github/workflows/docker.yml vendored Normal file
View file

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

View file

@ -8,6 +8,11 @@ on:
- synchronize
workflow_dispatch:
concurrency:
# If we ever end up with two concurrent CI jobs for the same commit, the former should be cancelled
group: ci-${{ github.sha }}
cancel-in-progress: true
jobs:
ci:
name: CI
@ -33,94 +38,83 @@ jobs:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-22.04
os: ubuntu-24.04
target-apt-arch: amd64
- target: x86_64-unknown-linux-musl
os: ubuntu-22.04
os: ubuntu-24.04
target-apt-arch: amd64
- target: aarch64-unknown-linux-gnu
os: ubuntu-22.04
os: ubuntu-24.04-arm
target-apt-arch: arm64
- target: aarch64-unknown-linux-musl
os: ubuntu-22.04
os: ubuntu-24.04-arm
target-apt-arch: arm64
- target: x86_64-pc-windows-msvc
os: windows-latest
- target: i686-pc-windows-msvc
os: windows-latest
- target: x86_64-apple-darwin
os: macos-12
os: macos-15
- target: aarch64-apple-darwin
os: macos-14 # ARM64 runner
os: macos-15
env:
CARGO_BUILD_TARGET: ${{ matrix.target }}
RUSTFLAGS: -Zlocation-detail=none
steps:
- name: Checkout source
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Ubuntu multiarch
if: startsWith(matrix.os, 'ubuntu') && matrix.target-apt-arch != 'amd64'
run: |
readonly DISTRO_CODENAME=jammy
sudo dpkg --add-architecture "${{ matrix.target-apt-arch }}"
sudo sed -i "s/^deb http/deb [arch=$(dpkg-architecture -q DEB_HOST_ARCH)] http/" /etc/apt/sources.list
sudo sed -i "s/^deb mirror/deb [arch=$(dpkg-architecture -q DEB_HOST_ARCH)] mirror/" /etc/apt/sources.list
for suite in '' '-updates' '-backports' '-security'; do
echo "deb [arch=${{ matrix.target-apt-arch }}] http://ports.ubuntu.com/ $DISTRO_CODENAME$suite main universe multiverse" | \
sudo tee -a /etc/apt/sources.list >/dev/null
done
- name: Install musl development files
if: endsWith(matrix.target, '-musl')
run: |
sudo apt-get -yq update
sudo apt-get -yq install musl-tools musl-dev:${{ matrix.target-apt-arch }}
- name: Install QEMU and AArch64 cross compiler
if: startsWith(matrix.target, 'aarch64-unknown-linux')
run: |
sudo apt-get -yq update
# libc6 must be present to run executables dynamically linked
# against glibc for the target architecture
sudo apt-get -yq install qemu-user gcc-aarch64-linux-gnu libc6:arm64
- name: Cache Cargo artifacts
uses: Swatinem/rust-cache@v2
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: nightly
targets: ${{ env.CARGO_BUILD_TARGET }}
components: clippy, rustfmt
target: ${{ env.CARGO_BUILD_TARGET }}
components: clippy, rustfmt, rust-src
cache-bin: false
cache-shared-key: cache
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Install cargo-hack
if: matrix.target == 'x86_64-unknown-linux-gnu'
uses: taiki-e/install-action@cargo-hack
- name: Install clippy-sarif
if: matrix.target == 'x86_64-unknown-linux-gnu'
uses: taiki-e/install-action@v2
with:
tool: clippy-sarif
- name: Install sarif-fmt
if: matrix.target == 'x86_64-unknown-linux-gnu'
uses: taiki-e/install-action@v2
with:
tool: sarif-fmt
- name: Run rustfmt
if: matrix.target == 'x86_64-unknown-linux-gnu'
run: cargo fmt --check
- name: Run Clippy (no default features)
- name: Run Clippy for all feature combinations
if: matrix.target == 'x86_64-unknown-linux-gnu'
uses: giraffate/clippy-action@v1
with:
clippy_flags: --no-deps --all-targets --no-default-features -- -D warnings
reporter: github-check
fail_on_error: true
- name: Run Clippy (all features)
if: matrix.target == 'x86_64-unknown-linux-gnu'
uses: giraffate/clippy-action@v1
with:
clippy_flags: --no-deps --all-targets --all-features -- -D warnings
reporter: github-check
fail_on_error: true
run: >
set -o pipefail;
cargo hack clippy --no-deps --all-targets --feature-powerset \
--exclude-features sanity-checks,system-libdeflate \
--message-format=json -- -D warnings \
| clippy-sarif
| tee clippy-results.sarif
| sarif-fmt
- name: Run tests
run: |
@ -135,16 +129,26 @@ jobs:
run: cargo doc --release --no-deps
- name: Build CLI binary
run: cargo build --release
run: cargo build --release -Zbuild-std
env:
RUSTFLAGS: "-Zlocation-detail=none -Zunstable-options -Cpanic=immediate-abort"
- name: Upload CLI binary as artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: Oxipng binary (${{ matrix.target }})
path: |
target/${{ env.CARGO_BUILD_TARGET }}/release/oxipng
target/${{ env.CARGO_BUILD_TARGET }}/release/oxipng.exe
- name: Upload analysis results to GitHub
uses: github/codeql-action/upload-sarif@v4
if: always() && matrix.target == 'x86_64-unknown-linux-gnu'
continue-on-error: true
with:
sarif_file: clippy-results.sarif
category: clippy
msrv-check:
name: MSRV check
@ -156,15 +160,16 @@ jobs:
steps:
- name: Checkout source
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Cache Cargo artifacts
uses: Swatinem/rust-cache@v2
- name: Install MSRV Rust toolchain
uses: dtolnay/rust-toolchain@1.74.0
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.85.1
cache-bin: false
cache-shared-key: cache
- name: Install nextest
uses: taiki-e/install-action@nextest

8
.gitignore vendored
View file

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

View file

@ -1,3 +1,65 @@
## Version 10.1.1
- [Performance] Improve Bigrams performance, giving notably faster results at lower levels.
- [Improvement] Change `--np` flag to also prevent conversion to indexed from other color types.
- [Improvement] Improve support for recompressing ICC profiles with high compression ratios.
- [Misc] Add warning when `--ziwi` exceeds `--zi`.
- [Build] Further reduce size of binaries.
## Version 10.1.0
- [Feature] Add `--json` option for machine-readable output.
- [Improvement] New default output with file counter and summary. (Use an extra `-v` flag to get the same output as before.)
- [Bugfix] Fix fast mode sometimes giving suboptimal results for small, indexed images.
## Version 10.0.0
- [Breaking] CLI: Change short zopfli flag from `-Z` to `-z`.
- [Breaking] CLI: Change `--pretend`/`-P` to `--dry-run`/`-d`.
- [Breaking] CLI: Change `--interlace` options from `1`/`0` to `on`/`off`.
- [Breaking] API: Change `Options.interlace: Option<Interlacing>` to `Options.interlace: Option<bool>`.
- [Breaking] API: Change `Options.filter: IndexSet<RowFilter>` to `Options.filters: IndexSet<FilterStrategy>`.
- [Breaking] API: Change `Options.deflate: Deflaters` to `Options.deflater: Deflater`.
- [Breaking] API: Change `Deflaters::Zopfli { .. }` to `Deflater::Zopfli(ZopfliOptions { .. })`.
- [Breaking] API: Change `optimize()` to return `(usize, usize)`, with original and optimized sizes.
- [Feature] Allow zopfli iterations higher than 255.
- [Feature] Add `--ziwi` option for zopfli iterations without improvement.
- [Feature] Add `--max-raw-size` option to skip images that are too large.
- [Feature] Add `--brute-level` and `--brute-lines` options for advanced control of Brute filter.
- [Improvement] Reduce memory usage for `fast` mode.
- [Improvement] Slightly improve compression with Brute filter at levels 5 and 6.
- [Misc] Change `--preserve` option to no longer preserve last access time.
- [Misc] Make help output colored.
## Version 9.1.5
- [Feature] Add `--sequential` option to process files sequentially rather than in parallel.
- [Performance] Update to latest Zopfli with greatly improved performance.
- [Improvement] Reduce memory usage.
- [Bugfix] Correct handling of grayscale conversion when ICC profile is present.
## Version 9.1.4
- [Improvement] Improve optimization of APNG files (reductions still not supported yet).
- [Improvement] Improve reductions for small images and ensure consistent results for repeat runs.
- [Build] Add feature `system-libdeflate` to use the system-installed version of libdeflate.
- [Misc] Strip C2PA metadata by default.
## Version 9.1.3
- [Feature] Add `--zi` option to control the number of Zopfli iterations.
- [Improvement] Allow setting compression level to 0.
- [Performance] Improve filtering performance for some images.
- [Build] Move man page generation to an xtask.
## Version 9.1.2
- [Bugfix] Fix `--nx` still applying deinterlacing by default.
- [Bugfix] Fix wildcard matching being case-sensitive on Windows.
- [Bugfix] Fix optimized APNGs not being compatible with some programs.
- [Build] Fix feature `sanity-checks` not working without `parallel`.
- [Misc] Resolve ambiguity between optional dependencies and crate features.
## Version 9.1.1
- [Build] Change man page generation path to resolve issue with cargo publish.
@ -137,51 +199,51 @@
### Version 3.0.0
- [Breaking] Bump minimum Rust version to 1.41.0
- [Breaking] Use IndexMap/IndexSet to provide more consistent performance ([#202](https://github.com/shssoichiro/oxipng/pull/202))
- [Breaking] Use IndexMap/IndexSet to provide more consistent performance ([#202](https://github.com/oxipng/oxipng/pull/202))
- This changes some public-facing types.
`IndexMap` and `IndexSet` are reexported
at the crate root to aid migration.
- [Breaking] Remove fields from the `Options` struct which were never used ([#211](https://github.com/shssoichiro/oxipng/pull/211/files#diff-b4aea3e418ccdb71239b96952d9cddb6L217), [#212](https://github.com/shssoichiro/oxipng/pull/212/files#diff-b4aea3e418ccdb71239b96952d9cddb6L134))
- [Breaking] Refactor zlib-specific options in the `Options` struct ([#210](https://github.com/shssoichiro/oxipng/pull/210/files))
- [Feature] Add libdeflater as an option ([#203](https://github.com/shssoichiro/oxipng/pull/203))
- [Feature] Use standard `log` library ([#218](https://github.com/shssoichiro/oxipng/pull/218))
- [Feature] Add `-o max` setting which will always reference the highest compression preset ([#224](https://github.com/shssoichiro/oxipng/pull/224))
- [Breaking] Remove fields from the `Options` struct which were never used ([#211](https://github.com/oxipng/oxipng/pull/211/files#diff-b4aea3e418ccdb71239b96952d9cddb6L217), [#212](https://github.com/oxipng/oxipng/pull/212/files#diff-b4aea3e418ccdb71239b96952d9cddb6L134))
- [Breaking] Refactor zlib-specific options in the `Options` struct ([#210](https://github.com/oxipng/oxipng/pull/210/files))
- [Feature] Add libdeflater as an option ([#203](https://github.com/oxipng/oxipng/pull/203))
- [Feature] Use standard `log` library ([#218](https://github.com/oxipng/oxipng/pull/218))
- [Feature] Add `-o max` setting which will always reference the highest compression preset ([#224](https://github.com/oxipng/oxipng/pull/224))
- [Deprecated] `-o 4` was found to be equivalent to `-o 3` and is deprecated.
It will likely be removed in a future release.
For now it remains equivalent to `-o 3`. ([#224](https://github.com/shssoichiro/oxipng/pull/224))
- [Bugfix] Ensure output is deterministic ([#199](https://github.com/shssoichiro/oxipng/pull/199))
For now it remains equivalent to `-o 3`. ([#224](https://github.com/oxipng/oxipng/pull/224))
- [Bugfix] Ensure output is deterministic ([#199](https://github.com/oxipng/oxipng/pull/199))
- Update `image` crate to 0.23
- Update `itertools` crate to 0.9
- Various performance and internal improvements
### Version 2.3.0
- Allow disabling all alpha optimizations ([#181](https://github.com/shssoichiro/oxipng/pull/181))
- Fix interlacing issues on tiny images ([#182](https://github.com/shssoichiro/oxipng/pull/182))
- Reduce memory usage in filtering ([#191](https://github.com/shssoichiro/oxipng/pull/191))
- Implement palette sorting to improve compression ([#193](https://github.com/shssoichiro/oxipng/pull/193))
- Disable alpha optimizations by default ([#187](https://github.com/shssoichiro/oxipng/pull/187))
- Add support for WASM ([#194](https://github.com/shssoichiro/oxipng/pull/194))
- Allow disabling all alpha optimizations ([#181](https://github.com/oxipng/oxipng/pull/181))
- Fix interlacing issues on tiny images ([#182](https://github.com/oxipng/oxipng/pull/182))
- Reduce memory usage in filtering ([#191](https://github.com/oxipng/oxipng/pull/191))
- Implement palette sorting to improve compression ([#193](https://github.com/oxipng/oxipng/pull/193))
- Disable alpha optimizations by default ([#187](https://github.com/oxipng/oxipng/pull/187))
- Add support for WASM ([#194](https://github.com/oxipng/oxipng/pull/194))
### Version 2.2.2
- Fix grayscale bit-depth reduction ([#171](https://github.com/shssoichiro/oxipng/pull/171))
- Fix typos and incorrect log message ([#172](https://github.com/shssoichiro/oxipng/pull/172))
- Make metadata order deterministic ([#174](https://github.com/shssoichiro/oxipng/pull/174))
- Fix 32-bit builds ([#176](https://github.com/shssoichiro/oxipng/pull/176))
- Enable LTO in release builds ([#177](https://github.com/shssoichiro/oxipng/pull/177))
- Use deterministic compression strategy ([#179](https://github.com/shssoichiro/oxipng/pull/179))
- Fix decoding interlaced images with height or width <= 2 ([#175](https://github.com/shssoichiro/oxipng/pull/175))
- Preallocate memory in reduced_alpha_to_up ([#180](https://github.com/shssoichiro/oxipng/pull/180))
- Fix grayscale bit-depth reduction ([#171](https://github.com/oxipng/oxipng/pull/171))
- Fix typos and incorrect log message ([#172](https://github.com/oxipng/oxipng/pull/172))
- Make metadata order deterministic ([#174](https://github.com/oxipng/oxipng/pull/174))
- Fix 32-bit builds ([#176](https://github.com/oxipng/oxipng/pull/176))
- Enable LTO in release builds ([#177](https://github.com/oxipng/oxipng/pull/177))
- Use deterministic compression strategy ([#179](https://github.com/oxipng/oxipng/pull/179))
- Fix decoding interlaced images with height or width <= 2 ([#175](https://github.com/oxipng/oxipng/pull/175))
- Preallocate memory in reduced_alpha_to_up ([#180](https://github.com/oxipng/oxipng/pull/180))
- Update `bit-vec` crate to 0.6
### Version 2.2.1
- Fix compression of very large files ([#167](https://github.com/shssoichiro/oxipng/pull/167)) ([#168](https://github.com/shssoichiro/oxipng/pull/168))
- Fix compression of very large files ([#167](https://github.com/oxipng/oxipng/pull/167)) ([#168](https://github.com/oxipng/oxipng/pull/168))
### Version 2.2.0
- Various internal improvements ([#154](https://github.com/shssoichiro/oxipng/pull/154)) ([#158](https://github.com/shssoichiro/oxipng/pull/158)) ([#160](https://github.com/shssoichiro/oxipng/pull/160)) ([#161](https://github.com/shssoichiro/oxipng/pull/161)) ([#162](https://github.com/shssoichiro/oxipng/pull/162)) ([#163](https://github.com/shssoichiro/oxipng/pull/163))
- Various internal improvements ([#154](https://github.com/oxipng/oxipng/pull/154)) ([#158](https://github.com/oxipng/oxipng/pull/158)) ([#160](https://github.com/oxipng/oxipng/pull/160)) ([#161](https://github.com/oxipng/oxipng/pull/161)) ([#162](https://github.com/oxipng/oxipng/pull/162)) ([#163](https://github.com/oxipng/oxipng/pull/163))
- Update `image` crate to 0.21.0
- Update `itertools` crate to 0.8.0
- Update `zopfli` crate to 0.4.0
@ -190,24 +252,24 @@
### Version 2.1.8
- Fix non-standard sBIT headers in other code locations ([#153](https://github.com/shssoichiro/oxipng/issues/153))
- Fix non-standard sBIT headers in other code locations ([#153](https://github.com/oxipng/oxipng/issues/153))
### Version 2.1.7
- 80x faster palette reduction ([#150](https://github.com/shssoichiro/oxipng/pull/150))
- Optimize RGB to palette conversion ([#148](https://github.com/shssoichiro/oxipng/pull/148))
- Various microoptimizations ([#146](https://github.com/shssoichiro/oxipng/pull/146))
- Introduce third-party safe wrapper around cloudflare-zlib ([#149](https://github.com/shssoichiro/oxipng/pull/149))
- 80x faster palette reduction ([#150](https://github.com/oxipng/oxipng/pull/150))
- Optimize RGB to palette conversion ([#148](https://github.com/oxipng/oxipng/pull/148))
- Various microoptimizations ([#146](https://github.com/oxipng/oxipng/pull/146))
- Introduce third-party safe wrapper around cloudflare-zlib ([#149](https://github.com/oxipng/oxipng/pull/149))
### Version 2.1.6
- Identify and drop useless sRGB profiles ([#143](https://github.com/shssoichiro/oxipng/pull/143))
- Alpha heuristic improvements ([#144](https://github.com/shssoichiro/oxipng/pull/144))
- Identify and drop useless sRGB profiles ([#143](https://github.com/oxipng/oxipng/pull/143))
- Alpha heuristic improvements ([#144](https://github.com/oxipng/oxipng/pull/144))
- Bump `miniz_oxide` and `cloudflare-zlib-sys` to 0.2.0
### Version 2.1.5
- Fix issue where some images will incorrectly reduce bit depth ([#140](https://github.com/shssoichiro/oxipng/issues/140))
- Fix issue where some images will incorrectly reduce bit depth ([#140](https://github.com/oxipng/oxipng/issues/140))
### Version 2.1.4
@ -220,22 +282,22 @@
### Version 2.1.2
- Fix issue with PNG to Indexed reduction on images with transparency pixel ([#129](https://github.com/shssoichiro/oxipng/issues/129))
- Fix issue with PNG to Indexed reduction on images with transparency pixel ([#129](https://github.com/oxipng/oxipng/issues/129))
### Version 2.1.1
- More fixes for alpha optimization on interlaced images ([#133](https://github.com/shssoichiro/oxipng/issues/133))
- More fixes for alpha optimization on interlaced images ([#133](https://github.com/oxipng/oxipng/issues/133))
### Version 2.1.0
- [SEMVER_MINOR] Bump minimum Rust version to 1.27.0
- [SEMVER_MINOR] Reenable faster Cloudflare zlib compression on platforms that support it
- Fix memory leak with Cloudflare zlib ([#126](https://github.com/shssoichiro/oxipng/issues/126))
- Fix memory leak with Cloudflare zlib ([#126](https://github.com/oxipng/oxipng/issues/126))
- Minor fixes and cleanup
### Version 2.0.2
- Fix an issue in alpha optimization on interlaced images ([#113](https://github.com/shssoichiro/oxipng/issues/113))
- Fix an issue in alpha optimization on interlaced images ([#113](https://github.com/oxipng/oxipng/issues/113))
### Version 2.0.1
@ -272,12 +334,12 @@
### Version 1.0.1
- Bump rayon to 1.0 ([#99](https://github.com/shssoichiro/oxipng/pull/99) @cuviper)
- Bump rayon to 1.0 ([#99](https://github.com/oxipng/oxipng/pull/99) @cuviper)
- Bump minor versions of other dependencies for binary distribution
### Version 1.0.0
- Remove the C dependency on miniz, and replace it with a Rust version ([#57](https://github.com/shssoichiro/oxipng/issues/57))
- Remove the C dependency on miniz, and replace it with a Rust version ([#57](https://github.com/oxipng/oxipng/issues/57))
- This improves decompression speed by 15%. Compression speed is not affected.
- [SEMVER_MAJOR] This also obsoletes the `-zm` command line option and the `memory` key on the `Options` struct.
- Presets will be updated automatically. This means that presets 3 and higher will run significantly more quickly.
@ -304,30 +366,30 @@
### Version 0.18.3
- Return exit code of 1 if an error occurred while processing a file using the CLI app ([#93](https://github.com/shssoichiro/oxipng/issues/93))
- Return exit code of 1 if an error occurred while processing a file using the CLI app ([#93](https://github.com/oxipng/oxipng/issues/93))
### Version 0.18.2
- Bump `image` to 0.18
- Fix unfiltering of scan lines in interlaced images ([#92](https://github.com/shssoichiro/oxipng/issues/92))
- Fix unfiltering of scan lines in interlaced images ([#92](https://github.com/oxipng/oxipng/issues/92))
### Version 0.18.1
- Bump `rayon` to 0.9
- Fix failure to optimize on certain grayscale images ([#89](https://github.com/shssoichiro/oxipng/issues/89))
- Fix failure to optimize on certain grayscale images ([#89](https://github.com/oxipng/oxipng/issues/89))
### Version 0.18.0
- Bump `itertools` to 0.7
- Bump `image` to 0.17
- [SEMVER_MAJOR] Bump minimum rustc version to 1.20.0
- Fix parsing of glob paths on Windows ([#90](https://github.com/shssoichiro/oxipng/issues/90))
- Fix parsing of glob paths on Windows ([#90](https://github.com/oxipng/oxipng/issues/90))
### Version 0.17.2
- Bump `image` to 0.16
- Quickly pass over files that do not have a PNG header ([#85](https://github.com/shssoichiro/oxipng/issues/85) @emielbeinema)
- Return an error instead of crashing on APNG files ([#83](https://github.com/shssoichiro/oxipng/issues/83) @emielbeinema)
- Quickly pass over files that do not have a PNG header ([#85](https://github.com/oxipng/oxipng/issues/85) @emielbeinema)
- Return an error instead of crashing on APNG files ([#83](https://github.com/oxipng/oxipng/issues/83) @emielbeinema)
### Version 0.17.1
@ -343,14 +405,14 @@
trials for optimizing the alpha channel, using the previously mentioned fast heuristic.
This option will make optimization of images with transparency somewhat slower,
but may improve compression.
- Fixed a bug in reducing palettes for images with bit depth of two ([#80](https://github.com/shssoichiro/oxipng/issues/80))
- Fixed another bug in reducing palettes for images with bit depth less than eight ([#82](https://github.com/shssoichiro/oxipng/issues/82))
- Fixed a bug in reducing palettes for images with bit depth of two ([#80](https://github.com/oxipng/oxipng/issues/80))
- Fixed another bug in reducing palettes for images with bit depth less than eight ([#82](https://github.com/oxipng/oxipng/issues/82))
- Code cleanup
- Bump `image` to 0.15
### Version 0.16.3
- Fix command-line help text ([#70](https://github.com/shssoichiro/oxipng/issues/70))
- Fix command-line help text ([#70](https://github.com/oxipng/oxipng/issues/70))
### Version 0.16.2
@ -368,18 +430,18 @@
### Version 0.15.2
- Bump `image` to 0.13 ([#65](https://github.com/shssoichiro/oxipng/pull/65))
- Bump `image` to 0.13 ([#65](https://github.com/oxipng/oxipng/pull/65))
- Bump `rayon` to 0.7
- Bump `itertools` to 0.6
### Version 0.15.1
- Ignore color reductions that would increase file size ([#61](https://github.com/shssoichiro/oxipng/issues/61))
- Ignore color reductions that would increase file size ([#61](https://github.com/oxipng/oxipng/issues/61))
### Version 0.15.0
- [SEMVER_MINOR] Check images for correctness before writing result ([#60](https://github.com/shssoichiro/oxipng/issues/60))
- Fix invalid output when reducing image to a different color type but file size does not improve ([#60](https://github.com/shssoichiro/oxipng/issues/60))
- [SEMVER_MINOR] Check images for correctness before writing result ([#60](https://github.com/oxipng/oxipng/issues/60))
- Fix invalid output when reducing image to a different color type but file size does not improve ([#60](https://github.com/oxipng/oxipng/issues/60))
- Don't write new file if moving from interlaced to non-interlaced if new file would be larger
### Version 0.14.4
@ -413,14 +475,14 @@
### Version 0.13.0
- Fix bug in certain PNG headers when reducing color type ([#52](https://github.com/shssoichiro/oxipng/issues/52))
- Fix bug in certain PNG headers when reducing color type ([#52](https://github.com/oxipng/oxipng/issues/52))
- [SEMVER_MAJOR] Reduction functions now take `&mut PngData` and return a `bool` indicating whether the image was reduced
- [SMEVER_MAJOR] Bump minimum required rustc version to 1.12.0
### Version 0.12.0
- Performance optimizations
- Fix processing filenames that contain commas (@aliceatlas [#50](https://github.com/shssoichiro/oxipng/pull/50))
- Fix processing filenames that contain commas (@aliceatlas [#50](https://github.com/oxipng/oxipng/pull/50))
- [SEMVER_MINOR] Add zopfli option (-Z), disabled by default. Gives about 10% better compression, but is currently 50-100x slower.
### Version 0.11.0
@ -445,7 +507,7 @@
### Version 0.8.2
- Fix issue where images smaller than 4px width would crash on interlacing ([#42](https://github.com/shssoichiro/oxipng/issues/42))
- Fix issue where images smaller than 4px width would crash on interlacing ([#42](https://github.com/oxipng/oxipng/issues/42))
### Version 0.8.1
@ -461,7 +523,7 @@
- Minor compression improvement on interlaced images
- Performance optimizations
- [SEMVER_MINOR] Move default Options into a Default impl
- [SEMVER_MINOR] Add option for setting number of threads ([#39](https://github.com/shssoichiro/oxipng/issues/39))
- [SEMVER_MINOR] Add option for setting number of threads ([#39](https://github.com/oxipng/oxipng/issues/39))
### Version 0.6.0
@ -472,47 +534,47 @@
### Version 0.5.0
- [SEMVER_MINOR] Palette entries can now reduced, on by default ([#11](https://github.com/shssoichiro/oxipng/issues/11))
- [SEMVER_MINOR] Palette entries can now reduced, on by default ([#11](https://github.com/oxipng/oxipng/issues/11))
- Don't report that we are in pretend mode if verbosity is set to none
- Add cargo bench suite ([#7](https://github.com/shssoichiro/oxipng/issues/7))
- Add cargo bench suite ([#7](https://github.com/oxipng/oxipng/issues/7))
### Version 0.4.0
- Performance optimizations
- [SEMVER_MAJOR] `-s` automatically infers `--strip safe` ([#31](https://github.com/shssoichiro/oxipng/issues/31))
- [SEMVER_MAJOR] `-s` automatically infers `--strip safe` ([#31](https://github.com/oxipng/oxipng/issues/31))
- Update byteorder and clap crates
- Fix issue where interlaced images incorrectly applied filters on the first line of a pass
### Version 0.3.0
- Properly decode interlaced images
- [SEMVER_MINOR] Allow converting between progressive and interlaced images ([#3](https://github.com/shssoichiro/oxipng/issues/3))
- [SEMVER_MINOR] Allow converting between progressive and interlaced images ([#3](https://github.com/oxipng/oxipng/issues/3))
- Fix a bug that would cause oxipng to crash on very small images
### Version 0.2.2
- Limit number of threads to 1.5x number of cores
- Significantly improve memory usage, especially with high optimization levels. ([#32](https://github.com/shssoichiro/oxipng/issues/32))
- Refactor output code ([#19](https://github.com/shssoichiro/oxipng/issues/19))
- Significantly improve memory usage, especially with high optimization levels. ([#32](https://github.com/oxipng/oxipng/issues/32))
- Refactor output code ([#19](https://github.com/oxipng/oxipng/issues/19))
### Version 0.2.1
- Add rustdoc for public methods and structs
- Improve filter mode 5 heuristic ([#16](https://github.com/shssoichiro/oxipng/issues/16))
- Add tests for edge-case images with subtitles ([#29](https://github.com/shssoichiro/oxipng/issues/29))
- Improve filter mode 5 heuristic ([#16](https://github.com/oxipng/oxipng/issues/16))
- Add tests for edge-case images with subtitles ([#29](https://github.com/oxipng/oxipng/issues/29))
### Version 0.2.0
- Fix program version that is displayed when running `oxipng -V`
- Ensure `--quiet` mode is actually quiet (@SethDusek [#20](https://github.com/shssoichiro/oxipng/pull/20))
- Ensure `--quiet` mode is actually quiet (@SethDusek [#20](https://github.com/oxipng/oxipng/pull/20))
- Write status/debug information to stderr instead of stdout
- Use heuristics to determine best combination for `-o1` ([#21](https://github.com/shssoichiro/oxipng/issues/21))
- Use heuristics to determine best combination for `-o1` ([#21](https://github.com/oxipng/oxipng/issues/21))
- [SEMVER_MAJOR] Allow 'safe', 'all', or comma-separated list as options for `--strip`
- [SEMVER_MINOR] Add `-s` alias for `--strip`
### Version 0.1.1
- Fix `oxipng *` writing all input files to one output file ([#15](https://github.com/shssoichiro/oxipng/issues/15))
- Fix `oxipng *` writing all input files to one output file ([#15](https://github.com/oxipng/oxipng/issues/15))
### Version 0.1.0

588
Cargo.lock generated
View file

@ -1,78 +1,74 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "adler"
version = "1.0.2"
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "anstream"
version = "0.6.13"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.6"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.52.0",
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "autocfg"
version = "1.2.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
version = "1.3.2"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
name = "bitvec"
@ -87,43 +83,53 @@ dependencies = [
]
[[package]]
name = "bytemuck"
version = "1.15.0"
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "byteorder"
version = "1.5.0"
name = "bytemuck"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "cc"
version = "1.0.92"
version = "1.2.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41"
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clap"
version = "4.5.4"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.2"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
@ -134,49 +140,30 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "clap_mangen"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1dd95b5ebb5c1c54581dd6346f3ed6a79a3eef95dd372fc2ac13d535535300e"
dependencies = [
"clap",
"roff",
]
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
version = "1.0.0"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "crc32fast"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
@ -193,30 +180,30 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.19"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "either"
version = "1.10.0"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "env_filter"
version = "0.1.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea"
checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
dependencies = [
"log",
]
[[package]]
name = "env_logger"
version = "0.11.3"
version = "0.11.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9"
checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
dependencies = [
"anstream",
"anstyle",
@ -226,46 +213,40 @@ dependencies = [
[[package]]
name = "equivalent"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.8"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys",
]
[[package]]
name = "fdeflate"
version = "0.3.4"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]]
name = "filetime"
version = "0.2.23"
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"windows-sys 0.52.0",
]
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "flate2"
version = "1.0.28"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
@ -279,33 +260,34 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "glob"
version = "0.3.1"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "hashbrown"
version = "0.14.3"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]]
name = "image"
version = "0.25.1"
version = "0.25.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11"
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
dependencies = [
"bytemuck",
"byteorder",
"byteorder-lite",
"moxcms",
"num-traits",
"png",
]
[[package]]
name = "indexmap"
version = "2.2.6"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown",
@ -313,95 +295,164 @@ dependencies = [
]
[[package]]
name = "libc"
version = "0.2.153"
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.185"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
[[package]]
name = "libdeflate-sys"
version = "1.20.0"
version = "1.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "669ea17f9257bcb48c09c7ee4bef3957777504acffac557263e20c11001977bc"
checksum = "72753e0008ea87963d2f0770042d0df7abe51fafbb8dcaf618ac440f2f1fec0a"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "libdeflater"
version = "1.20.0"
version = "1.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8dfd6424f7010ee0a3416f1d796d0450e3ad3ac237a237644f728277c4ded016"
checksum = "d1ee41cf6fb1bb6030dfb59ffb7bc01ab26aade44142084c87f0fc7a1658fe71"
dependencies = [
"libdeflate-sys",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.13"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "log"
version = "0.4.21"
version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "miniz_oxide"
version = "0.7.2"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler",
"adler2",
"simd-adler32",
]
[[package]]
name = "num-traits"
version = "0.2.18"
name = "moxcms"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "oxipng"
version = "9.1.1"
version = "10.1.1"
dependencies = [
"bitvec",
"clap",
"clap_mangen",
"crossbeam-channel",
"env_logger",
"filetime",
"glob",
"image",
"indexmap",
"libdeflater",
"log",
"parse-size",
"rayon",
"rgb",
"rustc-hash",
"rustc_version",
"serde_json",
"zopfli",
]
[[package]]
name = "png"
version = "0.17.13"
name = "parse-size"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1"
checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b"
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "png"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [
"bitflags 1.3.2",
"bitflags",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pxfm"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "radium"
version = "0.7.0"
@ -410,9 +461,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rayon"
version = "1.10.0"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
@ -420,77 +471,95 @@ dependencies = [
[[package]]
name = "rayon-core"
version = "1.12.1"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "rgb"
version = "0.8.37"
version = "0.8.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8"
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
dependencies = [
"bytemuck",
]
[[package]]
name = "roff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
[[package]]
name = "rustc-hash"
version = "1.1.0"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver",
]
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "rustix"
version = "0.38.32"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags 2.5.0",
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys",
]
[[package]]
name = "semver"
version = "1.0.22"
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "simd-adler32"
version = "0.3.7"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "strsim"
@ -498,6 +567,17 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tap"
version = "1.0.1"
@ -506,165 +586,41 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "terminal_size"
version = "0.3.0"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
dependencies = [
"rustix",
"windows-sys 0.48.0",
"windows-sys",
]
[[package]]
name = "typed-arena"
version = "2.0.2"
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.48.0"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-targets 0.48.5",
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.5",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
dependencies = [
"windows_aarch64_gnullvm 0.52.5",
"windows_aarch64_msvc 0.52.5",
"windows_i686_gnu 0.52.5",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.5",
"windows_x86_64_gnu 0.52.5",
"windows_x86_64_gnullvm 0.52.5",
"windows_x86_64_msvc 0.52.5",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]]
name = "wyz"
version = "0.5.1"
@ -675,13 +631,19 @@ dependencies = [
]
[[package]]
name = "zopfli"
version = "0.8.0"
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1f48f3508a3a3f2faee01629564400bc12260f6214a056d06a3aaaa6ef0736"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zopfli"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
"typed-arena",
]

View file

@ -2,8 +2,9 @@
authors = ["Joshua Holmer <jholmer.in@gmail.com>"]
categories = ["command-line-utilities", "compression"]
description = "A lossless PNG compression optimizer"
keywords = ["png", "image-compression", "optimization", "multi-threading", "lossless"]
documentation = "https://docs.rs/oxipng"
edition = "2021"
edition = "2024"
exclude = [
".editorconfig",
".gitattributes",
@ -13,16 +14,17 @@ exclude = [
"Dockerfile",
"scripts/*",
"tests/*",
"xtask/*",
]
homepage = "https://github.com/shssoichiro/oxipng"
homepage = "https://github.com/oxipng/oxipng"
license = "MIT"
name = "oxipng"
repository = "https://github.com/shssoichiro/oxipng"
version = "9.1.1"
rust-version = "1.74.0"
repository = "https://github.com/oxipng/oxipng"
version = "10.1.1"
rust-version = "1.85.1"
[badges]
travis-ci = { repository = "shssoichiro/oxipng", branch = "master" }
travis-ci = { repository = "oxipng/oxipng", branch = "master" }
maintenance = { status = "actively-developed" }
[[bin]]
@ -36,58 +38,33 @@ name = "zopfli"
required-features = ["zopfli"]
[dependencies]
zopfli = { version = "0.8.0", optional = true, default-features = false, features = ["std", "zlib"] }
rgb = "0.8.37"
indexmap = "2.2.6"
libdeflater = "1.20.0"
log = "0.4.21"
bitvec = "1.0.1"
rustc-hash = "1.1.0"
clap = { version = "4.6.0", optional = true, features = ["wrap_help"] }
env_logger = { version = "0.11.10", optional = true, default-features = false, features = ["auto-color"] }
image = { version = "0.25.9", optional = true, default-features = false, features = ["png"] }
indexmap = "2.14.0"
libdeflater = "1.25.2"
log = "0.4.30"
parse-size = { version = "1.1.0", optional = true }
rayon = { version = "1.11.0", optional = true }
rgb = "0.8.53"
rustc-hash = "2.1.2"
zopfli = { version = "0.8.3", optional = true, default-features = false, features = ["std", "zlib"] }
[dependencies.env_logger]
optional = true
default-features = false
features = ["auto-color"]
version = "0.11.3"
[target.'cfg(windows)'.dependencies]
glob = { version = "0.3.3", optional = true }
[dependencies.crossbeam-channel]
optional = true
version = "0.5.12"
[dependencies.filetime]
optional = true
version = "0.2.23"
[dependencies.rayon]
optional = true
version = "1.10.0"
[dependencies.clap]
optional = true
version = "4.5.4"
features = ["wrap_help"]
[target.'cfg(windows)'.dependencies.glob]
optional = true
version = "0.3.1"
[dependencies.image]
optional = true
default-features = false
features = ["png"]
version = "0.25.1"
[build-dependencies]
clap = "4.5.4"
clap_mangen = "0.2.20"
rustc_version = "0.4.0"
[dev-dependencies]
serde_json = "1.0.150"
[features]
binary = ["clap", "glob", "env_logger"]
default = ["binary", "filetime", "parallel", "zopfli"]
parallel = ["rayon", "indexmap/rayon", "crossbeam-channel"]
binary = ["dep:clap", "dep:glob", "dep:env_logger", "dep:parse-size"]
default = ["binary", "parallel", "zopfli"]
parallel = ["dep:rayon", "indexmap/rayon"]
freestanding = ["libdeflater/freestanding"]
sanity-checks = ["image"]
sanity-checks = ["dep:image"]
zopfli = ["dep:zopfli"]
system-libdeflate = ["libdeflater/dynamic"]
[lib]
name = "oxipng"
@ -104,7 +81,41 @@ panic = "abort"
[package.metadata.deb]
assets = [
["target/release/oxipng", "usr/bin/", "755"],
["target/release/assets/oxipng.1", "usr/share/man/man1/", "644"],
["target/xtask/mangen/manpages/oxipng.1", "usr/share/man/man1/", "644"],
["README.md", "usr/share/doc/oxipng/", "644"],
["CHANGELOG.md", "usr/share/doc/oxipng/", "644"],
]
[lints.rust]
missing_copy_implementations = "deny"
missing_debug_implementations = "deny"
trivial_casts = "warn"
trivial_numeric_casts = "warn"
unused_import_braces = "warn"
[lints.clippy]
cloned_instead_of_copied = "warn"
expl_impl_clone_on_copy = "warn"
float_cmp_const = "warn"
if_not_else = "warn"
ignored_unit_patterns = "warn"
linkedlist = "warn"
manual_let_else = "warn"
map_flatten = "warn"
map_unwrap_or = "warn"
match_same_arms = "warn"
mem_forget = "warn"
missing_const_for_fn = "warn"
mut_mut = "warn"
mutex_integer = "warn"
needless_continue = "warn"
option_if_let_else = "warn"
path_buf_push_overwrite = "warn"
range_plus_one = "warn"
redundant_clone = "warn"
redundant_closure_for_method_calls = "warn"
semicolon_if_nothing_returned = "warn"
unnecessary_semicolon = "warn"
unseparated_literal_suffix = "warn"
use_self = "warn"
useless_let_if_seq = "warn"

View file

@ -1,20 +1,46 @@
FROM rust:alpine as base
# syntax=docker/dockerfile:1
# check=error=true
FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
COPY . /src
FROM --platform=$BUILDPLATFORM rust:1.85.1-alpine AS base
RUN rustup update 1.74 && rustup default 1.74
RUN apk update \
&& apk add \
RUN apk update && \
apk add \
gcc \
g++
g++ \
clang
RUN cd /src && cargo build --release
COPY --from=xx / /
FROM alpine as tool
ARG TARGETPLATFORM
RUN xx-info env
COPY --from=base /src/target/release/oxipng /usr/local/bin
RUN xx-apk add \
gcc \
musl-dev \
libdeflate
WORKDIR /src
COPY . .
RUN --mount=type=cache,target=/root/.cargo/git/db \
--mount=type=cache,target=/root/.cargo/registry/cache \
--mount=type=cache,target=/root/.cargo/registry/index \
xx-cargo build --release && \
xx-verify /src/target/$(xx-cargo --print-target-triple)/release/oxipng && \
cp /src/target/$(xx-cargo --print-target-triple)/release/oxipng /src/target/oxipng
FROM scratch AS tool
LABEL org.opencontainers.image.title="Oxipng"
LABEL org.opencontainers.image.description="Multithreaded PNG optimizer written in Rust"
LABEL org.opencontainers.image.authors="Joshua Holmer <jholmer.in@gmail.com>"
LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.source="https://github.com/oxipng/oxipng"
COPY --from=base /src/target/oxipng /usr/local/bin/oxipng
WORKDIR /work
ENTRYPOINT [ "oxipng" ]
CMD [ "--help" ]

View file

@ -1,4 +1,4 @@
oxipng 9.1.1
oxipng 10.1.0
Losslessly improve compression of PNG files
Usage: oxipng [OPTIONS] <files>...
@ -13,14 +13,14 @@ Options:
compression. Lower levels are faster, higher levels provide better compression, though
with increasingly diminishing returns.
0 => --zc 5 --fast (1 trial, determined heuristically)
1 => --zc 10 --fast (1 trial, determined heuristically)
2 => --zc 11 -f 0,1,6,7 --fast (4 fast trials, 1 main trial)
3 => --zc 11 -f 0,7,8,9 (4 trials)
4 => --zc 12 -f 0,7,8,9 (4 trials)
5 => --zc 12 -f 0,1,2,5,6,7,8,9 (8 trials)
6 => --zc 12 -f 0-9 (10 trials)
max => (stable alias for the max level)
0 => --zc 5 --fast (filter chosen heuristically)
1 => --zc 10 --fast (filter chosen heuristically)
2 => --zc 11 -f 0,1,6,7 --fast
3 => --zc 11 -f 0,7,8,9 --brute-level 1 --brute-lines 3
4 => --zc 12 -f 0,7,8,9 --brute-level 1 --brute-lines 4
5 => --zc 12 -f 0,1,2,5,6,7,8,9 --brute-level 4 --brute-lines 4
6 => --zc 12 -f 0-9 --brute-level 5 --brute-lines 8
max => (stable alias for the maximum level)
Manually specifying a compression option (zc, f, etc.) will override the optimization
preset, regardless of the order you write the arguments.
@ -45,7 +45,7 @@ Options:
-p, --preserve
Preserve file permissions and timestamps if possible
-P, --pretend
-d, --dry-run
Do not write any files, only show compression results
-s
@ -61,10 +61,14 @@ Options:
CAUTION: 'all' will convert APNGs to standard PNGs.
Note that 'bKGD', 'sBIT' and 'hIST' will be forcibly stripped if the color type or bit
depth is changed, regardless of any options set.
Please note that regardless of any options set, some chunks will necessarily be stripped
when invalidated by the optimization:
bKGD, sBIT, hIST: Stripped if the color type or bit depth changes.
iDOT: Always stripped.
caBX: Stripped if it contains C2PA metadata. If explicitly retained by `--keep`,
optimization will be aborted.
The default when --strip is not passed is to keep all metadata.
The default when --strip is not passed is to keep all chunks that remain valid.
--keep <list>
Strip all metadata chunks except those in the comma-separated list. The special value
@ -79,17 +83,17 @@ Options:
but take care as while this is “visually lossless”, it is technically a lossy
transformation and may be unsuitable for some applications.
-i, --interlace <type>
Set the PNG interlacing type, where <type> is one of:
-i, --interlace <mode>
Set the PNG interlacing mode, where <mode> is one of:
0 => Remove interlacing from all images that are processed
1 => Apply Adam7 interlacing on all images that are processed
keep => Keep the existing interlacing type of each image
off => Remove interlacing from all images that are processed
on => Apply Adam7 interlacing on all images that are processed
keep => Keep the existing interlacing mode of each image
Note that interlacing can add 25-50% to the size of an optimized image. Only use it if you
believe the benefits outweigh the costs for your use case.
[default: 0]
[default: off]
--scale16
Forcibly reduce images with 16 bits per channel to 8 bits per channel. This is a lossy
@ -101,10 +105,13 @@ Options:
losslessly.
-v, --verbose...
Run in verbose mode (use twice to increase verbosity)
Show per-file info (use multiple times for more detail)
-q, --quiet
Run in quiet mode
Suppress all output messages
-j, --json
Print results as JSON
-f, --filters <list>
Perform compression trials with each of the given filter types. You can specify a
@ -119,9 +126,9 @@ Options:
Heuristic strategies (try to find the best delta filter for each line)
5 => MinSum Minimum sum of absolute differences
6 => Entropy Highest Shannon entropy
6 => Entropy Smallest Shannon entropy
7 => Bigrams Lowest count of distinct bigrams
8 => BigEnt Highest Shannon entropy of bigrams
8 => BigEnt Smallest Shannon entropy of bigrams
9 => Brute Smallest compressed size (slow)
The default value depends on the optimization level preset.
@ -132,7 +139,7 @@ Options:
CPU cores.
--zc <level>
Deflate compression level (1-12) for main compression trials. The levels here are defined
Deflate compression level (0-12) for main compression trials. The levels here are defined
by the libdeflate compression library.
The default value depends on the optimization level preset.
@ -159,15 +166,33 @@ Options:
--fix
Do not perform checksum validation of PNG chunks. This may allow some files with errors to
be processed successfully.
be processed successfully. The output will always have correct checksums.
--force
Write the output even if it is larger than the input
-Z, --zopfli
-z, --zopfli
Use the much slower but stronger Zopfli compressor for main compression trials.
Recommended use is with '-o max' and '--fast'.
--zi <iterations>
Set the number of iterations to use for Zopfli compression. Using fewer iterations may
speed up compression for large files. This option requires '--zopfli' to be set.
[default: 15]
--ziwi <iterations>
Stop Zopfli compression after this number of iterations without improvement. Use this in
conjunction with a high value for '--zi' to achieve better compression in reasonable time.
--brute-level <level>
Set the libdeflate compression level to use with the Brute filter strategy. Sane values
are 1-5. Higher values are not necessarily better.
--brute-lines <lines>
Set the number of lines to compress at once with the Brute filter strategy. Sane values
are 2-16. Higher values are not necessarily better.
--timeout <secs>
Maximum amount of time, in seconds, to spend on optimizations. Oxipng will check the
timeout before each transformation or compression trial, and will stop trying to optimize
@ -175,8 +200,25 @@ Options:
are already in progress, so it is currently of limited effectiveness for large files with
high compression levels.
--max-raw-size <bytes>
Maximum size to allow for the input image. If the raw, decompressed image data (or the
file size) of the image exceeds this size, it will be skipped. This is useful for limiting
memory usage or avoiding long processing times on large images. The value may be specified
with a unit suffix such as k, KB, m, MB, etc.
The decompressed size of an image is roughly equal to width * height * bit-depth / 8. E.g.
a 1920x1080 image with 24-bit color depth would be roughly 6MB.
-t, --threads <num>
Set number of threads to use [default: num CPU cores]
Set the maximum number of threads to use. Oxipng uses multithreading to evaluate multiple
optimizations on the same file in parallel as well as process multiple files in parallel.
You can set this to a lower value if you need to limit memory or CPU usage.
[default: num logical CPUs]
--sequential
Process multiple files sequentially rather than in parallel. Use this if you need
determinism in the processing order. Note this is not necessary if using '--threads 1'.
-h, --help
Print help (see a summary with '-h')

204
README.md
View file

@ -1,41 +1,46 @@
# Oxipng
[![Build Status](https://github.com/shssoichiro/oxipng/workflows/oxipng/badge.svg)](https://github.com/shssoichiro/oxipng/actions?query=branch%3Amaster)
[![Build Status](https://github.com/oxipng/oxipng/workflows/oxipng/badge.svg)](https://github.com/oxipng/oxipng/actions?query=branch%3Amaster)
[![Version](https://img.shields.io/crates/v/oxipng.svg)](https://crates.io/crates/oxipng)
[![License](https://img.shields.io/crates/l/oxipng.svg)](https://github.com/shssoichiro/oxipng/blob/master/LICENSE)
[![License](https://img.shields.io/crates/l/oxipng.svg)](https://github.com/oxipng/oxipng/blob/master/LICENSE)
[![Docs](https://docs.rs/oxipng/badge.svg)](https://docs.rs/oxipng)
## Overview
Oxipng is a multithreaded lossless PNG/APNG compression optimizer. It can be used via a command-line
interface or as a library in other Rust programs.
interface or as a library in other Rust programs. It is fast and highly effective.
## Installing
Oxipng for Windows can be downloaded from the [Releases](https://github.com/shssoichiro/oxipng/releases) link on the GitHub page.
Oxipng for Windows can be downloaded via the
[Releases](https://github.com/oxipng/oxipng/releases) section on its GitHub page. Recently,
however, Oxipng has also been made available through package managers. Check the list below for
up-to-date options.
For MacOS or Linux, it is recommended to install from your distro's package
repository, if possible. Oxipng is known to be packaged for the environments listed below.
For MacOS or Linux, it is recommended to install from your distro's package repository, provided
Oxipng is available there in a not too outdated version for your use case.
Oxipng is known to be packaged for the environments listed below.
[![Packaging status](https://repology.org/badge/vertical-allrepos/oxipng.svg?exclude_unsupported=1&columns=3&exclude_sources=modules,site)](https://repology.org/project/oxipng/versions)
Alternatively, oxipng can be installed from Cargo, via the following command:
Alternatively, Oxipng can be installed from Cargo, via the following command:
```
cargo install oxipng
```
Oxipng can be built from source using the latest stable or nightly Rust.
This is primarily useful for developing on oxipng.
Oxipng can also be built from source using the latest stable or nightly Rust.
This is primarily useful for developing on Oxipng.
```
git clone https://github.com/shssoichiro/oxipng.git
git clone https://github.com/oxipng/oxipng.git
cd oxipng
cargo build --release
cp target/release/oxipng /usr/local/bin
```
The current minimum supported Rust version is **1.74.0**.
The current minimum supported Rust version is **1.85.1**.
Oxipng follows Semantic Versioning.
@ -51,7 +56,7 @@ The most commonly used options are as follows:
- Optimization: `-o 0` through `-o 6` (or `-o max`), lower is faster, higher is better compression.
The default (`-o 2`) is quite fast and provides good compression. Higher levels can be notably
better but generally have increasingly diminishing returns.
better* but generally have increasingly diminishing returns.
- Strip: Used to remove metadata info from processed images. Used via `--strip [safe,all]`.
Can save a few kilobytes if you don't need the metadata. "Safe" removes only metadata that
will never affect rendering of the image. "All" removes all metadata that is not critical
@ -66,7 +71,19 @@ More advanced options can be found by running `oxipng --help`, or viewed [here](
Some options have both short (`-a`) and long (`--alpha`) forms. Which form you use is just a
matter of preference. Multiple short options can be combined together, e.g.:
`-savvo6` is equivalent to to `--strip safe --alpha --verbose --verbose --opt 6`.
Note that all options are case-sensitive.
All options are case-sensitive.
\* Note that oxipng is not a brute-force optimizer. This means that while higher optimization levels
are almost always better or equal to lower levels, this is not guaranteed and it is possible in
rare circumstances that a lower level may give a marginally smaller output. Similarly, using Zopfli
compression (`-z`) is not guaranteed to always be better than default compression.
## APNG support
Oxipng currently only supports limited optimization of animated PNGs (APNGs). It can perform
alpha-optimization, refiltering and recompression of all frames, but all transformations will be
disabled. For best results, it is recommended to use another tool such as
[apngopt](https://sourceforge.net/projects/apng/files/APNG_Optimizer/) before running Oxipng.
## Git integration via [pre-commit]
@ -75,83 +92,59 @@ preamble to an already existing one:
```yaml
repos:
- repo: https://github.com/shssoichiro/oxipng
rev: v9.0.0
- repo: https://github.com/oxipng/oxipng
rev: v10.0.0
hooks:
- id: oxipng
args: ["-o", "4", "--strip", "safe", "--alpha"]
```
[pre-commit]: https://pre-commit.com/
## Git integration via [Trunk]
## Docker
[Trunk] is an extendable superlinter which can be used to run `oxipng` to automatically optimize `png`s when committing them into a git repo, or to gate any `png`s being added to a git repo on whether they are optimized. The [trunk] oxipng integration is [here](https://github.com/trunk-io/plugins/tree/main/linters/oxipng).
A Docker image is availlable at [`ghcr.io/oxipng/oxipng`](https://github.com/oxipng/oxipng/pkgs/container/oxipng) for `linux/amd64` and `linux/arm64`.
To enable oxipng via [trunk]:
You can use it the following way:
```bash
# to get the latest version:
trunk check enable oxipng
# to get a specific version:
trunk check enable oxipng@9.0.0
docker run --rm -v $(pwd):/work ghcr.io/oxipng/oxipng -o 4 /work/file.png
```
or modify `.trunk/trunk.yaml` in your repo to contain:
```
lint:
enabled:
- oxipng@9.0.0
```
Then just run:
```bash
# to optimize a png:
trunk fmt <file>
# to check if a png is already optimized:
trunk check <file>
```
You can setup [trunk] to [manage your git hooks](https://docs.trunk.io/docs/actions-git-hooks) and automatically optimize any `png`s you commit to git, _when_ you `git commit`. To enable this, run:
```bash
trunk actions enable trunk-fmt-pre-commit
```
[trunk]: https://docs.trunk.io
Some older images are also available at [`ghcr.io/shssoichiro/oxipng`](https://github.com/users/shssoichiro/packages/container/package/oxipng).
## Library Usage
Although originally intended to be used as an executable, oxipng can also be used as a library in
other Rust projects. To do so, simply add oxipng as a dependency in your Cargo.toml,
then `extern crate oxipng` in your project. You should then have access to all of the library
functions [documented here](https://docs.rs/oxipng). The simplest
method of usage involves creating an
[Options struct](https://docs.rs/oxipng/latest/oxipng/struct.Options.html) and
passing it, along with an input filename, into the
[optimize function](https://docs.rs/oxipng/latest/oxipng/fn.optimize.html).
Although originally intended to be used as an executable, Oxipng can also be used as a library in
other Rust projects. To do so, simply add Oxipng as a dependency in your Cargo.toml. You should then
have access to all of the library functions [documented here](https://docs.rs/oxipng). The simplest
method of usage involves creating an [Options
struct](https://docs.rs/oxipng/latest/oxipng/struct.Options.html) and passing it, along with an
input filename, into the [optimize function](https://docs.rs/oxipng/latest/oxipng/fn.optimize.html).
It is recommended to disable the "binary" feature when including oxipng as a library. Currently, there is
It is recommended to disable the "binary" feature when including Oxipng as a library. Currently, there is
no simple way to just disable one feature in Cargo, it has to be done by disabling default features
and specifying the desired ones, for example:
`oxipng = { version = "9.0", features = ["parallel", "zopfli", "filetime"], default-features = false }`
`oxipng = { version = "10.0", features = ["parallel", "zopfli", "filetime"], default-features = false }`
## Software using Oxipng
- [ImageOptim](https://imageoptim.com): Mac app and web service for optimizing images
- [Squoosh](https://squoosh.app): Web app for optimizing images
- [FileOptimizer](https://nikkhokkho.sourceforge.io/?page=FileOptimizer): Windows app for optimizing files
- [Curtail](https://github.com/Huluti/Curtail): Linux app for optimizing images
- [pyoxipng](https://pypi.org/project/pyoxipng/): Python wrapper for Oxipng
- [jSquash](https://github.com/jamsinclair/jSquash): Collection of WebAssembly image codecs
- [Trunk](https://trunk.io): Developer experience toolkit for managing code
## History
Oxipng began as a complete rewrite of the OptiPNG project,
which was assumed to be dead as no commit had been made to it since March 2014.
(OptiPNG has since released a new version, after Oxipng was first released.)
The name has been changed to avoid confusion and potential legal issues.
The core goal of rewriting OptiPNG was to implement multithreading,
which would be very difficult to do within the existing C codebase of OptiPNG.
Oxipng began in 2015 as a rewrite of the OptiPNG project. The core goal was to implement
multithreading, which would have been very difficult to do within the existing C codebase of OptiPNG.
This also served as an opportunity to choose a more modern, safer language (Rust).
Note that, while similar, Oxipng is not a drop-in replacement for OptiPNG.
If you are migrating from OptiPNG, please check the [help](MANUAL.txt) before using.
However, Oxipng has evolved considerably since then. While some of the options remain similar to
OptiPNG, the architecture and capabilities are now quite different. It is not a drop-in
replacement - if you are migrating from OptiPNG, please check the [help](MANUAL.txt) before use.
## Contributing
@ -161,78 +154,11 @@ to submit a fix with the bug report, it is preferred that you do so via pull req
however you do not need to be a Rust developer to contribute.
Other contributions (such as improving documentation or translations) are also welcome via GitHub.
## Benchmarks
An independent benchmark is linked here with permission by the author:\
[oxipng and friends: A comparison of PNG optimization tools](https://op111.net/posts/2025/09/png-compression-oxipng-optipng-fileoptimizer-cwebp/)
## License
Oxipng is open-source software, distributed under the MIT license.
## Benchmarks
Tested OxiPNG 9.0.0 (commit `c16519b38b0519988db625913be919d4f0e42f5d`, compiled
on `rustc 1.74.0-nightly (7b4d9e155 2023-09-28)`) against OptiPNG version 0.7.7,
as packaged by Debian unstable, on a Linux 6.5.0-2-amd64 kernel, Intel Core
i7-12700 CPU (8 performance cores, 4 efficiency cores, 20 threads), DDR5-5200
RAM in dual channel configuration.
```
Benchmark 1: ./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 59.6 ms ± 7.7 ms [User: 77.4 ms, System: 3.6 ms]
Range (min … max): 53.3 ms … 89.9 ms 32 runs
Benchmark 2: optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 132.4 ms ± 0.8 ms [User: 132.5 ms, System: 0.6 ms]
Range (min … max): 131.8 ms … 134.4 ms 22 runs
Summary
./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png ran
2.22 ± 0.29 times faster than optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
Benchmark 1: ./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 88.7 ms ± 4.3 ms [User: 270.3 ms, System: 11.0 ms]
Range (min … max): 86.8 ms … 109.4 ms 26 runs
Benchmark 2: optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 444.9 ms ± 0.3 ms [User: 444.8 ms, System: 0.7 ms]
Range (min … max): 444.4 ms … 445.6 ms 10 runs
Summary
./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png ran
5.01 ± 0.25 times faster than optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
```
<details>
<summary>Older benchmark</summary>
Tested oxipng 5.0.0 (compiled on rustc 1.55.0-nightly (7a16cfcff 2021-07-11)) against OptiPNG version 0.7.7 on AMD Ryzen 7 4800H with Radeon Graphics with 16 logical cores
```
Benchmark #1: ./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 128.8 ms ± 14.2 ms [User: 296.0 ms, System: 14.3 ms]
Range (min … max): 98.8 ms … 152.3 ms 21 runs
Benchmark #2: optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 254.2 ms ± 16.0 ms [User: 252.8 ms, System: 1.2 ms]
Range (min … max): 208.4 ms … 263.8 ms 14 runs
Summary
'./target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png' ran
1.97 ± 0.25 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 ± σ): 141.4 ms ± 14.9 ms [User: 611.7 ms, System: 21.1 ms]
Range (min … max): 100.2 ms … 160.4 ms 23 runs
Benchmark #2: optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png
Time (mean ± σ): 730.0 ms ± 25.9 ms [User: 728.0 ms, System: 1.2 ms]
Range (min … max): 713.3 ms … 768.2 ms 10 runs
Summary
'./target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png' ran
5.16 ± 0.58 times faster than 'optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png'
```
</details>

13
SECURITY.md Normal file
View file

@ -0,0 +1,13 @@
# Security Policy
## Supported Versions
Only the latest version will be supported with security updates. We will not maintain old branches.
For this reason, you should attempt to always use the latest released version.
## Reporting a Vulnerability
To privately report a vulnerability in oxipng, please visit [our advisories page](https://github.com/oxipng/oxipng/security/advisories) and use the button to report a vulnerability.
We will review it as soon as possible.
For vulnerabilities in dependencies, please open a pull request updating the dependency to a patched version.

View file

@ -13,10 +13,7 @@ fn deflate_16_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw.data.as_ref(), 12, &min)
});
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
}
#[bench]
@ -24,10 +21,7 @@ fn deflate_8_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw.data.as_ref(), 12, &min)
});
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
}
#[bench]
@ -37,10 +31,7 @@ fn deflate_4_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw.data.as_ref(), 12, &min)
});
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
}
#[bench]
@ -50,10 +41,7 @@ fn deflate_2_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw.data.as_ref(), 12, &min)
});
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
}
#[bench]
@ -63,10 +51,7 @@ fn deflate_1_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
let min = AtomicMin::new(None);
deflate(png.raw.data.as_ref(), 12, &min)
});
b.iter(|| deflate(png.raw.data.as_ref(), 12, None));
}
#[bench]

View file

@ -13,9 +13,7 @@ fn filters_16_bits_filter_0(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::None, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
}
#[bench]
@ -23,9 +21,7 @@ fn filters_8_bits_filter_0(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::None, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
}
#[bench]
@ -35,9 +31,7 @@ fn filters_4_bits_filter_0(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::None, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
}
#[bench]
@ -47,9 +41,7 @@ fn filters_2_bits_filter_0(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::None, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
}
#[bench]
@ -59,9 +51,7 @@ fn filters_1_bits_filter_0(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::None, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::NONE, false));
}
#[bench]
@ -69,9 +59,7 @@ fn filters_16_bits_filter_1(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Sub, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
}
#[bench]
@ -79,9 +67,7 @@ fn filters_8_bits_filter_1(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Sub, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
}
#[bench]
@ -91,9 +77,7 @@ fn filters_4_bits_filter_1(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Sub, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
}
#[bench]
@ -103,9 +87,7 @@ fn filters_2_bits_filter_1(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Sub, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
}
#[bench]
@ -115,9 +97,7 @@ fn filters_1_bits_filter_1(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Sub, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::SUB, false));
}
#[bench]
@ -125,9 +105,7 @@ fn filters_16_bits_filter_2(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Up, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
}
#[bench]
@ -135,9 +113,7 @@ fn filters_8_bits_filter_2(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Up, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
}
#[bench]
@ -147,9 +123,7 @@ fn filters_4_bits_filter_2(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Up, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
}
#[bench]
@ -159,9 +133,7 @@ fn filters_2_bits_filter_2(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Up, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
}
#[bench]
@ -171,9 +143,7 @@ fn filters_1_bits_filter_2(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Up, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::UP, false));
}
#[bench]
@ -181,9 +151,7 @@ fn filters_16_bits_filter_3(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Average, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
}
#[bench]
@ -191,9 +159,7 @@ fn filters_8_bits_filter_3(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Average, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
}
#[bench]
@ -203,9 +169,7 @@ fn filters_4_bits_filter_3(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Average, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
}
#[bench]
@ -215,9 +179,7 @@ fn filters_2_bits_filter_3(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Average, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
}
#[bench]
@ -227,9 +189,7 @@ fn filters_1_bits_filter_3(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Average, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::AVERAGE, false));
}
#[bench]
@ -237,9 +197,7 @@ fn filters_16_bits_filter_4(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Paeth, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
}
#[bench]
@ -247,9 +205,7 @@ fn filters_8_bits_filter_4(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Paeth, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
}
#[bench]
@ -259,9 +215,7 @@ fn filters_4_bits_filter_4(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Paeth, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
}
#[bench]
@ -271,9 +225,7 @@ fn filters_2_bits_filter_4(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Paeth, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
}
#[bench]
@ -283,9 +235,7 @@ fn filters_1_bits_filter_4(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Paeth, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::PAETH, false));
}
#[bench]
@ -293,9 +243,7 @@ fn filters_16_bits_filter_5(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::MinSum, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
}
#[bench]
@ -303,9 +251,7 @@ fn filters_8_bits_filter_5(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::MinSum, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
}
#[bench]
@ -315,9 +261,7 @@ fn filters_4_bits_filter_5(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::MinSum, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
}
#[bench]
@ -327,9 +271,7 @@ fn filters_2_bits_filter_5(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::MinSum, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
}
#[bench]
@ -339,7 +281,5 @@ fn filters_1_bits_filter_5(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::MinSum, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
}

View file

@ -13,7 +13,7 @@ fn interlacing_16_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
b.iter(|| png.raw.change_interlacing(true));
}
#[bench]
@ -21,7 +21,7 @@ fn interlacing_8_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
b.iter(|| png.raw.change_interlacing(true));
}
#[bench]
@ -31,7 +31,7 @@ fn interlacing_4_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
b.iter(|| png.raw.change_interlacing(true));
}
#[bench]
@ -41,7 +41,7 @@ fn interlacing_2_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
b.iter(|| png.raw.change_interlacing(true));
}
#[bench]
@ -51,7 +51,7 @@ fn interlacing_1_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
b.iter(|| png.raw.change_interlacing(true));
}
#[bench]
@ -61,7 +61,7 @@ fn deinterlacing_16_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::None));
b.iter(|| png.raw.change_interlacing(false));
}
#[bench]
@ -71,7 +71,7 @@ fn deinterlacing_8_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::None));
b.iter(|| png.raw.change_interlacing(false));
}
#[bench]
@ -81,7 +81,7 @@ fn deinterlacing_4_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::None));
b.iter(|| png.raw.change_interlacing(false));
}
#[bench]
@ -91,7 +91,7 @@ fn deinterlacing_2_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::None));
b.iter(|| png.raw.change_interlacing(false));
}
#[bench]
@ -101,5 +101,5 @@ fn deinterlacing_1_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::None));
b.iter(|| png.raw.change_interlacing(false));
}

View file

@ -54,36 +54,6 @@ fn reductions_8_to_1_bits(b: &mut Bencher) {
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
}
#[bench]
fn reductions_4_to_2_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_4_should_be_palette_2.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
}
#[bench]
fn reductions_4_to_1_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_4_should_be_palette_1.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
}
#[bench]
fn reductions_2_to_1_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_2_should_be_palette_1.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
}
#[bench]
fn reductions_grayscale_8_to_4_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
@ -114,42 +84,12 @@ fn reductions_grayscale_8_to_1_bits(b: &mut Bencher) {
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
}
#[bench]
fn reductions_grayscale_4_to_2_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/grayscale_4_should_be_grayscale_2.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
}
#[bench]
fn reductions_grayscale_4_to_1_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/grayscale_4_should_be_grayscale_1.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
}
#[bench]
fn reductions_grayscale_2_to_1_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/grayscale_2_should_be_grayscale_1.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
}
#[bench]
fn reductions_rgba_to_rgb_16(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, false));
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
}
#[bench]
@ -157,7 +97,15 @@ fn reductions_rgba_to_rgb_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, false));
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
}
#[bench]
fn reductions_rgba_to_rgb_trns_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_rgb_trns_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
}
#[bench]
@ -180,32 +128,6 @@ fn reductions_rgba_to_grayscale_alpha_8(b: &mut Bencher) {
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
}
#[bench]
fn reductions_rgba_to_grayscale_16(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/rgba_16_should_be_grayscale_16.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
color::reduced_rgb_to_grayscale(&png.raw)
.and_then(|r| alpha::reduced_alpha_channel(&r, false))
});
}
#[bench]
fn reductions_rgba_to_grayscale_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/rgba_8_should_be_grayscale_8.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
color::reduced_rgb_to_grayscale(&png.raw)
.and_then(|r| alpha::reduced_alpha_channel(&r, false))
});
}
#[bench]
fn reductions_rgb_to_grayscale_16(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
@ -240,6 +162,36 @@ fn reductions_rgb_to_palette_8(b: &mut Bencher) {
b.iter(|| color::reduced_to_indexed(&png.raw, true));
}
#[bench]
fn reductions_grayscale_alpha_to_grayscale_16(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/grayscale_alpha_16_should_be_grayscale_16.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
}
#[bench]
fn reductions_grayscale_alpha_to_grayscale_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/grayscale_alpha_8_should_be_grayscale_8.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
}
#[bench]
fn reductions_grayscale_alpha_to_grayscale_trns_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/grayscale_alpha_8_should_be_grayscale_trns_8.png",
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| alpha::reduced_alpha_channel(&png.raw, true));
}
#[bench]
fn reductions_grayscale_8_to_palette_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
@ -257,7 +209,7 @@ fn reductions_palette_8_to_grayscale_8(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| color::indexed_to_channels(&png.raw, true));
b.iter(|| color::indexed_to_channels(&png.raw, true, false));
}
#[bench]

View file

@ -13,9 +13,7 @@ fn filters_minsum(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::MinSum, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::MinSum, false));
}
#[bench]
@ -23,9 +21,7 @@ fn filters_entropy(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Entropy, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::Entropy, false));
}
#[bench]
@ -33,9 +29,7 @@ fn filters_bigrams(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Bigrams, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::Bigrams, false));
}
#[bench]
@ -43,9 +37,7 @@ fn filters_bigent(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::BigEnt, false);
});
b.iter(|| png.raw.filter_image(FilterStrategy::BigEnt, false));
}
#[bench]
@ -54,6 +46,12 @@ fn filters_brute(b: &mut Bencher) {
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
png.raw.filter_image(RowFilter::Brute, false);
png.raw.filter_image(
FilterStrategy::Brute {
num_lines: 4,
level: 1,
},
false,
)
});
}

View file

@ -3,21 +3,18 @@
extern crate oxipng;
extern crate test;
use std::{num::NonZeroU8, path::PathBuf};
use std::path::PathBuf;
use oxipng::{internal_tests::*, *};
use test::Bencher;
// SAFETY: trivially safe. Stopgap solution until const unwrap is stabilized.
const DEFAULT_ZOPFLI_ITERATIONS: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(15) };
#[bench]
fn zopfli_16_bits_strategy_0(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
});
}
@ -27,7 +24,7 @@ fn zopfli_8_bits_strategy_0(b: &mut Bencher) {
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
});
}
@ -39,7 +36,7 @@ fn zopfli_4_bits_strategy_0(b: &mut Bencher) {
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
});
}
@ -51,7 +48,7 @@ fn zopfli_2_bits_strategy_0(b: &mut Bencher) {
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
});
}
@ -63,6 +60,6 @@ fn zopfli_1_bits_strategy_0(b: &mut Bencher) {
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
});
}

View file

@ -1,40 +0,0 @@
use std::{
env,
fs::File,
io::{BufWriter, Error},
path::Path,
};
use clap_mangen::Man;
include!("src/cli.rs");
fn build_manpages(outdir: &Path) -> Result<(), Error> {
let app = build_command();
let file = Path::new(&outdir).join("oxipng.1");
let mut file = BufWriter::new(File::create(file)?);
Man::new(app).render(&mut file)?;
Ok(())
}
fn main() -> Result<(), Error> {
println!("cargo:rerun-if-changed=src/cli.rs");
println!("cargo:rerun-if-changed=src/display_chunks.rs");
// Create `target/<debug|release>/assets/` folder.
let outdir = match env::var_os("OUT_DIR") {
None => return Ok(()),
Some(outdir) => outdir,
};
let out_path = PathBuf::from(outdir);
let mut path = out_path.ancestors().nth(3).unwrap().to_owned();
path.push("assets");
std::fs::create_dir_all(&path).unwrap();
build_manpages(&path)?;
Ok(())
}

View file

@ -1,5 +1,6 @@
#!/bin/bash
#!/bin/sh -eu
cargo build
cargo xtask mangen
./target/debug/oxipng -V > MANUAL.txt
#Redirect all streams to prevent detection of the terminal width and force an internal default of 100

72
src/apng.rs Normal file
View file

@ -0,0 +1,72 @@
use crate::{
PngResult,
error::PngError,
headers::{read_be_u16, read_be_u32},
};
#[derive(Debug, Clone)]
/// Animated PNG frame
pub struct Frame {
/// Width of the frame
pub width: u32,
/// Height of the frame
pub height: u32,
/// X offset of the frame
pub x_offset: u32,
/// Y offset of the frame
pub y_offset: u32,
/// Frame delay numerator
pub delay_num: u16,
/// Frame delay denominator
pub delay_den: u16,
/// Frame disposal operation
pub dispose_op: u8,
/// Frame blend operation
pub blend_op: u8,
/// Frame data, from fdAT chunks
pub data: Vec<u8>,
}
impl Frame {
/// Construct a new Frame from the data in a fcTL chunk
pub fn from_fctl_data(byte_data: &[u8]) -> PngResult<Self> {
if byte_data.len() < 26 {
return Err(PngError::TruncatedData);
}
Ok(Self {
width: read_be_u32(&byte_data[4..8]),
height: read_be_u32(&byte_data[8..12]),
x_offset: read_be_u32(&byte_data[12..16]),
y_offset: read_be_u32(&byte_data[16..20]),
delay_num: read_be_u16(&byte_data[20..22]),
delay_den: read_be_u16(&byte_data[22..24]),
dispose_op: byte_data[24],
blend_op: byte_data[25],
data: vec![],
})
}
/// Construct the data for a fcTL chunk using the given sequence number
#[must_use]
pub fn fctl_data(&self, sequence_number: u32) -> Vec<u8> {
let mut byte_data = Vec::with_capacity(26);
byte_data.extend_from_slice(&sequence_number.to_be_bytes());
byte_data.extend_from_slice(&self.width.to_be_bytes());
byte_data.extend_from_slice(&self.height.to_be_bytes());
byte_data.extend_from_slice(&self.x_offset.to_be_bytes());
byte_data.extend_from_slice(&self.y_offset.to_be_bytes());
byte_data.extend_from_slice(&self.delay_num.to_be_bytes());
byte_data.extend_from_slice(&self.delay_den.to_be_bytes());
byte_data.extend_from_slice(&[self.dispose_op, self.blend_op]);
byte_data
}
/// Construct the data for a fdAT chunk using the given sequence number
#[must_use]
pub fn fdat_data(&self, sequence_number: u32) -> Vec<u8> {
let mut byte_data = Vec::with_capacity(4 + self.data.len());
byte_data.extend_from_slice(&sequence_number.to_be_bytes());
byte_data.extend_from_slice(&self.data);
byte_data
}
}

View file

@ -6,6 +6,7 @@ pub struct AtomicMin {
}
impl AtomicMin {
#[must_use]
pub fn new(init: Option<usize>) -> Self {
Self {
val: AtomicUsize::new(init.unwrap_or(usize::MAX)),
@ -14,16 +15,7 @@ impl AtomicMin {
pub fn get(&self) -> Option<usize> {
let val = self.val.load(SeqCst);
if val == usize::MAX {
None
} else {
Some(val)
}
}
/// Unset value is usize_max
pub fn as_atomic_usize(&self) -> &AtomicUsize {
&self.val
if val == usize::MAX { None } else { Some(val) }
}
/// Try a new value, returning true if it is the new minimum

View file

@ -1,9 +1,18 @@
use std::path::PathBuf;
use std::{num::NonZeroU64, path::PathBuf};
use clap::{value_parser, Arg, ArgAction, Command};
use clap::builder::Styles;
use clap::builder::styling::{AnsiColor, Effects};
use clap::{Arg, ArgAction, Command, builder::ArgPredicate, value_parser};
use parse_size::parse_size;
include!("display_chunks.rs");
const STYLES: Styles = Styles::styled()
.header(AnsiColor::Green.on_default().effects(Effects::BOLD))
.usage(AnsiColor::Green.on_default().effects(Effects::BOLD))
.literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD))
.placeholder(AnsiColor::Cyan.on_default());
pub fn build_command() -> Command {
// Note: clap 'wrap_help' is enabled to automatically wrap lines according to terminal width.
// To keep things tidy though, short help descriptions should be no more than 54 characters,
@ -14,6 +23,7 @@ pub fn build_command() -> Command {
.version(env!("CARGO_PKG_VERSION"))
.author("Joshua Holmer <jholmer.in@gmail.com>")
.about("Losslessly improve compression of PNG files")
.styles(STYLES)
.arg(
Arg::new("files")
.help("File(s) to compress (use '-' for stdin)")
@ -31,14 +41,14 @@ Set the optimization level preset. The default level 2 is quite fast and provide
compression. Lower levels are faster, higher levels provide better compression, though \
with increasingly diminishing returns.
0 => --zc 5 --fast (1 trial, determined heuristically)
1 => --zc 10 --fast (1 trial, determined heuristically)
2 => --zc 11 -f 0,1,6,7 --fast (4 fast trials, 1 main trial)
3 => --zc 11 -f 0,7,8,9 (4 trials)
4 => --zc 12 -f 0,7,8,9 (4 trials)
5 => --zc 12 -f 0,1,2,5,6,7,8,9 (8 trials)
6 => --zc 12 -f 0-9 (10 trials)
max => (stable alias for the max level)
0 => --zc 5 --fast (filter chosen heuristically)
1 => --zc 10 --fast (filter chosen heuristically)
2 => --zc 11 -f 0,1,6,7 --fast
3 => --zc 11 -f 0,7,8,9 --brute-level 1 --brute-lines 3
4 => --zc 12 -f 0,7,8,9 --brute-level 1 --brute-lines 4
5 => --zc 12 -f 0,1,2,5,6,7,8,9 --brute-level 4 --brute-lines 4
6 => --zc 12 -f 0-9 --brute-level 5 --brute-lines 8
max => (stable alias for the maximum level)
Manually specifying a compression option (zc, f, etc.) will override the optimization \
preset, regardless of the order you write the arguments.")
@ -49,14 +59,6 @@ preset, regardless of the order you write the arguments.")
.value_parser(["0", "1", "2", "3", "4", "5", "6", "max"])
.hide_possible_values(true),
)
.arg(
Arg::new("backup")
.help("Back up modified files")
.short('b')
.long("backup")
.hide(true)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("recursive")
.help("Recurse input directories, optimizing all PNG files")
@ -105,10 +107,10 @@ Note that this will not preserve the directory structure of the input files when
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("pretend")
Arg::new("dry-run")
.help("Do not write any files, only show compression results")
.short('P')
.long("pretend")
.short('d')
.long("dry-run")
.action(ArgAction::SetTrue),
)
.arg(
@ -131,10 +133,14 @@ Strip metadata chunks, where <mode> is one of:
CAUTION: 'all' will convert APNGs to standard PNGs.
Note that 'bKGD', 'sBIT' and 'hIST' will be forcibly stripped if the color type or bit \
depth is changed, regardless of any options set.
Please note that regardless of any options set, some chunks will necessarily be stripped \
when invalidated by the optimization:
bKGD, sBIT, hIST: Stripped if the color type or bit depth changes.
iDOT: Always stripped.
caBX: Stripped if it contains C2PA metadata. If explicitly retained by `--keep`, \
optimization will be aborted.
The default when --strip is not passed is to keep all metadata.",
The default when --strip is not passed is to keep all chunks that remain valid.",
DISPLAY_CHUNKS
.iter()
.map(|c| String::from_utf8_lossy(c))
@ -172,21 +178,22 @@ transformation and may be unsuitable for some applications.")
)
.arg(
Arg::new("interlace")
.help("Set PNG interlacing type (0, 1, keep)")
.help("Set PNG interlacing (off, on, keep)")
.long_help("\
Set the PNG interlacing type, where <type> is one of:
Set the PNG interlacing mode, where <mode> is one of:
0 => Remove interlacing from all images that are processed
1 => Apply Adam7 interlacing on all images that are processed
keep => Keep the existing interlacing type of each image
off => Remove interlacing from all images that are processed
on => Apply Adam7 interlacing on all images that are processed
keep => Keep the existing interlacing mode of each image
Note that interlacing can add 25-50% to the size of an optimized image. Only use it if you \
believe the benefits outweigh the costs for your use case.")
.short('i')
.long("interlace")
.value_name("type")
.default_value("0")
.value_parser(["0", "1", "keep"])
.value_name("mode")
.value_parser(["off", "on", "keep", "0", "1"])
.default_value("off")
.default_value_if("no-reductions", ArgPredicate::IsPresent, "keep")
.hide_possible_values(true),
)
.arg(
@ -205,7 +212,7 @@ losslessly.")
)
.arg(
Arg::new("verbose")
.help("Run in verbose mode (use twice to increase verbosity)")
.help("Show per-file info (use multiple times for more detail)")
.short('v')
.long("verbose")
.action(ArgAction::Count)
@ -213,12 +220,20 @@ losslessly.")
)
.arg(
Arg::new("quiet")
.help("Run in quiet mode")
.help("Suppress all output messages")
.short('q')
.long("quiet")
.action(ArgAction::SetTrue)
.conflicts_with("verbose"),
)
.arg(
Arg::new("json")
.help("Print results as JSON")
.short('j')
.long("json")
.action(ArgAction::SetTrue)
.conflicts_with("stdout"),
)
.arg(
Arg::new("filters")
.help("Filters to try (0-9; see '--help' for details)")
@ -235,9 +250,9 @@ PNG delta filters (apply the same filter to every line)
Heuristic strategies (try to find the best delta filter for each line)
5 => MinSum Minimum sum of absolute differences
6 => Entropy Highest Shannon entropy
6 => Entropy Smallest Shannon entropy
7 => Bigrams Lowest count of distinct bigrams
8 => BigEnt Highest Shannon entropy of bigrams
8 => BigEnt Smallest Shannon entropy of bigrams
9 => Brute Smallest compressed size (slow)
The default value depends on the optimization level preset.")
@ -257,15 +272,15 @@ CPU cores.")
)
.arg(
Arg::new("compression")
.help("Deflate compression level (1-12)")
.help("Deflate compression level (0-12)")
.long_help("\
Deflate compression level (1-12) for main compression trials. The levels here are defined \
Deflate compression level (0-12) for main compression trials. The levels here are defined \
by the libdeflate compression library.
The default value depends on the optimization level preset.")
.long("zc")
.value_name("level")
.value_parser(1..=12)
.value_parser(0..=12)
.conflicts_with("zopfli"),
)
.arg(
@ -283,6 +298,8 @@ The default value depends on the optimization level preset.")
.arg(
Arg::new("no-palette-reduction")
.help("Do not change color palette")
.long_help("\
Do not convert to indexed and do not modify an existing color palette.")
.long("np")
.action(ArgAction::SetTrue),
)
@ -315,7 +332,7 @@ compressed chunks (such as iCCP) will also be disabled. Note that the combinatio
.help("Disable checksum validation")
.long_help("\
Do not perform checksum validation of PNG chunks. This may allow some files with errors to \
be processed successfully.")
be processed successfully. The output will always have correct checksums.")
.long("fix")
.action(ArgAction::SetTrue),
)
@ -331,10 +348,54 @@ be processed successfully.")
.long_help("\
Use the much slower but stronger Zopfli compressor for main compression trials. \
Recommended use is with '-o max' and '--fast'.")
.short('Z')
.short('z')
.short_alias('Z') // Kept for backwards compatibility
.long("zopfli")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("iterations")
.help("Number of Zopfli iterations")
.long_help("\
Set the number of iterations to use for Zopfli compression. Using fewer iterations may \
speed up compression for large files. This option requires '--zopfli' to be set.")
.long("zi")
.value_name("iterations")
.default_value("15")
.value_parser(value_parser!(NonZeroU64))
.requires("zopfli"),
)
.arg(
Arg::new("iterations-without-improvement")
.hide_short_help(true)
.long_help("\
Stop Zopfli compression after this number of iterations without improvement. Use this in \
conjunction with a high value for '--zi' to achieve better compression in reasonable time.")
.long("ziwi")
.value_name("iterations")
.value_parser(value_parser!(NonZeroU64))
.requires("zopfli"),
)
.arg(
Arg::new("brute-level")
.hide_short_help(true)
.long_help("\
Set the libdeflate compression level to use with the Brute filter strategy. Sane values \
are 1-5. Higher values are not necessarily better.")
.long("brute-level")
.value_name("level")
.value_parser(1..=12),
)
.arg(
Arg::new("brute-lines")
.hide_short_help(true)
.long_help("\
Set the number of lines to compress at once with the Brute filter strategy. Sane values \
are 2-16. Higher values are not necessarily better.")
.long("brute-lines")
.value_name("lines")
.value_parser(value_parser!(usize)),
)
.arg(
Arg::new("timeout")
.help("Maximum amount of time to spend on optimizations")
@ -344,16 +405,46 @@ timeout before each transformation or compression trial, and will stop trying to
the file if the timeout is exceeded. Note that this does not cut short any operations that \
are already in progress, so it is currently of limited effectiveness for large files with \
high compression levels.")
.value_name("secs")
.long("timeout")
.value_name("secs")
.value_parser(value_parser!(u64)),
)
.arg(
Arg::new("max-size")
.help("Skip image if the decompressed size exceeds this limit")
.long_help("\
Maximum size to allow for the input image. If the raw, decompressed image data (or the \
file size) of the image exceeds this size, it will be skipped. This is useful for limiting \
memory usage or avoiding long processing times on large images. The value may be specified \
with a unit suffix such as k, KB, m, MB, etc.
The decompressed size of an image is roughly equal to width * height * bit-depth / 8. E.g. \
a 1920x1080 image with 24-bit color depth would be roughly 6MB.")
.long("max-raw-size")
.value_name("bytes")
.value_parser(|s: &str| parse_size(s)),
)
.arg(
Arg::new("threads")
.help("Set number of threads to use [default: num CPU cores]")
.long("threads")
.help("Number of threads to use [default: num logical CPUs]")
.long_help("\
Set the maximum number of threads to use. Oxipng uses multithreading to evaluate multiple \
optimizations on the same file in parallel as well as process multiple files in parallel. \
You can set this to a lower value if you need to limit memory or CPU usage.
[default: num logical CPUs]")
.short('t')
.long("threads")
.value_name("num")
.value_parser(value_parser!(usize)),
)
.arg(
Arg::new("parallel-files")
.help("Process multiple files sequentially")
.long_help("\
Process multiple files sequentially rather than in parallel. Use this if you need \
determinism in the processing order. Note this is not necessary if using '--threads 1'.")
.long("sequential")
.action(ArgAction::SetFalse),
)
}

View file

@ -32,13 +32,11 @@ impl Display for ColorType {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ColorType::Grayscale { .. } => Display::fmt("Grayscale", f),
ColorType::RGB { .. } => Display::fmt("RGB", f),
ColorType::Indexed { palette } => {
Display::fmt(&format!("Indexed ({} colors)", palette.len()), f)
}
ColorType::GrayscaleAlpha => Display::fmt("Grayscale + Alpha", f),
ColorType::RGBA => Display::fmt("RGB + Alpha", f),
Self::Grayscale { .. } => write!(f, "Grayscale"),
Self::RGB { .. } => write!(f, "RGB"),
Self::Indexed { palette } => write!(f, "Indexed ({} colors)", palette.len()),
Self::GrayscaleAlpha => write!(f, "Grayscale + Alpha"),
Self::RGBA => write!(f, "RGB + Alpha"),
}
}
}
@ -46,49 +44,47 @@ impl Display for ColorType {
impl ColorType {
/// Get the code used by the PNG specification to denote this color type
#[inline]
pub fn png_header_code(&self) -> u8 {
#[must_use]
pub const fn png_header_code(&self) -> u8 {
match self {
ColorType::Grayscale { .. } => 0,
ColorType::RGB { .. } => 2,
ColorType::Indexed { .. } => 3,
ColorType::GrayscaleAlpha => 4,
ColorType::RGBA => 6,
Self::Grayscale { .. } => 0,
Self::RGB { .. } => 2,
Self::Indexed { .. } => 3,
Self::GrayscaleAlpha => 4,
Self::RGBA => 6,
}
}
#[inline]
pub(crate) fn channels_per_pixel(&self) -> u8 {
pub(crate) const fn channels_per_pixel(&self) -> u8 {
match self {
ColorType::Grayscale { .. } | ColorType::Indexed { .. } => 1,
ColorType::GrayscaleAlpha => 2,
ColorType::RGB { .. } => 3,
ColorType::RGBA => 4,
Self::Grayscale { .. } | Self::Indexed { .. } => 1,
Self::GrayscaleAlpha => 2,
Self::RGB { .. } => 3,
Self::RGBA => 4,
}
}
#[inline]
pub(crate) fn is_rgb(&self) -> bool {
matches!(self, ColorType::RGB { .. } | ColorType::RGBA)
pub(crate) const fn is_rgb(&self) -> bool {
matches!(self, Self::RGB { .. } | Self::RGBA)
}
#[inline]
pub(crate) fn is_gray(&self) -> bool {
matches!(
self,
ColorType::Grayscale { .. } | ColorType::GrayscaleAlpha
)
pub(crate) const fn is_gray(&self) -> bool {
matches!(self, Self::Grayscale { .. } | Self::GrayscaleAlpha)
}
#[inline]
pub(crate) fn has_alpha(&self) -> bool {
matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA)
pub(crate) const fn has_alpha(&self) -> bool {
matches!(self, Self::GrayscaleAlpha | Self::RGBA)
}
#[inline]
pub(crate) fn has_trns(&self) -> bool {
pub(crate) const fn has_trns(&self) -> bool {
match self {
ColorType::Grayscale { transparent_shade } => transparent_shade.is_some(),
ColorType::RGB { transparent_color } => transparent_color.is_some(),
Self::Grayscale { transparent_shade } => transparent_shade.is_some(),
Self::RGB { transparent_color } => transparent_color.is_some(),
_ => false,
}
}
@ -120,7 +116,7 @@ impl TryFrom<u8> for BitDepth {
4 => Ok(Self::Four),
8 => Ok(Self::Eight),
16 => Ok(Self::Sixteen),
_ => Err(PngError::new("Unexpected bit depth")),
_ => Err(PngError::InvalidData),
}
}
}

View file

@ -1,12 +1,10 @@
use libdeflater::*;
use crate::{atomicmin::AtomicMin, PngError, PngResult};
use crate::{PngError, PngResult};
pub fn deflate(data: &[u8], level: u8, max_size: &AtomicMin) -> PngResult<Vec<u8>> {
pub fn deflate(data: &[u8], level: u8, max_size: Option<usize>) -> PngResult<Vec<u8>> {
let mut compressor = Compressor::new(CompressionLvl::new(level.into()).unwrap());
let capacity = max_size
.get()
.unwrap_or_else(|| compressor.zlib_compress_bound(data.len()));
let capacity = max_size.unwrap_or_else(|| compressor.zlib_compress_bound(data.len()));
let mut dest = vec![0; capacity];
let len = compressor
.zlib_compress(data, &mut dest)
@ -24,12 +22,13 @@ pub fn inflate(data: &[u8], out_size: usize) -> PngResult<Vec<u8>> {
.zlib_decompress(data, &mut dest)
.map_err(|err| match err {
DecompressionError::BadData => PngError::InvalidData,
DecompressionError::InsufficientSpace => PngError::new("inflated data too long"),
DecompressionError::InsufficientSpace => PngError::InflatedDataTooLong(out_size),
})?;
dest.truncate(len);
Ok(dest)
}
#[must_use]
pub fn crc32(data: &[u8]) -> u32 {
let mut crc = Crc::new();
crc.update(data);

View file

@ -1,42 +1,38 @@
mod deflater;
#[cfg(feature = "zopfli")]
use std::num::NonZeroU8;
use std::{fmt, fmt::Display};
pub use deflater::{crc32, deflate, inflate};
use crate::{AtomicMin, PngError, PngResult};
use crate::{PngError, PngResult};
#[cfg(feature = "zopfli")]
mod zopfli_oxipng;
#[cfg(feature = "zopfli")]
pub use zopfli::Options as ZopfliOptions;
#[cfg(feature = "zopfli")]
pub use zopfli_oxipng::deflate as zopfli_deflate;
/// DEFLATE algorithms supported by oxipng (for use in [`Options`][crate::Options])
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
/// DEFLATE algorithms supported by oxipng
pub enum Deflaters {
pub enum Deflater {
/// Use libdeflater.
Libdeflater {
/// Which compression level to use on the file (1-12)
/// Which compression level to use on the file (0-12)
compression: u8,
},
#[cfg(feature = "zopfli")]
/// Use the better but slower Zopfli implementation
Zopfli {
/// The number of compression iterations to do. 15 iterations are fine
/// for small files, but bigger files will need to be compressed with
/// less iterations, or else they will be too slow.
iterations: NonZeroU8,
},
Zopfli(ZopfliOptions),
}
impl Deflaters {
pub(crate) fn deflate(self, data: &[u8], max_size: &AtomicMin) -> PngResult<Vec<u8>> {
impl Deflater {
pub(crate) fn deflate(self, data: &[u8], max_size: Option<usize>) -> PngResult<Vec<u8>> {
let compressed = match self {
Self::Libdeflater { compression } => deflate(data, compression, max_size)?,
#[cfg(feature = "zopfli")]
Self::Zopfli { iterations } => zopfli_deflate(data, iterations)?,
Self::Zopfli(options) => zopfli_deflate(data, options)?,
};
if let Some(max) = max_size.get() {
if let Some(max) = max_size {
if compressed.len() > max {
return Err(PngError::DeflatedDataTooLong(max));
}
@ -45,13 +41,13 @@ impl Deflaters {
}
}
impl Display for Deflaters {
impl Display for Deflater {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Libdeflater { compression } => Display::fmt(compression, f),
Self::Libdeflater { compression } => write!(f, "zc = {compression}"),
#[cfg(feature = "zopfli")]
Self::Zopfli { .. } => Display::fmt("zopfli", f),
Self::Zopfli(options) => write!(f, "zopfli, zi = {}", options.iteration_count),
}
}
}

View file

@ -1,17 +1,13 @@
use std::num::NonZeroU8;
use crate::{PngError, PngResult};
pub fn deflate(data: &[u8], iterations: NonZeroU8) -> PngResult<Vec<u8>> {
pub fn deflate(data: &[u8], options: zopfli::Options) -> PngResult<Vec<u8>> {
let mut output = Vec::with_capacity(data.len());
let options = zopfli::Options {
iteration_count: iterations.into(),
..Default::default()
};
match zopfli::compress(options, zopfli::Format::Zlib, data, &mut output) {
Ok(_) => (),
// Since Rust v1.74, passing &[u8] directly into zopfli causes a regression in compressed size
// for some files. Wrapping the slice in another Read implementer such as Box fixes it for now.
match zopfli::compress(options, zopfli::Format::Zlib, Box::new(data), &mut output) {
Ok(()) => (),
Err(_) => return Err(PngError::new("Failed to compress in zopfli")),
};
}
output.shrink_to_fit();
Ok(output)
}

View file

@ -2,18 +2,22 @@ use std::{error::Error, fmt};
use crate::colors::{BitDepth, ColorType};
#[derive(Debug, Clone)]
#[derive(Debug)]
#[non_exhaustive]
pub enum PngError {
DeflatedDataTooLong(usize),
TimedOut,
NotPNG,
APNGNotSupported,
InvalidData,
TruncatedData,
APNGOutOfOrder,
C2PAMetadataPreventsChanges,
ChunkMissing(&'static str),
InvalidDepthForType(BitDepth, ColorType),
CRCMismatch([u8; 4]),
DeflatedDataTooLong(usize),
IncorrectDataLength(usize, usize),
InflatedDataTooLong(usize),
InvalidData,
InvalidDepthForType(BitDepth, ColorType),
NotPNG,
ReadFailed(String, std::io::Error),
TruncatedData,
WriteFailed(String, std::io::Error),
Other(Box<str>),
}
@ -24,31 +28,42 @@ impl fmt::Display for PngError {
#[cold]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
PngError::DeflatedDataTooLong(_) => f.write_str("deflated data too long"),
PngError::TimedOut => f.write_str("timed out"),
PngError::NotPNG => f.write_str("Invalid header detected; Not a PNG file"),
PngError::InvalidData => f.write_str("Invalid data found; unable to read PNG file"),
PngError::TruncatedData => {
f.write_str("Missing data in the file; the file is truncated")
}
PngError::APNGNotSupported => f.write_str("APNG files are not (yet) supported"),
PngError::ChunkMissing(s) => write!(f, "Chunk {} missing or empty", s),
PngError::InvalidDepthForType(d, ref c) => {
write!(f, "Invalid bit depth {} for color type {}", d, c)
}
PngError::IncorrectDataLength(l1, l2) => write!(
f,
"Data length {} does not match the expected length {}",
l1, l2
Self::APNGOutOfOrder => f.write_str("APNG chunks are out of order"),
Self::C2PAMetadataPreventsChanges => f.write_str(
"The image contains C2PA manifest that would be invalidated by any file changes",
),
PngError::Other(ref s) => f.write_str(s),
Self::ChunkMissing(s) => write!(f, "Chunk {s} missing or empty"),
Self::CRCMismatch(ref c) => write!(
f,
"CRC mismatch in {} chunk; May be recoverable by using --fix",
String::from_utf8_lossy(c)
),
Self::DeflatedDataTooLong(_) => f.write_str("Deflated data too long"),
Self::IncorrectDataLength(l1, l2) => write!(
f,
"Data length {l1} does not match the expected length {l2}"
),
Self::InflatedDataTooLong(max) => write!(
f,
"Inflated data would exceed the maximum size ({max} bytes)"
),
Self::InvalidData => f.write_str("Invalid data found; unable to read PNG file"),
Self::InvalidDepthForType(d, ref c) => {
write!(f, "Invalid bit depth {d} for color type {c}")
}
Self::NotPNG => f.write_str("Invalid header detected; Not a PNG file"),
Self::ReadFailed(ref s, ref e) => write!(f, "Failed to read from {s}: {e}"),
Self::TruncatedData => f.write_str("Missing data in the file; the file is truncated"),
Self::WriteFailed(ref s, ref e) => write!(f, "Failed to write to {s}: {e}"),
Self::Other(ref s) => f.write_str(s),
}
}
}
impl PngError {
#[cold]
pub fn new(description: &str) -> PngError {
PngError::Other(description.into())
#[must_use]
pub fn new(description: &str) -> Self {
Self::Other(description.into())
}
}

View file

@ -4,37 +4,44 @@
#[cfg(not(feature = "parallel"))]
use std::cell::RefCell;
use std::sync::{
atomic::{AtomicUsize, Ordering::*},
Arc,
atomic::{AtomicUsize, Ordering::*},
};
#[cfg(feature = "parallel")]
use crossbeam_channel::{unbounded, Receiver, Sender};
use deflate::Deflater;
use indexmap::IndexSet;
use log::trace;
use rayon::prelude::*;
#[cfg(feature = "parallel")]
use std::sync::mpsc::{Receiver, Sender, channel};
#[cfg(not(feature = "parallel"))]
use crate::rayon;
use crate::{atomicmin::AtomicMin, deflate, filters::RowFilter, png::PngImage, Deadline, PngError};
use crate::{
Deadline, PngError, atomicmin::AtomicMin, deflate, filters::FilterStrategy, png::PngImage,
};
pub struct Candidate {
pub(crate) struct Candidate {
pub image: Arc<PngImage>,
pub idat_data: Vec<u8>,
pub filtered: Vec<u8>,
pub filter: RowFilter,
// first wins tie-breaker
pub idat_data: Option<Vec<u8>>,
pub estimated_output_size: usize,
/// The input filter, which is retained for printing and for APNG frames.
pub filter: FilterStrategy,
/// The filter returned by the filter function, which may be Predefined.
/// Use this for the next round to avoid recomputing the filter.
pub filter_used: FilterStrategy,
/// For determining tie-breaker
nth: usize,
}
impl Candidate {
fn cmp_key(&self) -> impl Ord {
fn cmp_key(&self) -> impl Ord + use<> {
(
self.idat_data.len() + self.image.key_chunks_size(),
self.estimated_output_size,
self.image.data.len(),
self.image.ihdr.bit_depth,
self.filter,
self.nth,
self.filter.clone(),
// Prefer the later image added (e.g. baseline, which is always added last)
usize::MAX - self.nth,
)
}
}
@ -42,9 +49,10 @@ impl Candidate {
/// Collect image versions and pick one that compresses best
pub(crate) struct Evaluator {
deadline: Arc<Deadline>,
filters: IndexSet<RowFilter>,
compression: u8,
filters: IndexSet<FilterStrategy>,
deflater: Deflater,
optimize_alpha: bool,
final_round: bool,
nth: AtomicUsize,
executed: Arc<AtomicUsize>,
best_candidate_size: Arc<AtomicMin>,
@ -59,17 +67,19 @@ pub(crate) struct Evaluator {
impl Evaluator {
pub fn new(
deadline: Arc<Deadline>,
filters: IndexSet<RowFilter>,
compression: u8,
filters: IndexSet<FilterStrategy>,
deflater: Deflater,
optimize_alpha: bool,
final_round: bool,
) -> Self {
#[cfg(feature = "parallel")]
let eval_channel = unbounded();
let eval_channel = channel();
Self {
deadline,
filters,
compression,
deflater,
optimize_alpha,
final_round,
nth: AtomicUsize::new(0),
executed: Arc::new(AtomicUsize::new(0)),
best_candidate_size: Arc::new(AtomicMin::new(None)),
@ -108,7 +118,7 @@ impl Evaluator {
/// Check if the image is smaller than others
pub fn try_image(&self, image: Arc<PngImage>) {
let description = format!("{}", image.ihdr.color_type);
let description = image.ihdr.color_type.to_string();
self.try_image_with_description(image, &description);
}
@ -118,8 +128,9 @@ impl Evaluator {
// These clones are only cheap refcounts
let deadline = self.deadline.clone();
let filters = self.filters.clone();
let compression = self.compression;
let deflater = self.deflater;
let optimize_alpha = self.optimize_alpha;
let final_round = self.final_round;
let executed = self.executed.clone();
let best_candidate_size = self.best_candidate_size.clone();
let description = description.to_string();
@ -135,29 +146,37 @@ impl Evaluator {
// which are dangerous to do in side Rayon's loop.
// Instead, only update (atomic) best size in real time,
// and the best result later without need for locks.
filters_iter.for_each(|&filter| {
filters_iter.for_each(|filter| {
if deadline.passed() {
return;
}
let filtered = image.filter_image(filter, optimize_alpha);
let idat_data = deflate::deflate(&filtered, compression, &best_candidate_size);
let (filtered, filter_used) = image.filter_image(filter.clone(), optimize_alpha);
let idat_data = deflater.deflate(&filtered, best_candidate_size.get());
if let Ok(idat_data) = idat_data {
let size = idat_data.len() + image.key_chunks_size();
best_candidate_size.set_min(size);
let estimated_output_size = image.estimated_output_size(&idat_data);
trace!(
"Eval: {}-bit {:23} {:8} {} bytes",
image.ihdr.bit_depth,
description,
filter,
size
image.ihdr.bit_depth, description, filter, estimated_output_size
);
// Skip if it exceeds best known size. (This is important to ensure
// the evaluator returns no result when all candidates are too large.)
if let Some(max) = best_candidate_size.get() {
if estimated_output_size > max {
return;
}
}
// We only need to retain the IDAT data in the final round
let new = Candidate {
image: image.clone(),
idat_data,
filtered,
filter,
idat_data: if final_round { Some(idat_data) } else { None },
estimated_output_size,
filter: filter.clone(),
filter_used,
nth,
};
best_candidate_size.set_min(estimated_output_size);
#[cfg(feature = "parallel")]
{
@ -174,10 +193,7 @@ impl Evaluator {
} else if let Err(PngError::DeflatedDataTooLong(size)) = idat_data {
trace!(
"Eval: {}-bit {:23} {:8} >{} bytes",
image.ihdr.bit_depth,
description,
filter,
size
image.ihdr.bit_depth, description, filter, size
);
}
});

View file

@ -1,29 +1,67 @@
use std::{fmt, fmt::Display, mem::transmute};
use crate::error::PngError;
/// Filtering strategy for use in [`Options`][crate::Options]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
pub enum FilterStrategy {
/// Same filter for all rows
Basic(RowFilter),
/// Minimum sum of absolute differences
MinSum,
/// Shannon entropy
Entropy,
/// Count of distinct bigrams
Bigrams,
/// Shannon entropy of bigrams
BigEnt,
/// Deflate compression
Brute {
/// The number of lines to compress at once
num_lines: usize,
/// The compression level to use (1-12)
level: u8,
},
/// Predefined filter for each row
Predefined(Vec<RowFilter>),
}
impl FilterStrategy {
pub const NONE: Self = Self::Basic(RowFilter::None);
pub const SUB: Self = Self::Basic(RowFilter::Sub);
pub const UP: Self = Self::Basic(RowFilter::Up);
pub const AVERAGE: Self = Self::Basic(RowFilter::Average);
pub const PAETH: Self = Self::Basic(RowFilter::Paeth);
}
impl Display for FilterStrategy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Basic(filter) => filter.fmt(f),
Self::MinSum => "MinSum".fmt(f),
Self::Entropy => "Entropy".fmt(f),
Self::Bigrams => "Bigrams".fmt(f),
Self::BigEnt => "BigEnt".fmt(f),
Self::Brute { .. } => "Brute".fmt(f),
Self::Predefined(_) => "Predefined".fmt(f),
}
}
}
/// PNG delta filters
#[repr(u8)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
pub enum RowFilter {
// Standard filter types
None,
Sub,
Up,
Average,
Paeth,
// Heuristic strategies
MinSum,
Entropy,
Bigrams,
BigEnt,
Brute,
}
impl TryFrom<u8> for RowFilter {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
if value > Self::LAST {
if value > 4 {
return Err(());
}
unsafe { transmute(value as i8) }
@ -39,11 +77,6 @@ impl Display for RowFilter {
Self::Up => "Up",
Self::Average => "Average",
Self::Paeth => "Paeth",
Self::MinSum => "MinSum",
Self::Entropy => "Entropy",
Self::Bigrams => "Bigrams",
Self::BigEnt => "BigEnt",
Self::Brute => "Brute",
},
f,
)
@ -51,9 +84,7 @@ impl Display for RowFilter {
}
impl RowFilter {
pub const LAST: u8 = Self::Brute as u8;
pub(crate) const STANDARD: [Self; 5] =
[Self::None, Self::Sub, Self::Up, Self::Average, Self::Paeth];
pub(crate) const ALL: [Self; 5] = [Self::None, Self::Sub, Self::Up, Self::Average, Self::Paeth];
pub(crate) const SINGLE_LINE: [Self; 2] = [Self::None, Self::Sub];
pub(crate) fn filter_line(
@ -96,25 +127,20 @@ impl RowFilter {
}
Self::Average => {
for (i, byte) in data.iter().enumerate() {
buf.push(match i.checked_sub(bpp) {
Some(x) => byte.wrapping_sub(
((u16::from(data[x]) + u16::from(prev_line[i])) >> 1) as u8,
),
None => byte.wrapping_sub(prev_line[i] >> 1),
});
buf.push(byte.wrapping_sub(i.checked_sub(bpp).map_or_else(
|| prev_line[i] >> 1,
|x| ((u16::from(data[x]) + u16::from(prev_line[i])) >> 1) as u8,
)));
}
}
Self::Paeth => {
for (i, byte) in data.iter().enumerate() {
buf.push(match i.checked_sub(bpp) {
Some(x) => {
byte.wrapping_sub(paeth_predictor(data[x], prev_line[i], prev_line[x]))
}
None => byte.wrapping_sub(prev_line[i]),
});
buf.push(byte.wrapping_sub(i.checked_sub(bpp).map_or_else(
|| prev_line[i],
|x| paeth_predictor(data[x], prev_line[i], prev_line[x]),
)));
}
}
_ => unreachable!(),
}
}
@ -125,8 +151,8 @@ impl RowFilter {
return;
}
let mut pixels: Vec<_> = data.chunks_mut(bpp).collect();
let prev_pixels: Vec<_> = prev_line.chunks(bpp).collect();
let mut pixels: Vec<_> = data.chunks_exact_mut(bpp).collect();
let prev_pixels: Vec<_> = prev_line.chunks_exact(bpp).collect();
for i in 0..pixels.len() {
if pixels[i].iter().skip(color_bytes).all(|b| *b == 0) {
// If the first pixel in the row is transparent, find the next non-transparent pixel and pretend
@ -138,10 +164,28 @@ impl RowFilter {
.unwrap_or(i),
_ => i - 1,
};
// These assertions help eliminate a few bounds checks in the slice accesses below
assert!(prev < pixels.len());
assert!(i < prev_pixels.len());
match self {
Self::None => unreachable!(),
Self::Sub => {
for j in 0..color_bytes {
pixels[i][j] = pixels[prev][j];
// The code below is roughly equivalent to pixels[i][0..color_bytes].copy_from_slice(&pixels[prev][0..color_bytes]),
// if such a thing was possible to do without violating Rust aliasing rules. See:
// https://users.rust-lang.org/t/problem-borrowing-two-elements-of-vec-mutably/21446/2
if prev < i {
let (pixels_head, pixels_tail) = pixels.split_at_mut(prev + 1);
pixels_tail[i - prev - 1][0..color_bytes]
.copy_from_slice(&pixels_head[prev][0..color_bytes]);
} else if prev > i {
let (pixels_head, pixels_tail) = pixels.split_at_mut(i + 1);
pixels_head[i][0..color_bytes]
.copy_from_slice(&pixels_tail[prev - i - 1][0..color_bytes]);
} else {
// If prev == i, we'd be copying the pixels onto themselves, which is useless
}
}
Self::Up => {
@ -170,7 +214,6 @@ impl RowFilter {
};
}
}
_ => unreachable!(),
}
}
}
@ -182,7 +225,7 @@ impl RowFilter {
data: &[u8],
prev_line: &[u8],
buf: &mut Vec<u8>,
) -> Result<(), PngError> {
) {
buf.clear();
buf.reserve(data.len());
assert!(data.len() >= bpp);
@ -194,10 +237,7 @@ impl RowFilter {
Self::Sub => {
for (i, &cur) in data.iter().enumerate() {
let prev_byte = i.checked_sub(bpp).and_then(|x| buf.get(x).copied());
buf.push(match prev_byte {
Some(b) => cur.wrapping_add(b),
None => cur,
});
buf.push(prev_byte.map_or(cur, |b| cur.wrapping_add(b)));
}
}
Self::Up => {
@ -210,10 +250,10 @@ impl RowFilter {
Self::Average => {
for (i, (&cur, &last)) in data.iter().zip(prev_line).enumerate() {
let prev_byte = i.checked_sub(bpp).and_then(|x| buf.get(x).copied());
buf.push(match prev_byte {
Some(b) => cur.wrapping_add(((u16::from(b) + u16::from(last)) >> 1) as u8),
None => cur.wrapping_add(last >> 1),
});
buf.push(cur.wrapping_add(prev_byte.map_or_else(
|| last >> 1,
|b| ((u16::from(b) + u16::from(last)) >> 1) as u8,
)));
}
}
Self::Paeth => {
@ -231,9 +271,7 @@ impl RowFilter {
);
}
}
_ => return Err(PngError::InvalidData),
}
Ok(())
}
}

View file

@ -1,14 +1,13 @@
use indexmap::IndexSet;
use log::warn;
use log::{debug, trace, warn};
use rgb::{RGB16, RGBA8};
use crate::{
Deflater, Options, PngResult,
colors::{BitDepth, ColorType},
deflate::{crc32, inflate},
display_chunks::DISPLAY_CHUNKS,
error::PngError,
interlace::Interlacing,
AtomicMin, Deflaters, PngResult,
};
#[derive(Debug, Clone)]
@ -22,32 +21,30 @@ pub struct IhdrData {
pub color_type: ColorType,
/// The bit depth of the image
pub bit_depth: BitDepth,
/// The interlacing mode of the image
pub interlaced: Interlacing,
/// Whether the image is interlaced
pub interlaced: bool,
}
impl IhdrData {
/// Bits per pixel
#[must_use]
#[inline]
pub fn bpp(&self) -> usize {
pub const fn bpp(&self) -> usize {
self.bit_depth as usize * self.color_type.channels_per_pixel() as usize
}
/// Byte length of IDAT that is correct for this IHDR
#[must_use]
pub fn raw_data_size(&self) -> usize {
pub const fn raw_data_size(&self) -> usize {
let w = self.width as usize;
let h = self.height as usize;
let bpp = self.bpp();
fn bitmap_size(bpp: usize, w: usize, h: usize) -> usize {
((w * bpp + 7) / 8) * h
const fn bitmap_size(bpp: usize, w: usize, h: usize) -> usize {
(w * bpp).div_ceil(8) * h
}
if self.interlaced == Interlacing::None {
bitmap_size(bpp, w, h) + h
} else {
if self.interlaced {
let mut size = bitmap_size(bpp, (w + 7) >> 3, (h + 7) >> 3) + ((h + 7) >> 3);
if w > 4 {
size += bitmap_size(bpp, (w + 3) >> 3, (h + 7) >> 3) + ((h + 7) >> 3);
@ -61,6 +58,8 @@ impl IhdrData {
size += bitmap_size(bpp, w >> 1, (h + 1) >> 1) + ((h + 1) >> 1);
}
size + bitmap_size(bpp, w, h >> 1) + (h >> 1)
} else {
bitmap_size(bpp, w, h) + h
}
}
}
@ -71,10 +70,12 @@ pub struct Chunk {
pub data: Vec<u8>,
}
/// [`Options`][crate::Options] to use when stripping chunks (metadata)
#[derive(Debug, PartialEq, Eq, Clone)]
/// Options to use when stripping chunks
pub enum StripChunks {
/// None
///
/// ...except caBX chunk if it contains a C2PA.org signature.
None,
/// Remove specific chunks
Strip(IndexSet<[u8; 4]>),
@ -89,11 +90,11 @@ pub enum StripChunks {
impl StripChunks {
pub(crate) fn keep(&self, name: &[u8; 4]) -> bool {
match &self {
StripChunks::None => true,
StripChunks::Keep(names) => names.contains(name),
StripChunks::Strip(names) => !names.contains(name),
StripChunks::Safe => DISPLAY_CHUNKS.contains(name),
StripChunks::All => false,
Self::None => true,
Self::Keep(names) => names.contains(name),
Self::Strip(names) => !names.contains(name),
Self::Safe => DISPLAY_CHUNKS.contains(name),
Self::All => false,
}
}
}
@ -111,6 +112,36 @@ pub struct RawChunk<'a> {
pub data: &'a [u8],
}
impl RawChunk<'_> {
// Is it a chunk for C2PA/CAI JUMBF metadata
pub(crate) fn is_c2pa(&self) -> bool {
if self.name == *b"caBX" {
if let Some((b"jumb", data)) = parse_jumbf_box(self.data) {
if let Some((b"jumd", data)) = parse_jumbf_box(data) {
if data.get(..4) == Some(b"c2pa") {
return true;
}
}
}
}
false
}
}
fn parse_jumbf_box(data: &[u8]) -> Option<(&[u8], &[u8])> {
if data.len() < 8 {
return None;
}
let (len, rest) = data.split_at(4);
let len = read_be_u32(len) as usize;
if len < 8 || len > data.len() {
return None;
}
let (box_name, data) = rest.split_at(4);
let data = data.get(..len - 8)?;
Some((box_name, data))
}
pub fn parse_next_chunk<'a>(
byte_data: &'a [u8],
byte_offset: &mut usize,
@ -121,37 +152,27 @@ pub fn parse_next_chunk<'a>(
.get(*byte_offset..*byte_offset + 4)
.ok_or(PngError::TruncatedData)?,
);
if byte_data.len() < *byte_offset + 12 + length as usize {
return Err(PngError::TruncatedData);
}
*byte_offset += 4;
let chunk_start = *byte_offset;
let chunk_name = byte_data
.get(chunk_start..chunk_start + 4)
.ok_or(PngError::TruncatedData)?;
let chunk_name = &byte_data[chunk_start..chunk_start + 4];
if chunk_name == b"IEND" {
// End of data
return Ok(None);
}
*byte_offset += 4;
let data = byte_data
.get(*byte_offset..*byte_offset + length as usize)
.ok_or(PngError::TruncatedData)?;
let data = &byte_data[*byte_offset..*byte_offset + length as usize];
*byte_offset += length as usize;
let crc = read_be_u32(
byte_data
.get(*byte_offset..*byte_offset + 4)
.ok_or(PngError::TruncatedData)?,
);
let crc = read_be_u32(&byte_data[*byte_offset..*byte_offset + 4]);
*byte_offset += 4;
let chunk_bytes = byte_data
.get(chunk_start..chunk_start + 4 + length as usize)
.ok_or(PngError::TruncatedData)?;
let chunk_bytes = &byte_data[chunk_start..chunk_start + 4 + length as usize];
if !fix_errors && crc32(chunk_bytes) != crc {
return Err(PngError::new(&format!(
"CRC Mismatch in {} chunk; May be recoverable by using --fix",
String::from_utf8_lossy(chunk_name)
)));
return Err(PngError::CRCMismatch(chunk_name.try_into().unwrap()));
}
let name: [u8; 4] = chunk_name.try_into().unwrap();
@ -170,13 +191,13 @@ pub fn parse_ihdr_chunk(
0 => ColorType::Grayscale {
transparent_shade: trns_data
.filter(|t| t.len() >= 2)
.map(|t| u16::from_be_bytes([t[0], t[1]])),
.map(|t| read_be_u16(&t[0..2])),
},
2 => ColorType::RGB {
transparent_color: trns_data.filter(|t| t.len() >= 6).map(|t| RGB16 {
r: u16::from_be_bytes([t[0], t[1]]),
g: u16::from_be_bytes([t[2], t[3]]),
b: u16::from_be_bytes([t[4], t[5]]),
r: read_be_u16(&t[0..2]),
g: read_be_u16(&t[2..4]),
b: read_be_u16(&t[4..6]),
}),
},
3 => ColorType::Indexed {
@ -184,12 +205,16 @@ pub fn parse_ihdr_chunk(
},
4 => ColorType::GrayscaleAlpha,
6 => ColorType::RGBA,
_ => return Err(PngError::new("Unexpected color type in header")),
_ => return Err(PngError::InvalidData),
},
bit_depth: byte_data[8].try_into()?,
width: read_be_u32(&byte_data[0..4]),
height: read_be_u32(&byte_data[4..8]),
interlaced: interlaced.try_into()?,
interlaced: match interlaced {
0 => false,
1 => true,
_ => return Err(PngError::InvalidData),
},
})
}
@ -198,9 +223,9 @@ fn palette_to_rgba(
palette_data: Option<Vec<u8>>,
trns_data: Option<Vec<u8>>,
) -> Result<Vec<RGBA8>, PngError> {
let palette_data = palette_data.ok_or_else(|| PngError::new("no palette in indexed image"))?;
let palette_data = palette_data.ok_or(PngError::ChunkMissing("PLTE"))?;
let mut palette: Vec<_> = palette_data
.chunks(3)
.chunks_exact(3)
.map(|color| RGBA8::new(color[0], color[1], color[2], 255))
.collect();
@ -213,12 +238,17 @@ fn palette_to_rgba(
}
#[inline]
fn read_be_u32(bytes: &[u8]) -> u32 {
pub fn read_be_u16(bytes: &[u8]) -> u16 {
u16::from_be_bytes(bytes.try_into().unwrap())
}
#[inline]
pub fn read_be_u32(bytes: &[u8]) -> u32 {
u32::from_be_bytes(bytes.try_into().unwrap())
}
/// Extract and decompress the ICC profile from an iCCP chunk
pub fn extract_icc(iccp: &Chunk) -> Option<Vec<u8>> {
pub fn extract_icc(iccp: &Chunk, max_size: Option<usize>) -> Option<Vec<u8>> {
// Skip (useless) profile name
let mut data = iccp.data.as_slice();
loop {
@ -233,21 +263,24 @@ pub fn extract_icc(iccp: &Chunk) -> Option<Vec<u8>> {
if compression_method != 0 {
return None; // The profile is supposed to be compressed (method 0)
}
// The decompressed size is unknown so we have to guess the required buffer size
let max_size = compressed_data.len() * 2 + 1000;
match inflate(compressed_data, max_size) {
// Libdeflate works with a fixed size buffer. Since the decompressed size is unknown we have to
// guess the required buffer size. We allow a fairly generous 10x factor with a minimum of 1000.
let mut out_size = (compressed_data.len() * 10).max(1000);
// For sanity, impose a default limit of 1MB.
out_size = out_size.min(max_size.unwrap_or(1_000_000));
match inflate(compressed_data, out_size) {
Ok(icc) => Some(icc),
Err(e) => {
// Log the error so we can know if the buffer size needs to be adjusted
warn!("Failed to decompress icc: {}", e);
warn!("Failed to decompress icc: {e}");
None
}
}
}
/// Construct an iCCP chunk by compressing the ICC profile
pub fn construct_iccp(icc: &[u8], deflater: Deflaters) -> PngResult<Chunk> {
let mut compressed = deflater.deflate(icc, &AtomicMin::new(None))?;
/// Make an iCCP chunk by compressing the ICC profile
pub fn make_iccp(icc: &[u8], deflater: Deflater, max_size: Option<usize>) -> PngResult<Chunk> {
let mut compressed = deflater.deflate(icc, max_size)?;
let mut data = Vec::with_capacity(compressed.len() + 5);
data.extend(b"icc"); // Profile name - generally unused, can be anything
data.extend([0, 0]); // Null separator, zlib compression method
@ -284,3 +317,109 @@ pub fn srgb_rendering_intent(icc_data: &[u8]) -> Option<u8> {
_ => None,
}
}
/// Process aux chunks and potentially adjust options before optimizing
pub fn preprocess_chunks(aux_chunks: &mut Vec<Chunk>, opts: &mut Options) {
let has_srgb = aux_chunks.iter().any(|c| &c.name == b"sRGB");
// Grayscale conversion should not be performed if the image is not in the sRGB colorspace
// An sRGB profile would need to be stripped on conversion, so disallow if stripping is disabled
let mut allow_grayscale = !has_srgb || opts.strip != StripChunks::None;
if let Some(iccp_idx) = aux_chunks.iter().position(|c| &c.name == b"iCCP") {
allow_grayscale = false;
// See if we can replace an iCCP chunk with an sRGB chunk
let may_replace_iccp = opts.strip != StripChunks::None && opts.strip.keep(b"sRGB");
if may_replace_iccp && has_srgb {
// Files aren't supposed to have both chunks, so we chose to honor sRGB
trace!("Removing iCCP chunk due to conflict with sRGB chunk");
aux_chunks.remove(iccp_idx);
allow_grayscale = true;
} else if let Some(icc) = extract_icc(&aux_chunks[iccp_idx], opts.max_decompressed_size) {
let intent = if may_replace_iccp {
srgb_rendering_intent(&icc)
} else {
None
};
// sRGB-like profile can be replaced with an sRGB chunk with the same rendering intent
if let Some(intent) = intent {
trace!("Replacing iCCP chunk with equivalent sRGB chunk");
aux_chunks[iccp_idx] = Chunk {
name: *b"sRGB",
data: vec![intent],
};
allow_grayscale = true;
} else if opts.idat_recoding {
// Try recompressing the profile
let cur_len = aux_chunks[iccp_idx].data.len();
if let Ok(iccp) = make_iccp(&icc, opts.deflater, Some(cur_len - 1)) {
debug!(
"Recompressed iCCP chunk: {} ({} bytes decrease)",
iccp.data.len(),
cur_len - iccp.data.len()
);
aux_chunks[iccp_idx] = iccp;
}
}
}
}
if !allow_grayscale && opts.grayscale_reduction {
debug!("Disabling grayscale reduction due to presence of sRGB or iCCP chunk");
opts.grayscale_reduction = false;
}
// Check for APNG by presence of acTL chunk
if aux_chunks.iter().any(|c| &c.name == b"acTL") {
warn!("APNG detected, disabling all reductions");
opts.interlace = None;
opts.bit_depth_reduction = false;
opts.color_type_reduction = false;
opts.palette_reduction = false;
opts.grayscale_reduction = false;
}
}
/// Perform cleanup of certain aux chunks after optimization has been completed
pub fn postprocess_chunks(aux_chunks: &mut Vec<Chunk>, ihdr: &IhdrData, orig_ihdr: &IhdrData) {
// If the depth/color type has changed, some chunks may be invalid and should be dropped
// While these could potentially be converted, they have no known use case today and are
// generally more trouble than they're worth
if orig_ihdr.bit_depth != ihdr.bit_depth || orig_ihdr.color_type != ihdr.color_type {
aux_chunks.retain(|c| {
let invalid = &c.name == b"bKGD" || &c.name == b"sBIT" || &c.name == b"hIST";
if invalid {
warn!(
"Removing {} chunk as it no longer matches the image data",
std::str::from_utf8(&c.name).unwrap()
);
}
!invalid
});
}
// Remove any sRGB or iCCP chunks if the image was converted to or from grayscale
if orig_ihdr.color_type.is_gray() != ihdr.color_type.is_gray() {
aux_chunks.retain(|c| {
let invalid = &c.name == b"sRGB" || &c.name == b"iCCP";
if invalid {
trace!(
"Removing {} chunk as it no longer matches the color type",
std::str::from_utf8(&c.name).unwrap()
);
}
!invalid
});
}
// Remove iDOT which will necessarily be invalid after successful optimization
aux_chunks.retain(|c| {
let invalid = &c.name == b"iDOT";
if invalid {
trace!(
"Removing {} chunk as it no longer matches the IDAT",
std::str::from_utf8(&c.name).unwrap()
);
}
!invalid
});
}

View file

@ -1,39 +1,6 @@
use std::{fmt, fmt::Display};
use bitvec::prelude::*;
use crate::{headers::IhdrData, png::PngImage, PngError};
#[repr(u8)]
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Interlacing {
None,
Adam7,
}
impl TryFrom<u8> for Interlacing {
type Error = PngError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::None),
1 => Ok(Self::Adam7),
_ => Err(PngError::new("Unexpected interlacing in header")),
}
}
}
impl Display for Interlacing {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(
match self {
Self::None => "non-interlaced",
Self::Adam7 => "interlaced",
},
f,
)
}
}
use crate::{headers::IhdrData, png::PngImage};
#[must_use]
pub fn interlace_image(png: &PngImage) -> PngImage {
@ -86,7 +53,7 @@ pub fn interlace_image(png: &PngImage) -> PngImage {
data: output,
ihdr: IhdrData {
color_type: png.ihdr.color_type.clone(),
interlaced: Interlacing::Adam7,
interlaced: true,
..png.ihdr
},
}
@ -100,7 +67,7 @@ pub fn deinterlace_image(png: &PngImage) -> PngImage {
},
ihdr: IhdrData {
color_type: png.ihdr.color_type.clone(),
interlaced: Interlacing::None,
interlaced: false,
..png.ihdr
},
}
@ -118,10 +85,8 @@ fn deinterlace_bits(png: &PngImage) -> Vec<u8> {
let mut current_y: usize = pass_constants.y_shift as usize;
for line in png.scan_lines(false) {
let bit_vec = line.data.view_bits::<Msb0>();
let bits_in_line = ((png.ihdr.width - u32::from(pass_constants.x_shift)
+ u32::from(pass_constants.x_step)
- 1)
/ u32::from(pass_constants.x_step)) as usize
let bits_in_line = (png.ihdr.width - u32::from(pass_constants.x_shift))
.div_ceil(u32::from(pass_constants.x_step)) as usize
* bits_per_pixel;
for (i, bit) in bit_vec.iter().by_vals().enumerate() {
// Avoid moving padded 0's into new image
@ -184,7 +149,7 @@ fn deinterlace_bytes(png: &PngImage) -> Vec<u8> {
lines.concat()
}
fn increment_pass(current_pass: &mut u8, ihdr: &IhdrData) -> bool {
const fn increment_pass(current_pass: &mut u8, ihdr: &IhdrData) -> bool {
if *current_pass == 7 {
return false;
}

View file

@ -1,23 +1,4 @@
#![warn(trivial_casts, trivial_numeric_casts, unused_import_braces)]
#![deny(missing_debug_implementations, missing_copy_implementations)]
#![warn(clippy::expl_impl_clone_on_copy)]
#![warn(clippy::float_cmp_const)]
#![warn(clippy::linkedlist)]
#![warn(clippy::map_flatten)]
#![warn(clippy::match_same_arms)]
#![warn(clippy::mem_forget)]
#![warn(clippy::mut_mut)]
#![warn(clippy::mutex_integer)]
#![warn(clippy::needless_continue)]
#![warn(clippy::path_buf_push_overwrite)]
#![warn(clippy::range_plus_one)]
#![allow(clippy::cognitive_complexity)]
#![allow(clippy::upper_case_acronyms)]
#![cfg_attr(
not(feature = "zopfli"),
allow(irrefutable_let_patterns),
allow(unreachable_patterns)
)]
#![cfg_attr(not(feature = "zopfli"), allow(unreachable_patterns))]
#[cfg(feature = "parallel")]
extern crate rayon;
@ -26,39 +7,39 @@ extern crate rayon;
mod rayon;
use std::{
borrow::Cow,
fs::{File, Metadata},
io::{stdin, stdout, BufWriter, Read, Write},
path::Path,
fs::File,
io::{BufWriter, Read, Write, stdin, stdout},
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
atomic::{AtomicBool, Ordering},
},
time::{Duration, Instant},
};
pub use indexmap::{indexset, IndexSet};
pub use indexmap::{IndexSet, indexset};
use log::{debug, info, trace, warn};
use rayon::prelude::*;
pub use rgb::{RGB16, RGBA8};
#[cfg(feature = "zopfli")]
pub use crate::deflate::ZopfliOptions;
pub use crate::{
colors::{BitDepth, ColorType},
deflate::Deflater,
error::PngError,
filters::{FilterStrategy, RowFilter},
headers::StripChunks,
options::{InFile, Options, OutFile},
};
use crate::{
atomicmin::AtomicMin,
evaluate::Evaluator,
evaluate::{Candidate, Evaluator},
headers::*,
png::{PngData, PngImage},
reduction::*,
};
pub use crate::{
colors::{BitDepth, ColorType},
deflate::Deflaters,
error::PngError,
filters::RowFilter,
headers::StripChunks,
interlace::Interlacing,
options::{InFile, Options, OutFile},
};
mod apng;
mod atomicmin;
mod colors;
mod deflate;
@ -79,10 +60,11 @@ mod sanity_checks;
pub mod internal_tests {
#[cfg(feature = "sanity-checks")]
pub use crate::sanity_checks::*;
pub use crate::{atomicmin::*, deflate::*, png::*, reduction::*};
pub use crate::{deflate::*, png::*, reduction::*};
}
pub type PngResult<T> = Result<T, PngError>;
pub type OptimizationResult = PngResult<(usize, usize)>;
#[derive(Debug)]
/// A raw image definition which can be used to create an optimized png
@ -105,7 +87,7 @@ impl RawImage {
color_type: ColorType,
bit_depth: BitDepth,
data: Vec<u8>,
) -> Result<Self, PngError> {
) -> PngResult<Self> {
// Validate bit depth
let valid_depth = match color_type {
ColorType::Grayscale { .. } => true,
@ -118,7 +100,7 @@ impl RawImage {
// Validate data length
let bpp = bit_depth as usize * color_type.channels_per_pixel() as usize;
let row_bytes = (bpp * width as usize + 7) / 8;
let row_bytes = (bpp * width as usize).div_ceil(8);
let expected_len = row_bytes * height as usize;
if data.len() != expected_len {
return Err(PngError::IncorrectDataLength(data.len(), expected_len));
@ -131,7 +113,7 @@ impl RawImage {
height,
color_type,
bit_depth,
interlaced: Interlacing::None,
interlaced: false,
},
data,
}),
@ -147,71 +129,56 @@ impl RawImage {
/// Add an ICC profile for the image
pub fn add_icc_profile(&mut self, data: &[u8]) {
// Compress with fastest compression level - will be recompressed during optimization
let deflater = Deflaters::Libdeflater { compression: 1 };
if let Ok(iccp) = construct_iccp(data, deflater) {
let deflater = Deflater::Libdeflater { compression: 1 };
if let Ok(iccp) = make_iccp(data, deflater, None) {
self.aux_chunks.push(iccp);
}
}
/// Create an optimized png from the raw image data using the options provided
pub fn create_optimized_png(&self, opts: &Options) -> PngResult<Vec<u8>> {
let deadline = Arc::new(Deadline::new(opts.timeout));
let mut png = optimize_raw(self.png.clone(), opts, deadline.clone(), None)
.ok_or_else(|| PngError::new("Failed to optimize input data"))?;
// Process aux chunks
png.aux_chunks = self
let mut opts = opts.to_owned();
let mut aux_chunks: Vec<_> = self
.aux_chunks
.iter()
.filter(|c| opts.strip.keep(&c.name))
.cloned()
.collect();
postprocess_chunks(&mut png, opts, deadline, &self.png.ihdr);
preprocess_chunks(&mut aux_chunks, &mut opts);
let deadline = Arc::new(Deadline::new(opts.timeout));
let Some(result) = optimize_raw(self.png.clone(), &opts, deadline, None) else {
return Err(PngError::new("Failed to optimize input data"));
};
let mut png = PngData {
raw: result.image,
idat_data: result.idat_data.unwrap(),
aux_chunks,
frames: Vec::new(),
};
postprocess_chunks(&mut png.aux_chunks, &png.raw.ihdr, &self.png.ihdr);
Ok(png.output())
}
}
/// Perform optimization on the input file using the options provided
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<()> {
///
/// Returns the original and optimized file sizes
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> OptimizationResult {
// Read in the file and try to decode as PNG.
info!("Processing: {}", input);
info!("Processing: {input}");
let deadline = Arc::new(Deadline::new(opts.timeout));
// grab metadata before even opening input file to preserve atime
let opt_metadata_preserved;
let in_data = match *input {
InFile::Path(ref input_path) => {
if matches!(
output,
OutFile::Path {
preserve_attrs: true,
..
}
) {
opt_metadata_preserved = input_path
.metadata()
.map_err(|err| {
// Fail if metadata cannot be preserved
PngError::new(&format!(
"Unable to read metadata from input file {:?}: {}",
input_path, err
))
})
.map(Some)?;
trace!("preserving metadata: {:?}", opt_metadata_preserved);
} else {
opt_metadata_preserved = None;
}
PngData::read_file(input_path)?
}
InFile::Path(ref input_path) => PngData::read_file(input_path)?,
InFile::StdIn => {
opt_metadata_preserved = None;
let mut data = Vec::new();
stdin()
.read_to_end(&mut data)
.map_err(|e| PngError::new(&format!("Error reading stdin: {}", e)))?;
.map_err(|e| PngError::ReadFailed("stdin".into(), e))?;
data
}
};
@ -223,14 +190,14 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
let in_length = in_data.len();
if is_fully_optimized(in_data.len(), optimized_output.len(), opts) {
if is_fully_optimized(in_length, optimized_output.len(), opts) {
match (output, input) {
// if p is None, it also means same as the input path
(OutFile::Path { path, .. }, InFile::Path(ref input_path))
if path.as_ref().map_or(true, |p| p == input_path) =>
// If output path is None, it also means same as the input path
(OutFile::Path { path, .. }, InFile::Path(input_path))
if path.as_ref().is_none_or(|p| p == input_path) =>
{
info!("{}: Could not optimize further, no change written", input);
return Ok(());
info!("Could not optimize further, no change written: {input}");
return Ok((in_length, in_length));
}
_ => {
optimized_output = in_data;
@ -254,51 +221,65 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
match (output, input) {
(OutFile::None, _) => {
info!("{}: Running in pretend mode, no output", savings);
info!("{savings}: Dry run, no output");
}
(&OutFile::StdOut, _) | (&OutFile::Path { path: None, .. }, &InFile::StdIn) => {
let mut buffer = BufWriter::new(stdout());
buffer
.write_all(&optimized_output)
.map_err(|e| PngError::new(&format!("Unable to write to stdout: {}", 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
.as_ref()
.map(|p| p.as_path())
.unwrap_or_else(|| input.path().unwrap());
let out_file = File::create(output_path).map_err(|err| {
PngError::new(&format!(
"Unable to write to file {}: {}",
output_path.display(),
err
))
})?;
if let Some(metadata_input) = &opt_metadata_preserved {
copy_permissions(metadata_input, &out_file)?;
}
.map_or_else(|| input.path().unwrap(), PathBuf::as_path);
let out_file = File::create(output_path)
.map_err(|err| PngError::WriteFailed(output_path.display().to_string(), err))?;
let mut buffer = BufWriter::new(out_file);
let mut buffer = BufWriter::new(&out_file);
buffer
.write_all(&optimized_output)
// flush BufWriter so IO errors don't get swallowed silently on close() by drop!
.and_then(|()| buffer.flush())
.map_err(|e| {
PngError::new(&format!(
"Unable to write to {}: {}",
output_path.display(),
e
))
})?;
.map_err(|e| PngError::WriteFailed(output_path.display().to_string(), e))?;
// force drop and thereby closing of file handle before modifying any timestamp
std::mem::drop(buffer);
if let Some(metadata_input) = &opt_metadata_preserved {
copy_times(metadata_input, output_path)?;
if let Some(metadata_input) = &input_metadata {
let set_time = metadata_input
.modified()
.and_then(|m| out_file.set_modified(m));
if let Err(e) = set_time {
warn!("Unable to set modification time on {output_path:?}: {e}");
}
let set_perm = out_file.set_permissions(metadata_input.permissions());
if let Err(e) = set_perm {
warn!("Unable to set permissions on {output_path:?}: {e}");
}
}
info!("{}: {}", savings, output_path.display());
}
}
Ok(())
Ok((in_length, optimized_output.len()))
}
/// Perform optimization on the input file using the options provided, where the file is already
@ -323,8 +304,6 @@ pub fn optimize_from_memory(data: &[u8], opts: &Options) -> PngResult<Vec<u8>> {
}
}
type TrialResult = (RowFilter, Vec<u8>);
/// Perform optimization on the input PNG object using the options provided
fn optimize_png(
png: &mut PngData,
@ -341,34 +320,24 @@ fn optimize_png(
raw.ihdr.width, raw.ihdr.height
);
report_format(" ", &raw);
debug!(" IDAT size = {} bytes", idat_original_size);
debug!(" File size = {} bytes", file_original_size);
debug!(" IDAT size = {idat_original_size} bytes");
debug!(" File size = {file_original_size} bytes");
let mut opts = opts.to_owned();
preprocess_chunks(&mut png.aux_chunks, &mut opts);
// Check for APNG by presence of acTL chunk
let opts = if png.aux_chunks.iter().any(|c| &c.name == b"acTL") {
warn!("APNG detected, disabling all reductions");
let mut opts = opts.to_owned();
opts.interlace = None;
opts.bit_depth_reduction = false;
opts.color_type_reduction = false;
opts.palette_reduction = false;
opts.grayscale_reduction = false;
Cow::Owned(opts)
} else {
Cow::Borrowed(opts)
};
let max_size = if opts.force {
None
} else {
Some(png.estimated_output_size())
Some(png.raw.estimated_output_size(&png.idat_data))
};
if let Some(new_png) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_size) {
png.raw = new_png.raw;
png.idat_data = new_png.idat_data;
if let Some(result) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_size) {
png.raw = result.image;
png.idat_data = result.idat_data.unwrap();
recompress_frames(png, &opts, deadline, result.filter)?;
postprocess_chunks(&mut png.aux_chunks, &png.raw.ihdr, &raw.ihdr);
}
postprocess_chunks(png, &opts, deadline, &raw.ihdr);
let output = png.output();
if idat_original_size >= png.idat_data.len() {
@ -400,6 +369,12 @@ fn optimize_png(
);
}
if opts.interlace == Some(true) && !png.raw.ihdr.interlaced {
warn!(
"Interlacing was not enabled as it would result in a larger file. To override this, use `--force`."
);
}
#[cfg(feature = "sanity-checks")]
assert!(sanity_checks::validate_output(&output, original_data));
@ -412,186 +387,155 @@ fn optimize_raw(
opts: &Options,
deadline: Arc<Deadline>,
max_size: Option<usize>,
) -> Option<PngData> {
// Libdeflate has four algorithms: 1-4 = 'greedy', 5-7 = 'lazy', 8-9 = 'lazy2', 10-12 = 'near-optimal'
) -> Option<Candidate> {
// Libdeflate has four algorithms: 0 = 'uncompressed', 1-4 = 'greedy', 5-7 = 'lazy', 8-9 = 'lazy2', 10-12 = 'near-optimal'
// 5 is the minimumm required for a decent evaluation result
// 7 is not noticeably slower than 5 and improves evaluation of filters in 'fast' mode (o2 and lower)
// 8 is a little slower but not noticeably when used only for reductions (o3 and higher)
// 9 is not appreciably better than 8
// 10 and higher are quite slow - good for filters but only good for reductions if matching the main zc level
let eval_compression = match opts.deflate {
Deflaters::Libdeflater { compression } => {
let compression = match opts.deflater {
Deflater::Libdeflater { compression } => {
if opts.fast_evaluation { 7 } else { 8 }.min(compression)
}
_ => 8,
};
let eval_deflater = Deflater::Libdeflater { compression };
// If only one filter is selected, use this for evaluations
let eval_filters = if opts.filter.len() == 1 {
opts.filter.clone()
let eval_filters = if opts.filters.len() == 1 {
opts.filters.clone()
} else {
// None and Bigrams work well together, especially for alpha reductions
indexset! {RowFilter::None, RowFilter::Bigrams}
indexset! {FilterStrategy::NONE, FilterStrategy::Bigrams}
};
// This will collect all versions of images and pick one that compresses best
let eval = Evaluator::new(
deadline.clone(),
eval_filters.clone(),
eval_compression,
eval_deflater,
false,
opts.deflater == eval_deflater,
);
let mut png = perform_reductions(image.clone(), opts, &deadline, &eval);
let mut eval_result = eval.get_best_candidate();
let mut new_image = perform_reductions(image.clone(), opts, &deadline, &eval);
let eval_result = eval.get_best_candidate();
if let Some(ref result) = eval_result {
png = result.image.clone();
new_image = result.image.clone();
}
let reduction_occurred = png.ihdr.color_type != image.ihdr.color_type
|| png.ihdr.bit_depth != image.ihdr.bit_depth
|| png.ihdr.interlaced != image.ihdr.interlaced;
let reduction_occurred = new_image.ihdr.color_type != image.ihdr.color_type
|| new_image.ihdr.bit_depth != image.ihdr.bit_depth
|| new_image.ihdr.interlaced != image.ihdr.interlaced;
if reduction_occurred {
report_format("Transformed image to ", &png);
report_format("Transformed image to ", &new_image);
}
if opts.idat_recoding || reduction_occurred {
let mut filters = opts.filter.clone();
let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_result.is_some());
let best: Option<TrialResult> = if fast_eval {
// Perform a fast evaluation of selected filters followed by a single main compression trial
if eval_result.is_some() {
// Some filters have already been evaluated, we don't need to try them again
filters = filters.difference(&eval_filters).cloned().collect();
}
if !filters.is_empty() {
trace!("Evaluating: {} filters", filters.len());
let eval = Evaluator::new(deadline, filters, eval_compression, opts.optimize_alpha);
if let Some(ref result) = eval_result {
eval.set_best_size(result.idat_data.len());
}
eval.try_image(png.clone());
if let Some(result) = eval.get_best_candidate() {
eval_result = Some(result);
}
}
// We should have a result here - fail if not (e.g. deadline passed)
let result = eval_result?;
match opts.deflate {
Deflaters::Libdeflater { compression } if compression <= eval_compression => {
// No further compression required
Some((result.filter, result.idat_data))
}
_ => {
debug!("Trying: {}", result.filter);
let best_size = AtomicMin::new(max_size);
perform_trial(&result.filtered, opts, result.filter, &best_size)
}
}
} else {
// Perform full compression trials of selected filters and determine the best
if filters.is_empty() {
// Pick a filter automatically
if png.ihdr.bit_depth as u8 >= 8 {
// Bigrams is the best all-rounder when there's at least one byte per pixel
filters.insert(RowFilter::Bigrams);
} else {
// Otherwise delta filters generally don't work well, so just stick with None
filters.insert(RowFilter::None);
}
}
debug!("Trying: {} filters", filters.len());
let best_size = AtomicMin::new(max_size);
let results_iter = filters.into_par_iter().with_max_len(1);
let best = results_iter.filter_map(|filter| {
if deadline.passed() {
return None;
}
let filtered = &png.filter_image(filter, opts.optimize_alpha);
perform_trial(filtered, opts, filter, &best_size)
});
best.reduce_with(|i, j| {
if i.1.len() < j.1.len() || (i.1.len() == j.1.len() && i.0 < j.0) {
i
} else {
j
}
})
};
if let Some((filter, idat_data)) = best {
let image = PngData {
raw: png,
idat_data,
aux_chunks: Vec::new(),
};
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
debug!("Found better combination:");
debug!(
" zc = {} f = {:8} {} bytes",
opts.deflate,
filter,
image.idat_data.len()
);
return Some(image);
}
}
} else if let Some(result) = eval_result {
let (result, deflater) = if opts.idat_recoding || reduction_occurred {
let result = perform_trials(
new_image,
opts,
deadline,
max_size,
eval_result,
eval_filters,
eval_deflater,
);
(result?, opts.deflater)
} else {
// If idat_recoding is off and reductions were attempted but ended up choosing the baseline,
// we should still check if the evaluator compressed the baseline smaller than the original.
let image = PngData {
raw: result.image,
idat_data: result.idat_data,
aux_chunks: Vec::new(),
};
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
debug!("Found better combination:");
debug!(
" zc = {} f = {:8} {} bytes",
eval_compression,
result.filter,
image.idat_data.len()
);
return Some(image);
}
}
(eval_result?, eval_deflater)
};
if result.idat_data.is_some()
&& max_size.is_none_or(|max_size| result.estimated_output_size < max_size)
{
debug!("Found better result:");
debug!(" {}, f = {}", deflater, result.filter);
return Some(result);
}
None
}
/// Execute a compression trial
fn perform_trial(
filtered: &[u8],
/// Perform compression trials
fn perform_trials(
image: Arc<PngImage>,
opts: &Options,
filter: RowFilter,
best_size: &AtomicMin,
) -> Option<TrialResult> {
match opts.deflate.deflate(filtered, best_size) {
Ok(new_idat) => {
let bytes = new_idat.len();
best_size.set_min(bytes);
trace!(
" zc = {} f = {:8} {} bytes",
opts.deflate,
filter,
bytes
);
Some((filter, new_idat))
deadline: Arc<Deadline>,
max_size: Option<usize>,
mut eval_result: Option<Candidate>,
eval_filters: IndexSet<FilterStrategy>,
eval_deflater: Deflater,
) -> Option<Candidate> {
let mut filters = opts.filters.clone();
let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_result.is_some());
if fast_eval {
// Perform a fast evaluation of selected filters followed by a single main compression trial
if eval_result.is_some() {
// Some filters have already been evaluated, we don't need to try them again
filters = filters.difference(&eval_filters).cloned().collect();
}
Err(PngError::DeflatedDataTooLong(bytes)) => {
trace!(
" zc = {} f = {:8} >{} bytes",
opts.deflate,
filter,
bytes,
if !filters.is_empty() {
trace!("Evaluating {} filters", filters.len());
let eval = Evaluator::new(
deadline,
filters,
eval_deflater,
opts.optimize_alpha,
opts.deflater == eval_deflater,
);
None
if let Some(result) = &eval_result {
eval.set_best_size(result.estimated_output_size);
}
eval.try_image(image.clone());
if let Some(result) = eval.get_best_candidate() {
eval_result = Some(result);
}
}
Err(_) => None,
// We should have a result here - fail if not (e.g. deadline passed)
let mut result = eval_result?;
if result.idat_data.is_none() {
// Compress with the main deflater
debug!("Trying filter {} with {}", result.filter, opts.deflater);
let (data, _) = image.filter_image(result.filter_used.clone(), opts.optimize_alpha);
match opts.deflater.deflate(&data, max_size) {
Ok(idat_data) => {
result.estimated_output_size = result.image.estimated_output_size(&idat_data);
result.idat_data = Some(idat_data);
trace!("{} bytes", result.estimated_output_size);
}
Err(PngError::DeflatedDataTooLong(bytes)) => {
trace!(">{bytes} bytes");
}
Err(_) => (),
}
}
return Some(result);
}
// Perform full compression trials of selected filters and determine the best
if filters.is_empty() {
// Pick a filter automatically
if image.ihdr.bit_depth as u8 >= 8 {
// Bigrams is the best all-rounder when there's at least one byte per pixel
filters.insert(FilterStrategy::Bigrams);
} else {
// Otherwise delta filters generally don't work well, so just stick with None
filters.insert(FilterStrategy::NONE);
}
}
debug!("Trying {} filters with {}", filters.len(), opts.deflater);
let eval = Evaluator::new(deadline, filters, opts.deflater, opts.optimize_alpha, true);
if let Some(max_size) = max_size {
eval.set_best_size(max_size);
}
eval.try_image(image);
eval.get_best_candidate()
}
#[derive(Debug)]
@ -609,6 +553,7 @@ pub struct Deadline {
}
impl Deadline {
#[must_use]
pub fn new(timeout: Option<Duration>) -> Self {
Self {
imp: timeout.map(|timeout| DeadlineImp {
@ -645,141 +590,57 @@ impl Deadline {
/// Display the format of the image data
fn report_format(prefix: &str, png: &PngImage) {
let interlaced = if png.ihdr.interlaced {
"interlaced"
} else {
"non-interlaced"
};
debug!(
"{}{}-bit {}, {}",
prefix, png.ihdr.bit_depth, png.ihdr.color_type, png.ihdr.interlaced
prefix, png.ihdr.bit_depth, png.ihdr.color_type, interlaced
);
}
/// Perform cleanup of certain chunks from the `PngData` object, after optimization has been completed
fn postprocess_chunks(
/// Recompress the additional frames of an APNG
fn recompress_frames(
png: &mut PngData,
opts: &Options,
deadline: Arc<Deadline>,
orig_ihdr: &IhdrData,
) {
if let Some(iccp_idx) = png.aux_chunks.iter().position(|c| &c.name == b"iCCP") {
// See if we can replace an iCCP chunk with an sRGB chunk
let may_replace_iccp = opts.strip != StripChunks::None && opts.strip.keep(b"sRGB");
if may_replace_iccp && png.aux_chunks.iter().any(|c| &c.name == b"sRGB") {
// Files aren't supposed to have both chunks, so we chose to honor sRGB
trace!("Removing iCCP chunk due to conflict with sRGB chunk");
png.aux_chunks.remove(iccp_idx);
} else if let Some(icc) = extract_icc(&png.aux_chunks[iccp_idx]) {
let intent = if may_replace_iccp {
srgb_rendering_intent(&icc)
} else {
None
};
// sRGB-like profile can be replaced with an sRGB chunk with the same rendering intent
if let Some(intent) = intent {
trace!("Replacing iCCP chunk with equivalent sRGB chunk");
png.aux_chunks[iccp_idx] = Chunk {
name: *b"sRGB",
data: vec![intent],
};
} else if opts.idat_recoding {
// Try recompressing the profile
if let Ok(iccp) = construct_iccp(&icc, opts.deflate) {
let cur_len = png.aux_chunks[iccp_idx].data.len();
let new_len = iccp.data.len();
if new_len < cur_len {
debug!(
"Recompressed iCCP chunk: {} ({} bytes decrease)",
new_len,
cur_len - new_len
);
png.aux_chunks[iccp_idx] = iccp;
}
}
}
}
filter: FilterStrategy,
) -> PngResult<()> {
if !opts.idat_recoding || png.frames.is_empty() {
return Ok(());
}
// If the depth/color type has changed, some chunks may be invalid and should be dropped
// While these could potentially be converted, they have no known use case today and are
// generally more trouble than they're worth
let ihdr = &png.raw.ihdr;
if orig_ihdr.bit_depth != ihdr.bit_depth || orig_ihdr.color_type != ihdr.color_type {
png.aux_chunks.retain(|c| {
let invalid = &c.name == b"bKGD" || &c.name == b"sBIT" || &c.name == b"hIST";
if invalid {
warn!(
"Removing {} chunk as it no longer matches the image data",
std::str::from_utf8(&c.name).unwrap()
// Ensure we don't try to recompress frames with a predefined filter
debug_assert!(!matches!(filter, FilterStrategy::Predefined { .. }));
png.frames
.par_iter_mut()
.with_max_len(1)
.enumerate()
.try_for_each(|(i, frame)| {
if deadline.passed() {
return Ok(());
}
let mut ihdr = png.raw.ihdr.clone();
ihdr.width = frame.width;
ihdr.height = frame.height;
let image = PngImage::new(ihdr, &frame.data)?;
let (filtered, _) = image.filter_image(filter.clone(), opts.optimize_alpha);
let max_size = Some(frame.data.len() - 1);
if let Ok(data) = opts.deflater.deflate(&filtered, max_size) {
debug!(
"Recompressed fdAT #{:<2}: {} ({} bytes decrease)",
i,
data.len(),
frame.data.len() - data.len()
);
frame.data = data;
}
!invalid
});
}
// Find fdAT chunks and attempt to recompress them
// Note if there are multiple fdATs per frame then decompression will fail and nothing will change
let mut fdat: Vec<_> = png
.aux_chunks
.iter_mut()
.filter(|c| &c.name == b"fdAT")
.collect();
if opts.idat_recoding && !fdat.is_empty() {
let buffer_size = orig_ihdr.raw_data_size();
fdat.par_iter_mut()
.with_max_len(1)
.enumerate()
.for_each(|(i, c)| {
if deadline.passed() || c.data.len() <= 4 {
return;
}
if let Ok(mut data) = deflate::inflate(&c.data[4..], buffer_size).and_then(|data| {
let max_size = AtomicMin::new(Some(c.data.len() - 5));
opts.deflate.deflate(&data, &max_size)
}) {
debug!(
"Recompressed fdAT #{:<2}: {} ({} bytes decrease)",
i,
c.data.len(),
c.data.len() - 4 - data.len()
);
c.data.truncate(4);
c.data.append(&mut data);
}
})
}
}
/// Check if an image was already optimized prior to oxipng's operations
fn is_fully_optimized(original_size: usize, optimized_size: usize, opts: &Options) -> bool {
original_size <= optimized_size && !opts.force
}
fn copy_permissions(metadata_input: &Metadata, out_file: &File) -> PngResult<()> {
out_file
.set_permissions(metadata_input.permissions())
.map_err(|err_io| {
PngError::new(&format!(
"unable to set permissions for output file: {}",
err_io
))
Ok(())
})
}
#[cfg(not(feature = "filetime"))]
fn copy_times(_: &Metadata, _: &Path) -> PngResult<()> {
Ok(())
}
#[cfg(feature = "filetime")]
fn copy_times(input_path_meta: &Metadata, out_path: &Path) -> PngResult<()> {
let atime = filetime::FileTime::from_last_access_time(input_path_meta);
let mtime = filetime::FileTime::from_last_modification_time(input_path_meta);
trace!(
"attempting to set file times: atime: {:?}, mtime: {:?}",
atime,
mtime
);
filetime::set_file_times(out_path, atime, mtime).map_err(|err_io| {
PngError::new(&format!(
"unable to set file times on {:?}: {}",
out_path, err_io
))
})
/// Check if an image was already optimized prior to oxipng's operations
const fn is_fully_optimized(original_size: usize, optimized_size: usize, opts: &Options) -> bool {
original_size <= optimized_size && !opts.force
}

View file

@ -1,99 +1,172 @@
#![warn(trivial_casts, trivial_numeric_casts, unused_import_braces)]
#![deny(missing_debug_implementations, missing_copy_implementations)]
#![warn(clippy::expl_impl_clone_on_copy)]
#![warn(clippy::float_cmp_const)]
#![warn(clippy::linkedlist)]
#![warn(clippy::map_flatten)]
#![warn(clippy::match_same_arms)]
#![warn(clippy::mem_forget)]
#![warn(clippy::mut_mut)]
#![warn(clippy::mutex_integer)]
#![warn(clippy::needless_continue)]
#![warn(clippy::path_buf_push_overwrite)]
#![warn(clippy::range_plus_one)]
#![allow(clippy::cognitive_complexity)]
#[cfg(not(feature = "parallel"))]
mod rayon;
#[cfg(feature = "zopfli")]
use std::num::NonZeroU8;
use std::{ffi::OsString, fs::DirBuilder, io::Write, path::PathBuf, process::exit, time::Duration};
use std::num::NonZeroU64;
use std::{
ffi::{OsStr, OsString},
fs::DirBuilder,
io::{IsTerminal, Write, stdout},
path::PathBuf,
process::ExitCode,
sync::atomic::{AtomicUsize, Ordering::AcqRel},
time::Duration,
};
use clap::ArgMatches;
mod cli;
use indexmap::IndexSet;
use log::{error, warn, Level, LevelFilter};
use oxipng::{Deflaters, InFile, Options, OutFile, RowFilter, StripChunks};
use log::{Level, LevelFilter, error, warn};
#[cfg(feature = "zopfli")]
use oxipng::ZopfliOptions;
use oxipng::{
Deflater, FilterStrategy, InFile, OptimizationResult, Options, OutFile, PngError, StripChunks,
};
use rayon::prelude::*;
use crate::cli::DISPLAY_CHUNKS;
fn main() {
fn main() -> ExitCode {
let matches = cli::build_command()
// Set the value parser for filters which isn't appropriate to do in the build_command function
.mut_arg("filters", |arg| {
arg.value_parser(|x: &str| {
parse_numeric_range_opts(x, 0, RowFilter::LAST)
.map_err(|_| "Invalid option for filters")
parse_numeric_range_opts(x, 0, 9).map_err(|_| "Invalid option for filters")
})
})
.after_help("Run `oxipng --help` to see full details of all options")
.after_long_help("")
.get_matches_from(std::env::args());
if matches.get_flag("backup") {
eprintln!("The --backup flag is no longer supported. Please use --out or --dir to preserve your existing files.");
exit(1)
}
let (out_file, out_dir, opts) = match parse_opts_into_struct(&matches) {
let (mut out_file, out_dir, opts) = match parse_opts_into_struct(&matches) {
Ok(x) => x,
Err(x) => {
error!("{}", x);
exit(1)
error!("{x}");
return ExitCode::FAILURE;
}
};
let files = collect_files(
#[cfg(windows)]
matches
.get_many::<PathBuf>("files")
.unwrap()
.cloned()
.flat_map(apply_glob_pattern)
.collect(),
#[cfg(not(windows))]
matches
.get_many::<PathBuf>("files")
.unwrap()
.cloned()
.collect(),
&out_dir,
&out_file,
matches.get_flag("recursive"),
true,
);
// Determine input and output
let file_args = matches.get_many::<PathBuf>("files").unwrap().cloned();
#[cfg(windows)]
let inputs: Vec<_> = file_args.flat_map(apply_glob_pattern).collect();
#[cfg(not(windows))]
let inputs: Vec<_> = file_args.collect();
let using_stdin = inputs.len() == 1 && inputs[0].to_str() == Some("-");
if using_stdin && out_dir.is_some() {
error!("Cannot use --dir when reading from stdin.");
return ExitCode::FAILURE;
}
if using_stdin && matches!(out_file, OutFile::Path { path: None, .. }) {
out_file = OutFile::StdOut;
}
let using_stdout = matches!(out_file, OutFile::StdOut);
let json = matches.get_flag("json");
if using_stdout && json {
error!("Cannot use --json when writing to stdout.");
return ExitCode::FAILURE;
}
let success = files.into_par_iter().filter(|(input, output)| {
match oxipng::optimize(input, output, &opts) {
// For optimizing single files, this will return the correct exit code always.
// For recursive optimization, the correct choice is a bit subjective.
// We're choosing to return a 0 exit code if ANY file in the set
// runs correctly.
// The reason for this is that recursion may pick up files that are not
// PNG files, and return an error for them.
// We don't really want to return an error code for those files.
Ok(_) => true,
Err(e) => {
error!("{}: {}", input, e);
false
}
let files = if using_stdin {
vec![(InFile::StdIn, out_file)]
} else {
collect_files(
inputs,
&out_dir,
&out_file,
matches.get_flag("recursive"),
true,
)
};
let is_verbose = matches.get_count("verbose") > 0;
let print_summary = !matches.get_flag("quiet") && !using_stdout;
let print_progress = print_summary && !is_verbose && stdout().is_terminal();
let total_files = files.len();
let num_processed = AtomicUsize::new(0);
if print_progress {
print!("Files processed: 0/{}...", total_files);
stdout().flush().ok();
}
let process = |(input, output): &(InFile, OutFile)| {
let result = process_file(input, output, &opts);
if print_progress && matches!(result, OptimizationResult::Ok(_)) {
let value = num_processed.fetch_add(1, AcqRel) + 1;
print!("\rFiles processed: {}/{}...", value, total_files);
stdout().flush().ok();
}
});
result
};
let results: Vec<OptimizationResult> = if matches.get_flag("parallel-files") {
files.par_iter().map(process).collect()
} else {
files.iter().map(process).collect()
};
if success.count() == 0 {
exit(1);
// Collect stats
let mut num_succeeded = 0;
let mut num_not_optimized = 0;
let mut num_failed = 0;
let mut total_in: i64 = 0;
let mut total_out: i64 = 0;
for result in &results {
match result {
Ok((insize, outsize)) => {
num_succeeded += 1;
total_in += *insize as i64;
total_out += *outsize as i64;
if !opts.force && insize == outsize {
num_not_optimized += 1;
}
}
Err(PngError::C2PAMetadataPreventsChanges | PngError::InflatedDataTooLong(_)) => {}
Err(_) => num_failed += 1,
}
}
// Print results
if json {
json_output(&files, &results);
} else if print_summary {
let in_bytes = format_bytes(total_in, true);
let out_bytes = format_bytes(total_out, true);
let saved = total_in - total_out;
let saved_bytes = format_bytes(saved, false);
let percent = if total_in > 0 {
saved as f64 / total_in as f64 * 100_f64
} else {
0_f64
};
if is_verbose {
println!("--------------------");
}
println!("\rFiles processed: {num_succeeded}/{total_files} ");
println!("Input size: {}", in_bytes);
println!("Output size: {}", out_bytes);
println!("Total saved: {} ({:.2}%)", saved_bytes, percent);
if num_not_optimized == 1 {
println!("({num_not_optimized} file could not be optimized further)");
} else if num_not_optimized > 0 {
println!("({num_not_optimized} files could not be optimized further)");
}
if matches.get_flag("dry-run") {
println!("Dry run, no changes saved");
}
}
// For optimizing single files, this will return the correct exit code always.
// For recursive optimization, the correct choice is a bit subjective.
// We're choosing to return a 0 exit code if ANY file in the set
// runs correctly.
// The reason for this is that recursion may pick up files that are not
// PNG files, and return an error for them.
// We don't really want to return an error code for those files.
if num_succeeded > 0 {
ExitCode::SUCCESS
} else if num_failed > 0 {
ExitCode::FAILURE
} else {
ExitCode::from(3)
}
}
@ -105,10 +178,8 @@ fn collect_files(
top_level: bool, //explicitly specify files
) -> Vec<(InFile, OutFile)> {
let mut in_out_pairs = Vec::new();
let allow_stdin = top_level && files.len() == 1;
for input in files {
let using_stdin = allow_stdin && input.to_str().map_or(false, |p| p == "-");
if !using_stdin && input.is_dir() {
if input.is_dir() {
if recursive {
match input.read_dir() {
Ok(dir) => {
@ -124,7 +195,16 @@ fn collect_files(
warn!("{} is a directory, skipping", input.display());
}
continue;
};
}
// Skip non png files if not given on top level
if !top_level && {
let extension = input.extension().map(OsStr::to_ascii_lowercase);
extension != Some(OsString::from("png")) && extension != Some(OsString::from("apng"))
} {
continue;
}
let out_file =
if let (Some(out_dir), &OutFile::Path { preserve_attrs, .. }) = (out_dir, out_file) {
let path = Some(out_dir.join(input.file_name().unwrap()));
@ -135,19 +215,7 @@ fn collect_files(
} else {
(*out_file).clone()
};
let in_file = if using_stdin {
InFile::StdIn
} else {
// Skip non png files if not given on top level
if !top_level && {
let extension = input.extension().map(|f| f.to_ascii_lowercase());
extension != Some(OsString::from("png"))
&& extension != Some(OsString::from("apng"))
} {
continue;
}
InFile::Path(input)
};
let in_file = InFile::Path(input);
in_out_pairs.push((in_file, out_file));
}
in_out_pairs
@ -157,7 +225,8 @@ fn collect_files(
fn apply_glob_pattern(path: PathBuf) -> Vec<PathBuf> {
let matches = path
.to_str()
.and_then(|pattern| glob::glob(pattern).ok())
// Use MatchOptions::default() to disable case-sensitivity
.and_then(|pattern| glob::glob_with(pattern, glob::MatchOptions::default()).ok())
.map(|paths| paths.flatten().collect::<Vec<_>>());
match matches {
@ -171,8 +240,9 @@ fn parse_opts_into_struct(
) -> Result<(OutFile, Option<PathBuf>, Options), String> {
let log_level = match matches.get_count("verbose") {
_ if matches.get_flag("quiet") => LevelFilter::Off,
0 => LevelFilter::Info,
1 => LevelFilter::Debug,
0 => LevelFilter::Warn,
1 => LevelFilter::Info,
2 => LevelFilter::Debug,
_ => LevelFilter::Trace,
};
env_logger::builder()
@ -181,7 +251,8 @@ fn parse_opts_into_struct(
match record.level() {
Level::Error | Level::Warn => {
let style = buf.default_level_style(record.level());
writeln!(buf, "{style}{}{style:#}", record.args())
// Prepend carriage return to clear progress line
writeln!(buf, "\r{style}{}{style:#}", record.args())
}
// Leave info, debug and trace unstyled
_ => writeln!(buf, "{}", record.args()),
@ -195,11 +266,39 @@ fn parse_opts_into_struct(
Some(level) => Options::from_preset(level.parse::<u8>().unwrap()),
};
if let Some(x) = matches.get_one::<IndexSet<u8>>("filters") {
opts.filter.clear();
for &f in x {
opts.filter.insert(f.try_into().unwrap());
// Get custom brute settings and rebuild the filter set to apply them
let mut brute_lines = matches.get_one::<usize>("brute-lines").copied();
let mut brute_level = matches.get_one::<i64>("brute-level").map(|x| *x as u8);
let mut new_filters = IndexSet::new();
for mut f in opts.filters.drain(..) {
if let FilterStrategy::Brute { num_lines, level } = &mut f {
*num_lines = brute_lines.unwrap_or(*num_lines);
*level = brute_level.unwrap_or(*level);
// If custom settings were not given, we still need to retain the default values
// from the preset so we can re-apply them if the filters are overridden below
brute_lines = Some(*num_lines);
brute_level = Some(*level);
}
new_filters.insert(f);
}
opts.filters = new_filters;
if let Some(x) = matches.get_one::<IndexSet<u8>>("filters") {
opts.filters = x
.iter()
.map(|&f| match f {
0..=4 => FilterStrategy::Basic(f.try_into().unwrap()),
5 => FilterStrategy::MinSum,
6 => FilterStrategy::Entropy,
7 => FilterStrategy::Bigrams,
8 => FilterStrategy::BigEnt,
9 => FilterStrategy::Brute {
num_lines: brute_lines.unwrap_or(3),
level: brute_level.unwrap_or(1),
},
_ => unreachable!(),
})
.collect();
}
if let Some(&num) = matches.get_one::<u64>("timeout") {
@ -209,9 +308,9 @@ fn parse_opts_into_struct(
let out_dir = if let Some(path) = matches.get_one::<PathBuf>("output_dir") {
if !path.exists() {
match DirBuilder::new().recursive(true).create(path) {
Ok(_) => (),
Err(x) => return Err(format!("Could not create output directory {}", x)),
};
Ok(()) => (),
Err(x) => return Err(format!("Could not create output directory {x}")),
}
} else if !path.is_dir() {
return Err(format!(
"{} is an existing file (not a directory), cannot create directory",
@ -223,7 +322,7 @@ fn parse_opts_into_struct(
None
};
let out_file = if matches.get_flag("pretend") {
let out_file = if matches.get_flag("dry-run") {
OutFile::None
} else if matches.get_flag("stdout") {
OutFile::StdOut
@ -247,6 +346,8 @@ fn parse_opts_into_struct(
opts.fix_errors = matches.get_flag("fix");
opts.max_decompressed_size = matches.get_one::<u64>("max-size").map(|&x| x as usize);
opts.bit_depth_reduction = !matches.get_flag("no-bit-reduction");
opts.color_type_reduction = !matches.get_flag("no-color-reduction");
@ -266,10 +367,10 @@ fn parse_opts_into_struct(
opts.idat_recoding = !matches.get_flag("no-recoding");
if let Some(x) = matches.get_one::<String>("interlace") {
opts.interlace = if x == "keep" {
None
} else {
x.parse::<u8>().unwrap().try_into().ok()
opts.interlace = match x.as_str() {
"off" | "0" => Some(false),
"on" | "1" => Some(true),
_ => None, // keep
};
}
@ -286,9 +387,9 @@ fn parse_opts_into_struct(
})
.collect::<Result<IndexSet<_>, _>>()?;
if keep_display {
names.extend(DISPLAY_CHUNKS.iter().cloned());
names.extend(DISPLAY_CHUNKS.iter().copied());
}
opts.strip = StripChunks::Keep(names)
opts.strip = StripChunks::Keep(names);
}
if let Some(strip) = matches.get_one::<String>("strip") {
@ -310,7 +411,7 @@ fn parse_opts_into_struct(
}
let name = parse_chunk_name(x)?;
if FORBIDDEN_CHUNKS.contains(&name) {
return Err(format!("{} chunk is not allowed to be stripped", x));
return Err(format!("{x} chunk is not allowed to be stripped"));
}
Ok(name)
})
@ -323,15 +424,32 @@ fn parse_opts_into_struct(
opts.strip = StripChunks::Safe;
}
#[cfg(feature = "zopfli")]
if matches.get_flag("zopfli") {
#[cfg(feature = "zopfli")]
if let Some(iterations) = NonZeroU8::new(15) {
opts.deflate = Deflaters::Zopfli { iterations };
}
} else if let Deflaters::Libdeflater { compression } = &mut opts.deflate {
if let Some(x) = matches.get_one::<i64>("compression") {
*compression = *x as u8;
let iteration_count = *matches.get_one::<NonZeroU64>("iterations").unwrap();
let iterations_without_improvement = *matches
.get_one::<NonZeroU64>("iterations-without-improvement")
.unwrap_or(&NonZeroU64::MAX);
if iterations_without_improvement > iteration_count
&& iterations_without_improvement != NonZeroU64::MAX
{
warn!(
"--ziwi ({}) is higher than --zi ({}) and will never be reached.",
iterations_without_improvement, iteration_count
);
}
opts.deflater = Deflater::Zopfli(ZopfliOptions {
iteration_count,
iterations_without_improvement,
..Default::default()
});
}
if let (Deflater::Libdeflater { compression }, Some(x)) =
(&mut opts.deflater, matches.get_one::<i64>("compression"))
{
*compression = *x as u8;
}
#[cfg(feature = "parallel")]
@ -349,7 +467,7 @@ fn parse_chunk_name(name: &str) -> Result<[u8; 4], String> {
name.trim()
.as_bytes()
.try_into()
.map_err(|_| format!("Invalid chunk name {}", name))
.map_err(|_| format!("Invalid chunk name {name}"))
}
fn parse_numeric_range_opts(
@ -404,3 +522,133 @@ fn parse_numeric_range_opts(
Err(ERROR_MESSAGE.to_owned())
}
fn process_file(input: &InFile, output: &OutFile, opts: &Options) -> OptimizationResult {
if let (Some(max_size), InFile::Path(path)) = (opts.max_decompressed_size, input) {
if path.metadata().is_ok_and(|m| m.len() > max_size as u64) {
warn!("{input}: Skipped: File exceeds the maximum size ({max_size} bytes)");
return Err(PngError::InflatedDataTooLong(max_size));
}
}
let result = oxipng::optimize(input, output, opts);
match &result {
Ok(_) => {}
Err(e @ PngError::C2PAMetadataPreventsChanges | e @ PngError::InflatedDataTooLong(_)) => {
warn!("{input}: Skipped: {e}");
}
Err(e) => {
error!("{input}: {e}");
}
}
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

@ -4,11 +4,13 @@ use std::{
time::Duration,
};
use indexmap::{indexset, IndexSet};
use indexmap::{IndexSet, indexset};
use log::warn;
use crate::{deflate::Deflaters, filters::RowFilter, headers::StripChunks, interlace::Interlacing};
use crate::{deflate::Deflater, filters::FilterStrategy, headers::StripChunks};
/// Write destination for [`optimize`][crate::optimize].
/// You can use [`optimize_from_memory`](crate::optimize_from_memory) to avoid external I/O.
#[derive(Clone, Debug)]
pub enum OutFile {
/// Don't actually write any output, just calculate the best results.
@ -29,16 +31,18 @@ impl OutFile {
/// Construct a new `OutFile` with the given path.
///
/// This is a convenience method for `OutFile::Path { path: Some(path), preserve_attrs: false }`.
pub fn from_path(path: PathBuf) -> Self {
OutFile::Path {
#[must_use]
pub const fn from_path(path: PathBuf) -> Self {
Self::Path {
path: Some(path),
preserve_attrs: false,
}
}
#[must_use]
pub fn path(&self) -> Option<&Path> {
match *self {
OutFile::Path {
Self::Path {
path: Some(ref p), ..
} => Some(p.as_path()),
_ => None,
@ -46,7 +50,8 @@ impl OutFile {
}
}
/// Where to read images from
/// Where to read images from in [`optimize`][crate::optimize].
/// You can use [`optimize_from_memory`](crate::optimize_from_memory) to avoid external I/O.
#[derive(Clone, Debug)]
pub enum InFile {
Path(PathBuf),
@ -54,10 +59,11 @@ pub enum InFile {
}
impl InFile {
#[must_use]
pub fn path(&self) -> Option<&Path> {
match *self {
InFile::Path(ref p) => Some(p.as_path()),
InFile::StdIn => None,
Self::Path(ref p) => Some(p.as_path()),
Self::StdIn => None,
}
}
}
@ -65,15 +71,15 @@ impl InFile {
impl fmt::Display for InFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
InFile::Path(ref p) => write!(f, "{}", p.display()),
InFile::StdIn => f.write_str("stdin"),
Self::Path(ref p) => write!(f, "{}", p.display()),
Self::StdIn => f.write_str("stdin"),
}
}
}
impl<T: Into<PathBuf>> From<T> for InFile {
fn from(s: T) -> Self {
InFile::Path(s.into())
Self::Path(s.into())
}
}
@ -88,19 +94,20 @@ pub struct Options {
///
/// Default: `false`
pub force: bool,
/// Which RowFilters to try on the file
/// Which `FilterStrategy` to try on the file
///
/// Default: `None,Sub,Entropy,Bigrams`
pub filter: IndexSet<RowFilter>,
/// Whether to change the interlacing type of the file.
pub filters: IndexSet<FilterStrategy>,
/// Whether to change the interlacing of the file.
///
/// `None` will not change the current interlacing type.
/// - `None` will not change the current interlacing.
/// - `Some(x)` will turn interlacing on or off.
///
/// `Some(x)` will change the file to interlacing mode `x`.
///
/// Default: `Some(Interlacing::None)`
pub interlace: Option<Interlacing>,
/// Default: `Some(false)`
pub interlace: Option<bool>,
/// Whether to allow transparent pixels to be altered to improve compression.
///
/// Default: `false`
pub optimize_alpha: bool,
/// Whether to attempt bit depth reduction
///
@ -133,23 +140,31 @@ pub struct Options {
///
/// Default: `None`
pub strip: StripChunks,
/// Which DEFLATE algorithm to use
/// Which DEFLATE (zlib) algorithm to use
#[cfg_attr(feature = "zopfli", doc = "(e.g. Zopfli)")]
///
/// Default: `Libdeflater`
pub deflate: Deflaters,
pub deflater: Deflater,
/// Whether to use fast evaluation to pick the best filter
///
/// Default: `true`
pub fast_evaluation: bool,
/// Maximum amount of time to spend on optimizations.
/// Further potential optimizations are skipped if the timeout is exceeded.
///
/// Default: `None`
pub timeout: Option<Duration>,
/// Maximum decompressed size of the input IDAT.
/// If decompression would exceed this size, it will be rejected.
///
/// Default: `None`
pub max_decompressed_size: Option<usize>,
}
impl Options {
pub fn from_preset(level: u8) -> Options {
let opts = Options::default();
#[must_use]
pub fn from_preset(level: u8) -> Self {
let opts = Self::default();
match level {
0 => opts.apply_preset_0(),
1 => opts.apply_preset_1(),
@ -165,77 +180,112 @@ impl Options {
}
}
pub fn max_compression() -> Options {
Options::from_preset(6)
#[must_use]
pub fn max_compression() -> Self {
Self::from_preset(6)
}
// The following methods make assumptions that they are operating
// on an `Options` struct generated by the `default` method.
fn apply_preset_0(mut self) -> Self {
self.filter.clear();
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
*compression = 5;
}
self.filters.clear();
self.deflater = Deflater::Libdeflater { compression: 5 };
self
}
fn apply_preset_1(mut self) -> Self {
self.filter.clear();
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
*compression = 10;
}
self.filters.clear();
self.deflater = Deflater::Libdeflater { compression: 10 };
self
}
fn apply_preset_2(self) -> Self {
const fn apply_preset_2(self) -> Self {
self
}
fn apply_preset_3(mut self) -> Self {
self.fast_evaluation = false;
self.filter = indexset! {
RowFilter::None,
RowFilter::Bigrams,
RowFilter::BigEnt,
RowFilter::Brute
self.filters = indexset! {
FilterStrategy::NONE,
FilterStrategy::Bigrams,
FilterStrategy::BigEnt,
FilterStrategy::Brute {
num_lines: 3,
level: 1,
},
};
self
}
fn apply_preset_4(mut self) -> Self {
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
*compression = 12;
}
self.apply_preset_3()
self.fast_evaluation = false;
self.filters = indexset! {
FilterStrategy::NONE,
FilterStrategy::Bigrams,
FilterStrategy::BigEnt,
FilterStrategy::Brute {
num_lines: 4,
level: 1,
},
};
self.deflater = Deflater::Libdeflater { compression: 12 };
self
}
fn apply_preset_5(mut self) -> Self {
self.fast_evaluation = false;
self.filter.insert(RowFilter::Up);
self.filter.insert(RowFilter::MinSum);
self.filter.insert(RowFilter::BigEnt);
self.filter.insert(RowFilter::Brute);
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
*compression = 12;
}
self.filters = indexset! {
FilterStrategy::NONE,
FilterStrategy::SUB,
FilterStrategy::UP,
FilterStrategy::MinSum,
FilterStrategy::Entropy,
FilterStrategy::Bigrams,
FilterStrategy::BigEnt,
FilterStrategy::Brute {
num_lines: 4,
level: 4,
},
};
self.deflater = Deflater::Libdeflater { compression: 12 };
self
}
fn apply_preset_6(mut self) -> Self {
self.filter.insert(RowFilter::Average);
self.filter.insert(RowFilter::Paeth);
self.apply_preset_5()
self.fast_evaluation = false;
self.filters = indexset! {
FilterStrategy::NONE,
FilterStrategy::SUB,
FilterStrategy::UP,
FilterStrategy::AVERAGE,
FilterStrategy::PAETH,
FilterStrategy::MinSum,
FilterStrategy::Entropy,
FilterStrategy::Bigrams,
FilterStrategy::BigEnt,
FilterStrategy::Brute {
num_lines: 8,
level: 5,
},
};
self.deflater = Deflater::Libdeflater { compression: 12 };
self
}
}
impl Default for Options {
fn default() -> Options {
fn default() -> Self {
// Default settings based on -o 2 from the CLI interface
Options {
Self {
fix_errors: false,
force: false,
filter: indexset! {RowFilter::None, RowFilter::Sub, RowFilter::Entropy, RowFilter::Bigrams},
interlace: Some(Interlacing::None),
filters: indexset! {
FilterStrategy::NONE,
FilterStrategy::SUB,
FilterStrategy::Entropy,
FilterStrategy::Bigrams
},
interlace: Some(false),
optimize_alpha: false,
bit_depth_reduction: true,
color_type_reduction: true,
@ -244,9 +294,10 @@ impl Default for Options {
idat_recoding: true,
scale_16: false,
strip: StripChunks::None,
deflate: Deflaters::Libdeflater { compression: 11 },
deflater: Deflater::Libdeflater { compression: 11 },
fast_evaluation: true,
timeout: None,
max_decompressed_size: None,
}
}
}

View file

@ -1,35 +1,24 @@
use std::{
fs::File,
io::{BufReader, Read, Write},
path::Path,
sync::Arc,
};
use std::{fs, path::Path, sync::Arc};
use bitvec::bitarr;
use libdeflater::{CompressionLvl, Compressor};
use log::warn;
use rgb::ComponentSlice;
use rustc_hash::FxHashMap;
use crate::{
Options, PngResult,
apng::*,
colors::{BitDepth, ColorType},
deflate,
error::PngError,
filters::*,
headers::*,
interlace::{deinterlace_image, interlace_image, Interlacing},
Options,
interlace::{deinterlace_image, interlace_image},
};
pub(crate) mod scan_lines;
use self::scan_lines::ScanLines;
/// Compression level to use for the Brute filter strategy
const BRUTE_LEVEL: i32 = 1; // 1 is fastest, 2-4 are not useful, 5 is slower but more effective
/// Number of lines to compress with the Brute filter strategy
const BRUTE_LINES: usize = 4; // Values over 8 are generally not useful
#[derive(Debug, Clone)]
pub struct PngImage {
/// The headers stored in the IHDR chunk
@ -47,44 +36,25 @@ pub struct PngData {
pub idat_data: Vec<u8>,
/// All non-critical chunks from the PNG are stored here
pub aux_chunks: Vec<Chunk>,
/// APNG frames
pub frames: Vec<Frame>,
}
impl PngData {
/// Create a new `PngData` struct by opening a file
#[inline]
pub fn new(filepath: &Path, opts: &Options) -> Result<Self, PngError> {
pub fn new(filepath: &Path, opts: &Options) -> PngResult<Self> {
let byte_data = Self::read_file(filepath)?;
Self::from_slice(&byte_data, opts)
}
pub fn read_file(filepath: &Path) -> Result<Vec<u8>, PngError> {
let file = match File::open(filepath) {
Ok(f) => f,
Err(_) => return Err(PngError::new("Failed to open file for reading")),
};
let file_len = file.metadata().map(|m| m.len() as usize).unwrap_or(0);
let mut reader = BufReader::new(file);
// Check file for PNG header
let mut header = [0; 8];
if reader.read_exact(&mut header).is_err() {
return Err(PngError::new("Not a PNG file: too small"));
}
if !file_header_is_valid(&header) {
return Err(PngError::new("Invalid PNG header detected"));
}
// Read raw png data into memory
let mut byte_data: Vec<u8> = Vec::with_capacity(file_len);
byte_data.extend_from_slice(&header);
match reader.read_to_end(&mut byte_data) {
Ok(_) => (),
Err(_) => return Err(PngError::new("Failed to read from file")),
}
Ok(byte_data)
pub fn read_file(filepath: &Path) -> PngResult<Vec<u8>> {
fs::read(filepath).map_err(|e| PngError::ReadFailed(filepath.display().to_string(), e))
}
/// Create a new `PngData` struct by reading a slice
pub fn from_slice(byte_data: &[u8], opts: &Options) -> Result<Self, PngError> {
pub fn from_slice(byte_data: &[u8], opts: &Options) -> PngResult<Self> {
let mut byte_offset: usize = 0;
// Test that png header is valid
let header = byte_data.get(0..8).ok_or(PngError::TruncatedData)?;
@ -97,6 +67,8 @@ impl PngData {
let mut idat_data: Vec<u8> = Vec::new();
let mut key_chunks: FxHashMap<[u8; 4], Vec<u8>> = FxHashMap::default();
let mut aux_chunks: Vec<Chunk> = Vec::new();
let mut frames: Vec<Frame> = Vec::new();
let mut sequence_number = 0;
while let Some(chunk) = parse_next_chunk(byte_data, &mut byte_offset, opts.fix_errors)? {
match &chunk.name {
b"IDAT" => {
@ -105,25 +77,53 @@ impl PngData {
aux_chunks.push(Chunk {
name: chunk.name,
data: Vec::new(),
})
});
}
idat_data.extend_from_slice(chunk.data);
}
b"IHDR" | b"PLTE" | b"tRNS" => {
key_chunks.insert(chunk.name, chunk.data.to_owned());
}
_ => {
if opts.strip.keep(&chunk.name) {
aux_chunks.push(Chunk {
name: chunk.name,
data: chunk.data.to_owned(),
})
} else if chunk.name == *b"acTL" {
warn!(
"Stripping animation data from APNG - image will become standard PNG"
);
_ if opts.strip.keep(&chunk.name) => {
if chunk.is_c2pa() {
// StripChunks::None is the default value, so to keep optimizing by default,
// interpret it as stripping the C2PA metadata.
// The C2PA metadata is invalidated if the file changes, so it shouldn't be kept.
if opts.strip == StripChunks::None {
continue;
}
return Err(PngError::C2PAMetadataPreventsChanges);
}
if chunk.name == *b"fcTL" || chunk.name == *b"fdAT" {
// Validate the sequence number
if read_be_u32(&chunk.data[0..4]) != sequence_number {
return Err(PngError::APNGOutOfOrder);
}
sequence_number += 1;
if chunk.name == *b"fcTL" && !idat_data.is_empty() {
// Only create a Frame if it's after the IDAT (else store it as an aux chunk)
frames.push(Frame::from_fctl_data(chunk.data)?);
continue;
} else if chunk.name == *b"fdAT" {
// Append the data to the last frame
frames
.last_mut()
.ok_or(PngError::APNGOutOfOrder)?
.data
.extend_from_slice(&chunk.data[4..]);
continue;
}
}
// Regular ancillary chunk
aux_chunks.push(Chunk {
name: chunk.name,
data: chunk.data.to_owned(),
});
}
b"acTL" => {
warn!("Stripping animation data from APNG - image will become standard PNG");
}
_ => (),
}
}
@ -131,64 +131,57 @@ impl PngData {
if idat_data.is_empty() {
return Err(PngError::ChunkMissing("IDAT"));
}
let ihdr_chunk = match key_chunks.remove(b"IHDR") {
Some(ihdr) => ihdr,
None => return Err(PngError::ChunkMissing("IHDR")),
let Some(ihdr_chunk) = key_chunks.remove(b"IHDR") else {
return Err(PngError::ChunkMissing("IHDR"));
};
let ihdr = parse_ihdr_chunk(
&ihdr_chunk,
key_chunks.remove(b"PLTE"),
key_chunks.remove(b"tRNS"),
)?;
let raw_data = deflate::inflate(idat_data.as_ref(), ihdr.raw_data_size())?;
// Reject files with incorrect width/height or truncated data
if raw_data.len() != ihdr.raw_data_size() {
return Err(PngError::TruncatedData);
if let Some(max) = opts.max_decompressed_size {
if ihdr.raw_data_size() > max {
return Err(PngError::InflatedDataTooLong(max));
}
}
let mut raw = PngImage {
ihdr,
data: raw_data,
};
raw.data = raw.unfilter_image()?;
let raw = PngImage::new(ihdr, &idat_data)?;
// Return the PngData
Ok(Self {
idat_data,
raw: Arc::new(raw),
aux_chunks,
frames,
})
}
/// Return an estimate of the output size which can help with evaluation of very small data
pub fn estimated_output_size(&self) -> usize {
self.idat_data.len() + self.raw.key_chunks_size()
}
/// Format the `PngData` struct into a valid PNG bytestream
#[must_use]
pub fn output(&self) -> Vec<u8> {
// PNG header
let mut output = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
// IHDR
let mut ihdr_data = Vec::with_capacity(13);
ihdr_data.write_all(&self.raw.ihdr.width.to_be_bytes()).ok();
ihdr_data
.write_all(&self.raw.ihdr.height.to_be_bytes())
.ok();
ihdr_data.write_all(&[self.raw.ihdr.bit_depth as u8]).ok();
ihdr_data
.write_all(&[self.raw.ihdr.color_type.png_header_code()])
.ok();
ihdr_data.write_all(&[0]).ok(); // Compression -- deflate
ihdr_data.write_all(&[0]).ok(); // Filter method -- 5-way adaptive filtering
ihdr_data.write_all(&[self.raw.ihdr.interlaced as u8]).ok();
ihdr_data.extend_from_slice(&self.raw.ihdr.width.to_be_bytes());
ihdr_data.extend_from_slice(&self.raw.ihdr.height.to_be_bytes());
ihdr_data.extend_from_slice(&[self.raw.ihdr.bit_depth as u8]);
ihdr_data.extend_from_slice(&[
self.raw.ihdr.color_type.png_header_code(),
0, // Compression -- deflate
0, // Filter method -- 5-way adaptive filtering
self.raw.ihdr.interlaced as u8,
]);
write_png_block(b"IHDR", &ihdr_data, &mut output);
// Ancillary chunks - split into those that come before IDAT and those that come after
let mut aux_split = self.aux_chunks.split(|c| &c.name == b"IDAT");
let aux_pre = aux_split.next().unwrap();
// Many chunks need to be before PLTE, so write all except those that explicitly need to be after
// Note: the PNG spec does not say that fcTL needs to be after PLTE, but some decoders expect
// that (see issue #625)
for chunk in aux_pre
.iter()
.filter(|c| !(&c.name == b"bKGD" || &c.name == b"hIST" || &c.name == b"tRNS"))
.filter(|c| !matches!(&c.name, b"bKGD" | b"hIST" | b"tRNS" | b"fcTL"))
{
write_png_block(&chunk.name, &chunk.data, &mut output);
}
@ -197,7 +190,7 @@ impl PngData {
ColorType::Indexed { palette } => {
let mut palette_data = Vec::with_capacity(palette.len() * 3);
for px in palette {
palette_data.extend_from_slice(px.rgb().as_slice());
palette_data.extend_from_slice(px.rgb().as_ref());
}
write_png_block(b"PLTE", &palette_data, &mut output);
if let Some(last_trns) = palette.iter().rposition(|px| px.a != 255) {
@ -215,20 +208,30 @@ impl PngData {
transparent_color: Some(trns),
} => {
// Transparency pixel - 6 byte RGB16
let trns_data: Vec<_> = trns.iter().flat_map(|c| c.to_be_bytes()).collect();
let trns_data: Vec<_> = trns.iter().flat_map(u16::to_be_bytes).collect();
write_png_block(b"tRNS", &trns_data, &mut output);
}
_ => {}
}
// Special ancillary chunks that need to come after PLTE but before IDAT
let mut sequence_number = 0;
for chunk in aux_pre
.iter()
.filter(|c| &c.name == b"bKGD" || &c.name == b"hIST" || &c.name == b"tRNS")
.filter(|c| matches!(&c.name, b"bKGD" | b"hIST" | b"tRNS" | b"fcTL"))
{
write_png_block(&chunk.name, &chunk.data, &mut output);
if &chunk.name == b"fcTL" {
sequence_number += 1;
}
}
// IDAT data
write_png_block(b"IDAT", &self.idat_data, &mut output);
// APNG frames
for frame in self.frames.iter() {
write_png_block(b"fcTL", &frame.fctl_data(sequence_number), &mut output);
write_png_block(b"fdAT", &frame.fdat_data(sequence_number + 1), &mut output);
sequence_number += 2;
}
// Ancillary chunks that come after IDAT
for aux_post in aux_split {
for chunk in aux_post {
@ -243,18 +246,33 @@ impl PngData {
}
impl PngImage {
/// Convert the image to the specified interlacing type
/// Returns true if the interlacing was changed, false otherwise
/// The `interlace` parameter specifies the *new* interlacing mode
pub fn new(ihdr: IhdrData, compressed_data: &[u8]) -> PngResult<Self> {
let raw_data = deflate::inflate(compressed_data, ihdr.raw_data_size())?;
// Reject files with incorrect width/height or truncated data
if raw_data.len() != ihdr.raw_data_size() {
return Err(PngError::TruncatedData);
}
let mut image = Self {
ihdr,
data: raw_data,
};
image.data = image.unfilter_image()?;
Ok(image)
}
/// Enable or disable interlacing
/// Returns the new image if the interlacing was changed, None otherwise
/// Assumes that the data has already been de-filtered
#[inline]
#[must_use]
pub fn change_interlacing(&self, interlace: Interlacing) -> Option<PngImage> {
pub fn change_interlacing(&self, interlace: bool) -> Option<Self> {
if interlace == self.ihdr.interlaced {
return None;
}
Some(if interlace == Interlacing::Adam7 {
Some(if interlace {
// Convert progressive to interlaced data
interlace_image(self)
} else {
@ -265,13 +283,15 @@ impl PngImage {
/// Return the number of channels in the image, based on color type
#[inline]
pub fn channels_per_pixel(&self) -> usize {
#[must_use]
pub const fn channels_per_pixel(&self) -> usize {
self.ihdr.color_type.channels_per_pixel() as usize
}
/// Return the number of bytes per channel in the image
#[inline]
pub fn bytes_per_channel(&self) -> usize {
#[must_use]
pub const fn bytes_per_channel(&self) -> usize {
match self.ihdr.bit_depth {
BitDepth::Sixteen => 2,
// Depths lower than 8 will round up to 1 byte
@ -280,15 +300,15 @@ impl PngImage {
}
/// Calculate the size of the PLTE and tRNS chunks
#[must_use]
pub fn key_chunks_size(&self) -> usize {
match &self.ihdr.color_type {
ColorType::Indexed { palette } => {
let plte = 12 + palette.len() * 3;
if let Some(trns) = palette.iter().rposition(|p| p.a != 255) {
plte + 12 + trns + 1
} else {
plte
}
palette
.iter()
.rposition(|p| p.a != 255)
.map_or(plte, |trns| plte + 12 + trns + 1)
}
ColorType::Grayscale { transparent_shade } if transparent_shade.is_some() => 12 + 2,
ColorType::RGB { transparent_color } if transparent_color.is_some() => 12 + 6,
@ -296,14 +316,21 @@ impl PngImage {
}
}
/// Return an estimate of the output size which can help with evaluation of very small data
#[must_use]
pub fn estimated_output_size(&self, idat_data: &[u8]) -> usize {
idat_data.len() + self.key_chunks_size()
}
/// Return an iterator over the scanlines of the image
#[inline]
#[must_use]
pub fn scan_lines(&self, has_filter: bool) -> ScanLines<'_> {
ScanLines::new(self, has_filter)
}
/// Reverse all filters applied on the image, returning an unfiltered IDAT bytestream
fn unfilter_image(&self) -> Result<Vec<u8>, PngError> {
fn unfilter_image(&self) -> PngResult<Vec<u8>> {
let mut unfiltered = Vec::with_capacity(self.data.len());
let bpp = self.bytes_per_channel() * self.channels_per_pixel();
let mut last_line: Vec<u8> = Vec::new();
@ -315,8 +342,8 @@ impl PngImage {
last_pass = line.pass;
}
last_line.resize(line.data.len(), 0);
let filter = RowFilter::try_from(line.filter).map_err(|_| PngError::InvalidData)?;
filter.unfilter_line(bpp, line.data, &last_line, &mut unfiltered_buf)?;
let filter = RowFilter::try_from(line.filter).map_err(|()| PngError::InvalidData)?;
filter.unfilter_line(bpp, line.data, &last_line, &mut unfiltered_buf);
unfiltered.extend_from_slice(&unfiltered_buf);
std::mem::swap(&mut last_line, &mut unfiltered_buf);
unfiltered_buf.clear();
@ -325,7 +352,12 @@ impl PngImage {
}
/// Apply the specified filter type to all rows in the image
pub fn filter_image(&self, filter: RowFilter, optimize_alpha: bool) -> Vec<u8> {
#[must_use]
pub fn filter_image(
&self,
strategy: FilterStrategy,
optimize_alpha: bool,
) -> (Vec<u8>, FilterStrategy) {
let mut filtered = Vec::with_capacity(self.data.len());
let bpp = self.bytes_per_channel() * self.channels_per_pixel();
// If alpha optimization is enabled, determine how many bytes of alpha there are per pixel
@ -338,35 +370,58 @@ impl PngImage {
let mut prev_line = Vec::new();
let mut prev_pass: Option<u8> = None;
let mut f_buf = Vec::new();
for line in self.scan_lines(false) {
// For heuristic strategies, keep track of the actual filter used for each line
let mut filters_used = Vec::new();
// Pre-allocate buffers for the Bigrams strategy to avoid per-line allocations
let (mut bigram_seen, mut bigram_touched) = if matches!(strategy, FilterStrategy::Bigrams) {
(vec![false; 0x10000], Vec::<u16>::new())
} else {
(Vec::new(), Vec::new())
};
for (i, line) in self.scan_lines(false).enumerate() {
if prev_pass != line.pass || line.data.len() != prev_line.len() {
prev_line = vec![0; line.data.len()];
}
// Alpha optimisation may alter the line data, so we need a mutable copy of it
let mut line_data = line.data.to_vec();
if filter <= RowFilter::Paeth {
if let FilterStrategy::Basic(mut filter) = strategy {
// Standard filters
let filter = if prev_pass == line.pass || filter <= RowFilter::Sub {
filter
} else {
RowFilter::None
};
if prev_pass != line.pass && filter > RowFilter::Sub {
filter = RowFilter::None;
}
filter.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
filtered.extend_from_slice(&f_buf);
prev_line = line_data;
} else if let FilterStrategy::Predefined(lines) = &strategy {
// Predefined filter for each line
let filter = lines.get(i).unwrap_or(&RowFilter::None);
filter.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
filtered.extend_from_slice(&f_buf);
prev_line = line_data;
} else {
// Heuristic filter selection strategies
let mut best_filter = RowFilter::None;
if line_data.iter().all(|&x| x == 0) {
// Assume None if the line is all zeros
filtered.push(best_filter as u8);
filtered.extend_from_slice(&line_data);
prev_line = line_data;
filters_used.push(best_filter);
continue;
}
let mut best_line = Vec::new();
let mut best_line_raw = Vec::new();
// Avoid vertical filtering on first line of each interlacing pass
let try_filters = if prev_pass == line.pass {
RowFilter::STANDARD.iter()
RowFilter::ALL.iter()
} else {
RowFilter::SINGLE_LINE.iter()
};
match filter {
RowFilter::MinSum => {
match strategy {
FilterStrategy::MinSum => {
// MSAD algorithm mentioned in libpng reference docs
// http://www.libpng.org/pub/png/book/chapter09.html
let mut best_size = usize::MAX;
@ -380,17 +435,18 @@ impl PngImage {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data);
best_filter = *f;
}
}
}
RowFilter::Entropy => {
FilterStrategy::Entropy => {
// Shannon entropy algorithm, from LodePNG
// https://github.com/lvandeve/lodepng
let mut best_size = i32::MIN;
for f in try_filters {
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
let mut counts = vec![0; 0x100];
for &i in f_buf.iter() {
for &i in &f_buf {
counts[i as usize] += 1;
}
let size = counts.into_iter().fold(0, |acc, x| {
@ -403,29 +459,42 @@ impl PngImage {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data);
best_filter = *f;
}
}
}
RowFilter::Bigrams => {
FilterStrategy::Bigrams => {
// Count distinct bigrams, from pngwolf
// https://bjoern.hoehrmann.de/pngwolf/
let mut best_size = usize::MAX;
for f in try_filters {
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
let mut set = bitarr![0; 0x10000];
let mut count = 0;
for pair in f_buf.windows(2) {
let bigram = (pair[0] as usize) << 8 | pair[1] as usize;
set.set(bigram, true);
let bigram = ((pair[0] as usize) << 8) | pair[1] as usize;
if !bigram_seen[bigram] {
count += 1;
if count >= best_size {
break;
}
bigram_seen[bigram] = true;
bigram_touched.push(bigram as u16);
}
}
let size = set.count_ones();
if size < best_size {
best_size = size;
if count < best_size {
best_size = count;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data);
best_filter = *f;
}
// Clear only the entries that were touched
for &idx in &bigram_touched {
bigram_seen[idx as usize] = false;
}
bigram_touched.clear();
}
}
RowFilter::BigEnt => {
FilterStrategy::BigEnt => {
// Bigram entropy, combined from Entropy and Bigrams filters
let mut best_size = i32::MIN;
// FxHasher is the fastest rust hasher currently available for this purpose
@ -434,7 +503,7 @@ impl PngImage {
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
counts.clear();
for pair in f_buf.windows(2) {
let bigram = (pair[0] as u16) << 8 | pair[1] as u16;
let bigram = (u16::from(pair[0]) << 8) | u16::from(pair[1]);
counts.entry(bigram).and_modify(|e| *e += 1).or_insert(1);
}
let size = counts.values().fold(0, |acc, &x| acc + ilog2i(x)) as i32;
@ -442,31 +511,33 @@ impl PngImage {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data);
best_filter = *f;
}
}
}
RowFilter::Brute => {
FilterStrategy::Brute { num_lines, level } => {
// Brute force by compressing each filter attempt
// Similar to that of LodePNG but includes some previous lines for context
let mut best_size = usize::MAX;
let line_start = filtered.len();
filtered.resize(filtered.len() + line.data.len() + 1, 0);
let mut compressor =
Compressor::new(CompressionLvl::new(BRUTE_LEVEL).unwrap());
let limit = filtered.len().min((line.data.len() + 1) * BRUTE_LINES);
let capacity = compressor.zlib_compress_bound(limit);
Compressor::new(CompressionLvl::new(level.into()).unwrap());
let limit = filtered.len().min((line.data.len() + 1) * num_lines);
let capacity = compressor.deflate_compress_bound(limit);
let mut dest = vec![0; capacity];
for f in try_filters {
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
filtered[line_start..].copy_from_slice(&f_buf);
let size = compressor
.zlib_compress(&filtered[filtered.len() - limit..], &mut dest)
.deflate_compress(&filtered[filtered.len() - limit..], &mut dest)
.unwrap_or(usize::MAX);
if size < best_size {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw.clone_from(&line_data);
best_filter = *f;
}
}
filtered.resize(line_start, 0);
@ -475,11 +546,17 @@ impl PngImage {
}
filtered.extend_from_slice(&best_line);
prev_line = best_line_raw;
filters_used.push(best_filter);
}
prev_pass = line.pass;
}
filtered
if filters_used.is_empty() {
(filtered, strategy)
} else {
(filtered, FilterStrategy::Predefined(filters_used))
}
}
}
@ -495,7 +572,7 @@ fn write_png_block(key: &[u8], chunk: &[u8], output: &mut Vec<u8>) {
}
// Integer approximation for i * log2(i) - much faster than float calculations
fn ilog2i(i: u32) -> u32 {
const fn ilog2i(i: u32) -> u32 {
let log = 32 - i.leading_zeros() - 1;
i * log + ((i - (1 << log)) << 1)
}

View file

@ -1,4 +1,4 @@
use crate::{interlace::Interlacing, png::PngImage};
use crate::png::PngImage;
/// An iterator over the scan lines of a PNG image
#[derive(Debug, Clone)]
@ -11,6 +11,7 @@ pub struct ScanLines<'a> {
}
impl<'a> ScanLines<'a> {
#[must_use]
pub fn new(png: &'a PngImage, has_filter: bool) -> Self {
Self {
iter: ScanLineRanges::new(png, has_filter),
@ -22,6 +23,7 @@ impl<'a> ScanLines<'a> {
impl<'a> Iterator for ScanLines<'a> {
type Item = ScanLine<'a>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let (len, pass, num_pixels) = self.iter.next()?;
@ -67,7 +69,7 @@ impl ScanLineRanges {
width: png.ihdr.width,
height: png.ihdr.height,
left: png.data.len(),
pass: if png.ihdr.interlaced == Interlacing::Adam7 {
pass: if png.ihdr.interlaced {
Some((1, 0))
} else {
None
@ -79,6 +81,7 @@ impl ScanLineRanges {
impl Iterator for ScanLineRanges {
type Item = (usize, Option<u8>, usize);
fn next(&mut self) -> Option<Self::Item> {
if self.left == 0 {
return None;
@ -135,7 +138,7 @@ impl Iterator for ScanLineRanges {
pixels_per_line += 1;
}
_ => (),
};
}
let current_pass = Some(pass.0);
if pass.1 + y_steps >= self.height {
pass.0 += 1;
@ -154,7 +157,7 @@ impl Iterator for ScanLineRanges {
(self.width, None)
};
let bits_per_line = pixels_per_line as usize * self.bits_per_pixel;
let mut len = (bits_per_line + 7) / 8;
let mut len = bits_per_line.div_ceil(8);
if self.has_filter {
len += 1;
}

View file

@ -1,3 +1,5 @@
#![allow(dead_code)]
pub mod prelude {
pub use super::*;
}
@ -70,12 +72,10 @@ where
impl<I: Iterator> ParallelIterator for I {}
#[allow(dead_code)]
pub fn join<A, B>(a: impl FnOnce() -> A, b: impl FnOnce() -> B) -> (A, B) {
(a(), b())
}
#[allow(dead_code)]
pub fn spawn<A>(a: impl FnOnce() -> A) -> A {
a()
}

View file

@ -7,6 +7,7 @@ use crate::{
};
/// Clean the alpha channel by setting the color of all fully transparent pixels to black
#[must_use]
pub fn cleaned_alpha_channel(png: &PngImage) -> Option<PngImage> {
if !png.ihdr.color_type.has_alpha() {
return None;
@ -16,7 +17,7 @@ pub fn cleaned_alpha_channel(png: &PngImage) -> Option<PngImage> {
let colored_bytes = bpp - byte_depth;
let mut reduced = Vec::with_capacity(png.data.len());
for pixel in png.data.chunks(bpp) {
for pixel in png.data.chunks_exact(bpp) {
if pixel.iter().skip(colored_bytes).all(|b| *b == 0) {
reduced.resize(reduced.len() + bpp, 0);
} else {
@ -45,7 +46,7 @@ pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option<Png
let mut has_transparency = false;
let mut used_colors = vec![false; 256];
for pixel in png.data.chunks(bpp) {
for pixel in png.data.chunks_exact(bpp) {
if optimize_alpha && pixel.iter().skip(colored_bytes).all(|b| *b == 0) {
// Fully transparent, we may be able to reduce with tRNS
has_transparency = true;
@ -74,19 +75,19 @@ pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option<Png
};
let mut raw_data = Vec::with_capacity(png.data.len());
for pixel in png.data.chunks(bpp) {
for pixel in png.data.chunks_exact(bpp) {
match transparency_pixel {
Some(trns) if pixel.iter().skip(colored_bytes).all(|b| *b == 0) => {
raw_data.resize(raw_data.len() + colored_bytes, trns);
}
_ => raw_data.extend_from_slice(&pixel[0..colored_bytes]),
};
}
}
// Construct the color type with appropriate transparency data
let transparent = transparency_pixel.map(|trns| match png.ihdr.bit_depth {
BitDepth::Sixteen => (trns as u16) << 8 | trns as u16,
_ => trns as u16,
BitDepth::Sixteen => (u16::from(trns) << 8) | u16::from(trns),
_ => u16::from(trns),
});
let target_color_type = match png.ihdr.color_type {
ColorType::GrayscaleAlpha => ColorType::Grayscale {

View file

@ -16,13 +16,13 @@ pub fn reduced_bit_depth_16_to_8(png: &PngImage, force_scale: bool) -> Option<Pn
}
// Reduce from 16 to 8 bits per channel per pixel
if png.data.chunks(2).any(|pair| pair[0] != pair[1]) {
if png.data.chunks_exact(2).any(|pair| pair[0] != pair[1]) {
// Can't reduce
return None;
}
Some(PngImage {
data: png.data.iter().step_by(2).cloned().collect(),
data: png.data.chunks_exact(2).map(|pair| pair[0]).collect(),
ihdr: IhdrData {
color_type: png.ihdr.color_type.clone(),
bit_depth: BitDepth::Eight,
@ -41,15 +41,15 @@ pub fn scaled_bit_depth_16_to_8(png: &PngImage) -> Option<PngImage> {
// Reduce from 16 to 8 bits per channel per pixel by scaling when necessary
let data = png
.data
.chunks(2)
.chunks_exact(2)
.map(|pair| {
if pair[0] == pair[1] {
return pair[0];
}
// See: http://www.libpng.org/pub/png/spec/1.2/PNG-Decoders.html#D.Sample-depth-rescaling
// This allows values such as 0x00FF to be rounded to 0x01 rather than truncated to 0x00
let val = u16::from_be_bytes([pair[0], pair[1]]) as f64;
(val * 255.0 / 65535.0).round() as u8
let val = f32::from(u16::from_be_bytes([pair[0], pair[1]]));
(val * (255.0 / 65535.0)).round() as u8
})
.collect();
@ -70,17 +70,17 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage) -> Option<PngImage> {
return None;
}
let mut minimum_bits = 1;
if let ColorType::Indexed { palette } = &png.ihdr.color_type {
let minimum_bits = if let ColorType::Indexed { palette } = &png.ihdr.color_type {
// We can easily determine minimum depth by the palette size
minimum_bits = match palette.len() {
match palette.len() {
0..=2 => 1,
3..=4 => 2,
5..=16 => 4,
_ => return None,
};
}
} else {
let mut minimum_bits = 1;
// Finding minimum depth for grayscale is much more complicated
let mut mask = 1;
let mut divisions = 1..8;
@ -110,7 +110,9 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage) -> Option<PngImage> {
break;
}
}
}
minimum_bits
};
let mut reduced = Vec::with_capacity(png.data.len());
let mask = (1 << minimum_bits) - 1;
@ -138,7 +140,7 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage) -> Option<PngImage> {
let mut check = reduced_trans;
let mut bits = minimum_bits;
while bits < 8 {
check = check << bits | check;
check = (check << bits) | check;
bits <<= 1;
}
// If the transparency doesn't fit the new bit depth it is therefore unused - set it to None
@ -188,7 +190,7 @@ pub fn expanded_bit_depth_to_8(png: &PngImage) -> Option<PngImage> {
// Expand gray by repeating the bits
let mut bits = bit_depth;
while bits < 8 {
val = val << bits | val;
val = (val << bits) | val;
bits <<= 1;
}
}
@ -207,7 +209,7 @@ pub fn expanded_bit_depth_to_8(png: &PngImage) -> Option<PngImage> {
{
let mut bits = bit_depth;
while bits < 8 {
trans = trans << bits | trans;
trans = (trans << bits) | trans;
bits <<= 1;
}
ColorType::Grayscale {

View file

@ -1,7 +1,7 @@
use std::hash::{BuildHasherDefault, Hash};
use indexmap::IndexSet;
use rgb::{alt::Gray, ComponentMap, ComponentSlice, FromSlice, RGB, RGBA};
use rgb::{ComponentMap, FromSlice, RGB, RGBA, alt::Gray};
use rustc_hash::FxHasher;
use crate::{
@ -49,39 +49,39 @@ pub fn reduced_to_indexed(png: &PngImage, allow_grayscale: bool) -> Option<PngIm
let mut raw_data = Vec::with_capacity(png.data.len() / png.channels_per_pixel());
let palette: Vec<_> = match png.ihdr.color_type {
ColorType::Grayscale { transparent_shade } => {
let pmap = build_palette(png.data.as_gray().iter().cloned(), &mut raw_data)?;
let pmap = build_palette(png.data.as_gray().iter().copied(), &mut raw_data)?;
// Convert the Gray16 transparency to Gray8
let transparency_pixel = transparent_shade.map(|t| Gray::from(t as u8));
pmap.into_iter()
.map(|px| {
RGB::from(px).alpha(if Some(px) != transparency_pixel {
255
} else {
RGB::from(px).with_alpha(if Some(px) == transparency_pixel {
0
} else {
255
})
})
.collect()
}
ColorType::RGB { transparent_color } => {
let pmap = build_palette(png.data.as_rgb().iter().cloned(), &mut raw_data)?;
let pmap = build_palette(png.data.as_rgb().iter().copied(), &mut raw_data)?;
// Convert the RGB16 transparency to RGB8
let transparency_pixel = transparent_color.map(|t| t.map(|c| c as u8));
pmap.into_iter()
.map(|px| {
px.alpha(if Some(px) != transparency_pixel {
255
} else {
px.with_alpha(if Some(px) == transparency_pixel {
0
} else {
255
})
})
.collect()
}
ColorType::GrayscaleAlpha => {
let pmap = build_palette(png.data.as_gray_alpha().iter().cloned(), &mut raw_data)?;
let pmap = build_palette(png.data.as_gray_alpha().iter().copied(), &mut raw_data)?;
pmap.into_iter().map(RGBA::from).collect()
}
ColorType::RGBA => {
let pmap = build_palette(png.data.as_rgba().iter().cloned(), &mut raw_data)?;
let pmap = build_palette(png.data.as_rgba().iter().copied(), &mut raw_data)?;
pmap.into_iter().collect()
}
_ => return None,
@ -106,7 +106,7 @@ pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
let byte_depth = png.bytes_per_channel();
let bpp = png.channels_per_pixel() * byte_depth;
let last_color = 2 * byte_depth;
for pixel in png.data.chunks(bpp) {
for pixel in png.data.chunks_exact(bpp) {
if byte_depth == 1 {
if pixel[0] != pixel[1] || pixel[1] != pixel[2] {
return None;
@ -138,15 +138,30 @@ pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
/// Attempt to convert indexed to a different color type, returning the resulting image if successful
#[must_use]
pub fn indexed_to_channels(png: &PngImage, allow_grayscale: bool) -> Option<PngImage> {
pub fn indexed_to_channels(
png: &PngImage,
allow_grayscale: bool,
optimize_alpha: bool,
) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight {
return None;
}
let palette = match &png.ihdr.color_type {
ColorType::Indexed { palette } => palette,
let mut palette = match &png.ihdr.color_type {
ColorType::Indexed { palette } => palette.clone(),
_ => return None,
};
// Ensure fully transparent colors are black, which can help with grayscale conversion
if optimize_alpha {
for color in &mut palette {
if color.a == 0 {
color.r = 0;
color.g = 0;
color.b = 0;
}
}
}
// Determine which channels are required
let is_gray = if allow_grayscale {
palette.iter().all(|c| c.r == c.g && c.g == c.b)
@ -178,7 +193,7 @@ pub fn indexed_to_channels(png: &PngImage, allow_grayscale: bool) -> Option<PngI
let mut data = Vec::with_capacity(out_size);
for b in &png.data {
let color = palette.get(*b as usize).unwrap_or(&black);
data.extend_from_slice(&color.as_slice()[ch_start..=ch_end]);
data.extend_from_slice(&color.as_ref()[ch_start..=ch_end]);
}
Some(PngImage {

View file

@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::{evaluate::Evaluator, png::PngImage, ColorType, Deadline, Deflaters, Options};
use crate::{ColorType, Deadline, Deflater, Options, evaluate::Evaluator, png::PngImage};
pub mod alpha;
use crate::alpha::*;
@ -21,8 +21,8 @@ pub(crate) fn perform_reductions(
// At low compression levels, skip some transformations which are less likely to be effective
// This currently affects optimization presets 0-2
let cheap = match opts.deflate {
Deflaters::Libdeflater { compression } => compression < 12 && opts.fast_evaluation,
let cheap = match opts.deflater {
Deflater::Libdeflater { compression } => compression < 12 && opts.fast_evaluation,
_ => false,
};
@ -106,7 +106,9 @@ pub(crate) fn perform_reductions(
// Attempt to convert from indexed to channels
// This may give a better result due to dropping the PLTE chunk
if !cheap && opts.color_type_reduction && !deadline.passed() {
if let Some(reduced) = indexed_to_channels(&png, opts.grayscale_reduction) {
if let Some(reduced) =
indexed_to_channels(&png, opts.grayscale_reduction, opts.optimize_alpha)
{
// This result should not be passed on to subsequent reductions
eval.try_image(Arc::new(reduced));
evaluation_added = true;
@ -116,7 +118,7 @@ pub(crate) fn perform_reductions(
// Attempt to reduce to indexed
// Keep the existing `png` var in case it is grayscale - we can test both for depth reduction later
let mut indexed = None;
if opts.color_type_reduction && !deadline.passed() {
if opts.color_type_reduction && opts.palette_reduction && !deadline.passed() {
if let Some(reduced) = reduced_to_indexed(&png, opts.grayscale_reduction) {
// Make sure the palette gets sorted (but don't bother evaluating both results)
let new = Arc::new(sorted_palette(&reduced).unwrap_or(reduced));
@ -181,7 +183,7 @@ pub(crate) fn perform_reductions(
if (!cheap || reduced.is_none()) && !deadline.passed() {
if let Some(indexed) = indexed.and_then(|png| reduced_bit_depth_8_or_less(&png)) {
// Only evaluate this if it's different from the first result (which must be grayscale if it exists)
if reduced.as_ref().map_or(true, |r| r.data != indexed.data) {
if reduced.as_ref().is_none_or(|r| r.data != indexed.data) {
eval.try_image(Arc::new(indexed));
evaluation_added = true;
}

View file

@ -4,8 +4,7 @@ use rgb::RGBA8;
use crate::{
colors::{BitDepth, ColorType},
headers::IhdrData,
png::{scan_lines::ScanLine, PngImage},
Interlacing,
png::{PngImage, scan_lines::ScanLine},
};
/// Attempt to reduce the number of colors in the palette, returning the reduced image if successful
@ -87,7 +86,7 @@ pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
let mut enumerated: Vec<_> = palette.iter().enumerate().collect();
// Put the most popular edge color first, which can help slightly if the filter bytes are 0
let keep_first = most_popular_edge_color(palette.len(), png);
let first = enumerated.remove(keep_first);
let first = keep_first.map(|f| enumerated.remove(f));
// Sort the palette
enumerated.sort_by(|a, b| {
@ -104,7 +103,9 @@ pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
};
color_val(a.1).cmp(&color_val(b.1))
});
enumerated.insert(0, first);
if let Some(first) = first {
enumerated.insert(0, first);
}
// Extract the new palette and determine if anything changed
let (remapping, palette): (Vec<_>, Vec<RGBA8>) = enumerated.into_iter().unzip();
@ -132,7 +133,7 @@ pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
#[must_use]
pub fn sorted_palette_mzeng(png: &PngImage) -> Option<PngImage> {
// Interlacing not currently supported
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced != Interlacing::None {
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced {
return None;
}
let palette = match &png.ihdr.color_type {
@ -154,7 +155,7 @@ pub fn sorted_palette_mzeng(png: &PngImage) -> Option<PngImage> {
#[must_use]
pub fn sorted_palette_battiato(png: &PngImage) -> Option<PngImage> {
// Interlacing not currently supported
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced != Interlacing::None {
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced {
return None;
}
let palette = match &png.ihdr.color_type {
@ -204,27 +205,31 @@ fn apply_palette_reorder(png: &PngImage, remapping: &[usize]) -> Option<PngImage
}
// Find the most popular color on the image edges (the pixels neighboring the filter bytes)
fn most_popular_edge_color(num_colors: usize, png: &PngImage) -> usize {
let mut counts = [0u32; 256];
fn most_popular_edge_color(num_colors: usize, png: &PngImage) -> Option<usize> {
let mut counts = [0_u32; 256];
for line in png.scan_lines(false) {
if let &[first, .., last] = line.data {
counts[first as usize] += 1;
counts[last as usize] += 1;
}
}
counts
let max = counts
.iter()
.copied()
.take(num_colors)
.enumerate()
.max_by_key(|&(_, v)| v)
.unwrap_or_default()
.0
.unwrap();
// Ensure there's a clear winner - return None if multiple colors are tied
let max_equal = counts.iter().filter(|&v| v == max.1).count();
if max_equal > 1 {
return None;
}
Some(max.0)
}
// Find the most popular color in the image, along with its count
fn most_popular_color(num_colors: usize, png: &PngImage) -> (usize, u32) {
let mut counts = [0u32; 256];
let mut counts = [0_u32; 256];
for &val in &png.data {
counts[val as usize] += 1;
}
@ -256,7 +261,7 @@ fn apply_most_popular_color(png: &PngImage, remapping: &mut [usize]) {
// Calculate co-occurences matrix
fn co_occurrence_matrix(num_colors: usize, png: &PngImage) -> Vec<Vec<u32>> {
let mut matrix = vec![vec![0u32; num_colors]; num_colors];
let mut matrix = vec![vec![0_u32; num_colors]; num_colors];
let mut prev: Option<ScanLine> = None;
let mut prev_val = None;
for line in png.scan_lines(false) {
@ -278,7 +283,7 @@ fn co_occurrence_matrix(num_colors: usize, png: &PngImage) -> Vec<Vec<u32>> {
matrix[val][prev_val] += 1;
}
}
prev = Some(line)
prev = Some(line);
}
matrix
}

View file

@ -1,6 +1,10 @@
use std::io::Cursor;
use image::{codecs::png::PngDecoder, *};
use log::{error, warn};
use std::io::Cursor;
#[cfg(not(feature = "parallel"))]
use crate::rayon;
/// Validate that the output png data still matches the original image
pub fn validate_output(output: &[u8], original_data: &[u8]) -> bool {
@ -40,7 +44,7 @@ fn load_png_image_from_memory(png_data: &[u8]) -> Result<Vec<RgbaImage>, image::
decoder
.apng()?
.into_frames()
.map(|f| f.map(|f| f.into_buffer()))
.map(|f| f.map(image::Frame::into_buffer))
.collect()
} else {
DynamicImage::from_decoder(decoder).map(|i| vec![i.into_rgba8()])

346
tests/alpha.rs Normal file
View file

@ -0,0 +1,346 @@
use std::{
fs::remove_file,
path::{Path, PathBuf},
};
use oxipng::{internal_tests::*, *};
const GRAYSCALE_ALPHA: u8 = 4;
const RGBA: u8 = 6;
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
let options = oxipng::Options {
force: true,
optimize_alpha: true,
..Default::default()
};
(OutFile::from_path(input.with_extension("out.png")), options)
}
fn test_it_converts(
input: &str,
filter: FilterStrategy,
color_type_in: u8,
bit_depth_in: BitDepth,
color_type_out: u8,
bit_depth_out: BitDepth,
) {
let input = PathBuf::from(input);
let (output, mut opts) = get_opts(&input);
let png = PngData::new(&input, &opts).unwrap();
opts.filters = indexset! {filter};
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
Err(x) => panic!("{}", x),
}
let output = output.path().unwrap();
assert!(output.exists());
let png = match PngData::new(output, &opts) {
Ok(x) => x,
Err(x) => {
remove_file(output).ok();
panic!("{}", x)
}
};
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_out);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out);
if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type {
assert!(palette.len() <= 1 << (png.raw.ihdr.bit_depth as u8));
}
remove_file(output).ok();
}
#[test]
fn alpha_filter_0_for_rgba_16() {
test_it_converts(
"tests/files/filter_0_for_rgba_16.png",
FilterStrategy::NONE,
RGBA,
BitDepth::Sixteen,
RGBA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_1_for_rgba_16() {
test_it_converts(
"tests/files/filter_1_for_rgba_16.png",
FilterStrategy::SUB,
RGBA,
BitDepth::Sixteen,
RGBA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_2_for_rgba_16() {
test_it_converts(
"tests/files/filter_2_for_rgba_16.png",
FilterStrategy::UP,
RGBA,
BitDepth::Sixteen,
RGBA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_3_for_rgba_16() {
test_it_converts(
"tests/files/filter_3_for_rgba_16.png",
FilterStrategy::AVERAGE,
RGBA,
BitDepth::Sixteen,
RGBA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_4_for_rgba_16() {
test_it_converts(
"tests/files/filter_4_for_rgba_16.png",
FilterStrategy::PAETH,
RGBA,
BitDepth::Sixteen,
RGBA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_5_for_rgba_16() {
test_it_converts(
"tests/files/filter_5_for_rgba_16.png",
FilterStrategy::MinSum,
RGBA,
BitDepth::Sixteen,
RGBA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_0_for_rgba_8() {
test_it_converts(
"tests/files/filter_0_for_rgba_8.png",
FilterStrategy::NONE,
RGBA,
BitDepth::Eight,
RGBA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_1_for_rgba_8() {
test_it_converts(
"tests/files/filter_1_for_rgba_8.png",
FilterStrategy::SUB,
RGBA,
BitDepth::Eight,
RGBA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_2_for_rgba_8() {
test_it_converts(
"tests/files/filter_2_for_rgba_8.png",
FilterStrategy::UP,
RGBA,
BitDepth::Eight,
RGBA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_3_for_rgba_8() {
test_it_converts(
"tests/files/filter_3_for_rgba_8.png",
FilterStrategy::AVERAGE,
RGBA,
BitDepth::Eight,
RGBA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_4_for_rgba_8() {
test_it_converts(
"tests/files/filter_4_for_rgba_8.png",
FilterStrategy::PAETH,
RGBA,
BitDepth::Eight,
RGBA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_5_for_rgba_8() {
test_it_converts(
"tests/files/filter_5_for_rgba_8.png",
FilterStrategy::MinSum,
RGBA,
BitDepth::Eight,
RGBA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_0_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_0_for_grayscale_alpha_16.png",
FilterStrategy::NONE,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_1_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_1_for_grayscale_alpha_16.png",
FilterStrategy::SUB,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_2_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_2_for_grayscale_alpha_16.png",
FilterStrategy::UP,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_3_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_3_for_grayscale_alpha_16.png",
FilterStrategy::AVERAGE,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_4_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_4_for_grayscale_alpha_16.png",
FilterStrategy::PAETH,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_5_for_grayscale_alpha_16() {
test_it_converts(
"tests/files/filter_5_for_grayscale_alpha_16.png",
FilterStrategy::MinSum,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
GRAYSCALE_ALPHA,
BitDepth::Sixteen,
);
}
#[test]
fn alpha_filter_0_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_0_for_grayscale_alpha_8.png",
FilterStrategy::NONE,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_1_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_1_for_grayscale_alpha_8.png",
FilterStrategy::SUB,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_2_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_2_for_grayscale_alpha_8.png",
FilterStrategy::UP,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_3_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_3_for_grayscale_alpha_8.png",
FilterStrategy::AVERAGE,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_4_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_4_for_grayscale_alpha_8.png",
FilterStrategy::PAETH,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
BitDepth::Eight,
);
}
#[test]
fn alpha_filter_5_for_grayscale_alpha_8() {
test_it_converts(
"tests/files/filter_5_for_grayscale_alpha_8.png",
FilterStrategy::MinSum,
GRAYSCALE_ALPHA,
BitDepth::Eight,
GRAYSCALE_ALPHA,
BitDepth::Eight,
);
}

BIN
tests/files/c2pa-signed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 285 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 285 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 285 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 285 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 285 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 206 KiB

Some files were not shown because too many files have changed in this diff Show more