diff --git a/.travis.yml b/.travis.yml index 626b2b9d..6e4b97f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ matrix: env: TARGET=x86_64-apple-darwin cache: cargo - os: linux - rust: 1.36.0 + rust: 1.41.0 env: TARGET=x86_64-unknown-linux-gnu cache: cargo - os: linux diff --git a/Cargo.lock b/Cargo.lock index d65aec9a..b97d72f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,7 +154,7 @@ dependencies = [ [[package]] name = "deflate" -version = "0.7.20" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -181,7 +181,7 @@ dependencies = [ [[package]] name = "image" -version = "0.23.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bytemuck 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -189,7 +189,16 @@ dependencies = [ "num-iter 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", "num-rational 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "png 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)", + "png 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "indexmap" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -202,7 +211,7 @@ dependencies = [ [[package]] name = "itertools" -version = "0.8.2" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -218,6 +227,14 @@ name = "libc" version = "0.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "libdeflater" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "maybe-uninit" version = "2.0.0" @@ -294,24 +311,27 @@ dependencies = [ "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "cloudflare-zlib 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "image 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)", - "itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "image 0.23.2 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libdeflater 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "miniz_oxide 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "rgb 0.8.16 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "wild 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "zopfli 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "png" -version = "0.15.3" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "deflate 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)", + "deflate 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", "inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -455,15 +475,17 @@ dependencies = [ "checksum crossbeam-epoch 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" "checksum crossbeam-queue 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db" "checksum crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" -"checksum deflate 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)" = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4" +"checksum deflate 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "050ef6de42a33903b30a7497b76b40d3d58691d4d3eec355348c122444a388f0" "checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" "checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" "checksum hermit-abi 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8" -"checksum image 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ef4e336ec01a678e7ab692914c641181528e8656451e6252f8f9e33728882eaf" +"checksum image 0.23.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9062b90712d25bc6bb165d110aa59c6b47c849246e341e7b86a98daff9d49f60" +"checksum indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" "checksum inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" -"checksum itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +"checksum itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" +"checksum libdeflater 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "66dca08b13369865b2f6dca1dd05f833985cbe6c12a676b04d55f78b85e80246" "checksum maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" "checksum memoffset 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "75189eb85871ea5c2e2c15abbdd541185f63b408415e5051f5cac122d8c774b9" "checksum miniz_oxide 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5" @@ -472,7 +494,7 @@ dependencies = [ "checksum num-rational 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "da4dc79f9e6c81bef96148c8f6b8e72ad4541caa4a24373e900a36da07de03a3" "checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" "checksum num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6" -"checksum png 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ef859a23054bbfee7811284275ae522f0434a3c8e7f4b74bd4a35ae7e1c4a283" +"checksum png 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)" = "46060468187c21c00ffa2a920690b29997d7fd543f5a4d400461e4a7d4fccde8" "checksum rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098" "checksum rayon-core 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9" "checksum rgb 0.8.16 (registry+https://github.com/rust-lang/crates.io-index)" = "5ec4ab2cf0b27e111e266e161cf7f9efd20125a161190da1c0945c4a4408fef3" diff --git a/Cargo.toml b/Cargo.toml index a59df56f..55e409d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,11 +31,13 @@ required-features = ["binary"] bit-vec = "^0.6.0" byteorder = "^1.0.0" crc = "^1.2.0" -itertools = "^0.8.0" +itertools = "^0.9.0" num_cpus = "^1.0.0" zopfli = "^0.4.0" miniz_oxide = "0.3" rgb = "0.8.11" +indexmap = { version = "1.3.2", features = ["rayon"] } +libdeflater = "0.2.0" [dependencies.rayon] optional = true diff --git a/README.md b/README.md index 5d524d9d..1bd742ea 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ cargo build --release cp target/release/oxipng /usr/local/bin ``` -The current minimum supported Rust version is **1.36.0**. Oxipng may compile on earlier versions of Rust, +The current minimum supported Rust version is **1.41.0**. Oxipng may compile on earlier versions of Rust, but there is no guarantee. Oxipng follows Semantic Versioning. diff --git a/README.template.md b/README.template.md index 942e6148..c1d6adf5 100644 --- a/README.template.md +++ b/README.template.md @@ -26,7 +26,7 @@ cargo build --release cp target/release/oxipng /usr/local/bin ``` -The current minimum supported Rust version is **1.36.0**. Oxipng may compile on earlier versions of Rust, +The current minimum supported Rust version is **1.41.0**. Oxipng may compile on earlier versions of Rust, but there is no guarantee. Oxipng follows Semantic Versioning. diff --git a/benches/libdeflater.rs b/benches/libdeflater.rs new file mode 100644 index 00000000..d3323253 --- /dev/null +++ b/benches/libdeflater.rs @@ -0,0 +1,64 @@ +#![feature(test)] + +extern crate oxipng; +extern crate test; + +use oxipng::internal_tests::*; +use std::path::PathBuf; +use test::Bencher; + +#[bench] +fn libdeflater_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, false).unwrap(); + + b.iter(|| { + libdeflater_deflate(png.raw.data.as_ref()).ok(); + }); +} + +#[bench] +fn libdeflater_8_bits_strategy_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, false).unwrap(); + + b.iter(|| { + libdeflater_deflate(png.raw.data.as_ref()).ok(); + }); +} + +#[bench] +fn libdeflater_4_bits_strategy_0(b: &mut Bencher) { + let input = test::black_box(PathBuf::from( + "tests/files/palette_4_should_be_palette_4.png", + )); + let png = PngData::new(&input, false).unwrap(); + + b.iter(|| { + libdeflater_deflate(png.raw.data.as_ref()).ok(); + }); +} + +#[bench] +fn libdeflater_2_bits_strategy_0(b: &mut Bencher) { + let input = test::black_box(PathBuf::from( + "tests/files/palette_2_should_be_palette_2.png", + )); + let png = PngData::new(&input, false).unwrap(); + + b.iter(|| { + libdeflater_deflate(png.raw.data.as_ref()).ok(); + }); +} + +#[bench] +fn libdeflater_1_bits_strategy_0(b: &mut Bencher) { + let input = test::black_box(PathBuf::from( + "tests/files/palette_1_should_be_palette_1.png", + )); + let png = PngData::new(&input, false).unwrap(); + + b.iter(|| { + libdeflater_deflate(png.raw.data.as_ref()).ok(); + }); +} diff --git a/package-lock.json b/package-lock.json index 811dc5b4..1d65199e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -177,9 +177,9 @@ } }, "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, "normalize-package-data": { diff --git a/src/deflate/deflater.rs b/src/deflate/deflater.rs new file mode 100644 index 00000000..19f93db8 --- /dev/null +++ b/src/deflate/deflater.rs @@ -0,0 +1,31 @@ +use crate::{PngError, PngResult}; +use crate::atomicmin::AtomicMin; +use libdeflater::{CompressionError, CompressionLvl, Compressor}; + +pub fn deflate(data: &[u8], max_size: &AtomicMin) -> PngResult> { + let mut compressor = Compressor::new(CompressionLvl::best()); + let capacity = max_size.get().unwrap_or(data.len() / 2); + let mut dest = Vec::with_capacity(capacity); + unsafe { + // This is ok because the Vec contains Copy-able data (u8) + // and because libdeflater wrapper doesn't try to read + // the bytes from the target. + // + // That said, it should be able to accept MaybeUninit instead, + // so I raised an upstream issue that should make this safer: + // https://github.com/adamkewley/libdeflater/issues/1 + dest.set_len(capacity); + } + let len = compressor + .zlib_compress(data, &mut dest) + .map_err(|err| match err { + CompressionError::InsufficientSpace => PngError::DeflatedDataTooLong(capacity), + })?; + if let Some(max) = max_size.get() { + if len > max { + return Err(PngError::DeflatedDataTooLong(max)); + } + } + dest.truncate(len); + Ok(dest) +} diff --git a/src/deflate/mod.rs b/src/deflate/mod.rs index b0931cff..9648c813 100644 --- a/src/deflate/mod.rs +++ b/src/deflate/mod.rs @@ -9,6 +9,9 @@ use zopfli; #[doc(hidden)] pub mod miniz_stream; +mod deflater; +pub use deflater::deflate as libdeflater_deflate; + #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] pub mod cfzlib; @@ -62,4 +65,6 @@ pub enum Deflaters { Zlib, /// Use the better but slower Zopfli implementation Zopfli, + /// Use libdeflater. + Libdeflater, } diff --git a/src/evaluate.rs b/src/evaluate.rs index c6be4d9a..cc083b7f 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -24,8 +24,6 @@ use std::thread; struct Candidate { image: PngData, - // compressed size multiplier. Fudge factor to prefer more promising formats. - bias: f32, // if false, that's baseline file to throw away is_reduction: bool, filter: u8, @@ -42,30 +40,16 @@ impl Comparator { fn evaluate(&mut self, new: Candidate) { // a tie-breaker is required to make evaluation deterministic let is_best = if let Some(ref old) = self.best_result { - // ordering is important - later file gets to use bias over earlier, but not the other way - // (this way bias=0 replaces, but doesn't forbid later optimizations) - let new_len = (new.image.idat_data.len() as f64 - * if new.nth > old.nth { - f64::from(new.bias) - } else { - 1.0 - }) as usize; - let old_len = (old.image.idat_data.len() as f64 - * if new.nth < old.nth { - f64::from(old.bias) - } else { - 1.0 - }) as usize; // choose smallest compressed, or if compresses the same, smallest uncompressed, or cheaper filter let new = ( - new_len, + new.image.idat_data.len(), new.image.raw.data.len(), new.image.raw.ihdr.bit_depth, new.filter, new.nth, ); let old = ( - old_len, + old.image.idat_data.len(), old.image.raw.data.len(), old.image.raw.ihdr.bit_depth, old.filter, @@ -141,17 +125,15 @@ impl Evaluator { /// Set baseline image. It will be used only to measure minimum compression level required pub fn set_baseline(&self, image: Arc) { - self.try_image_inner(image, 1.0, false) + self.try_image_inner(image, false) } /// Check if the image is smaller than others - /// Bias is a value in 0..=1 range. Compressed size is multiplied by - /// this fraction when comparing to the best, so 0.95 allows 5% larger size. - pub fn try_image(&self, image: Arc, bias: f32) { - self.try_image_inner(image, bias, true) + pub fn try_image(&self, image: Arc) { + self.try_image_inner(image, true) } - fn try_image_inner(&self, image: Arc, bias: f32, is_reduction: bool) { + fn try_image_inner(&self, image: Arc, is_reduction: bool) { let nth = self.nth.fetch_add(1, SeqCst); // These clones are only cheap refcounts let deadline = self.deadline.clone(); @@ -186,7 +168,6 @@ impl Evaluator { idat_data, raw: Arc::clone(&image), }, - bias, filter, is_reduction, nth, diff --git a/src/headers.rs b/src/headers.rs index 1667bdbf..fcf92ae5 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -1,9 +1,9 @@ +use indexmap::IndexSet; use crate::colors::{BitDepth, ColorType}; use crate::error::PngError; use crate::PngResult; use byteorder::{BigEndian, ReadBytesExt}; use crc::crc32; -use std::collections::HashSet; use std::io::Cursor; #[derive(Debug, Clone, Copy)] @@ -35,7 +35,7 @@ pub enum Headers { /// Headers that won't affect rendering (all but cHRM, gAMA, iCCP, sBIT, sRGB, bKGD, hIST, pHYs, sPLT) Safe, /// Remove all non-critical chunks except these - Keep(HashSet), + Keep(IndexSet), /// All non-critical headers All, } diff --git a/src/lib.rs b/src/lib.rs index 5227bc0f..a22b1c38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,8 +28,8 @@ use crate::png::PngImage; use crate::reduction::*; use crc::crc32; use image::{DynamicImage, GenericImageView, ImageFormat, Pixel}; +use indexmap::{IndexSet, IndexMap}; use rayon::prelude::*; -use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt; use std::fs::{copy, File}; use std::io::{stdin, stdout, BufWriter, Read, Write}; @@ -154,7 +154,7 @@ pub struct Options { /// Which filters to try on the file (0-5) /// /// Default: `0,5` - pub filter: HashSet, + pub filter: IndexSet, /// Whether to change the interlacing type of the file. /// /// `None` will not change the current interlacing type. @@ -166,11 +166,11 @@ pub struct Options { /// Which zlib compression levels to try on the file (1-9) /// /// Default: `9` - pub compression: HashSet, + pub compression: IndexSet, /// Which zlib compression strategies to try on the file (0-3) /// /// Default: `0-3` - pub strategies: HashSet, + pub strategies: IndexSet, /// Window size to use when compressing the file, as `2^window` bytes. /// /// Doesn't affect compression but may affect speed and memory usage. @@ -179,7 +179,7 @@ pub struct Options { /// Default: `15` pub window: u8, /// Alpha filtering strategies to use - pub alphas: HashSet, + pub alphas: IndexSet, /// Whether to attempt bit depth reduction /// /// Default: `true` @@ -286,17 +286,17 @@ impl Options { impl Default for Options { fn default() -> Options { // Default settings based on -o 2 from the CLI interface - let mut filter = HashSet::new(); + let mut filter = IndexSet::new(); filter.insert(0); filter.insert(5); - let mut compression = HashSet::new(); + let mut compression = IndexSet::new(); compression.insert(9); - let mut strategies = HashSet::new(); + let mut strategies = IndexSet::new(); for i in 0..4 { strategies.insert(i); } // We always need NoOp to be present - let mut alphas = HashSet::new(); + let mut alphas = IndexSet::new(); alphas.insert(AlphaOptim::NoOp); Options { @@ -353,6 +353,7 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<( data } }; + let mut png = PngData::from_slice(&in_data, opts.fix_errors)?; // Run the optimizer on the decoded PNG. @@ -501,7 +502,7 @@ fn optimize_png( eprintln!(" File size = {} bytes", file_original_size); } - let mut filter = opts.filter.iter().cloned().collect::>(); + let mut filter = opts.filter.clone(); let compression = &opts.compression; let mut strategies = opts.strategies.clone(); @@ -511,14 +512,14 @@ fn optimize_png( && png.raw.ihdr.color_type != colors::ColorType::Indexed { if filter.is_empty() { - filter.push(5); + filter.insert(5); } if strategies.is_empty() { strategies.insert(1); } } else { if filter.is_empty() { - filter.push(0); + filter.insert(0); } if strategies.is_empty() { strategies.insert(0); @@ -565,7 +566,7 @@ fn optimize_png( } } } else { - // Zopfli compression has no additional options + // Zopfli and Libdeflater compression have no additional options. results.push(TrialOptions { filter: *f, compression: 0, @@ -582,8 +583,10 @@ fn optimize_png( eprintln!("Trying: {} combinations", results.len()); } - let filter_iter = filter.par_iter().with_max_len(1); - let filters: HashMap> = filter_iter + let filters: IndexMap> = + filter + .par_iter() + .with_max_len(1) .map(|f| { let png = png.clone(); (*f, png.raw.filter_image(*f)) @@ -600,17 +603,17 @@ fn optimize_png( return None; } let filtered = &filters[&trial.filter]; - let new_idat = if opts.deflate == Deflaters::Zlib { - deflate::deflate( + let new_idat = match opts.deflate { + Deflaters::Zlib => deflate::deflate( filtered, trial.compression, trial.strategy, opts.window, &best_size, &deadline, - ) - } else { - deflate::zopfli_deflate(filtered) + ), + Deflaters::Zopfli => deflate::zopfli_deflate(filtered), + Deflaters::Libdeflater => deflate::libdeflater_deflate(filtered, &best_size), }; let new_idat = match new_idat { @@ -740,7 +743,7 @@ fn perform_reductions( if let Some(interlacing) = opts.interlace { if let Some(reduced) = png.change_interlacing(interlacing) { png = Arc::new(reduced); - eval.try_image(png.clone(), 0.); + eval.try_image(png.clone()); } if deadline.passed() { return; @@ -750,7 +753,7 @@ fn perform_reductions( if opts.palette_reduction { if let Some(reduced) = reduced_palette(&png) { png = Arc::new(reduced); - eval.try_image(png.clone(), 0.95); + eval.try_image(png.clone()); if opts.verbosity == Some(1) { report_reduction(&png); } @@ -765,13 +768,13 @@ fn perform_reductions( let previous = png.clone(); let bits = reduced.ihdr.bit_depth; png = Arc::new(reduced); - eval.try_image(png.clone(), 1.0); + eval.try_image(png.clone()); if (bits == BitDepth::One || bits == BitDepth::Two) && previous.ihdr.bit_depth != BitDepth::Four { // Also try 16-color mode for all lower bits images, since that may compress better if let Some(reduced) = reduce_bit_depth(&previous, 4) { - eval.try_image(Arc::new(reduced), 0.98); + eval.try_image(Arc::new(reduced)); } } if opts.verbosity == Some(1) { @@ -786,7 +789,7 @@ fn perform_reductions( if opts.color_type_reduction { if let Some(reduced) = reduce_color_type(&png) { png = Arc::new(reduced); - eval.try_image(png.clone(), 0.96); + eval.try_image(png.clone()); if opts.verbosity == Some(1) { report_reduction(&png); } @@ -864,15 +867,10 @@ fn perform_strip(png: &mut PngData, opts: &Options) { // Strip headers Headers::None => (), Headers::Keep(ref hdrs) => { - let keys: Vec<[u8; 4]> = raw.aux_headers.keys().cloned().collect(); - for hdr in &keys { - let preserve = std::str::from_utf8(hdr) - .ok() - .map_or(false, |name| hdrs.contains(name)); - if !preserve { - raw.aux_headers.remove(hdr); - } - } + raw.aux_headers.retain(|hdr, _| { + std::str::from_utf8(hdr) + .map_or(false, |name| hdrs.contains(name)) + }) } Headers::Strip(ref hdrs) => { for hdr in hdrs { @@ -892,7 +890,7 @@ fn perform_strip(png: &mut PngData, opts: &Options) { } } Headers::All => { - raw.aux_headers = BTreeMap::new(); + raw.aux_headers = IndexMap::new(); } } diff --git a/src/main.rs b/src/main.rs index 29631f58..bafdc2bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,13 +14,13 @@ #![allow(clippy::cognitive_complexity)] use clap::{App, AppSettings, Arg, ArgMatches}; +use indexmap::IndexSet; use oxipng::AlphaOptim; use oxipng::Deflaters; use oxipng::Headers; use oxipng::Options; use oxipng::PngResult; use oxipng::{InFile, OutFile}; -use std::collections::HashSet; use std::fs::DirBuilder; use std::path::PathBuf; use std::process::exit; @@ -193,6 +193,10 @@ fn main() { .help("Use the slower but better compressing Zopfli algorithm, overrides zlib-specific options") .short("Z") .long("zopfli")) + .arg(Arg::with_name("libdeflater") + .help("Use an alternative Libdeflater algorithm, overrides zlib-specific options") + .short("D") + .long("libdeflater")) .arg(Arg::with_name("timeout") .help("Maximum amount of time, in seconds, to spend on optimizations") .takes_value(true) @@ -481,6 +485,10 @@ fn parse_opts_into_struct( opts.deflate = Deflaters::Zopfli; } + if matches.is_present("libdeflater") { + opts.deflate = Deflaters::Libdeflater; + } + if let Some(x) = matches.value_of("threads") { opts.threads = x.parse::().unwrap(); } @@ -492,9 +500,9 @@ fn parse_numeric_range_opts( input: &str, min_value: u8, max_value: u8, -) -> Result, String> { +) -> Result, String> { const ERROR_MESSAGE: &str = "Not a valid input"; - let mut items = HashSet::new(); + let mut items = IndexSet::new(); // one value if let Ok(one_value) = input.parse::() { diff --git a/src/png/mod.rs b/src/png/mod.rs index 5ad3bed6..ee425ae6 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -6,9 +6,9 @@ use crate::headers::*; use crate::interlace::{deinterlace_image, interlace_image}; use byteorder::{BigEndian, WriteBytesExt}; use crc::crc32; +use indexmap::IndexMap; use rgb::ComponentSlice; use rgb::RGBA8; -use std::collections::BTreeMap; use std::fs::File; use std::io::{Read, Seek, SeekFrom}; use std::iter::Iterator; @@ -38,7 +38,7 @@ pub struct PngImage { /// The pixel value that should be rendered as transparent pub transparency_pixel: Option>, /// All non-critical headers from the PNG are stored here - pub aux_headers: BTreeMap<[u8; 4], Vec>, + pub aux_headers: IndexMap<[u8; 4], Vec>, } /// Contains all data relevant to a PNG image @@ -97,7 +97,7 @@ impl PngData { } byte_offset += 8; // Read the data headers - let mut aux_headers: BTreeMap<[u8; 4], Vec> = BTreeMap::new(); + let mut aux_headers: IndexMap<[u8; 4], Vec> = IndexMap::new(); let mut idat_headers: Vec = Vec::new(); while let Some(header) = parse_next_header(byte_data, &mut byte_offset, fix_errors)? { match &header.name { diff --git a/src/reduction/alpha.rs b/src/reduction/alpha.rs index 95b03744..71d23a08 100644 --- a/src/reduction/alpha.rs +++ b/src/reduction/alpha.rs @@ -1,3 +1,4 @@ +use indexmap::IndexSet; use crate::colors::AlphaOptim; use crate::colors::ColorType; use crate::evaluate::Evaluator; @@ -8,23 +9,22 @@ use crate::png::PngImage; use crate::rayon::prelude::*; #[cfg(feature = "parallel")] use rayon::prelude::*; -use std::collections::HashSet; use std::sync::Arc; pub(crate) fn try_alpha_reductions( png: Arc, - alphas: &HashSet, + alphas: &IndexSet, eval: &Evaluator, ) { if alphas.is_empty() { return; } - let alphas = alphas.iter().collect::>(); - let alphas_iter = alphas.par_iter().with_max_len(1); - alphas_iter - .filter_map(|&alpha| filtered_alpha_channel(&png, *alpha)) - .for_each(|image| eval.try_image(Arc::new(image), 0.99)); + alphas + .par_iter() + .with_max_len(1) + .filter_map(|&alpha| filtered_alpha_channel(&png, alpha)) + .for_each(|image| eval.try_image(Arc::new(image))); } pub fn filtered_alpha_channel(png: &PngImage, optim: AlphaOptim) -> Option { diff --git a/src/reduction/color.rs b/src/reduction/color.rs index bd692dc8..8be17402 100644 --- a/src/reduction/color.rs +++ b/src/reduction/color.rs @@ -1,9 +1,9 @@ use crate::colors::{BitDepth, ColorType}; use crate::headers::IhdrData; use crate::png::PngImage; +use indexmap::IndexMap; use itertools::Itertools; use rgb::{FromSlice, RGB8, RGBA8}; -use std::collections::HashMap; use std::hash::Hash; #[must_use] @@ -76,7 +76,7 @@ pub fn reduce_rgba_to_grayscale_alpha(png: &PngImage) -> Option { fn reduce_scanline_to_palette( iter: impl IntoIterator, - palette: &mut HashMap, + palette: &mut IndexMap, reduced: &mut Vec, ) -> bool where @@ -105,7 +105,7 @@ pub fn reduced_color_to_palette(png: &PngImage) -> Option { return None; } let mut raw_data = Vec::with_capacity(png.data.len()); - let mut palette = HashMap::with_capacity(257); + let mut palette = IndexMap::with_capacity(257); let transparency_pixel = png .transparency_pixel .as_ref() diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index 702e9cf0..b486d8fc 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -1,10 +1,9 @@ use crate::colors::{BitDepth, ColorType}; use crate::headers::IhdrData; use crate::png::PngImage; +use indexmap::map::{IndexMap, Entry::*}; use rgb::RGBA8; use std::borrow::Cow; -use std::collections::hash_map::Entry::*; -use std::collections::HashMap; pub mod alpha; use crate::alpha::*; @@ -79,7 +78,7 @@ pub fn reduced_palette(png: &PngImage) -> Option { }); let mut next_index = 0u16; - let mut seen = HashMap::with_capacity(palette.len()); + let mut seen = IndexMap::with_capacity(palette.len()); for (i, used) in used_enumerated.iter().cloned() { if !used { continue; diff --git a/tests/filters.rs b/tests/filters.rs index 3bc4e56e..41f92ca8 100644 --- a/tests/filters.rs +++ b/tests/filters.rs @@ -1,8 +1,6 @@ -use oxipng; - +use indexmap::IndexSet; use oxipng::internal_tests::*; use oxipng::{InFile, OutFile}; -use std::collections::HashSet; use std::fs::remove_file; use std::path::Path; use std::path::PathBuf; @@ -11,7 +9,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { let mut options = oxipng::Options::default(); options.verbosity = None; options.force = true; - let mut filter = HashSet::new(); + let mut filter = IndexSet::new(); filter.insert(0); options.filter = filter; @@ -33,7 +31,7 @@ fn test_it_converts( let (output, mut opts) = get_opts(&input); let png = PngData::new(&input, opts.fix_errors).unwrap(); - opts.filter = HashSet::new(); + opts.filter = IndexSet::new(); opts.filter.insert(filter); assert_eq!(png.raw.ihdr.color_type, color_type_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); diff --git a/tests/flags.rs b/tests/flags.rs index 1e9a43f7..928420a8 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -1,8 +1,6 @@ -extern crate oxipng; - +use indexmap::IndexSet; use oxipng::internal_tests::*; use oxipng::{InFile, OutFile}; -use std::collections::HashSet; use std::fs::remove_file; use std::path::Path; use std::path::PathBuf; @@ -11,7 +9,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { let mut options = oxipng::Options::default(); options.verbosity = None; options.force = true; - let mut filter = HashSet::new(); + let mut filter = IndexSet::new(); filter.insert(0); options.filter = filter; @@ -342,7 +340,7 @@ fn interlaced_0_to_1_other_filter_mode() { let input = PathBuf::from("tests/files/interlaced_0_to_1_other_filter_mode.png"); let (output, mut opts) = get_opts(&input); opts.interlace = Some(1); - let mut filter = HashSet::new(); + let mut filter = IndexSet::new(); filter.insert(4); opts.filter = filter; @@ -438,3 +436,21 @@ fn zopfli_mode() { BitDepth::Eight, ); } + + +#[test] +fn libdeflater_mode() { + let input = PathBuf::from("tests/files/zopfli_mode.png"); + let (output, mut opts) = get_opts(&input); + opts.deflate = Deflaters::Libdeflater; + + test_it_converts( + input, + &output, + &opts, + ColorType::RGB, + BitDepth::Eight, + ColorType::RGB, + BitDepth::Eight, + ); +} diff --git a/tests/interlaced.rs b/tests/interlaced.rs index 7b93f0ee..8bc93df1 100644 --- a/tests/interlaced.rs +++ b/tests/interlaced.rs @@ -1,8 +1,6 @@ -extern crate oxipng; - +use indexmap::IndexSet; use oxipng::internal_tests::*; use oxipng::{InFile, OutFile}; -use std::collections::HashSet; use std::fs::remove_file; use std::path::Path; use std::path::PathBuf; @@ -11,7 +9,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { let mut options = oxipng::Options::default(); options.verbosity = None; options.force = true; - let mut filter = HashSet::new(); + let mut filter = IndexSet::new(); filter.insert(0); options.filter = filter; diff --git a/tests/reduction.rs b/tests/reduction.rs index d3b5cc5a..a27cd2a0 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -1,8 +1,6 @@ -use oxipng; - +use indexmap::IndexSet; use oxipng::internal_tests::*; use oxipng::{InFile, OutFile}; -use std::collections::HashSet; use std::fs::remove_file; use std::path::Path; use std::path::PathBuf; @@ -11,7 +9,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { let mut options = oxipng::Options::default(); options.verbosity = None; options.force = true; - let mut filter = HashSet::new(); + let mut filter = IndexSet::new(); filter.insert(0); options.filter = filter; diff --git a/tests/regression.rs b/tests/regression.rs index 867902ab..096fe8d8 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -1,8 +1,6 @@ -use oxipng; - +use indexmap::IndexSet; use oxipng::internal_tests::*; use oxipng::{InFile, OutFile}; -use std::collections::HashSet; use std::fs::remove_file; use std::path::Path; use std::path::PathBuf; @@ -11,7 +9,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { let mut options = oxipng::Options::default(); options.verbosity = None; options.force = true; - let mut filter = HashSet::new(); + let mut filter = IndexSet::new(); filter.insert(0); options.filter = filter; @@ -309,7 +307,7 @@ fn issue_113_white() { let input = "tests/files/issue-113.png"; let (_, mut opts) = get_opts(Path::new(input)); opts.interlace = Some(1); - opts.alphas = HashSet::new(); + opts.alphas = IndexSet::new(); opts.alphas.insert(AlphaOptim::Black); let output = OutFile::Path(Some( Path::new(input).with_extension("-white-out.png").to_owned(), @@ -329,7 +327,7 @@ fn issue_113_black() { let input = "tests/files/issue-113.png"; let (_, mut opts) = get_opts(Path::new(input)); opts.interlace = Some(1); - opts.alphas = HashSet::new(); + opts.alphas = IndexSet::new(); opts.alphas.insert(AlphaOptim::Black); let output = OutFile::Path(Some( Path::new(input).with_extension("-black-out.png").to_owned(), @@ -349,7 +347,7 @@ fn issue_113_right() { let input = "tests/files/issue-113.png"; let (_, mut opts) = get_opts(Path::new(input)); opts.interlace = Some(1); - opts.alphas = HashSet::new(); + opts.alphas = IndexSet::new(); opts.alphas.insert(AlphaOptim::Right); let output = OutFile::Path(Some( Path::new(input).with_extension("-right-out.png").to_owned(), @@ -369,7 +367,7 @@ fn issue_113_left() { let input = "tests/files/issue-113.png"; let (_, mut opts) = get_opts(Path::new(input)); opts.interlace = Some(1); - opts.alphas = HashSet::new(); + opts.alphas = IndexSet::new(); opts.alphas.insert(AlphaOptim::Left); let output = OutFile::Path(Some( Path::new(input).with_extension("-left-out.png").to_owned(), @@ -389,7 +387,7 @@ fn issue_113_up() { let input = "tests/files/issue-113.png"; let (_, mut opts) = get_opts(Path::new(input)); opts.interlace = Some(1); - opts.alphas = HashSet::new(); + opts.alphas = IndexSet::new(); opts.alphas.insert(AlphaOptim::Up); let output = OutFile::Path(Some( Path::new(input).with_extension("-up-out.png").to_owned(), @@ -409,7 +407,7 @@ fn issue_113_down() { let input = "tests/files/issue-113.png"; let (_, mut opts) = get_opts(Path::new(input)); opts.interlace = Some(1); - opts.alphas = HashSet::new(); + opts.alphas = IndexSet::new(); opts.alphas.insert(AlphaOptim::Down); let output = OutFile::Path(Some( Path::new(input).with_extension("-down-out.png").to_owned(), @@ -441,7 +439,7 @@ fn issue_129() { fn issue_133_black() { let input = "tests/files/issue-133.png"; let (_, mut opts) = get_opts(Path::new(input)); - opts.alphas = HashSet::new(); + opts.alphas = IndexSet::new(); opts.alphas.insert(AlphaOptim::Black); let output = OutFile::Path(Some( Path::new(input).with_extension("-black-out.png").to_owned(), @@ -460,7 +458,7 @@ fn issue_133_black() { fn issue_133_white() { let input = "tests/files/issue-133.png"; let (_, mut opts) = get_opts(Path::new(input)); - opts.alphas = HashSet::new(); + opts.alphas = IndexSet::new(); opts.alphas.insert(AlphaOptim::White); let output = OutFile::Path(Some( Path::new(input).with_extension("-white-out.png").to_owned(), @@ -479,7 +477,7 @@ fn issue_133_white() { fn issue_133_up() { let input = "tests/files/issue-133.png"; let (_, mut opts) = get_opts(Path::new(input)); - opts.alphas = HashSet::new(); + opts.alphas = IndexSet::new(); opts.alphas.insert(AlphaOptim::Up); let output = OutFile::Path(Some( Path::new(input).with_extension("-up-out.png").to_owned(), @@ -498,7 +496,7 @@ fn issue_133_up() { fn issue_133_down() { let input = "tests/files/issue-133.png"; let (_, mut opts) = get_opts(Path::new(input)); - opts.alphas = HashSet::new(); + opts.alphas = IndexSet::new(); opts.alphas.insert(AlphaOptim::Down); let output = OutFile::Path(Some( Path::new(input).with_extension("-down-out.png").to_owned(), @@ -517,7 +515,7 @@ fn issue_133_down() { fn issue_133_right() { let input = "tests/files/issue-133.png"; let (_, mut opts) = get_opts(Path::new(input)); - opts.alphas = HashSet::new(); + opts.alphas = IndexSet::new(); opts.alphas.insert(AlphaOptim::Right); let output = OutFile::Path(Some( Path::new(input).with_extension("-right-out.png").to_owned(), @@ -536,7 +534,7 @@ fn issue_133_right() { fn issue_133_left() { let input = "tests/files/issue-133.png"; let (_, mut opts) = get_opts(Path::new(input)); - opts.alphas = HashSet::new(); + opts.alphas = IndexSet::new(); opts.alphas.insert(AlphaOptim::Left); let output = OutFile::Path(Some( Path::new(input).with_extension("-left-out.png").to_owned(),