From 798fdfe24cabaf833d5528c2282c81e4249e27f9 Mon Sep 17 00:00:00 2001 From: andrews05 Date: Mon, 26 Jun 2023 06:24:23 +1200 Subject: [PATCH 1/5] Update rust and dependencies (#525) * Bump rust version * Update Clap to v4 * Update other dependencies --- .github/workflows/oxipng.yml | 2 +- Cargo.lock | 224 +++++++++++++++++++++++++-------- Cargo.toml | 10 +- README.md | 2 +- README.template.md | 2 +- src/main.rs | 233 +++++++++++++++-------------------- 6 files changed, 281 insertions(+), 192 deletions(-) diff --git a/.github/workflows/oxipng.yml b/.github/workflows/oxipng.yml index eb0edabf1..a51fa5b8e 100644 --- a/.github/workflows/oxipng.yml +++ b/.github/workflows/oxipng.yml @@ -22,7 +22,7 @@ jobs: - x86_64-apple-darwin toolchain: # Minimum stable - - "1.61.0" + - "1.65.0" - stable - beta - nightly diff --git a/Cargo.lock b/Cargo.lock index 6dc42816a..aa4dc97c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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 = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +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.144" +version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +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 = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "log" -version = "0.4.17" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" - -[[package]] -name = "os_str_bytes" -version = "6.5.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "oxipng" @@ -374,9 +486,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", @@ -446,6 +558,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" @@ -497,12 +623,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" @@ -519,6 +639,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" diff --git a/Cargo.toml b/Cargo.toml index cf3ab8dc9..20f25ffc9 100644 --- a/Cargo.toml +++ b/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" } @@ -29,9 +29,9 @@ required-features = ["zopfli"] [dependencies] zopfli = { version = "0.7.4", optional = true, default-features = false, features = ["std", "zlib"] } 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" @@ -50,7 +50,7 @@ version = "1.7.0" [dependencies.clap] optional = true -version = "3.2.25" +version = "4.3.8" [dependencies.wild] optional = true diff --git a/README.md b/README.md index b8d13c28c..d2e7262e5 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/README.template.md b/README.template.md index 4da791e70..265e9f9b3 100644 --- a/README.template.md +++ b/README.template.md @@ -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. diff --git a/src/main.rs b/src/main.rs index 71c66fa3a..d848d5eff 100644 --- a/src/main.rs +++ b/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; @@ -33,49 +33,43 @@ fn main() { .version(env!("CARGO_PKG_VERSION")) .author("Joshua Holmer ") .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 ") .long("dir") - .takes_value(true) .value_name("directory") + .value_parser(value_parser!(PathBuf)) .conflicts_with("output_file") .conflicts_with("stdout"), ) @@ -83,8 +77,8 @@ fn main() { Arg::new("output_file") .help("Write output file to ") .long("out") - .takes_value(true) .value_name("file") + .value_parser(value_parser!(PathBuf)) .conflicts_with("output_dir") .conflicts_with("stdout"), ) @@ -92,6 +86,7 @@ fn main() { Arg::new("stdout") .help("Write output to stdout") .long("stdout") + .action(ArgAction::SetTrue) .conflicts_with("output_dir") .conflicts_with("output_file"), ) @@ -99,31 +94,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]") .long("strip") - .takes_value(true) .value_name("mode") .conflicts_with("strip-safe"), ) @@ -131,7 +129,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"), @@ -140,23 +137,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") @@ -171,33 +167,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"), @@ -205,65 +197,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::() { - 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: @@ -304,13 +303,13 @@ Heuristic filter selection strategies: let files = collect_files( matches - .values_of("files") + .get_many::("files") .unwrap() - .map(PathBuf::from) + .cloned() .collect(), &out_dir, &out_file, - matches.is_present("recursive"), + matches.get_flag("recursive"), true, ); @@ -387,19 +386,19 @@ fn parse_opts_into_struct( ) -> Result<(OutFile, Option, 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::("optimization") { None => Options::default(), - Some("max") => Options::max_compression(), + Some(x) if x == "max" => Options::max_compression(), Some(level) => Options::from_preset(level.parse::().unwrap()), }; - if let Some(x) = matches.value_of("interlace") { + if let Some(x) = matches.get_one::("interlace") { opts.interlace = if x == "keep" { None } else { @@ -407,110 +406,76 @@ fn parse_opts_into_struct( }; } - if let Some(x) = matches.value_of("filters") { + if let Some(x) = matches.get_one::>("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::("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::("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::("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::("keep") { let names = keep .split(',') .map(parse_chunk_name) @@ -518,7 +483,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::("strip") { if strip == "safe" { opts.strip = StripChunks::Safe; } else if strip == "all" { @@ -546,11 +511,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")] if let Some(iterations) = NonZeroU8::new(15) { opts.deflate = Deflaters::Zopfli { iterations }; @@ -562,9 +527,7 @@ fn parse_opts_into_struct( } #[cfg(feature = "parallel")] - if let Some(x) = matches.value_of("threads") { - let threads = x.parse::().unwrap(); - + if let Some(&threads) = matches.get_one::("threads") { rayon::ThreadPoolBuilder::new() .num_threads(threads) .build_global() From 129f1e6f76de1cacd55a4d95f066c94dc0a44dd4 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 19 Jun 2023 22:34:34 +1200 Subject: [PATCH 2/5] Try to fix deadlock in parallel mode --- src/evaluate.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/evaluate.rs b/src/evaluate.rs index 0d1871983..da84ac131 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -85,7 +85,10 @@ impl Evaluator { #[cfg(feature = "parallel")] pub fn get_best_candidate(self) -> Option { 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) } From d81236f71e47e9ed11f6d300095b3ba5f52f173c Mon Sep 17 00:00:00 2001 From: Josh Holmer Date: Wed, 5 Jul 2023 00:44:06 -0400 Subject: [PATCH 3/5] Add default features note to readme (#528) --- README.md | 5 +++++ README.template.md | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/README.md b/README.md index d2e7262e5..04189a9ce 100644 --- a/README.md +++ b/README.md @@ -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, diff --git a/README.template.md b/README.template.md index 265e9f9b3..6f14ed8d4 100644 --- a/README.template.md +++ b/README.template.md @@ -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, From 75a0f0de95854deaaa00170a6d9e9ae5af0dedb9 Mon Sep 17 00:00:00 2001 From: andrews05 Date: Wed, 5 Jul 2023 16:47:43 +1200 Subject: [PATCH 4/5] Allow APNG with reductions disabled (#511) --- src/headers.rs | 4 ++- src/lib.rs | 61 ++++++++++++++++++++++++++++++++++++++++---- src/main.rs | 2 +- src/png/mod.rs | 33 +++++++++++++++++++----- src/rayon.rs | 18 +++++++++++++ src/sanity_checks.rs | 41 +++++++++++++++++++---------- tests/lib.rs | 6 ++--- 7 files changed, 134 insertions(+), 31 deletions(-) diff --git a/src/headers.rs b/src/headers.rs index 274173d74..8a70a85ee 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -86,7 +86,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 { diff --git a/src/lib.rs b/src/lib.rs index d870da55c..13242044e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,7 @@ use crate::png::PngImage; use crate::reduction::*; use log::{debug, 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}; @@ -388,7 +389,7 @@ impl RawImage { /// Create an optimized png from the raw image data using the options provided pub fn create_optimized_png(&self, opts: &Options) -> PngResult> { let deadline = Arc::new(Deadline::new(opts.timeout)); - let mut png = optimize_raw(self.png.clone(), opts, deadline, None) + 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 @@ -398,7 +399,7 @@ impl RawImage { .filter(|c| opts.strip.keep(&c.name)) .cloned() .collect(); - postprocess_chunks(&mut png, opts, &self.png.ihdr); + postprocess_chunks(&mut png, opts, deadline, &self.png.ihdr); Ok(png.output()) } @@ -564,17 +565,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) { + 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; } - postprocess_chunks(png, opts, &raw.ihdr); + postprocess_chunks(png, &opts, deadline, &raw.ihdr); let output = png.output(); @@ -844,7 +858,12 @@ fn report_format(prefix: &str, png: &PngImage) { } /// Perform cleanup of certain chunks from the `PngData` object, after optimization has been completed -fn postprocess_chunks(png: &mut PngData, opts: &Options, orig_ihdr: &IhdrData) { +fn postprocess_chunks( + png: &mut PngData, + opts: &Options, + deadline: Arc, + 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"); @@ -897,6 +916,38 @@ fn postprocess_chunks(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 diff --git a/src/main.rs b/src/main.rs index d848d5eff..519d68b4f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -120,7 +120,7 @@ fn main() { ) .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") .value_name("mode") .conflicts_with("strip-safe"), diff --git a/src/png/mod.rs b/src/png/mod.rs index 49f7a89dc..46ccc04a0 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -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 = 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); diff --git a/src/rayon.rs b/src/rayon.rs index ae1914c2b..210106684 100644 --- a/src/rayon.rs +++ b/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; + type Item: Send + 'data; + fn par_iter_mut(&'data mut self) -> Self::Iter; +} + impl 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 ParallelIterator for I {} #[allow(dead_code)] diff --git a/src/sanity_checks.rs b/src/sanity_checks.rs index 496b5dfda..8150d72d1 100644 --- a/src/sanity_checks.rs +++ b/src/sanity_checks.rs @@ -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 { - 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, 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) diff --git a/tests/lib.rs b/tests/lib.rs index a32de8b15..3c27f8be0 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -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] From b846a2e9092cf1c2aaf489e9c240e56840fd6652 Mon Sep 17 00:00:00 2001 From: andrews05 Date: Sat, 8 Jul 2023 12:56:15 +1200 Subject: [PATCH 5/5] Try to pick tRNS value that's valid at low depth (#520) --- src/reduction/alpha.rs | 10 +++++++++- ...yscale_alpha_8_should_be_grayscale_trns_1.png | Bin 0 -> 2361 bytes tests/reduction.rs | 12 ++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 tests/files/grayscale_alpha_8_should_be_grayscale_trns_1.png diff --git a/src/reduction/alpha.rs b/src/reduction/alpha.rs index 3045ddf65..3115c6cd5 100644 --- a/src/reduction/alpha.rs +++ b/src/reduction/alpha.rs @@ -57,8 +57,16 @@ pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option [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 }; diff --git a/tests/files/grayscale_alpha_8_should_be_grayscale_trns_1.png b/tests/files/grayscale_alpha_8_should_be_grayscale_trns_1.png new file mode 100644 index 0000000000000000000000000000000000000000..3434291fc4ad3bcb0042b29f26da7f024c70a6b3 GIT binary patch literal 2361 zcmeHI&1(}u6o1>K5K~hH3zimPjo5>d{i=y(*(}7wnuuF!tTh#pZqrR&Om>&uE!lbs zDpo;gk5Y;h1@)rnO?oQyFA$*@FCMG(B*lXib!PL?H8vN|8F@;3BG~^S5>Y-FzLG3cM{>eG)AU$Qz2s%Los17NRp&r z&J_7vdZbQAt3>#=Z5upIS1J{%GEC{_EX~H_ahl<1j*B3Vh&8X-vJ=s)6SW|XIBCTy zn5to`x<=x-^0Yo@C&FR8QFGMuEE~;@G^>6pbUxaV4VtAGdOwolsC()C#ln`hG+QmU zpI_d1TR=82sCauPXpGv+eJ}( zi$PTG##0f4-7mQx=p(b7O((~lTic(A5HNvt#{B(k@zCbz#?bcZ>zjjLgcqsm$K&f; zho|fS!~6S>bv@}NMi!t0cWTmUi2r{L^5<7R{5yr@ug&{6dqa