Merge branch 'master' into master
This commit is contained in:
commit
9f29aa3afc
16 changed files with 453 additions and 228 deletions
2
.github/workflows/oxipng.yml
vendored
2
.github/workflows/oxipng.yml
vendored
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
- x86_64-apple-darwin
|
||||
toolchain:
|
||||
# Minimum stable
|
||||
- "1.61.0"
|
||||
- "1.65.0"
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
|
|
|
|||
228
Cargo.lock
generated
228
Cargo.lock
generated
|
|
@ -8,6 +8,55 @@ version = "1.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is-terminal",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
|
|
@ -69,27 +118,31 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.25"
|
||||
version = "4.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
|
||||
checksum = "d9394150f5b4273a1763355bd1c2ec54cc5a2593f790587bcd6b2c947cfa9211"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a78fbdd3cc2914ddf37ba444114bc7765bbdcb55ec9cbe6fa054f0137400717"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"bitflags",
|
||||
"clap_lex",
|
||||
"indexmap",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.2.4"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
|
|
@ -97,6 +150,12 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.3.2"
|
||||
|
|
@ -129,9 +188,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.14"
|
||||
version = "0.9.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695"
|
||||
checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
|
|
@ -142,9 +201,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.15"
|
||||
version = "0.8.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
|
||||
checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
|
@ -155,6 +214,33 @@ version = "1.8.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno-dragonfly"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.0"
|
||||
|
|
@ -200,9 +286,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
|||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
|
|
@ -222,6 +308,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.6"
|
||||
|
|
@ -238,53 +330,79 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.144"
|
||||
name = "io-lifetimes"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
|
||||
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.1",
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.1",
|
||||
"io-lifetimes",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.146"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b"
|
||||
|
||||
[[package]]
|
||||
name = "libdeflate-sys"
|
||||
version = "0.11.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb6784b6b84b67d71b4307963d456a9c7c29f9b47c658f533e598de369e34277"
|
||||
checksum = "012437ac39c1e7d7ba12af3aceceb5c93149779aa17c2b1c483f33954957ddc8"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libdeflater"
|
||||
version = "0.11.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8e285aa6a046fd338b2592c16bee148b2b00789138ed6b7bb56bb13d585050d"
|
||||
checksum = "58b30f982ddb14aae2a24a7ed7b3d512d687c2483493f95de7a6d167942a19c3"
|
||||
dependencies = [
|
||||
"libdeflate-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.17"
|
||||
name = "linux-raw-sys"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1"
|
||||
checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
|
@ -341,15 +459,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.17.1"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267"
|
||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
|
||||
[[package]]
|
||||
name = "oxipng"
|
||||
|
|
@ -375,9 +487,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.8"
|
||||
version = "0.17.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aaeebc51f9e7d2c150d3f3bfeb667f2aa985db5ef1e3d212847bdedb488beeaa"
|
||||
checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
|
|
@ -447,6 +559,20 @@ dependencies = [
|
|||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.37.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"io-lifetimes",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
|
|
@ -498,12 +624,6 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.7"
|
||||
|
|
@ -520,6 +640,12 @@ version = "2.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "wild"
|
||||
version = "2.1.0"
|
||||
|
|
|
|||
10
Cargo.toml
10
Cargo.toml
|
|
@ -10,7 +10,7 @@ license = "MIT"
|
|||
name = "oxipng"
|
||||
repository = "https://github.com/shssoichiro/oxipng"
|
||||
version = "8.0.0"
|
||||
rust-version = "1.61.0"
|
||||
rust-version = "1.65.0"
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "shssoichiro/oxipng", branch = "master" }
|
||||
|
|
@ -30,9 +30,9 @@ required-features = ["zopfli"]
|
|||
zopfli = { version = "0.7.4", optional = true, default-features = false, features = ["std", "zlib"] }
|
||||
simd-adler32 = { version = "0.3.5", optional = true, default-features = false }
|
||||
rgb = "0.8.36"
|
||||
indexmap = "1.9.3"
|
||||
libdeflater = "0.11.0"
|
||||
log = "0.4.17"
|
||||
indexmap = "2.0.0"
|
||||
libdeflater = "0.14.0"
|
||||
log = "0.4.19"
|
||||
stderrlog = { version = "0.5.4", optional = true, default-features = false }
|
||||
bitvec = "1.0.1"
|
||||
rustc-hash = "1.1.0"
|
||||
|
|
@ -51,7 +51,7 @@ version = "1.7.0"
|
|||
|
||||
[dependencies.clap]
|
||||
optional = true
|
||||
version = "3.2.25"
|
||||
version = "4.3.8"
|
||||
|
||||
[dependencies.wild]
|
||||
optional = true
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ cargo build --release
|
|||
cp target/release/oxipng /usr/local/bin
|
||||
```
|
||||
|
||||
The current minimum supported Rust version is **1.61.0**.
|
||||
The current minimum supported Rust version is **1.65.0**.
|
||||
|
||||
Oxipng follows Semantic Versioning.
|
||||
|
||||
|
|
@ -115,6 +115,11 @@ method of usage involves creating an
|
|||
passing it, along with an input filename, into the
|
||||
[optimize function](https://docs.rs/oxipng/3.0.1/oxipng/fn.optimize.html).
|
||||
|
||||
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 = "8.0", features = ["parallel", "zopfli", "filetime"], default-features = false }`
|
||||
|
||||
## History
|
||||
|
||||
Oxipng began as a complete rewrite of the OptiPNG project,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ cargo build --release
|
|||
cp target/release/oxipng /usr/local/bin
|
||||
```
|
||||
|
||||
The current minimum supported Rust version is **1.61.0**.
|
||||
The current minimum supported Rust version is **1.65.0**.
|
||||
|
||||
Oxipng follows Semantic Versioning.
|
||||
|
||||
|
|
@ -75,6 +75,11 @@ method of usage involves creating an
|
|||
passing it, along with an input filename, into the
|
||||
[optimize function](https://docs.rs/oxipng/3.0.1/oxipng/fn.optimize.html).
|
||||
|
||||
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 = "8.0", features = ["parallel", "zopfli", "filetime"], default-features = false }`
|
||||
|
||||
## History
|
||||
|
||||
Oxipng began as a complete rewrite of the OptiPNG project,
|
||||
|
|
|
|||
|
|
@ -85,7 +85,10 @@ impl Evaluator {
|
|||
#[cfg(feature = "parallel")]
|
||||
pub fn get_best_candidate(self) -> Option<Candidate> {
|
||||
let (eval_send, eval_recv) = self.eval_channel;
|
||||
drop(eval_send); // disconnect the sender, breaking the loop in the thread
|
||||
// Disconnect the sender, breaking the loop in the thread
|
||||
drop(eval_send);
|
||||
// Yield to ensure evaluations are finished - this can prevent deadlocks when run within an existing thread pool
|
||||
while let Some(rayon::Yield::Executed) = rayon::yield_local() {}
|
||||
eval_recv.into_iter().min_by_key(Candidate::cmp_key)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,9 @@ pub enum StripChunks {
|
|||
|
||||
impl StripChunks {
|
||||
/// List of chunks that will be kept when using the `Safe` option
|
||||
pub const KEEP_SAFE: [[u8; 4]; 4] = [*b"cICP", *b"iCCP", *b"sRGB", *b"pHYs"];
|
||||
pub const KEEP_SAFE: [[u8; 4]; 7] = [
|
||||
*b"cICP", *b"iCCP", *b"sRGB", *b"pHYs", *b"acTL", *b"fcTL", *b"fdAT",
|
||||
];
|
||||
|
||||
pub(crate) fn keep(&self, name: &[u8; 4]) -> bool {
|
||||
match &self {
|
||||
|
|
|
|||
63
src/lib.rs
63
src/lib.rs
|
|
@ -32,6 +32,7 @@ use crate::png::PngImage;
|
|||
use crate::reduction::*;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use rayon::prelude::*;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::fs::{copy, File, Metadata};
|
||||
use std::io::{stdin, stdout, BufWriter, Read, Write};
|
||||
|
|
@ -393,7 +394,7 @@ impl RawImage {
|
|||
deflater: &T,
|
||||
) -> PngResult<Vec<u8>> {
|
||||
let deadline = Arc::new(Deadline::new(opts.timeout));
|
||||
let mut png = optimize_raw(self.png.clone(), opts, deadline, None, deflater)
|
||||
let mut png = optimize_raw(self.png.clone(), opts, deadline.clone(), None, deflater)
|
||||
.ok_or_else(|| PngError::new("Failed to optimize input data"))?;
|
||||
|
||||
// Process aux chunks
|
||||
|
|
@ -403,7 +404,7 @@ impl RawImage {
|
|||
.filter(|c| opts.strip.keep(&c.name))
|
||||
.cloned()
|
||||
.collect();
|
||||
postprocess_chunks(&mut png, opts, &self.png.ihdr, deflater);
|
||||
postprocess_chunks(&mut png, opts, deadline, &self.png.ihdr, deflater);
|
||||
|
||||
Ok(png.output())
|
||||
}
|
||||
|
|
@ -569,17 +570,30 @@ fn optimize_png(
|
|||
debug!(" IDAT size = {} bytes", idat_original_size);
|
||||
debug!(" File size = {} bytes", file_original_size);
|
||||
|
||||
// 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())
|
||||
};
|
||||
if let Some(new_png) = optimize_raw(raw.clone(), opts, deadline, max_size, &opts.deflate) {
|
||||
if let Some(new_png) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_siz, &opts.deflatee) {
|
||||
png.raw = new_png.raw;
|
||||
png.idat_data = new_png.idat_data;
|
||||
}
|
||||
|
||||
postprocess_chunks(png, opts, &raw.ihdr, &opts.deflate);
|
||||
postprocess_chunks(png, &opts, deadline, &raw.ihdr, &opts.deflate);
|
||||
|
||||
let output = png.output();
|
||||
|
||||
|
|
@ -847,8 +861,13 @@ fn report_format(prefix: &str, png: &PngImage) {
|
|||
}
|
||||
|
||||
/// Perform cleanup of certain chunks from the `PngData` object, after optimization has been completed
|
||||
fn postprocess_chunks<T>(png: &mut PngData, opts: &Options, orig_ihdr: &IhdrData, deflater: &T)
|
||||
where T: Deflater {
|
||||
fn postprocess_chunks(
|
||||
png: &mut PngData,
|
||||
opts: &Options,
|
||||
deadline: Arc<Deadline>,
|
||||
orig_ihdr: &IhdrData,
|
||||
deflater: &T,
|
||||
) where T: Deflater {
|
||||
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");
|
||||
|
|
@ -901,6 +920,38 @@ fn postprocess_chunks<T>(png: &mut PngData, opts: &Options, orig_ihdr: &IhdrData
|
|||
!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 !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
|
||||
|
|
|
|||
235
src/main.rs
235
src/main.rs
|
|
@ -13,7 +13,7 @@
|
|||
#![warn(clippy::range_plus_one)]
|
||||
#![allow(clippy::cognitive_complexity)]
|
||||
|
||||
use clap::{AppSettings, Arg, ArgAction, ArgMatches, Command};
|
||||
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
|
||||
use indexmap::IndexSet;
|
||||
use log::{error, warn};
|
||||
use oxipng::Deflaters;
|
||||
|
|
@ -31,49 +31,43 @@ fn main() {
|
|||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.author("Joshua Holmer <jholmer.in@gmail.com>")
|
||||
.about("Losslessly improves compression of PNG files")
|
||||
.setting(AppSettings::DeriveDisplayOrder)
|
||||
.arg(
|
||||
Arg::new("files")
|
||||
.help("File(s) to compress (use \"-\" for stdin)")
|
||||
.index(1)
|
||||
.multiple_values(true)
|
||||
.num_args(1..)
|
||||
.use_value_delimiter(false)
|
||||
.required(true),
|
||||
.required(true)
|
||||
.value_parser(value_parser!(PathBuf)),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("optimization")
|
||||
.help("Optimization level - Default: 2")
|
||||
.short('o')
|
||||
.long("opt")
|
||||
.takes_value(true)
|
||||
.value_name("level")
|
||||
.possible_value("0")
|
||||
.possible_value("1")
|
||||
.possible_value("2")
|
||||
.possible_value("3")
|
||||
.possible_value("4")
|
||||
.possible_value("5")
|
||||
.possible_value("6")
|
||||
.possible_value("max"),
|
||||
.value_parser(["0", "1", "2", "3", "4", "5", "6", "max"]),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("backup")
|
||||
.help("Back up modified files")
|
||||
.short('b')
|
||||
.long("backup"),
|
||||
.long("backup")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("recursive")
|
||||
.help("Recurse into subdirectories")
|
||||
.short('r')
|
||||
.long("recursive"),
|
||||
.long("recursive")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("output_dir")
|
||||
.help("Write output file(s) to <directory>")
|
||||
.long("dir")
|
||||
.takes_value(true)
|
||||
.value_name("directory")
|
||||
.value_parser(value_parser!(PathBuf))
|
||||
.conflicts_with("output_file")
|
||||
.conflicts_with("stdout"),
|
||||
)
|
||||
|
|
@ -81,8 +75,8 @@ fn main() {
|
|||
Arg::new("output_file")
|
||||
.help("Write output file to <file>")
|
||||
.long("out")
|
||||
.takes_value(true)
|
||||
.value_name("file")
|
||||
.value_parser(value_parser!(PathBuf))
|
||||
.conflicts_with("output_dir")
|
||||
.conflicts_with("stdout"),
|
||||
)
|
||||
|
|
@ -90,6 +84,7 @@ fn main() {
|
|||
Arg::new("stdout")
|
||||
.help("Write output to stdout")
|
||||
.long("stdout")
|
||||
.action(ArgAction::SetTrue)
|
||||
.conflicts_with("output_dir")
|
||||
.conflicts_with("output_file"),
|
||||
)
|
||||
|
|
@ -97,31 +92,34 @@ fn main() {
|
|||
Arg::new("preserve")
|
||||
.help("Preserve file attributes if possible")
|
||||
.short('p')
|
||||
.long("preserve"),
|
||||
.long("preserve")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("check")
|
||||
.help("Do not run any optimization passes")
|
||||
.short('c')
|
||||
.long("check"),
|
||||
.long("check")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("pretend")
|
||||
.help("Do not write any files, only calculate compression gains")
|
||||
.short('P')
|
||||
.long("pretend"),
|
||||
.long("pretend")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("strip-safe")
|
||||
.help("Strip safely-removable metadata objects")
|
||||
.short('s')
|
||||
.action(ArgAction::SetTrue)
|
||||
.conflicts_with("strip"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("strip")
|
||||
.help("Strip metadata objects ['safe', 'all', or comma-separated list]")
|
||||
.help("Strip metadata objects ['safe', 'all', or comma-separated list]\nCAUTION: stripping 'all' will convert APNGs to standard PNGs")
|
||||
.long("strip")
|
||||
.takes_value(true)
|
||||
.value_name("mode")
|
||||
.conflicts_with("strip-safe"),
|
||||
)
|
||||
|
|
@ -129,7 +127,6 @@ fn main() {
|
|||
Arg::new("keep")
|
||||
.help("Strip all optional metadata except objects in the comma-separated list")
|
||||
.long("keep")
|
||||
.takes_value(true)
|
||||
.value_name("list")
|
||||
.conflicts_with("strip")
|
||||
.conflicts_with("strip-safe"),
|
||||
|
|
@ -138,23 +135,22 @@ fn main() {
|
|||
Arg::new("alpha")
|
||||
.help("Perform additional alpha optimizations")
|
||||
.short('a')
|
||||
.long("alpha"),
|
||||
.long("alpha")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("interlace")
|
||||
.help("PNG interlace type - Default: 0")
|
||||
.short('i')
|
||||
.long("interlace")
|
||||
.takes_value(true)
|
||||
.value_name("type")
|
||||
.possible_value("0")
|
||||
.possible_value("1")
|
||||
.possible_value("keep"),
|
||||
.value_parser(["0", "1", "keep"]),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("scale16")
|
||||
.help("Forcibly reduce 16-bit images to 8-bit")
|
||||
.long("scale16"),
|
||||
.long("scale16")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("verbose")
|
||||
|
|
@ -169,33 +165,29 @@ fn main() {
|
|||
.help("Run in quiet mode")
|
||||
.short('q')
|
||||
.long("quiet")
|
||||
.action(ArgAction::SetTrue)
|
||||
.conflicts_with("verbose"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("filters")
|
||||
.help(&*format!(
|
||||
"PNG delta filters (0-{}) - Default: 0,{}",
|
||||
RowFilter::LAST,
|
||||
RowFilter::MinSum as u8
|
||||
))
|
||||
.help(format!("PNG delta filters (0-{})", RowFilter::LAST))
|
||||
.short('f')
|
||||
.long("filters")
|
||||
.takes_value(true)
|
||||
.validator(|x| match parse_numeric_range_opts(x, 0, RowFilter::LAST) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err("Invalid option for filters".to_owned()),
|
||||
.value_parser(|x: &str| {
|
||||
parse_numeric_range_opts(x, 0, RowFilter::LAST)
|
||||
.map_err(|_| "Invalid option for filters")
|
||||
}),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("fast")
|
||||
.help("Use fast filter evaluation")
|
||||
.long("fast"),
|
||||
.long("fast")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("compression")
|
||||
.help("zlib compression level (1-12) - Default: 11")
|
||||
.help("zlib compression level (1-12)")
|
||||
.long("zc")
|
||||
.takes_value(true)
|
||||
.value_name("level")
|
||||
.value_parser(1..=12)
|
||||
.conflicts_with("zopfli"),
|
||||
|
|
@ -203,65 +195,72 @@ fn main() {
|
|||
.arg(
|
||||
Arg::new("no-bit-reduction")
|
||||
.help("No bit depth reduction")
|
||||
.long("nb"),
|
||||
.long("nb")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-color-reduction")
|
||||
.help("No color type reduction")
|
||||
.long("nc"),
|
||||
.long("nc")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-palette-reduction")
|
||||
.help("No palette reduction")
|
||||
.long("np"),
|
||||
.long("np")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-grayscale-reduction")
|
||||
.help("No grayscale reduction")
|
||||
.long("ng"),
|
||||
.long("ng")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-reductions")
|
||||
.help("No reductions")
|
||||
.long("nx")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(Arg::new("no-reductions").help("No reductions").long("nx"))
|
||||
.arg(
|
||||
Arg::new("no-recoding")
|
||||
.help("No IDAT recoding unless necessary")
|
||||
.long("nz"),
|
||||
.long("nz")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("fix")
|
||||
.help("Enable error recovery")
|
||||
.long("fix")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(Arg::new("fix").help("Enable error recovery").long("fix"))
|
||||
.arg(
|
||||
Arg::new("force")
|
||||
.help("Write the output even if it is larger than the input")
|
||||
.long("force"),
|
||||
.long("force")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("zopfli")
|
||||
.help("Use the slower but better compressing Zopfli algorithm")
|
||||
.short('Z')
|
||||
.long("zopfli"),
|
||||
.long("zopfli")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("timeout")
|
||||
.help("Maximum amount of time, in seconds, to spend on optimizations")
|
||||
.takes_value(true)
|
||||
.value_name("secs")
|
||||
.long("timeout"),
|
||||
.long("timeout")
|
||||
.value_parser(value_parser!(u64)),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("threads")
|
||||
.help("Set number of threads to use - default 1.5x CPU cores")
|
||||
.help("Set number of threads to use - Default: num CPU cores")
|
||||
.long("threads")
|
||||
.short('t')
|
||||
.takes_value(true)
|
||||
.value_name("num")
|
||||
.validator(|x| match x.parse::<usize>() {
|
||||
Ok(val) => {
|
||||
if val > 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Thread count must be >= 1".to_owned())
|
||||
}
|
||||
}
|
||||
Err(_) => Err("Thread count must be >= 1".to_owned()),
|
||||
}),
|
||||
.value_parser(value_parser!(usize)),
|
||||
)
|
||||
.after_help(
|
||||
"Optimization levels:
|
||||
|
|
@ -302,13 +301,13 @@ Heuristic filter selection strategies:
|
|||
|
||||
let files = collect_files(
|
||||
matches
|
||||
.values_of("files")
|
||||
.get_many::<PathBuf>("files")
|
||||
.unwrap()
|
||||
.map(PathBuf::from)
|
||||
.cloned()
|
||||
.collect(),
|
||||
&out_dir,
|
||||
&out_file,
|
||||
matches.is_present("recursive"),
|
||||
matches.get_flag("recursive"),
|
||||
true,
|
||||
);
|
||||
|
||||
|
|
@ -385,19 +384,19 @@ fn parse_opts_into_struct(
|
|||
) -> Result<(OutFile, Option<PathBuf>, Options), String> {
|
||||
stderrlog::new()
|
||||
.module(module_path!())
|
||||
.quiet(matches.is_present("quiet"))
|
||||
.quiet(matches.get_flag("quiet"))
|
||||
.verbosity(matches.get_count("verbose") as usize + 2)
|
||||
.show_level(false)
|
||||
.init()
|
||||
.unwrap();
|
||||
|
||||
let mut opts = match matches.value_of("optimization") {
|
||||
let mut opts = match matches.get_one::<String>("optimization") {
|
||||
None => Options::default(),
|
||||
Some("max") => Options::max_compression(),
|
||||
Some(x) if x == "max" => Options::max_compression(),
|
||||
Some(level) => Options::from_preset(level.parse::<u8>().unwrap()),
|
||||
};
|
||||
|
||||
if let Some(x) = matches.value_of("interlace") {
|
||||
if let Some(x) = matches.get_one::<String>("interlace") {
|
||||
opts.interlace = if x == "keep" {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -405,110 +404,76 @@ fn parse_opts_into_struct(
|
|||
};
|
||||
}
|
||||
|
||||
if let Some(x) = matches.value_of("filters") {
|
||||
if let Some(x) = matches.get_one::<IndexSet<u8>>("filters") {
|
||||
opts.filter.clear();
|
||||
for f in parse_numeric_range_opts(x, 0, RowFilter::LAST).unwrap() {
|
||||
for &f in x {
|
||||
opts.filter.insert(f.try_into().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(x) = matches.value_of("timeout") {
|
||||
let num = x
|
||||
.parse()
|
||||
.map_err(|_| "Timeout must be a number".to_owned())?;
|
||||
if let Some(&num) = matches.get_one::<u64>("timeout") {
|
||||
opts.timeout = Some(Duration::from_secs(num));
|
||||
}
|
||||
|
||||
let out_dir = if let Some(x) = matches.value_of("output_dir") {
|
||||
let path = PathBuf::from(x);
|
||||
let out_dir = if let Some(path) = matches.get_one::<PathBuf>("output_dir") {
|
||||
if !path.exists() {
|
||||
match DirBuilder::new().recursive(true).create(&path) {
|
||||
match DirBuilder::new().recursive(true).create(path) {
|
||||
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",
|
||||
x
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
Some(path)
|
||||
Some(path.to_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let out_file = if matches.is_present("stdout") {
|
||||
let out_file = if matches.get_flag("stdout") {
|
||||
OutFile::StdOut
|
||||
} else if let Some(x) = matches.value_of("output_file") {
|
||||
OutFile::Path(Some(PathBuf::from(x)))
|
||||
} else {
|
||||
OutFile::Path(None)
|
||||
OutFile::Path(matches.get_one::<PathBuf>("output_file").cloned())
|
||||
};
|
||||
|
||||
if matches.is_present("alpha") {
|
||||
opts.optimize_alpha = true;
|
||||
}
|
||||
opts.optimize_alpha = matches.get_flag("alpha");
|
||||
|
||||
if matches.is_present("scale16") {
|
||||
opts.scale_16 = true;
|
||||
}
|
||||
opts.scale_16 = matches.get_flag("scale16");
|
||||
|
||||
if matches.is_present("fast") {
|
||||
opts.fast_evaluation = true;
|
||||
}
|
||||
opts.fast_evaluation = matches.get_flag("fast");
|
||||
|
||||
if matches.is_present("backup") {
|
||||
opts.backup = true;
|
||||
}
|
||||
opts.backup = matches.get_flag("backup");
|
||||
|
||||
if matches.is_present("force") {
|
||||
opts.force = true;
|
||||
}
|
||||
opts.force = matches.get_flag("force");
|
||||
|
||||
if matches.is_present("fix") {
|
||||
opts.fix_errors = true;
|
||||
}
|
||||
opts.fix_errors = matches.get_flag("fix");
|
||||
|
||||
if matches.is_present("check") {
|
||||
opts.check = true;
|
||||
}
|
||||
opts.check = matches.get_flag("check");
|
||||
|
||||
if matches.is_present("pretend") {
|
||||
opts.pretend = true;
|
||||
}
|
||||
opts.pretend = matches.get_flag("pretend");
|
||||
|
||||
if matches.is_present("preserve") {
|
||||
opts.preserve_attrs = true;
|
||||
}
|
||||
opts.preserve_attrs = matches.get_flag("preserve");
|
||||
|
||||
if matches.is_present("no-bit-reduction") {
|
||||
opts.bit_depth_reduction = false;
|
||||
}
|
||||
opts.bit_depth_reduction = !matches.get_flag("no-bit-reduction");
|
||||
|
||||
if matches.is_present("no-color-reduction") {
|
||||
opts.color_type_reduction = false;
|
||||
}
|
||||
opts.color_type_reduction = !matches.get_flag("no-color-reduction");
|
||||
|
||||
if matches.is_present("no-palette-reduction") {
|
||||
opts.palette_reduction = false;
|
||||
}
|
||||
opts.palette_reduction = !matches.get_flag("no-palette-reduction");
|
||||
|
||||
if matches.is_present("no-grayscale-reduction") {
|
||||
opts.grayscale_reduction = false;
|
||||
}
|
||||
opts.grayscale_reduction = !matches.get_flag("no-grayscale-reduction");
|
||||
|
||||
if matches.is_present("no-reductions") {
|
||||
if matches.get_flag("no-reductions") {
|
||||
opts.bit_depth_reduction = false;
|
||||
opts.color_type_reduction = false;
|
||||
opts.palette_reduction = false;
|
||||
opts.grayscale_reduction = false;
|
||||
}
|
||||
|
||||
if matches.is_present("no-recoding") {
|
||||
opts.idat_recoding = false;
|
||||
}
|
||||
opts.idat_recoding = !matches.get_flag("no-recoding");
|
||||
|
||||
if let Some(keep) = matches.value_of("keep") {
|
||||
if let Some(keep) = matches.get_one::<String>("keep") {
|
||||
let names = keep
|
||||
.split(',')
|
||||
.map(parse_chunk_name)
|
||||
|
|
@ -516,7 +481,7 @@ fn parse_opts_into_struct(
|
|||
opts.strip = StripChunks::Keep(names)
|
||||
}
|
||||
|
||||
if let Some(strip) = matches.value_of("strip") {
|
||||
if let Some(strip) = matches.get_one::<String>("strip") {
|
||||
if strip == "safe" {
|
||||
opts.strip = StripChunks::Safe;
|
||||
} else if strip == "all" {
|
||||
|
|
@ -544,11 +509,11 @@ fn parse_opts_into_struct(
|
|||
}
|
||||
}
|
||||
|
||||
if matches.is_present("strip-safe") {
|
||||
if matches.get_flag("strip-safe") {
|
||||
opts.strip = StripChunks::Safe;
|
||||
}
|
||||
|
||||
if matches.is_present("zopfli") {
|
||||
if matches.get_flag("zopfli") {
|
||||
#[cfg(feature = "zopfli")]
|
||||
let zopfli_opts = zopfli::Options::default();
|
||||
opts.deflate = Deflaters::Zopfli { options: zopfli_opts };
|
||||
|
|
@ -559,9 +524,7 @@ fn parse_opts_into_struct(
|
|||
}
|
||||
|
||||
#[cfg(feature = "parallel")]
|
||||
if let Some(x) = matches.value_of("threads") {
|
||||
let threads = x.parse::<usize>().unwrap();
|
||||
|
||||
if let Some(&threads) = matches.get_one::<usize>("threads") {
|
||||
rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(threads)
|
||||
.build_global()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use crate::interlace::{deinterlace_image, interlace_image, Interlacing};
|
|||
use crate::Options;
|
||||
use bitvec::bitarr;
|
||||
use libdeflater::{CompressionLvl, Compressor};
|
||||
use log::warn;
|
||||
use rgb::ComponentSlice;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::fs::File;
|
||||
|
|
@ -93,8 +94,16 @@ impl PngData {
|
|||
let mut aux_chunks: Vec<Chunk> = Vec::new();
|
||||
while let Some(chunk) = parse_next_chunk(byte_data, &mut byte_offset, opts.fix_errors)? {
|
||||
match &chunk.name {
|
||||
b"IDAT" => idat_data.extend_from_slice(chunk.data),
|
||||
b"acTL" => return Err(PngError::APNGNotSupported),
|
||||
b"IDAT" => {
|
||||
if idat_data.is_empty() {
|
||||
// Keep track of where the first IDAT sits relative to other chunks
|
||||
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());
|
||||
}
|
||||
|
|
@ -104,6 +113,10 @@ impl PngData {
|
|||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -165,9 +178,10 @@ impl PngData {
|
|||
ihdr_data.write_all(&[0]).ok(); // Filter method -- 5-way adaptive filtering
|
||||
ihdr_data.write_all(&[self.raw.ihdr.interlaced as u8]).ok();
|
||||
write_png_block(b"IHDR", &ihdr_data, &mut output);
|
||||
// Ancillary chunks
|
||||
for chunk in self
|
||||
.aux_chunks
|
||||
// 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();
|
||||
for chunk in aux_pre
|
||||
.iter()
|
||||
.filter(|c| !(&c.name == b"bKGD" || &c.name == b"hIST" || &c.name == b"tRNS"))
|
||||
{
|
||||
|
|
@ -202,8 +216,7 @@ impl PngData {
|
|||
_ => {}
|
||||
}
|
||||
// Special ancillary chunks that need to come after PLTE but before IDAT
|
||||
for chunk in self
|
||||
.aux_chunks
|
||||
for chunk in aux_pre
|
||||
.iter()
|
||||
.filter(|c| &c.name == b"bKGD" || &c.name == b"hIST" || &c.name == b"tRNS")
|
||||
{
|
||||
|
|
@ -211,6 +224,12 @@ impl PngData {
|
|||
}
|
||||
// IDAT data
|
||||
write_png_block(b"IDAT", &self.idat_data, &mut output);
|
||||
// Ancillary chunks that come after IDAT
|
||||
for aux_post in aux_split {
|
||||
for chunk in aux_post {
|
||||
write_png_block(&chunk.name, &chunk.data, &mut output);
|
||||
}
|
||||
}
|
||||
// Stream end
|
||||
write_png_block(b"IEND", &[], &mut output);
|
||||
|
||||
|
|
|
|||
18
src/rayon.rs
18
src/rayon.rs
|
|
@ -26,6 +26,12 @@ pub trait IntoParallelRefIterator<'data> {
|
|||
fn par_iter(&'data self) -> Self::Iter;
|
||||
}
|
||||
|
||||
pub trait IntoParallelRefMutIterator<'data> {
|
||||
type Iter: ParallelIterator<Item = Self::Item>;
|
||||
type Item: Send + 'data;
|
||||
fn par_iter_mut(&'data mut self) -> Self::Iter;
|
||||
}
|
||||
|
||||
impl<I: IntoIterator> IntoParallelIterator for I
|
||||
where
|
||||
I::Item: Send,
|
||||
|
|
@ -50,6 +56,18 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'data, I: 'data + ?Sized> IntoParallelRefMutIterator<'data> for I
|
||||
where
|
||||
&'data mut I: IntoParallelIterator,
|
||||
{
|
||||
type Iter = <&'data mut I as IntoParallelIterator>::Iter;
|
||||
type Item = <&'data mut I as IntoParallelIterator>::Item;
|
||||
|
||||
fn par_iter_mut(&'data mut self) -> Self::Iter {
|
||||
self.into_par_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Iterator> ParallelIterator for I {}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
|
|
|||
|
|
@ -57,8 +57,16 @@ pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option<Png
|
|||
}
|
||||
|
||||
let transparency_pixel = if has_transparency {
|
||||
// For grayscale, start by checking 4 specific values in the hope that we may reduce depth
|
||||
let unused = match png.ihdr.color_type {
|
||||
ColorType::GrayscaleAlpha => [0x00, 0xFF, 0x55, 0xAA]
|
||||
.into_iter()
|
||||
.find(|&v| !used_colors[v as usize]),
|
||||
_ => None,
|
||||
}
|
||||
.or_else(|| used_colors.iter().position(|&u| !u).map(|v| v as u8));
|
||||
// If no unused color was found we will have to fail here
|
||||
Some(used_colors.iter().position(|b| !*b)? as u8)
|
||||
Some(unused?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
use image::{DynamicImage, GenericImageView, ImageFormat, Pixel};
|
||||
use image::{codecs::png::PngDecoder, *};
|
||||
use log::{error, warn};
|
||||
use std::io::Cursor;
|
||||
|
||||
/// Validate that the output png data still matches the original image
|
||||
pub fn validate_output(output: &[u8], original_data: &[u8]) -> bool {
|
||||
let (old_png, new_png) = rayon::join(
|
||||
let (old_frames, new_frames) = rayon::join(
|
||||
|| load_png_image_from_memory(original_data),
|
||||
|| load_png_image_from_memory(output),
|
||||
);
|
||||
|
||||
match (new_png, old_png) {
|
||||
match (new_frames, old_frames) {
|
||||
(Err(new_err), _) => {
|
||||
error!("Failed to read output image for validation: {}", new_err);
|
||||
false
|
||||
|
|
@ -21,26 +20,40 @@ pub fn validate_output(output: &[u8], original_data: &[u8]) -> bool {
|
|||
warn!("Failed to read input image for validation: {}", old_err);
|
||||
true
|
||||
}
|
||||
(Ok(new_png), Ok(old_png)) => images_equal(&old_png, &new_png),
|
||||
(Ok(new_frames), Ok(old_frames)) if new_frames.len() != old_frames.len() => false,
|
||||
(Ok(new_frames), Ok(old_frames)) => {
|
||||
for (a, b) in old_frames.iter().zip(new_frames) {
|
||||
if !images_equal(&a, &b) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a PNG image from memory to a [DynamicImage]
|
||||
fn load_png_image_from_memory(png_data: &[u8]) -> Result<DynamicImage, image::ImageError> {
|
||||
let mut reader = image::io::Reader::new(Cursor::new(png_data));
|
||||
reader.set_format(ImageFormat::Png);
|
||||
reader.no_limits();
|
||||
reader.decode()
|
||||
/// Loads a PNG image from memory to frames of [RgbaImage]
|
||||
fn load_png_image_from_memory(png_data: &[u8]) -> Result<Vec<RgbaImage>, image::ImageError> {
|
||||
let decoder = PngDecoder::new(png_data)?;
|
||||
if decoder.is_apng() {
|
||||
decoder
|
||||
.apng()
|
||||
.into_frames()
|
||||
.map(|f| f.map(|f| f.into_buffer()))
|
||||
.collect()
|
||||
} else {
|
||||
DynamicImage::from_decoder(decoder).map(|i| vec![i.into_rgba8()])
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares images pixel by pixel for equivalent content
|
||||
fn images_equal(old_png: &DynamicImage, new_png: &DynamicImage) -> bool {
|
||||
fn images_equal(old_png: &RgbaImage, new_png: &RgbaImage) -> bool {
|
||||
let a = old_png.pixels().filter(|x| {
|
||||
let p = x.2.channels();
|
||||
let p = x.channels();
|
||||
!(p.len() == 4 && p[3] == 0)
|
||||
});
|
||||
let b = new_png.pixels().filter(|x| {
|
||||
let p = x.2.channels();
|
||||
let p = x.channels();
|
||||
!(p.len() == 4 && p[3] == 0)
|
||||
});
|
||||
a.eq(b)
|
||||
|
|
|
|||
BIN
tests/files/grayscale_alpha_8_should_be_grayscale_trns_1.png
Normal file
BIN
tests/files/grayscale_alpha_8_should_be_grayscale_trns_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
|
|
@ -30,7 +30,7 @@ fn optimize_from_memory_apng() {
|
|||
in_file.read_to_end(&mut in_file_buf).unwrap();
|
||||
|
||||
let result = oxipng::optimize_from_memory(&in_file_buf, &Options::default());
|
||||
assert!(result.is_err());
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -58,9 +58,9 @@ fn optimize_apng() {
|
|||
let result = oxipng::optimize(
|
||||
&"tests/files/apng_file.png".into(),
|
||||
&OutFile::Path(None),
|
||||
&Options::default(),
|
||||
&Options::from_preset(0),
|
||||
);
|
||||
assert!(result.is_err());
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -916,6 +916,18 @@ fn grayscale_alpha_8_should_be_grayscale_trns_8() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grayscale_alpha_8_should_be_grayscale_trns_1() {
|
||||
test_it_converts(
|
||||
"tests/files/grayscale_alpha_8_should_be_grayscale_trns_1.png",
|
||||
true,
|
||||
GRAYSCALE_ALPHA,
|
||||
BitDepth::Eight,
|
||||
GRAYSCALE,
|
||||
BitDepth::One,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_files() {
|
||||
test_it_converts(
|
||||
|
|
|
|||
Loading…
Reference in a new issue