diff --git a/Cargo.lock b/Cargo.lock index 48ae1e76..00e84300 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,7 +85,7 @@ dependencies = [ [[package]] name = "cloudflare-zlib" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cloudflare-zlib-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -265,7 +265,7 @@ dependencies = [ "bit-vec 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cloudflare-zlib 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cloudflare-zlib 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "image 0.20.1 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)", @@ -422,7 +422,7 @@ dependencies = [ "checksum cc 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "f159dfd43363c4d08055a07703eb7a3406b0dac4d0584d96965a3262db3c9d16" "checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4" "checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e" -"checksum cloudflare-zlib 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "398f5d6b4431f230c89858e5389803bd0135bc764fbf9b7b50d4be0ca2657a6c" +"checksum cloudflare-zlib 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cd97a72b7777ef134de513cef589e48c393dc2ea7180ff6dca87dcd3ee078dac" "checksum cloudflare-zlib-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7e195cb274a0d6ee87e718838a09baecd7cbc9f6075dac256a84cb5842739c06" "checksum crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" "checksum crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f739f8c5363aca78cfb059edf753d8f0d36908c348f3d8d1503f03d8b75d9cf3" diff --git a/Cargo.toml b/Cargo.toml index 1c3528ce..9540ced0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ optional = true version = "2.0.0" [target.'cfg(any(target_arch = "x86_64", target_arch = "aarch64"))'.dependencies] -cloudflare-zlib = "^0.2.0" +cloudflare-zlib = "^0.2.2" [dependencies.image] default-features = false diff --git a/benches/deflate.rs b/benches/deflate.rs index 1b4ef863..378007e5 100644 --- a/benches/deflate.rs +++ b/benches/deflate.rs @@ -14,7 +14,7 @@ fn deflate_16_bits_strategy_0(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 0, 15, &min) + deflate(png.raw.data.as_ref(), 9, 0, 15, &min) }); } @@ -25,7 +25,7 @@ fn deflate_8_bits_strategy_0(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 0, 15, &min) + deflate(png.raw.data.as_ref(), 9, 0, 15, &min) }); } @@ -38,7 +38,7 @@ fn deflate_4_bits_strategy_0(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 0, 15, &min) + deflate(png.raw.data.as_ref(), 9, 0, 15, &min) }); } @@ -51,7 +51,7 @@ fn deflate_2_bits_strategy_0(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 0, 15, &min) + deflate(png.raw.data.as_ref(), 9, 0, 15, &min) }); } @@ -64,7 +64,7 @@ fn deflate_1_bits_strategy_0(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 0, 15, &min) + deflate(png.raw.data.as_ref(), 9, 0, 15, &min) }); } @@ -75,7 +75,7 @@ fn deflate_16_bits_strategy_1(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 1, 15, &min) + deflate(png.raw.data.as_ref(), 9, 1, 15, &min) }); } @@ -86,7 +86,7 @@ fn deflate_8_bits_strategy_1(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 1, 15, &min) + deflate(png.raw.data.as_ref(), 9, 1, 15, &min) }); } @@ -99,7 +99,7 @@ fn deflate_4_bits_strategy_1(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 1, 15, &min) + deflate(png.raw.data.as_ref(), 9, 1, 15, &min) }); } @@ -112,7 +112,7 @@ fn deflate_2_bits_strategy_1(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 1, 15, &min) + deflate(png.raw.data.as_ref(), 9, 1, 15, &min) }); } @@ -125,7 +125,7 @@ fn deflate_1_bits_strategy_1(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 1, 15, &min) + deflate(png.raw.data.as_ref(), 9, 1, 15, &min) }); } @@ -136,7 +136,7 @@ fn deflate_16_bits_strategy_2(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 2, 15, &min) + deflate(png.raw.data.as_ref(), 9, 2, 15, &min) }); } @@ -147,7 +147,7 @@ fn deflate_8_bits_strategy_2(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 2, 15, &min) + deflate(png.raw.data.as_ref(), 9, 2, 15, &min) }); } @@ -160,7 +160,7 @@ fn deflate_4_bits_strategy_2(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 2, 15, &min) + deflate(png.raw.data.as_ref(), 9, 2, 15, &min) }); } @@ -173,7 +173,7 @@ fn deflate_2_bits_strategy_2(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 2, 15, &min) + deflate(png.raw.data.as_ref(), 9, 2, 15, &min) }); } @@ -186,7 +186,7 @@ fn deflate_1_bits_strategy_2(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 2, 15, &min) + deflate(png.raw.data.as_ref(), 9, 2, 15, &min) }); } @@ -197,7 +197,7 @@ fn deflate_16_bits_strategy_3(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 3, 15, &min) + deflate(png.raw.data.as_ref(), 9, 3, 15, &min) }); } @@ -208,7 +208,7 @@ fn deflate_8_bits_strategy_3(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 3, 15, &min) + deflate(png.raw.data.as_ref(), 9, 3, 15, &min) }); } @@ -221,7 +221,7 @@ fn deflate_4_bits_strategy_3(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 3, 15, &min) + deflate(png.raw.data.as_ref(), 9, 3, 15, &min) }); } @@ -234,7 +234,7 @@ fn deflate_2_bits_strategy_3(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 3, 15, &min) + deflate(png.raw.data.as_ref(), 9, 3, 15, &min) }); } @@ -247,7 +247,7 @@ fn deflate_1_bits_strategy_3(b: &mut Bencher) { b.iter(|| { let min = AtomicMin::new(None); - deflate(png.raw_data.as_ref(), 9, 3, 15, &min) + deflate(png.raw.data.as_ref(), 9, 3, 15, &min) }); } diff --git a/benches/reductions.rs b/benches/reductions.rs index 82381b96..83dc50b9 100644 --- a/benches/reductions.rs +++ b/benches/reductions.rs @@ -14,7 +14,7 @@ fn reductions_16_to_8_bits(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_bit_depth(); + safe_png.reduce_bit_depth() }); } @@ -27,7 +27,7 @@ fn reductions_8_to_4_bits(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_bit_depth(); + safe_png.reduce_bit_depth() }); } @@ -40,7 +40,7 @@ fn reductions_8_to_2_bits(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_bit_depth(); + safe_png.reduce_bit_depth() }); } @@ -53,7 +53,7 @@ fn reductions_8_to_1_bits(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_bit_depth(); + safe_png.reduce_bit_depth() }); } @@ -66,7 +66,7 @@ fn reductions_4_to_2_bits(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_bit_depth(); + safe_png.reduce_bit_depth() }); } @@ -79,7 +79,7 @@ fn reductions_4_to_1_bits(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_bit_depth(); + safe_png.reduce_bit_depth() }); } @@ -92,7 +92,7 @@ fn reductions_2_to_1_bits(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_bit_depth(); + safe_png.reduce_bit_depth() }); } @@ -103,7 +103,7 @@ fn reductions_rgba_to_rgb_16(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_color_type(); + safe_png.reduce_color_type() }); } @@ -114,7 +114,7 @@ fn reductions_rgba_to_rgb_8(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_color_type(); + safe_png.reduce_color_type() }); } @@ -127,7 +127,7 @@ fn reductions_rgba_to_grayscale_alpha_16(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_color_type(); + safe_png.reduce_color_type() }); } @@ -140,7 +140,7 @@ fn reductions_rgba_to_grayscale_alpha_8(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_color_type(); + safe_png.reduce_color_type() }); } @@ -153,7 +153,7 @@ fn reductions_rgba_to_grayscale_16(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_color_type(); + safe_png.reduce_color_type() }); } @@ -166,7 +166,7 @@ fn reductions_rgba_to_grayscale_8(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_color_type(); + safe_png.reduce_color_type() }); } @@ -179,7 +179,7 @@ fn reductions_rgb_to_grayscale_16(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_color_type(); + safe_png.reduce_color_type() }); } @@ -190,7 +190,7 @@ fn reductions_rgb_to_grayscale_8(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_color_type(); + safe_png.reduce_color_type() }); } @@ -201,7 +201,7 @@ fn reductions_rgba_to_palette_8(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_color_type(); + safe_png.reduce_color_type() }); } @@ -212,7 +212,7 @@ fn reductions_rgb_to_palette_8(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_color_type(); + safe_png.reduce_color_type() }); } @@ -225,7 +225,7 @@ fn reductions_palette_duplicate_reduction(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_palette(); + safe_png.reduce_palette() }); } @@ -238,7 +238,7 @@ fn reductions_palette_unused_reduction(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_palette(); + safe_png.reduce_palette() }); } @@ -251,7 +251,7 @@ fn reductions_palette_full_reduction(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.reduce_palette(); + safe_png.reduce_palette() }); } diff --git a/benches/zopfli.rs b/benches/zopfli.rs index 01209496..8c98b350 100644 --- a/benches/zopfli.rs +++ b/benches/zopfli.rs @@ -13,7 +13,7 @@ fn zopfli_16_bits_strategy_0(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - zopfli_deflate(png.raw_data.as_ref()).ok(); + zopfli_deflate(png.raw.data.as_ref()).ok(); }); } @@ -23,7 +23,7 @@ fn zopfli_8_bits_strategy_0(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - zopfli_deflate(png.raw_data.as_ref()).ok(); + zopfli_deflate(png.raw.data.as_ref()).ok(); }); } @@ -35,7 +35,7 @@ fn zopfli_4_bits_strategy_0(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - zopfli_deflate(png.raw_data.as_ref()).ok(); + zopfli_deflate(png.raw.data.as_ref()).ok(); }); } @@ -47,7 +47,7 @@ fn zopfli_2_bits_strategy_0(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - zopfli_deflate(png.raw_data.as_ref()).ok(); + zopfli_deflate(png.raw.data.as_ref()).ok(); }); } @@ -59,6 +59,6 @@ fn zopfli_1_bits_strategy_0(b: &mut Bencher) { let png = PngData::new(&input, false).unwrap(); b.iter(|| { - zopfli_deflate(png.raw_data.as_ref()).ok(); + zopfli_deflate(png.raw.data.as_ref()).ok(); }); } diff --git a/src/colors.rs b/src/colors.rs index 248f4e68..7e0644ae 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -56,7 +56,7 @@ impl ColorType { } } -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] /// The number of bits to be used per channel per pixel pub enum BitDepth { /// One bit per channel per pixel diff --git a/src/deflate/mod.rs b/src/deflate/mod.rs index 70a62ea7..b2339c90 100644 --- a/src/deflate/mod.rs +++ b/src/deflate/mod.rs @@ -9,7 +9,14 @@ use PngResult; pub mod miniz_stream; #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] -mod cfzlib; +pub mod cfzlib; + +#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] +pub mod cfzlib { + pub fn is_supported() -> bool { + return false; + } +} /// Decompress a data stream using the DEFLATE algorithm pub fn inflate(data: &[u8]) -> PngResult> { diff --git a/src/evaluate.rs b/src/evaluate.rs new file mode 100644 index 00000000..5260bc2d --- /dev/null +++ b/src/evaluate.rs @@ -0,0 +1,112 @@ +//! Check if a reduction makes file smaller, and keep best reductions. +//! Works asynchronously when possible + +use atomicmin::AtomicMin; +use deflate; +use png::PngData; +use png::PngImage; +use png::STD_COMPRESSION; +use png::STD_FILTERS; +use png::STD_STRATEGY; +use png::STD_WINDOW; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use std::sync::mpsc::*; +use std::sync::Arc; +use std::sync::Mutex; +use std::thread; + +/// Collect image versions and pick one that compresses best +pub struct Evaluator { + /// images are sent to the thread for evaluation + eval_send: Option, f32, bool)>>, + // the thread helps evaluate images asynchronously + eval_thread: thread::JoinHandle>, +} + +impl Evaluator { + pub fn new() -> Self { + // queue size ensures we're not using too much memory for pending reductions + let (tx, rx) = sync_channel(4); + Self { + eval_send: Some(tx), + eval_thread: thread::spawn(move || Self::evaluate_images(rx)), + } + } + + /// Wait for all evaluations to finish and return smallest reduction + /// Or `None` if all reductions were worse than baseline. + pub fn get_result(mut self) -> Option { + let _ = self.eval_send.take(); // disconnect the sender, breaking the loop in the thread + self.eval_thread.join().expect("eval thread") + } + + /// 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) + } + + /// 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) + } + + fn try_image_inner(&self, image: Arc, bias: f32, is_reduction: bool) { + self.eval_send.as_ref().expect("not finished yet").send((image, bias, is_reduction)).expect("send") + } + + /// Main loop of evaluation thread + fn evaluate_images(from_channel: Receiver<(Arc, f32, bool)>) -> Option { + let best_candidate_size = AtomicMin::new(None); + let best_result: Mutex> = Mutex::new(None); + // ends when sender is dropped + for (nth, (image, bias, is_reduction)) in from_channel.iter().enumerate() { + #[cfg(feature = "parallel")] + let filters_iter = STD_FILTERS.par_iter().with_max_len(1); + #[cfg(not(feature = "parallel"))] + let filters_iter = STD_FILTERS.iter(); + + filters_iter.for_each(|&f| { + if let Ok(idat_data) = deflate::deflate( + &image.filter_image(f), + STD_COMPRESSION, + STD_STRATEGY, + STD_WINDOW, + &best_candidate_size, + ) { + let mut res = best_result.lock().unwrap(); + if best_candidate_size.get().map_or(true, |old_best_len| { + let new_len = (idat_data.len() as f64 * bias as f64) as usize; + // a tie-breaker is required to make evaluation deterministic + if let Some(res) = res.as_ref() { + // choose smallest compressed, or if compresses the same, smallest uncompressed, or cheaper filter + let old_img = &res.0.raw; + let new = (new_len, image.data.len(), image.ihdr.bit_depth, f, nth); + let old = (old_best_len, old_img.data.len(), old_img.ihdr.bit_depth, res.1, res.2); + new < old + } else if new_len < old_best_len { + true + } else { + false + } + }) { + best_candidate_size.set_min(idat_data.len()); + *res = if is_reduction { + Some((PngData { + idat_data, + raw: Arc::clone(&image), + }, f, nth)) + } else { + None + }; + } + } + }); + } + best_result.into_inner().expect("filters should be done") + .map(|(img, _, _)| img) + } +} + diff --git a/src/interlace.rs b/src/interlace.rs index c681dc4e..d7916e1a 100644 --- a/src/interlace.rs +++ b/src/interlace.rs @@ -1,38 +1,38 @@ -use reduction::ReducedPng; +use headers::IhdrData; use bit_vec::BitVec; -use png::PngData; +use png::PngImage; #[must_use] -pub fn interlace_image(png: &PngData) -> ReducedPng { +pub fn interlace_image(png: &PngImage) -> PngImage { let mut passes: Vec = vec![BitVec::new(); 7]; - let bits_per_pixel = png.ihdr_data.bit_depth.as_u8() * png.channels_per_pixel(); + let bits_per_pixel = png.ihdr.bit_depth.as_u8() * png.channels_per_pixel(); for (index, line) in png.scan_lines().enumerate() { match index % 8 { // Add filter bytes to passes that will be in the output image 0 => { passes[0].extend(BitVec::from_elem(8, false)); - if png.ihdr_data.width >= 5 { + if png.ihdr.width >= 5 { passes[1].extend(BitVec::from_elem(8, false)); } - if png.ihdr_data.width >= 3 { + if png.ihdr.width >= 3 { passes[3].extend(BitVec::from_elem(8, false)); } - if png.ihdr_data.width >= 2 { + if png.ihdr.width >= 2 { passes[5].extend(BitVec::from_elem(8, false)); } } 4 => { passes[2].extend(BitVec::from_elem(8, false)); - if png.ihdr_data.width >= 3 { + if png.ihdr.width >= 3 { passes[3].extend(BitVec::from_elem(8, false)); } - if png.ihdr_data.width >= 2 { + if png.ihdr.width >= 2 { passes[5].extend(BitVec::from_elem(8, false)); } } 2 | 6 => { passes[4].extend(BitVec::from_elem(8, false)); - if png.ihdr_data.width >= 2 { + if png.ihdr.width >= 2 { passes[5].extend(BitVec::from_elem(8, false)); } } @@ -43,7 +43,7 @@ pub fn interlace_image(png: &PngData) -> ReducedPng { let bit_vec = BitVec::from_bytes(&line.data); for (i, bit) in bit_vec.iter().enumerate() { // Avoid moving padded 0's into new image - if i >= (png.ihdr_data.width * u32::from(bits_per_pixel)) as usize { + if i >= (png.ihdr.width * u32::from(bits_per_pixel)) as usize { break; } // Copy pixels into interlaced passes @@ -77,35 +77,36 @@ pub fn interlace_image(png: &PngData) -> ReducedPng { } } - let mut output = Vec::with_capacity(png.raw_data.len()); + let mut output = Vec::with_capacity(png.data.len()); for pass in &passes { output.extend(pass.to_bytes()); } - ReducedPng { - raw_data: output, - interlaced: 1, - color_type: png.ihdr_data.color_type, - bit_depth: png.ihdr_data.bit_depth, - aux_headers: Default::default(), - palette: None, - transparency_pixel: None, + PngImage { + data: output, + ihdr: IhdrData { + interlaced: 1, + ..png.ihdr + }, + aux_headers: png.aux_headers.clone(), + palette: png.palette.clone(), + transparency_pixel: png.transparency_pixel.clone(), } } -pub fn deinterlace_image(png: &PngData) -> ReducedPng { - let bits_per_pixel = png.ihdr_data.bit_depth.as_u8() * png.channels_per_pixel(); - let bits_per_line = 8 + bits_per_pixel as usize * png.ihdr_data.width as usize; +pub fn deinterlace_image(png: &PngImage) -> PngImage { + let bits_per_pixel = png.ihdr.bit_depth.as_u8() * png.channels_per_pixel(); + let bits_per_line = 8 + bits_per_pixel as usize * png.ihdr.width as usize; // Initialize each output line with a starting filter byte of 0 // as well as some blank data let mut lines: Vec = - vec![BitVec::from_elem(bits_per_line, false); png.ihdr_data.height as usize]; + vec![BitVec::from_elem(bits_per_line, false); png.ihdr.height as usize]; let mut current_pass = 1; let mut pass_constants = interlaced_constants(current_pass); let mut current_y: usize = pass_constants.y_shift as usize; for line in png.scan_lines() { let bit_vec = BitVec::from_bytes(&line.data); - let bits_in_line = ((png.ihdr_data.width - u32::from(pass_constants.x_shift) + let bits_in_line = ((png.ihdr.width - u32::from(pass_constants.x_shift) + u32::from(pass_constants.x_step) - 1) / u32::from(pass_constants.x_step)) as usize @@ -123,36 +124,37 @@ pub fn deinterlace_image(png: &PngData) -> ReducedPng { } // Calculate the next line and move to next pass if necessary current_y += pass_constants.y_step as usize; - if current_y >= png.ihdr_data.height as usize { + if current_y >= png.ihdr.height as usize { if current_pass == 7 { break; } current_pass += 1; - if current_pass == 2 && png.ihdr_data.width <= 4 { + if current_pass == 2 && png.ihdr.width <= 4 { current_pass += 1; } - if current_pass == 3 && png.ihdr_data.height <= 4 { + if current_pass == 3 && png.ihdr.height <= 4 { current_pass += 1; } pass_constants = interlaced_constants(current_pass); current_y = pass_constants.y_shift as usize; } } - let mut output = Vec::with_capacity(png.raw_data.len()); + let mut output = Vec::with_capacity(png.data.len()); for line in &mut lines { while line.len() % 8 != 0 { line.push(false); } output.extend(line.to_bytes()); } - ReducedPng { - raw_data: output, - interlaced: 0, - color_type: png.ihdr_data.color_type, - bit_depth: png.ihdr_data.bit_depth, - aux_headers: Default::default(), - palette: None, - transparency_pixel: None, + PngImage { + data: output, + ihdr: IhdrData { + interlaced: 0, + ..png.ihdr + }, + aux_headers: png.aux_headers.clone(), + palette: png.palette.clone(), + transparency_pixel: png.transparency_pixel.clone(), } } diff --git a/src/lib.rs b/src/lib.rs index 3f1e8ebd..1a54cf82 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,8 +16,11 @@ use reduction::*; use atomicmin::AtomicMin; use crc::crc32; use deflate::inflate; +use evaluate::Evaluator; use image::{DynamicImage, GenericImageView, ImageFormat, Pixel}; +use png::PngImage; use png::PngData; +use colors::BitDepth; #[cfg(feature = "parallel")] use rayon::prelude::*; use std::collections::{HashMap, HashSet}; @@ -27,6 +30,7 @@ use std::io::{stdin, stdout, BufWriter, Read, Write}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; +use std::sync::Arc; pub use colors::AlphaOptim; pub use deflate::Deflaters; @@ -37,6 +41,7 @@ mod atomicmin; mod colors; mod deflate; mod error; +mod evaluate; mod filters; mod headers; mod interlace; @@ -357,7 +362,9 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<( let mut optimized_output = optimize_png(&mut png, &in_data, opts)?; if is_fully_optimized(in_data.len(), optimized_output.len(), opts) { - eprintln!("File already optimized"); + if opts.verbosity.is_some() { + eprintln!("File already optimized"); + } match (output, input) { // if p is None, it also means same as the input path (&OutFile::Path(ref p), &InFile::Path(ref input_path)) @@ -473,20 +480,20 @@ fn optimize_png(png: &mut PngData, original_data: &[u8], opts: &Options) -> PngR if opts.verbosity.is_some() { eprintln!( " {}x{} pixels, PNG format", - png.ihdr_data.width, png.ihdr_data.height + png.raw.ihdr.width, png.raw.ihdr.height ); - if let Some(ref palette) = png.palette { + if let Some(ref palette) = png.raw.palette { eprintln!( " {} bits/pixel, {} colors in palette", - png.ihdr_data.bit_depth, + png.raw.ihdr.bit_depth, palette.len() / 3 ); } else { eprintln!( " {}x{} bits/pixel, {:?}", - png.channels_per_pixel(), - png.ihdr_data.bit_depth, - png.ihdr_data.color_type + png.raw.channels_per_pixel(), + png.raw.ihdr.bit_depth, + png.raw.ihdr.color_type ); } eprintln!(" IDAT size = {} bytes", idat_original_size); @@ -499,8 +506,8 @@ fn optimize_png(png: &mut PngData, original_data: &[u8], opts: &Options) -> PngR if opts.use_heuristics { // Heuristically determine which set of options to use - if png.ihdr_data.bit_depth.as_u8() >= 8 - && png.ihdr_data.color_type != colors::ColorType::Indexed + if png.raw.ihdr.bit_depth.as_u8() >= 8 + && png.raw.ihdr.color_type != colors::ColorType::Indexed { if filter.is_empty() { filter.push(5); @@ -518,7 +525,20 @@ fn optimize_png(png: &mut PngData, original_data: &[u8], opts: &Options) -> PngR } } - let reduction_occurred = perform_reductions(png, opts, &deadline); + // This will collect all versions of images and pick one that compresses best + let eval = Evaluator::new(); + // Usually we want transformations that are smaller than the unmodified original, + // but if we're interlacing, we have to accept a possible file size increase. + if opts.interlace.is_none() { + eval.set_baseline(png.raw.clone()); + } + perform_reductions(png.raw.clone(), opts, &deadline, &eval); + let reduction_occurred = if let Some(result) = eval.get_result() { + *png = result; + true + } else { + false + }; if opts.idat_recoding || reduction_occurred { // Go through selected permutations and determine the best @@ -564,11 +584,11 @@ fn optimize_png(png: &mut PngData, original_data: &[u8], opts: &Options) -> PngR let filters: HashMap> = filter_iter .map(|f| { let png = png.clone(); - (*f, png.filter_image(*f)) + (*f, png.raw.filter_image(*f)) }).collect(); let original_len = original_png.idat_data.len(); - let added_interlacing = opts.interlace == Some(1) && original_png.ihdr_data.interlaced == 0; + let added_interlacing = opts.interlace == Some(1) && original_png.raw.ihdr.interlaced == 0; let best_size = AtomicMin::new(if opts.force { None } else { Some(original_len) }); #[cfg(feature = "parallel")] @@ -653,7 +673,7 @@ fn optimize_png(png: &mut PngData, original_data: &[u8], opts: &Options) -> PngR ); } } else if reduction_occurred { - png.reset_from_original(&original_png); + *png = original_png; } } @@ -721,66 +741,67 @@ fn optimize_png(png: &mut PngData, original_data: &[u8], opts: &Options) -> PngR Err(PngError::new("The resulting image is corrupted")) } -/// Attempt all reduction operations requested by the given `Options` struct -/// and apply them directly to the `PngData` passed in -fn perform_reductions(png: &mut PngData, opts: &Options, deadline: &Deadline) -> bool { - let mut reduction_occurred = false; +fn perform_reductions(mut png: Arc, opts: &Options, deadline: &Deadline, eval: &Evaluator) { - if opts.palette_reduction { - if let Some(reduced) = reduced_palette(png) { - png.apply_reduction(reduced); - reduction_occurred = true; - if opts.verbosity == Some(1) { - report_reduction(png); - } + // must be done first to evaluate rest with the correct interlacing + if let Some(interlacing) = opts.interlace { + if let Some(reduced) = png.change_interlacing(interlacing) { + png = Arc::new(reduced); + eval.try_image(png.clone(), 0.); + } + if deadline.passed() { + return; } } - if deadline.passed() { - return reduction_occurred; + if opts.palette_reduction { + if let Some(reduced) = reduced_palette(&png) { + png = Arc::new(reduced); + eval.try_image(png.clone(), 0.95); + if opts.verbosity == Some(1) { + report_reduction(&png); + } + } + if deadline.passed() { + return; + } } if opts.bit_depth_reduction { - if let Some(reduced) = png.reduce_bit_depth() { - png.apply_reduction(reduced); - reduction_occurred = true; + if let Some(reduced) = reduce_bit_depth(&png, 1) { + let previous = png.clone(); + let bits = reduced.ihdr.bit_depth; + png = Arc::new(reduced); + eval.try_image(png.clone(), 1.0); + 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); + } + } if opts.verbosity == Some(1) { - report_reduction(png); + report_reduction(&png); } } - } - - if deadline.passed() { - return reduction_occurred; - } - - if opts.color_type_reduction && png.reduce_color_type() { - reduction_occurred = true; - if opts.verbosity == Some(1) { - report_reduction(png); + if deadline.passed() { + return; } } - if reduction_occurred && opts.verbosity.is_some() { - report_reduction(png); - } - - if let Some(interlacing) = opts.interlace { - if let Some(reduced) = png.change_interlacing(interlacing) { - png.apply_reduction(reduced); - reduction_occurred = true; + if opts.color_type_reduction { + if let Some(reduced) = reduce_color_type(&png) { + png = Arc::new(reduced); + eval.try_image(png.clone(), 0.96); + if opts.verbosity == Some(1) { + report_reduction(&png); + } + } + if deadline.passed() { + return; } } - if deadline.passed() { - return reduction_occurred; - } - - if png.try_alpha_reduction(&opts.alphas) { - reduction_occurred = true; - } - - reduction_occurred + try_alpha_reductions(png, &opts.alphas, eval); } /// Keep track of processing timeout @@ -817,48 +838,49 @@ impl Deadline { } /// Display the status of the image data after a reduction has taken place -fn report_reduction(png: &PngData) { +fn report_reduction(png: &PngImage) { if let Some(ref palette) = png.palette { eprintln!( "Reducing image to {} bits/pixel, {} colors in palette", - png.ihdr_data.bit_depth, + png.ihdr.bit_depth, palette.len() / 3 ); } else { eprintln!( "Reducing image to {}x{} bits/pixel, {}", png.channels_per_pixel(), - png.ihdr_data.bit_depth, - png.ihdr_data.color_type + png.ihdr.bit_depth, + png.ihdr.color_type ); } } /// Strip headers from the `PngData` object, as requested by the passed `Options` fn perform_strip(png: &mut PngData, opts: &Options) { + let raw = Arc::make_mut(&mut png.raw); match opts.strip { // Strip headers Headers::None => (), Headers::Keep(ref hdrs) => { - png.aux_headers.retain(|chunk, _| { + raw.aux_headers.retain(|chunk, _| { std::str::from_utf8(chunk) .ok() .map_or(false, |name| hdrs.contains(name)) }); } Headers::Strip(ref hdrs) => for hdr in hdrs { - png.aux_headers.remove(hdr.as_bytes()); + raw.aux_headers.remove(hdr.as_bytes()); }, Headers::Safe => { const PRESERVED_HEADERS: [[u8; 4]; 9] = [ *b"cHRM", *b"gAMA", *b"iCCP", *b"sBIT", *b"sRGB", *b"bKGD", *b"hIST", *b"pHYs", *b"sPLT", ]; - png.aux_headers + raw.aux_headers .retain(|hdr, _| PRESERVED_HEADERS.contains(hdr)); } Headers::All => { - png.aux_headers = HashMap::new(); + raw.aux_headers = HashMap::new(); } } @@ -871,18 +893,18 @@ fn perform_strip(png: &mut PngData, opts: &Options) { }; if may_replace_iccp { - if png.aux_headers.get(b"sRGB").is_some() { + if raw.aux_headers.get(b"sRGB").is_some() { // Files aren't supposed to have both chunks, so we chose to honor sRGB - png.aux_headers.remove(b"iCCP"); - } else if let Some(intent) = png + raw.aux_headers.remove(b"iCCP"); + } else if let Some(intent) = raw .aux_headers .get(b"iCCP") .and_then(|iccp| srgb_rendering_intent(iccp)) { // sRGB-like profile can be safely replaced with // an sRGB chunk with the same rendering intent - png.aux_headers.remove(b"iCCP"); - png.aux_headers.insert(*b"sRGB", vec![intent]); + raw.aux_headers.remove(b"iCCP"); + raw.aux_headers.insert(*b"sRGB", vec![intent]); } } } diff --git a/src/png/mod.rs b/src/png/mod.rs index 25526cdc..d9ebd764 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -1,46 +1,37 @@ -use atomicmin::AtomicMin; use byteorder::{BigEndian, WriteBytesExt}; -use colors::{AlphaOptim, BitDepth, ColorType}; +use colors::ColorType; use crc::crc32; use deflate; use error::PngError; -use reduction::*; use filters::*; use headers::*; use interlace::{deinterlace_image, interlace_image}; -use itertools::flatten; -#[cfg(feature = "parallel")] -use rayon::prelude::*; -use reduction::bit_depth::*; -use reduction::color::*; -use reduction::alpha::*; use rgb::ComponentSlice; use rgb::RGBA8; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::fs::File; use std::io::{Read, Seek, SeekFrom}; use std::iter::Iterator; use std::path::Path; +use std::sync::Arc; -const STD_COMPRESSION: u8 = 8; +pub(crate) const STD_COMPRESSION: u8 = 6; /// Must use normal compression, as faster ones (Huffman/RLE-only) are not representative -const STD_STRATEGY: u8 = 0; -const STD_WINDOW: u8 = 15; -const STD_FILTERS: [u8; 2] = [0, 5]; +pub(crate) const STD_STRATEGY: u8 = 0; +/// OK to use a bit smalller window for evaluation +pub(crate) const STD_WINDOW: u8 = 13; +pub(crate) const STD_FILTERS: [u8; 2] = [0, 5]; -mod scan_lines; +pub(crate) mod scan_lines; -use self::scan_lines::{ScanLine, ScanLines, ScanLinesMut}; +use self::scan_lines::{ScanLines, ScanLinesMut}; #[derive(Debug, Clone)] -/// Contains all data relevant to a PNG image -pub struct PngData { - /// The filtered and compressed data of the IDAT chunk - pub idat_data: Vec, +pub struct PngImage { /// The headers stored in the IHDR chunk - pub ihdr_data: IhdrData, + pub ihdr: IhdrData, /// The uncompressed, optionally filtered data from the IDAT chunk - pub raw_data: Vec, + pub data: Vec, /// The palette containing colors used in an Indexed image /// Contains 3 bytes per color (R+G+B), up to 768 pub palette: Option>, @@ -50,6 +41,15 @@ pub struct PngData { pub aux_headers: HashMap<[u8; 4], Vec>, } +/// Contains all data relevant to a PNG image +#[derive(Debug, Clone)] +pub struct PngData { + /// Uncompressed image data + pub raw: Arc, + /// The filtered and compressed data of the IDAT chunk + pub idat_data: Vec, +} + type PaletteWithTrns = (Option>, Option>); impl PngData { @@ -125,17 +125,19 @@ impl PngData { aux_headers.remove(b"tRNS"), )?; - let mut png_data = Self { - idat_data: idat_headers, - ihdr_data: ihdr_header, - raw_data, + let mut raw = PngImage { + ihdr: ihdr_header, + data: raw_data, palette, transparency_pixel, aux_headers, }; - png_data.raw_data = png_data.unfilter_image(); + raw.data = raw.unfilter_image(); // Return the PngData - Ok(png_data) + Ok(Self { + idat_data: idat_headers, + raw: Arc::new(raw), + }) } /// Handle transparency header @@ -163,37 +165,22 @@ impl PngData { } } - pub(crate) fn reset_from_original(&mut self, original: &Self) { - self.idat_data = original.idat_data.clone(); - self.ihdr_data = original.ihdr_data; - self.raw_data = original.raw_data.clone(); - self.palette = original.palette.clone(); - self.transparency_pixel = original.transparency_pixel.clone(); - self.aux_headers = original.aux_headers.clone(); - } - - /// Return the number of channels in the image, based on color type - #[inline] - pub fn channels_per_pixel(&self) -> u8 { - self.ihdr_data.color_type.channels_per_pixel() - } - /// Format the `PngData` struct into a valid PNG bytestream pub fn output(&self) -> Vec { // PNG header let mut output = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; // IHDR let mut ihdr_data = Vec::with_capacity(13); - let _ = ihdr_data.write_u32::(self.ihdr_data.width); - let _ = ihdr_data.write_u32::(self.ihdr_data.height); - let _ = ihdr_data.write_u8(self.ihdr_data.bit_depth.as_u8()); - let _ = ihdr_data.write_u8(self.ihdr_data.color_type.png_header_code()); + let _ = ihdr_data.write_u32::(self.raw.ihdr.width); + let _ = ihdr_data.write_u32::(self.raw.ihdr.height); + let _ = ihdr_data.write_u8(self.raw.ihdr.bit_depth.as_u8()); + let _ = ihdr_data.write_u8(self.raw.ihdr.color_type.png_header_code()); let _ = ihdr_data.write_u8(0); // Compression -- deflate let _ = ihdr_data.write_u8(0); // Filter method -- 5-way adaptive filtering - let _ = ihdr_data.write_u8(self.ihdr_data.interlaced); + let _ = ihdr_data.write_u8(self.raw.ihdr.interlaced); write_png_block(b"IHDR", &ihdr_data, &mut output); // Ancillary headers - for (key, header) in self + for (key, header) in self.raw .aux_headers .iter() .filter(|&(key, _)| !(key == b"bKGD" || key == b"hIST" || key == b"tRNS")) @@ -201,14 +188,15 @@ impl PngData { write_png_block(key, header, &mut output); } // Palette - if let Some(ref palette) = self.palette { + if let Some(ref palette) = self.raw.palette { let mut palette_data = Vec::with_capacity(palette.len() * 3); - for px in palette { + let max_palette_size = 1 << (self.raw.ihdr.bit_depth.as_u8() as usize); + for px in palette.iter().take(max_palette_size) { palette_data.extend_from_slice(px.rgb().as_slice()); } write_png_block(b"PLTE", &palette_data, &mut output); let num_transparent = - palette.iter().enumerate().fold( + palette.iter().take(max_palette_size).enumerate().fold( 0, |prev, (index, px)| { if px.a != 255 { @@ -222,12 +210,12 @@ impl PngData { let trns_data: Vec<_> = palette[0..num_transparent].iter().map(|px| px.a).collect(); write_png_block(b"tRNS", &trns_data, &mut output); } - } else if let Some(ref transparency_pixel) = self.transparency_pixel { + } else if let Some(ref transparency_pixel) = self.raw.transparency_pixel { // Transparency pixel write_png_block(b"tRNS", transparency_pixel, &mut output); } // Special ancillary headers that need to come after PLTE but before IDAT - for (key, header) in self + for (key, header) in self.raw .aux_headers .iter() .filter(|&(key, _)| key == b"bKGD" || key == b"hIST" || key == b"tRNS") @@ -241,6 +229,35 @@ impl PngData { output } +} + +impl PngImage { + + /// Convert the image to the specified interlacing type + /// Returns true if the interlacing was changed, false otherwise + /// The `interlace` parameter specifies the *new* interlacing mode + /// Assumes that the data has already been de-filtered + #[inline] + #[must_use] + pub fn change_interlacing(&self, interlace: u8) -> Option { + if interlace == self.ihdr.interlaced { + return None; + } + + Some(if interlace == 1 { + // Convert progressive to interlaced data + interlace_image(self) + } else { + // Convert interlaced to progressive data + deinterlace_image(self) + }) + } + + /// Return the number of channels in the image, based on color type + #[inline] + pub fn channels_per_pixel(&self) -> u8 { + self.ihdr.color_type.channels_per_pixel() + } /// Return an iterator over the scanlines of the image #[inline] @@ -255,9 +272,9 @@ impl PngData { } /// Reverse all filters applied on the image, returning an unfiltered IDAT bytestream - pub fn unfilter_image(&self) -> Vec { - let mut unfiltered = Vec::with_capacity(self.raw_data.len()); - let bpp = ((self.ihdr_data.bit_depth.as_u8() * self.channels_per_pixel() + 7) / 8) as usize; + fn unfilter_image(&self) -> Vec { + let mut unfiltered = Vec::with_capacity(self.data.len()); + let bpp = ((self.ihdr.bit_depth.as_u8() * self.channels_per_pixel() + 7) / 8) as usize; let mut last_line: Vec = Vec::new(); let mut last_pass = 1; for line in self.scan_lines() { @@ -283,8 +300,8 @@ impl PngData { /// 4: Paeth /// 5: All (heuristically pick the best filter for each line) pub fn filter_image(&self, filter: u8) -> Vec { - let mut filtered = Vec::with_capacity(self.raw_data.len()); - let bpp = ((self.ihdr_data.bit_depth.as_u8() * self.channels_per_pixel() + 7) / 8) as usize; + let mut filtered = Vec::with_capacity(self.data.len()); + let bpp = ((self.ihdr.bit_depth.as_u8() * self.channels_per_pixel() + 7) / 8) as usize; let mut last_line: &[u8] = &[]; let mut last_pass: Option = None; for line in self.scan_lines() { @@ -325,351 +342,7 @@ impl PngData { } filtered } - - /// Attempt to reduce the bit depth of the image - /// Returns true if the bit depth was reduced, false otherwise - #[must_use] - pub fn reduce_bit_depth(&self) -> Option { - if self.ihdr_data.bit_depth != BitDepth::Sixteen { - if self.ihdr_data.color_type == ColorType::Indexed - || self.ihdr_data.color_type == ColorType::Grayscale - { - return reduce_bit_depth_8_or_less(self); - } - return None; - } - - // Reduce from 16 to 8 bits per channel per pixel - let mut reduced = Vec::with_capacity( - (self.ihdr_data.width * self.ihdr_data.height * u32::from(self.channels_per_pixel()) - + self.ihdr_data.height) as usize, - ); - let mut high_byte = 0; - - for line in self.scan_lines() { - reduced.push(line.filter); - for (i, &byte) in line.data.iter().enumerate() { - if i % 2 == 0 { - // High byte - high_byte = byte; - } else { - // Low byte - if high_byte != byte { - // Can't reduce, exit early - return None; - } - reduced.push(byte); - } - } - } - - Some(ReducedPng { - color_type: self.ihdr_data.color_type, - interlaced: self.ihdr_data.interlaced, - bit_depth: BitDepth::Eight, - raw_data: reduced, - palette: self.palette.clone(), - transparency_pixel: self.transparency_pixel.clone(), - aux_headers: Default::default(), - }) - } - - /// Attempt to reduce the color type of the image - /// Returns true if the color type was reduced, false otherwise - pub fn reduce_color_type(&mut self) -> bool { - let mut changed = false; - let mut should_reduce_bit_depth = false; - - // Go down one step at a time - // Maybe not the most efficient, but it's safe - if self.ihdr_data.color_type == ColorType::RGBA { - if let Some(reduced) = reduce_rgba_to_grayscale_alpha(self).or_else(|| reduced_alpha_channel(self)) { - self.apply_reduction(reduced); - changed = true; - } else if let Some(reduced) = reduced_color_to_palette(self) { - self.apply_reduction(reduced); - changed = true; - should_reduce_bit_depth = true; - } - } - - if self.ihdr_data.color_type == ColorType::GrayscaleAlpha { - if let Some(reduced) = reduced_alpha_channel(self) { - self.apply_reduction(reduced); - changed = true; - should_reduce_bit_depth = true; - } - } - - if self.ihdr_data.color_type == ColorType::RGB { - if let Some(reduced) = reduce_rgb_to_grayscale(self).or_else(|| reduced_color_to_palette(self)) { - self.apply_reduction(reduced); - changed = true; - should_reduce_bit_depth = true; - } - } - - if should_reduce_bit_depth { - // Some conversions will allow us to perform bit depth reduction that - // wasn't possible before - if let Some(reduced) = reduce_bit_depth_8_or_less(self) { - self.apply_reduction(reduced); - } - } - - changed - } - - pub(crate) fn apply_reduction(&mut self, ReducedPng {color_type, bit_depth, raw_data, interlaced, palette, transparency_pixel, aux_headers}: ReducedPng) { - self.ihdr_data.color_type = color_type; - self.ihdr_data.bit_depth = bit_depth; - self.ihdr_data.interlaced = interlaced; - self.raw_data = raw_data; - if palette.is_some() { - self.transparency_pixel = None; - self.palette = palette; - } - if transparency_pixel.is_some() { - self.transparency_pixel = transparency_pixel; - } - self.idat_data.clear(); // this field is out of date and needs to be replaced - - for (header, val) in aux_headers { - match val { - Some(val) => self.aux_headers.insert(header, val), - None => self.aux_headers.remove(&header), - }; - } - } - - pub fn try_alpha_reduction(&mut self, alphas: &HashSet) -> bool { - assert!(!alphas.is_empty()); - let alphas = alphas.iter().collect::>(); - let best_size = AtomicMin::new(None); - #[cfg(feature = "parallel")] - let alphas_iter = alphas.par_iter().with_max_len(1); - #[cfg(not(feature = "parallel"))] - let alphas_iter = alphas.iter(); - let best = alphas_iter - .filter_map(|&alpha| { - let image = match self.reduced_alpha_channel(*alpha) { - Some(image) => image, - None => return None, - }; - #[cfg(feature = "parallel")] - let filters_iter = STD_FILTERS.par_iter().with_max_len(1); - #[cfg(not(feature = "parallel"))] - let filters_iter = STD_FILTERS.iter(); - filters_iter - .filter_map(|f| { - deflate::deflate( - &image.filter_image(*f), - STD_COMPRESSION, - STD_STRATEGY, - STD_WINDOW, - &best_size, - ).ok() - .as_ref() - .map(|l| { - best_size.set_min(l.len()); - l.len() - }) - }).min() - .map(|size| (size, image)) - }).min_by_key(|&(size, _)| size); - - if let Some(best) = best { - self.raw_data = best.1.raw_data; - return true; - } - false - } - - /// It doesn't recompress `idat_data`, so this field is out of date - /// after the reduction. - pub fn reduced_alpha_channel(&self, optim: AlphaOptim) -> Option { - let (bpc, bpp) = match self.ihdr_data.color_type { - ColorType::RGBA | ColorType::GrayscaleAlpha => { - let cpp = self.channels_per_pixel(); - let bpc = self.ihdr_data.bit_depth.as_u8() / 8; - (bpc as usize, (bpc * cpp) as usize) - } - _ => { - return None; - } - }; - - let raw_data = match optim { - AlphaOptim::NoOp => return None, - AlphaOptim::Black => self.reduced_alpha_to_black(bpc, bpp), - AlphaOptim::White => self.reduced_alpha_to_white(bpc, bpp), - AlphaOptim::Up => self.reduced_alpha_to_up(bpc, bpp), - AlphaOptim::Down => self.reduced_alpha_to_down(bpc, bpp), - AlphaOptim::Left => self.reduced_alpha_to_left(bpc, bpp), - AlphaOptim::Right => self.reduced_alpha_to_right(bpc, bpp), - }; - - Some(Self { - raw_data, - idat_data: vec![], - ihdr_data: self.ihdr_data, - palette: self.palette.clone(), - transparency_pixel: self.transparency_pixel.clone(), - aux_headers: self.aux_headers.clone(), - }) - } - - fn reduced_alpha_to_black(&self, bpc: usize, bpp: usize) -> Vec { - let mut reduced = Vec::with_capacity(self.raw_data.len()); - for line in self.scan_lines() { - reduced.push(line.filter); - for pixel in line.data.chunks(bpp) { - if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 { - for _ in 0..bpp { - reduced.push(0); - } - } else { - reduced.extend_from_slice(pixel); - } - } - } - reduced - } - - fn reduced_alpha_to_white(&self, bpc: usize, bpp: usize) -> Vec { - let mut reduced = Vec::with_capacity(self.raw_data.len()); - for line in self.scan_lines() { - reduced.push(line.filter); - for pixel in line.data.chunks(bpp) { - if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 { - for _ in 0..(bpp - bpc) { - reduced.push(255); - } - for _ in 0..bpc { - reduced.push(0); - } - } else { - reduced.extend_from_slice(pixel); - } - } - } - reduced - } - - fn reduced_alpha_to_up(&self, bpc: usize, bpp: usize) -> Vec { - let mut lines = Vec::new(); - let mut scan_lines = self.scan_lines().collect::>(); - scan_lines.reverse(); - let mut last_line = Vec::new(); - let mut current_line = Vec::with_capacity(last_line.len()); - for line in scan_lines { - if line.data.len() != last_line.len() { - last_line = vec![0; line.data.len()]; - } - current_line.push(line.filter); - for (pixel, last_pixel) in line.data.chunks(bpp).zip(last_line.chunks(bpp)) { - if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 { - current_line.extend_from_slice(&last_pixel[0..(bpp - bpc)]); - for _ in 0..bpc { - current_line.push(0); - } - } else { - current_line.extend_from_slice(pixel); - } - } - last_line = current_line.clone(); - lines.push(current_line.clone()); - current_line.clear(); - } - flatten(lines.into_iter().rev()).collect() - } - - fn reduced_alpha_to_down(&self, bpc: usize, bpp: usize) -> Vec { - let mut reduced = Vec::with_capacity(self.raw_data.len()); - let mut last_line = Vec::new(); - for line in self.scan_lines() { - if line.data.len() != last_line.len() { - last_line = vec![0; line.data.len()]; - } - reduced.push(line.filter); - for (pixel, last_pixel) in line.data.chunks(bpp).zip(last_line.chunks(bpp)) { - if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 { - reduced.extend_from_slice(&last_pixel[0..(bpp - bpc)]); - for _ in 0..bpc { - reduced.push(0); - } - } else { - reduced.extend_from_slice(pixel); - } - } - last_line = reduced.clone(); - } - reduced - } - - fn reduced_alpha_to_left(&self, bpc: usize, bpp: usize) -> Vec { - let mut reduced = Vec::with_capacity(self.raw_data.len()); - for line in self.scan_lines() { - let mut line_bytes = Vec::with_capacity(line.data.len()); - let mut last_pixel = vec![0; bpp]; - for pixel in line.data.chunks(bpp).rev() { - if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 { - line_bytes.extend_from_slice(&last_pixel[0..(bpp - bpc)]); - for _ in 0..bpc { - line_bytes.push(0); - } - } else { - line_bytes.extend_from_slice(pixel); - } - last_pixel = pixel.to_owned(); - } - reduced.push(line.filter); - reduced.extend(flatten(line_bytes.chunks(bpp).rev())); - } - reduced - } - - fn reduced_alpha_to_right(&self, bpc: usize, bpp: usize) -> Vec { - let mut reduced = Vec::with_capacity(self.raw_data.len()); - for line in self.scan_lines() { - reduced.push(line.filter); - let mut last_pixel = vec![0; bpp]; - for pixel in line.data.chunks(bpp) { - if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 { - reduced.extend_from_slice(&last_pixel[0..(bpp - bpc)]); - for _ in 0..bpc { - reduced.push(0); - } - } else { - reduced.extend_from_slice(pixel); - } - last_pixel = pixel.to_owned(); - } - } - reduced - } - - /// Convert the image to the specified interlacing type - /// Returns true if the interlacing was changed, false otherwise - /// The `interlace` parameter specifies the *new* interlacing mode - /// Assumes that the data has already been de-filtered - #[inline] - #[must_use] - pub fn change_interlacing(&mut self, interlace: u8) -> Option { - if interlace == self.ihdr_data.interlaced { - return None; - } - - Some(if interlace == 1 { - // Convert progressive to interlaced data - interlace_image(self) - } else { - // Convert interlaced to progressive data - deinterlace_image(self) - }) - } } - fn write_png_block(key: &[u8], header: &[u8], output: &mut Vec) { let mut header_data = Vec::with_capacity(header.len() + 4); header_data.extend_from_slice(key); diff --git a/src/png/scan_lines.rs b/src/png/scan_lines.rs index b7c0132d..0b1a6f30 100644 --- a/src/png/scan_lines.rs +++ b/src/png/scan_lines.rs @@ -1,4 +1,4 @@ -use super::PngData; +use png::PngImage; #[derive(Debug, Clone)] /// An iterator over the scan lines of a PNG image @@ -9,10 +9,10 @@ pub struct ScanLines<'a> { } impl<'a> ScanLines<'a> { - pub fn new(png: &'a PngData) -> Self { + pub fn new(png: &'a PngImage) -> Self { Self { iter: ScanLineRanges::new(png), - raw_data: &png.raw_data, + raw_data: &png.data, } } } @@ -39,10 +39,10 @@ pub struct ScanLinesMut<'a> { } impl<'a> ScanLinesMut<'a> { - pub fn new(png: &'a mut PngData) -> Self { + pub fn new(png: &'a mut PngImage) -> Self { Self { iter: ScanLineRanges::new(png), - raw_data: Some(&mut png.raw_data), + raw_data: Some(&mut png.data), } } } @@ -73,13 +73,13 @@ struct ScanLineRanges { } impl ScanLineRanges { - pub fn new(png: &PngData) -> Self { + pub fn new(png: &PngImage) -> Self { Self { - bits_per_pixel: png.ihdr_data.bit_depth.as_u8() * png.channels_per_pixel(), - width: png.ihdr_data.width, - height: png.ihdr_data.height, - left: png.raw_data.len(), - pass: if png.ihdr_data.interlaced == 1 { + bits_per_pixel: png.ihdr.bit_depth.as_u8() * png.channels_per_pixel(), + width: png.ihdr.width, + height: png.ihdr.height, + left: png.data.len(), + pass: if png.ihdr.interlaced == 1 { Some((1, 0)) } else { None @@ -153,6 +153,7 @@ impl Iterator for ScanLineRanges { let bits_per_line = pixels_per_line * u32::from(self.bits_per_pixel); let bytes_per_line = ((bits_per_line + 7) / 8) as usize; let len = bytes_per_line + 1; + assert!(self.left >= len); self.left -= len; Some((len, current_pass)) } diff --git a/src/reduction/alpha.rs b/src/reduction/alpha.rs index 04d59821..e2bba245 100644 --- a/src/reduction/alpha.rs +++ b/src/reduction/alpha.rs @@ -1,16 +1,196 @@ -use reduction::ReducedPng; -use png::PngData; +use evaluate::Evaluator; +use itertools::flatten; +use png::scan_lines::ScanLine; +use std::collections::HashSet; +use std::sync::Arc; +use colors::AlphaOptim; +use headers::IhdrData; +use png::PngImage; use colors::ColorType; -use std::collections::HashMap; +#[cfg(feature = "parallel")] +use rayon::prelude::*; + +pub fn try_alpha_reductions(png: Arc, alphas: &HashSet, eval: &Evaluator) { + assert!(!alphas.is_empty()); + let alphas = alphas.iter().collect::>(); + #[cfg(feature = "parallel")] + let alphas_iter = alphas.par_iter().with_max_len(1); + #[cfg(not(feature = "parallel"))] + let alphas_iter = alphas.iter(); + alphas_iter + .filter_map(|&alpha| filtered_alpha_channel(&png, *alpha)) + .for_each(|image| eval.try_image(Arc::new(image), 0.99)); +} + +pub fn filtered_alpha_channel(png: &PngImage, optim: AlphaOptim) -> Option { + let (bpc, bpp) = match png.ihdr.color_type { + ColorType::RGBA | ColorType::GrayscaleAlpha => { + let cpp = png.channels_per_pixel(); + let bpc = png.ihdr.bit_depth.as_u8() / 8; + (bpc as usize, (bpc * cpp) as usize) + } + _ => { + return None; + } + }; + + let raw_data = match optim { + AlphaOptim::NoOp => return None, + AlphaOptim::Black => reduced_alpha_to_black(png, bpc, bpp), + AlphaOptim::White => reduced_alpha_to_white(png, bpc, bpp), + AlphaOptim::Up => reduced_alpha_to_up(png, bpc, bpp), + AlphaOptim::Down => reduced_alpha_to_down(png, bpc, bpp), + AlphaOptim::Left => reduced_alpha_to_left(png, bpc, bpp), + AlphaOptim::Right => reduced_alpha_to_right(png, bpc, bpp), + }; + + Some(PngImage { + data: raw_data, + ihdr: png.ihdr, + palette: png.palette.clone(), + transparency_pixel: png.transparency_pixel.clone(), + aux_headers: png.aux_headers.clone(), + }) +} + +fn reduced_alpha_to_black(png: &PngImage, bpc: usize, bpp: usize) -> Vec { + let mut reduced = Vec::with_capacity(png.data.len()); + for line in png.scan_lines() { + reduced.push(line.filter); + for pixel in line.data.chunks(bpp) { + if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 { + for _ in 0..bpp { + reduced.push(0); + } + } else { + reduced.extend_from_slice(pixel); + } + } + } + reduced +} + +fn reduced_alpha_to_white(png: &PngImage, bpc: usize, bpp: usize) -> Vec { + let mut reduced = Vec::with_capacity(png.data.len()); + for line in png.scan_lines() { + reduced.push(line.filter); + for pixel in line.data.chunks(bpp) { + if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 { + for _ in 0..(bpp - bpc) { + reduced.push(255); + } + for _ in 0..bpc { + reduced.push(0); + } + } else { + reduced.extend_from_slice(pixel); + } + } + } + reduced +} + +fn reduced_alpha_to_up(png: &PngImage, bpc: usize, bpp: usize) -> Vec { + let mut lines = Vec::new(); + let mut scan_lines = png.scan_lines().collect::>(); + scan_lines.reverse(); + let mut last_line = Vec::new(); + let mut current_line = Vec::with_capacity(last_line.len()); + for line in scan_lines { + if line.data.len() != last_line.len() { + last_line = vec![0; line.data.len()]; + } + current_line.push(line.filter); + for (pixel, last_pixel) in line.data.chunks(bpp).zip(last_line.chunks(bpp)) { + if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 { + current_line.extend_from_slice(&last_pixel[0..(bpp - bpc)]); + for _ in 0..bpc { + current_line.push(0); + } + } else { + current_line.extend_from_slice(pixel); + } + } + last_line = current_line.clone(); + lines.push(current_line.clone()); + current_line.clear(); + } + flatten(lines.into_iter().rev()).collect() +} + +fn reduced_alpha_to_down(png: &PngImage, bpc: usize, bpp: usize) -> Vec { + let mut reduced = Vec::with_capacity(png.data.len()); + let mut last_line = Vec::new(); + for line in png.scan_lines() { + if line.data.len() != last_line.len() { + last_line = vec![0; line.data.len()]; + } + reduced.push(line.filter); + for (pixel, last_pixel) in line.data.chunks(bpp).zip(last_line.chunks(bpp)) { + if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 { + reduced.extend_from_slice(&last_pixel[0..(bpp - bpc)]); + for _ in 0..bpc { + reduced.push(0); + } + } else { + reduced.extend_from_slice(pixel); + } + } + last_line = reduced.clone(); + } + reduced +} + +fn reduced_alpha_to_left(png: &PngImage, bpc: usize, bpp: usize) -> Vec { + let mut reduced = Vec::with_capacity(png.data.len()); + for line in png.scan_lines() { + let mut line_bytes = Vec::with_capacity(line.data.len()); + let mut last_pixel = vec![0; bpp]; + for pixel in line.data.chunks(bpp).rev() { + if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 { + line_bytes.extend_from_slice(&last_pixel[0..(bpp - bpc)]); + for _ in 0..bpc { + line_bytes.push(0); + } + } else { + line_bytes.extend_from_slice(pixel); + } + last_pixel = pixel.to_owned(); + } + reduced.push(line.filter); + reduced.extend(flatten(line_bytes.chunks(bpp).rev())); + } + reduced +} + +fn reduced_alpha_to_right(png: &PngImage, bpc: usize, bpp: usize) -> Vec { + let mut reduced = Vec::with_capacity(png.data.len()); + for line in png.scan_lines() { + reduced.push(line.filter); + let mut last_pixel = vec![0; bpp]; + for pixel in line.data.chunks(bpp) { + if pixel.iter().skip(bpp - bpc).fold(0, |sum, i| sum | i) == 0 { + reduced.extend_from_slice(&last_pixel[0..(bpp - bpc)]); + for _ in 0..bpc { + reduced.push(0); + } + } else { + reduced.extend_from_slice(pixel); + } + last_pixel = pixel.to_owned(); + } + } + reduced +} #[must_use] -pub fn reduced_alpha_channel(png: &PngData) -> Option { - let target_color_type = match png.ihdr_data.color_type { +pub fn reduced_alpha_channel(png: &PngImage) -> Option { + let target_color_type = match png.ihdr.color_type { ColorType::GrayscaleAlpha => ColorType::Grayscale, ColorType::RGBA => ColorType::RGB, _ => return None, }; - let byte_depth = png.ihdr_data.bit_depth.as_u8() >> 3; + let byte_depth = png.ihdr.bit_depth.as_u8() >> 3; let channels = png.channels_per_pixel(); let bpp = channels * byte_depth; let bpp_mask = bpp - 1; @@ -24,7 +204,7 @@ pub fn reduced_alpha_channel(png: &PngData) -> Option { } } - let mut raw_data = Vec::with_capacity(png.raw_data.len()); + let mut raw_data = Vec::with_capacity(png.data.len()); for line in png.scan_lines() { raw_data.push(line.filter); for (i, &byte) in line.data.iter().enumerate() { @@ -36,19 +216,20 @@ pub fn reduced_alpha_channel(png: &PngData) -> Option { } } - let mut aux_headers = HashMap::new(); + let mut aux_headers = png.aux_headers.clone(); // sBIT contains information about alpha channel's original depth, // and alpha has just been removed if let Some(sbit_header) = png.aux_headers.get(b"sBIT") { // Some programs save the sBIT header as RGB even if the image is RGBA. - aux_headers.insert(*b"sBIT", Some(sbit_header.iter().cloned().take(3).collect())); + aux_headers.insert(*b"sBIT", sbit_header.iter().cloned().take(3).collect()); } - Some(ReducedPng { - raw_data, - bit_depth: png.ihdr_data.bit_depth, - interlaced: png.ihdr_data.interlaced, - color_type: target_color_type, + Some(PngImage { + data: raw_data, + ihdr: IhdrData { + color_type: target_color_type, + ..png.ihdr + }, aux_headers, transparency_pixel: None, palette: None, diff --git a/src/reduction/bit_depth.rs b/src/reduction/bit_depth.rs index 10afca51..491a2183 100644 --- a/src/reduction/bit_depth.rs +++ b/src/reduction/bit_depth.rs @@ -1,7 +1,7 @@ -use reduction::ReducedPng; +use headers::IhdrData; use bit_vec::BitVec; use colors::{BitDepth, ColorType}; -use png::PngData; +use png::PngImage; const ONE_BIT_PERMUTATIONS: [u8; 2] = [0b0000_0000, 0b1111_1111]; const TWO_BIT_PERMUTATIONS: [u8; 5] = [ @@ -25,32 +25,92 @@ const FOUR_BIT_PERMUTATIONS: [u8; 11] = [ 0b1111_1111, ]; +/// Attempt to reduce the bit depth of the image +/// Returns true if the bit depth was reduced, false otherwise #[must_use] -pub fn reduce_bit_depth_8_or_less(png: &PngData) -> Option { - let mut reduced = BitVec::with_capacity(png.raw_data.len() * 8); - let bit_depth: usize = png.ihdr_data.bit_depth.as_u8() as usize; - let mut allowed_bits = 1; +pub fn reduce_bit_depth(png: &PngImage, minimum_bits: usize) -> Option { + if png.ihdr.bit_depth != BitDepth::Sixteen { + if png.ihdr.color_type == ColorType::Indexed + || png.ihdr.color_type == ColorType::Grayscale + { + return reduce_bit_depth_8_or_less(png, minimum_bits); + } + return None; + } + + // Reduce from 16 to 8 bits per channel per pixel + let mut reduced = Vec::with_capacity( + (png.ihdr.width * png.ihdr.height * u32::from(png.channels_per_pixel()) + + png.ihdr.height) as usize, + ); + let mut high_byte = 0; + for line in png.scan_lines() { - let bit_vec = BitVec::from_bytes(&line.data); - if png.ihdr_data.color_type == ColorType::Indexed { - for (i, bit) in bit_vec.iter().enumerate() { - let bit_index = bit_depth - (i % bit_depth); - if bit && bit_index > allowed_bits { - allowed_bits = bit_index.next_power_of_two(); - if allowed_bits == bit_depth { - // Not reducable - return None; - } + reduced.push(line.filter); + for (i, &byte) in line.data.iter().enumerate() { + if i % 2 == 0 { + // High byte + high_byte = byte; + } else { + // Low byte + if high_byte != byte { + // Can't reduce, exit early + return None; + } + reduced.push(byte); + } + } + } + + Some(PngImage { + data: reduced, + ihdr: IhdrData { + bit_depth: BitDepth::Eight, + ..png.ihdr + }, + palette: None, + transparency_pixel: png.transparency_pixel.clone(), + aux_headers: png.aux_headers.clone(), + }) +} + +#[must_use] +pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Option { + assert!(minimum_bits >= 1 && minimum_bits < 8); + let mut reduced = BitVec::with_capacity(png.data.len() * 8); + let bit_depth: usize = png.ihdr.bit_depth.as_u8() as usize; + if minimum_bits >= bit_depth { + return None; + } + for line in png.scan_lines() { + if png.ihdr.color_type == ColorType::Indexed { + let line_max = line.data.iter().map(|&byte| match png.ihdr.bit_depth { + BitDepth::Two => (byte & 0x3).max((byte >> 2) & 0x3).max((byte >> 4) & 0x3).max(byte >> 6), + BitDepth::Four => (byte & 0xF).max(byte >> 4), + _ => byte, + }).max().unwrap_or(0); + let required_bits = match line_max { + x if x > 0x0F => 8, + x if x > 0x03 => 4, + x if x > 0x01 => 2, + _ => 1, + }; + if required_bits > minimum_bits { + minimum_bits = required_bits; + if minimum_bits >= bit_depth { + // Not reducable + return None; } } } else { + let bit_vec = BitVec::from_bytes(&line.data); for byte in bit_vec.to_bytes() { - while allowed_bits < bit_depth { - let permutations: &[u8] = if allowed_bits == 1 { + while minimum_bits < bit_depth { + let permutations: &[u8] = if minimum_bits == 1 { &ONE_BIT_PERMUTATIONS - } else if allowed_bits == 2 { + } else if minimum_bits == 2 { &TWO_BIT_PERMUTATIONS - } else if allowed_bits == 4 { + } else if minimum_bits == 4 { &FOUR_BIT_PERMUTATIONS } else { return None; @@ -58,7 +118,7 @@ pub fn reduce_bit_depth_8_or_less(png: &PngData) -> Option { if permutations.iter().any(|perm| *perm == byte) { break; } else { - allowed_bits <<= 1; + minimum_bits <<= 1; } } } @@ -70,7 +130,7 @@ pub fn reduce_bit_depth_8_or_less(png: &PngData) -> Option { let bit_vec = BitVec::from_bytes(&line.data); for (i, bit) in bit_vec.iter().enumerate() { let bit_index = bit_depth - (i % bit_depth); - if bit_index <= allowed_bits { + if bit_index <= minimum_bits { reduced.push(bit); } } @@ -80,12 +140,13 @@ pub fn reduce_bit_depth_8_or_less(png: &PngData) -> Option { } } - Some(ReducedPng { - color_type: png.ihdr_data.color_type, - interlaced: png.ihdr_data.interlaced, - raw_data: reduced.to_bytes(), - bit_depth: BitDepth::from_u8(allowed_bits as u8), - aux_headers: Default::default(), + Some(PngImage { + data: reduced.to_bytes(), + ihdr: IhdrData { + bit_depth: BitDepth::from_u8(minimum_bits as u8), + ..png.ihdr + }, + aux_headers: png.aux_headers.clone(), palette: png.palette.clone(), transparency_pixel: png.transparency_pixel.clone(), }) diff --git a/src/reduction/color.rs b/src/reduction/color.rs index c773b933..abebf38a 100644 --- a/src/reduction/color.rs +++ b/src/reduction/color.rs @@ -1,15 +1,15 @@ -use reduction::ReducedPng; +use headers::IhdrData; use colors::{BitDepth, ColorType}; use itertools::Itertools; -use png::PngData; +use png::PngImage; use rgb::{FromSlice, RGB8, RGBA8}; use std::collections::HashMap; use std::hash::Hash; #[must_use] -pub fn reduce_rgba_to_grayscale_alpha(png: &PngData) -> Option { - let mut reduced = Vec::with_capacity(png.raw_data.len()); - let byte_depth = png.ihdr_data.bit_depth.as_u8() >> 3; +pub fn reduce_rgba_to_grayscale_alpha(png: &PngImage) -> Option { + let mut reduced = Vec::with_capacity(png.data.len()); + let byte_depth = png.ihdr.bit_depth.as_u8() >> 3; let bpp = 4 * byte_depth; let bpp_mask = bpp - 1; assert_eq!(0, bpp & bpp_mask); @@ -49,20 +49,25 @@ pub fn reduce_rgba_to_grayscale_alpha(png: &PngData) -> Option { } } - let mut aux_headers = HashMap::new(); + let mut aux_headers = png.aux_headers.clone(); if let Some(sbit_header) = png.aux_headers.get(b"sBIT") { - aux_headers.insert(*b"sBIT", sbit_header.get(0).map(|&s| vec![s])); + if let Some(&s) = sbit_header.get(0) { + aux_headers.insert(*b"sBIT", vec![s]); + } } if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") { - aux_headers.insert(*b"bKGD", bkgd_header.get(0..2).map(|b| b.to_owned())); + if let Some(b) = bkgd_header.get(0..2) { + aux_headers.insert(*b"bKGD", b.to_owned()); + } } - Some(ReducedPng { - raw_data: reduced, - bit_depth: png.ihdr_data.bit_depth, - interlaced: png.ihdr_data.interlaced, - color_type: ColorType::GrayscaleAlpha, + Some(PngImage { + data: reduced, + ihdr: IhdrData { + color_type: ColorType::GrayscaleAlpha, + ..png.ihdr + }, palette: None, transparency_pixel: None, aux_headers, @@ -95,11 +100,11 @@ where } #[must_use] -pub fn reduced_color_to_palette(png: &PngData) -> Option { - if png.ihdr_data.bit_depth != BitDepth::Eight { +pub fn reduced_color_to_palette(png: &PngImage) -> Option { + if png.ihdr.bit_depth != BitDepth::Eight { return None; } - let mut raw_data = Vec::with_capacity(png.raw_data.len()); + let mut raw_data = Vec::with_capacity(png.data.len()); let mut palette = HashMap::with_capacity(257); let transparency_pixel = png .transparency_pixel @@ -107,7 +112,7 @@ pub fn reduced_color_to_palette(png: &PngData) -> Option { .map(|t| RGB8::new(t[1], t[3], t[5])); for line in png.scan_lines() { raw_data.push(line.filter); - let ok = if png.ihdr_data.color_type == ColorType::RGB { + let ok = if png.ihdr.color_type == ColorType::RGB { reduce_scanline_to_palette( line.data.as_rgb().iter().cloned().map(|px| { px.alpha(if Some(px) != transparency_pixel { @@ -120,7 +125,7 @@ pub fn reduced_color_to_palette(png: &PngData) -> Option { &mut raw_data, ) } else { - debug_assert_eq!(png.ihdr_data.color_type, ColorType::RGBA); + debug_assert_eq!(png.ihdr.color_type, ColorType::RGBA); reduce_scanline_to_palette( line.data.as_rgba().iter().cloned(), &mut palette, @@ -144,12 +149,12 @@ pub fn reduced_color_to_palette(png: &PngData) -> Option { let trns_size = num_transparent.map(|n| n + 8).unwrap_or(0); let headers_size = palette.len() * 3 + 8 + trns_size; - if raw_data.len() + headers_size > png.raw_data.len() { + if raw_data.len() + headers_size > png.data.len() { // Reduction would result in a larger image return None; } - let mut aux_headers = HashMap::new(); + let mut aux_headers = png.aux_headers.clone(); if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") { assert_eq!(bkgd_header.len(), 6); // In bKGD 16-bit values are used even for 8-bit images @@ -163,12 +168,12 @@ pub fn reduced_color_to_palette(png: &PngData) -> Option { } else { return None; // No space in palette to store the bg as an index }; - aux_headers.insert(*b"bKGD", Some(vec![entry])); + aux_headers.insert(*b"bKGD", vec![entry]); } if let Some(sbit_header) = png.aux_headers.get(b"sBIT") { // Some programs save the sBIT header as RGB even if the image is RGBA. - aux_headers.insert(*b"sBIT", Some(sbit_header.iter().cloned().take(3).collect())); + aux_headers.insert(*b"sBIT", sbit_header.iter().cloned().take(3).collect()); } let mut palette_vec = vec![RGBA8::new(0, 0, 0, 0); palette.len()]; @@ -176,21 +181,22 @@ pub fn reduced_color_to_palette(png: &PngData) -> Option { palette_vec[idx as usize] = color; } - Some(ReducedPng { - color_type: ColorType::Indexed, - bit_depth: png.ihdr_data.bit_depth, - interlaced: png.ihdr_data.interlaced, + Some(PngImage { + data: raw_data, + ihdr: IhdrData { + color_type: ColorType::Indexed, + ..png.ihdr + }, aux_headers, - raw_data, transparency_pixel: None, palette: Some(palette_vec), }) } #[must_use] -pub fn reduce_rgb_to_grayscale(png: &PngData) -> Option { - let mut reduced = Vec::with_capacity(png.raw_data.len()); - let byte_depth: u8 = png.ihdr_data.bit_depth.as_u8() >> 3; +pub fn reduce_rgb_to_grayscale(png: &PngImage) -> Option { + let mut reduced = Vec::with_capacity(png.data.len()); + let byte_depth: u8 = png.ihdr.bit_depth.as_u8() >> 3; let bpp: usize = 3 * byte_depth as usize; let mut cur_pixel = Vec::with_capacity(bpp); for line in png.scan_lines() { @@ -232,21 +238,26 @@ pub fn reduce_rgb_to_grayscale(png: &PngData) -> Option { png.transparency_pixel.clone() }; - let mut aux_headers = HashMap::new(); + let mut aux_headers = png.aux_headers.clone(); if let Some(sbit_header) = png.aux_headers.get(b"sBIT") { - aux_headers.insert(*b"sBIT", sbit_header.get(0).map(|&byte| vec![byte])); + if let Some(&byte) = sbit_header.get(0) { + aux_headers.insert(*b"sBIT", vec![byte]); + } } if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") { - aux_headers.insert(*b"bKGD", bkgd_header.get(0..2).map(|b| b.to_owned())); + if let Some(b) = bkgd_header.get(0..2) { + aux_headers.insert(*b"bKGD", b.to_owned()); + } } - Some(ReducedPng { - raw_data: reduced, - color_type: ColorType::Grayscale, - bit_depth: png.ihdr_data.bit_depth, - interlaced: png.ihdr_data.interlaced, + Some(PngImage { + data: reduced, + ihdr: IhdrData { + color_type: ColorType::Grayscale, + .. png.ihdr + }, + aux_headers, palette: None, transparency_pixel, - aux_headers, }) } diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index 09d32d31..47f0e483 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -1,36 +1,30 @@ +use headers::IhdrData; use std::collections::HashMap; use colors::{BitDepth, ColorType}; use std::collections::hash_map::Entry::*; -use png::PngData; +use png::PngImage; use rgb::RGBA8; +use std::borrow::Cow; pub mod alpha; +use alpha::*; pub mod bit_depth; +use bit_depth::*; pub mod color; +use color::*; -/// Fields to replace in PngData to apply the reduction -pub struct ReducedPng { - pub color_type: ColorType, - pub raw_data: Vec, - pub bit_depth: BitDepth, - /// replace if Some - pub palette: Option>, - /// replace if Some - pub transparency_pixel: Option>, - /// replace if Some, delete if None - pub aux_headers: HashMap<[u8; 4], Option>>, - pub interlaced: u8, -} +pub use bit_depth::reduce_bit_depth; +pub use alpha::try_alpha_reductions; /// Attempt to reduce the number of colors in the palette /// Returns `None` if palette hasn't changed #[must_use] -pub fn reduced_palette(png: &PngData) -> Option { - if png.ihdr_data.color_type != ColorType::Indexed { +pub fn reduced_palette(png: &PngImage) -> Option { + if png.ihdr.color_type != ColorType::Indexed { // Can't reduce if there is no palette return None; } - if png.ihdr_data.bit_depth == BitDepth::One { + if png.ihdr.bit_depth == BitDepth::One { // Gains from 1-bit images will be at most 1 byte // Not worth the CPU time return None; @@ -43,7 +37,7 @@ pub fn reduced_palette(png: &PngData) -> Option { // Find palette entries that are never used for line in png.scan_lines() { - match png.ihdr_data.bit_depth { + match png.ihdr.bit_depth { BitDepth::Eight => for &byte in line.data { used[byte as usize] = true; }, @@ -88,9 +82,9 @@ pub fn reduced_palette(png: &PngData) -> Option { } #[must_use] -fn do_palette_reduction(png: &PngData, palette_map: &[Option; 256]) -> Option { +fn do_palette_reduction(png: &PngImage, palette_map: &[Option; 256]) -> Option { let byte_map = palette_map_to_byte_map(png, palette_map)?; - let mut raw_data = Vec::with_capacity(png.raw_data.len()); + let mut raw_data = Vec::with_capacity(png.data.len()); // Reassign data bytes to new indices for line in png.scan_lines() { @@ -100,25 +94,26 @@ fn do_palette_reduction(png: &PngData, palette_map: &[Option; 256]) -> Optio } } - let mut aux_headers = HashMap::new(); + let mut aux_headers = png.aux_headers.clone(); if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") { if let Some(Some(map_to)) = bkgd_header.get(0).and_then(|&idx| palette_map.get(idx as usize)) { - aux_headers.insert(*b"bKGD", Some(vec![*map_to])); + aux_headers.insert(*b"bKGD", vec![*map_to]); } } - Some(ReducedPng { - color_type: ColorType::Indexed, - bit_depth: png.ihdr_data.bit_depth, - interlaced: png.ihdr_data.interlaced, - raw_data, + Some(PngImage { + ihdr: IhdrData { + color_type: ColorType::Indexed, + ..png.ihdr + }, + data: raw_data, transparency_pixel: None, palette: Some(reordered_palette(png.palette.as_ref()?, palette_map)), aux_headers, }) } -fn palette_map_to_byte_map(png: &PngData, palette_map: &[Option; 256]) -> Option<[u8; 256]> { +fn palette_map_to_byte_map(png: &PngImage, palette_map: &[Option; 256]) -> Option<[u8; 256]> { let len = png.palette.as_ref().map(|p| p.len()).unwrap_or(0); if (0..len).all(|i| palette_map[i].map_or(true, |to| to == i as u8)) { // No reduction necessary @@ -128,7 +123,7 @@ fn palette_map_to_byte_map(png: &PngData, palette_map: &[Option; 256]) -> Op let mut byte_map = [0u8; 256]; // low bit-depths can be pre-computed for every byte value - match png.ihdr_data.bit_depth { + match png.ihdr.bit_depth { BitDepth::Eight => { for byte in 0..=255 { byte_map[byte as usize] = palette_map[byte as usize].unwrap_or(0) @@ -167,3 +162,49 @@ fn reordered_palette(palette: &[RGBA8], palette_map: &[Option; 256]) -> Vec< } new_palette } + + +/// Attempt to reduce the color type of the image +/// Returns true if the color type was reduced, false otherwise +pub fn reduce_color_type(png: &PngImage) -> Option { + let mut should_reduce_bit_depth = false; + let mut reduced = Cow::Borrowed(png); + + // Go down one step at a time + // Maybe not the most efficient, but it's safe + if reduced.ihdr.color_type == ColorType::RGBA { + if let Some(r) = reduce_rgba_to_grayscale_alpha(&reduced).or_else(|| reduced_alpha_channel(&reduced)) { + reduced = Cow::Owned(r); + } else if let Some(r) = reduced_color_to_palette(&reduced) { + reduced = Cow::Owned(r); + should_reduce_bit_depth = true; + } + } + + if reduced.ihdr.color_type == ColorType::GrayscaleAlpha { + if let Some(r) = reduced_alpha_channel(&reduced) { + reduced = Cow::Owned(r); + should_reduce_bit_depth = true; + } + } + + if reduced.ihdr.color_type == ColorType::RGB { + if let Some(r) = reduce_rgb_to_grayscale(&reduced).or_else(|| reduced_color_to_palette(&reduced)) { + reduced = Cow::Owned(r); + should_reduce_bit_depth = true; + } + } + + if should_reduce_bit_depth { + // Some conversions will allow us to perform bit depth reduction that + // wasn't possible before + if let Some(r) = reduce_bit_depth_8_or_less(&reduced, 1) { + reduced = Cow::Owned(r); + } + } + + match reduced { + Cow::Owned(r) => Some(r), + _ => None, + } +} diff --git a/tests/files/issue-159.png b/tests/files/issue-159.png new file mode 100644 index 00000000..cd5585ab Binary files /dev/null and b/tests/files/issue-159.png differ diff --git a/tests/filters.rs b/tests/filters.rs index 5aff51c5..82d2b6d7 100644 --- a/tests/filters.rs +++ b/tests/filters.rs @@ -35,8 +35,8 @@ fn test_it_converts( let png = PngData::new(&input, opts.fix_errors).unwrap(); opts.filter = HashSet::new(); opts.filter.insert(filter); - assert_eq!(png.ihdr_data.color_type, color_type_in); - assert_eq!(png.ihdr_data.bit_depth, bit_depth_in); + assert_eq!(png.raw.ihdr.color_type, color_type_in); + assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -53,8 +53,13 @@ fn test_it_converts( } }; - assert_eq!(png.ihdr_data.color_type, color_type_out); - assert_eq!(png.ihdr_data.bit_depth, bit_depth_out); + assert_eq!(png.raw.ihdr.color_type, color_type_out); + assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out); + if let Some(palette) = png.raw.palette.as_ref() { + assert!(palette.len() <= 1 << (png.raw.ihdr.bit_depth.as_u8() as usize)); + } else { + assert!(png.raw.ihdr.color_type != ColorType::Indexed); + } remove_file(output).ok(); } diff --git a/tests/flags.rs b/tests/flags.rs index c4db250c..9f308854 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -32,8 +32,8 @@ fn test_it_converts( ) { let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert_eq!(png.ihdr_data.color_type, color_type_in); - assert_eq!(png.ihdr_data.bit_depth, bit_depth_in); + assert_eq!(png.raw.ihdr.color_type, color_type_in); + assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -50,8 +50,8 @@ fn test_it_converts( } }; - assert_eq!(png.ihdr_data.color_type, color_type_out); - assert_eq!(png.ihdr_data.bit_depth, bit_depth_out); + assert_eq!(png.raw.ihdr.color_type, color_type_out); + assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out); remove_file(output).ok(); } @@ -81,9 +81,9 @@ fn strip_headers_list() { let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert!(png.aux_headers.contains_key(b"tEXt")); - assert!(png.aux_headers.contains_key(b"iTXt")); - assert!(png.aux_headers.contains_key(b"iCCP")); + assert!(png.raw.aux_headers.contains_key(b"tEXt")); + assert!(png.raw.aux_headers.contains_key(b"iTXt")); + assert!(png.raw.aux_headers.contains_key(b"iCCP")); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -100,9 +100,9 @@ fn strip_headers_list() { } }; - assert!(!png.aux_headers.contains_key(b"tEXt")); - assert!(png.aux_headers.contains_key(b"iTXt")); - assert!(!png.aux_headers.contains_key(b"iCCP")); + assert!(!png.raw.aux_headers.contains_key(b"tEXt")); + assert!(png.raw.aux_headers.contains_key(b"iTXt")); + assert!(!png.raw.aux_headers.contains_key(b"iCCP")); remove_file(output).ok(); } @@ -115,9 +115,9 @@ fn strip_headers_safe() { let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert!(png.aux_headers.contains_key(b"tEXt")); - assert!(png.aux_headers.contains_key(b"iTXt")); - assert!(png.aux_headers.contains_key(b"iCCP")); + assert!(png.raw.aux_headers.contains_key(b"tEXt")); + assert!(png.raw.aux_headers.contains_key(b"iTXt")); + assert!(png.raw.aux_headers.contains_key(b"iCCP")); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -134,9 +134,9 @@ fn strip_headers_safe() { } }; - assert!(!png.aux_headers.contains_key(b"tEXt")); - assert!(!png.aux_headers.contains_key(b"iTXt")); - assert!(png.aux_headers.contains_key(b"sRGB")); + assert!(!png.raw.aux_headers.contains_key(b"tEXt")); + assert!(!png.raw.aux_headers.contains_key(b"iTXt")); + assert!(png.raw.aux_headers.contains_key(b"sRGB")); remove_file(output).ok(); } @@ -149,9 +149,9 @@ fn strip_headers_all() { let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert!(png.aux_headers.contains_key(b"tEXt")); - assert!(png.aux_headers.contains_key(b"iTXt")); - assert!(png.aux_headers.contains_key(b"iCCP")); + assert!(png.raw.aux_headers.contains_key(b"tEXt")); + assert!(png.raw.aux_headers.contains_key(b"iTXt")); + assert!(png.raw.aux_headers.contains_key(b"iCCP")); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -168,9 +168,9 @@ fn strip_headers_all() { } }; - assert!(!png.aux_headers.contains_key(b"tEXt")); - assert!(!png.aux_headers.contains_key(b"iTXt")); - assert!(!png.aux_headers.contains_key(b"iCCP")); + assert!(!png.raw.aux_headers.contains_key(b"tEXt")); + assert!(!png.raw.aux_headers.contains_key(b"iTXt")); + assert!(!png.raw.aux_headers.contains_key(b"iCCP")); remove_file(output).ok(); } @@ -183,9 +183,9 @@ fn strip_headers_none() { let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert!(png.aux_headers.contains_key(b"tEXt")); - assert!(png.aux_headers.contains_key(b"iTXt")); - assert!(png.aux_headers.contains_key(b"iCCP")); + assert!(png.raw.aux_headers.contains_key(b"tEXt")); + assert!(png.raw.aux_headers.contains_key(b"iTXt")); + assert!(png.raw.aux_headers.contains_key(b"iCCP")); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -202,9 +202,9 @@ fn strip_headers_none() { } }; - assert!(png.aux_headers.contains_key(b"tEXt")); - assert!(png.aux_headers.contains_key(b"iTXt")); - assert!(png.aux_headers.contains_key(b"iCCP")); + assert!(png.raw.aux_headers.contains_key(b"tEXt")); + assert!(png.raw.aux_headers.contains_key(b"iTXt")); + assert!(png.raw.aux_headers.contains_key(b"iCCP")); remove_file(output).ok(); } @@ -217,7 +217,7 @@ fn interlacing_0_to_1() { let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert_eq!(png.ihdr_data.interlaced, 0); + assert_eq!(png.raw.ihdr.interlaced, 0); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -234,7 +234,7 @@ fn interlacing_0_to_1() { } }; - assert_eq!(png.ihdr_data.interlaced, 1); + assert_eq!(png.raw.ihdr.interlaced, 1); remove_file(output).ok(); } @@ -247,7 +247,7 @@ fn interlacing_1_to_0() { let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert_eq!(png.ihdr_data.interlaced, 1); + assert_eq!(png.raw.ihdr.interlaced, 1); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -264,7 +264,7 @@ fn interlacing_1_to_0() { } }; - assert_eq!(png.ihdr_data.interlaced, 0); + assert_eq!(png.raw.ihdr.interlaced, 0); remove_file(output).ok(); } @@ -277,9 +277,9 @@ fn interlacing_0_to_1_small_files() { let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert_eq!(png.ihdr_data.interlaced, 0); - assert_eq!(png.ihdr_data.color_type, ColorType::Indexed); - assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); + assert_eq!(png.raw.ihdr.interlaced, 0); + assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -296,9 +296,9 @@ fn interlacing_0_to_1_small_files() { } }; - assert_eq!(png.ihdr_data.interlaced, 1); - assert_eq!(png.ihdr_data.color_type, ColorType::Indexed); - assert_eq!(png.ihdr_data.bit_depth, BitDepth::One); + assert_eq!(png.raw.ihdr.interlaced, 1); + assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::One); remove_file(output).ok(); } @@ -311,9 +311,9 @@ fn interlacing_1_to_0_small_files() { let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert_eq!(png.ihdr_data.interlaced, 1); - assert_eq!(png.ihdr_data.color_type, ColorType::Indexed); - assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); + assert_eq!(png.raw.ihdr.interlaced, 1); + assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -330,9 +330,9 @@ fn interlacing_1_to_0_small_files() { } }; - assert_eq!(png.ihdr_data.interlaced, 0); - assert_eq!(png.ihdr_data.color_type, ColorType::Indexed); - assert_eq!(png.ihdr_data.bit_depth, BitDepth::One); + assert_eq!(png.raw.ihdr.interlaced, 0); + assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + // the depth can't be asserted reliably, because on such small file different zlib implementaitons pick diferent depth as the best remove_file(output).ok(); } @@ -348,7 +348,7 @@ fn interlaced_0_to_1_other_filter_mode() { let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert_eq!(png.ihdr_data.interlaced, 0); + assert_eq!(png.raw.ihdr.interlaced, 0); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -365,7 +365,7 @@ fn interlaced_0_to_1_other_filter_mode() { } }; - assert_eq!(png.ihdr_data.interlaced, 1); + assert_eq!(png.raw.ihdr.interlaced, 1); remove_file(output).ok(); } @@ -397,8 +397,8 @@ fn fix_errors() { let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert_eq!(png.ihdr_data.color_type, ColorType::RGBA); - assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); + assert_eq!(png.raw.ihdr.color_type, ColorType::RGBA); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -415,8 +415,8 @@ fn fix_errors() { } }; - assert_eq!(png.ihdr_data.color_type, ColorType::Grayscale); - assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); + assert_eq!(png.raw.ihdr.color_type, ColorType::Grayscale); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); // Cannot check if pixels are equal because image crate cannot read corrupt (input) PNGs remove_file(output).ok(); diff --git a/tests/interlaced.rs b/tests/interlaced.rs index 2184e73f..7b93f0ee 100644 --- a/tests/interlaced.rs +++ b/tests/interlaced.rs @@ -32,9 +32,9 @@ fn test_it_converts( let (output, opts) = get_opts(&input); let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert_eq!(png.ihdr_data.color_type, color_type_in); - assert_eq!(png.ihdr_data.bit_depth, bit_depth_in); - assert_eq!(png.ihdr_data.interlaced, 1); + assert_eq!(png.raw.ihdr.color_type, color_type_in); + assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); + assert_eq!(png.raw.ihdr.interlaced, 1); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -51,8 +51,8 @@ fn test_it_converts( } }; - assert_eq!(png.ihdr_data.color_type, color_type_out); - assert_eq!(png.ihdr_data.bit_depth, bit_depth_out); + assert_eq!(png.raw.ihdr.color_type, color_type_out); + assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out); remove_file(output).ok(); } @@ -444,6 +444,10 @@ fn interlaced_palette_8_should_be_palette_8() { #[test] fn interlaced_palette_8_should_be_palette_4() { + // miniz doesn't estimate compression that well + if !oxipng::internal_tests::cfzlib::is_supported() { + return; + } test_it_converts( "tests/files/interlaced_palette_8_should_be_palette_4.png", ColorType::Indexed, diff --git a/tests/reduction.rs b/tests/reduction.rs index 23d95382..a9e5f947 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -36,9 +36,9 @@ fn test_it_converts( } let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert_eq!(png.ihdr_data.color_type, color_type_in); - assert_eq!(png.ihdr_data.bit_depth, bit_depth_in); - assert_eq!(png.ihdr_data.interlaced, 0); + assert_eq!(png.raw.ihdr.color_type, color_type_in); + assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in, "test file is broken"); + assert_eq!(png.raw.ihdr.interlaced, 0); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -55,8 +55,8 @@ fn test_it_converts( } }; - assert_eq!(png.ihdr_data.color_type, color_type_out); - assert_eq!(png.ihdr_data.bit_depth, bit_depth_out); + assert_eq!(png.raw.ihdr.color_type, color_type_out); + assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out); remove_file(output).ok(); } @@ -699,26 +699,13 @@ fn grayscale_8_should_be_grayscale_8() { #[test] fn small_files() { - test_it_converts( - "tests/files/small_files.png", - None, - ColorType::Indexed, - BitDepth::Eight, - ColorType::Indexed, - BitDepth::One, - ); -} - -#[test] -fn palette_should_be_reduced_with_dupes() { - let input = PathBuf::from("tests/files/palette_should_be_reduced_with_dupes.png"); + let input = PathBuf::from("tests/files/small_files.png"); let (output, opts) = get_opts(&input); let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert_eq!(png.ihdr_data.color_type, ColorType::Indexed); - assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); - assert_eq!(png.palette.unwrap().len(), 43); + assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -735,9 +722,41 @@ fn palette_should_be_reduced_with_dupes() { } }; - assert_eq!(png.ihdr_data.color_type, ColorType::Indexed); - assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); - assert_eq!(png.palette.unwrap().len(), 35); + assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + // depth varies depending on zlib implementation used + + remove_file(output).ok(); +} + +#[test] +fn palette_should_be_reduced_with_dupes() { + let input = PathBuf::from("tests/files/palette_should_be_reduced_with_dupes.png"); + let (output, opts) = get_opts(&input); + + let png = PngData::new(&input, opts.fix_errors).unwrap(); + + assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); + assert_eq!(png.raw.palette.as_ref().unwrap().len(), 43); + + match oxipng::optimize(&InFile::Path(input), &output, &opts) { + Ok(_) => (), + Err(x) => panic!("{}", x), + }; + let output = output.path().unwrap(); + assert!(output.exists()); + + let png = match PngData::new(&output, opts.fix_errors) { + Ok(x) => x, + Err(x) => { + remove_file(&output).ok(); + panic!("{}", x) + } + }; + + assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); + assert_eq!(png.raw.palette.as_ref().unwrap().len(), 35); remove_file(output).ok(); } @@ -749,9 +768,9 @@ fn palette_should_be_reduced_with_unused() { let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert_eq!(png.ihdr_data.color_type, ColorType::Indexed); - assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); - assert_eq!(png.palette.unwrap().len(), 35); + assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); + assert_eq!(png.raw.palette.as_ref().unwrap().len(), 35); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -768,9 +787,9 @@ fn palette_should_be_reduced_with_unused() { } }; - assert_eq!(png.ihdr_data.color_type, ColorType::Indexed); - assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); - assert_eq!(png.palette.unwrap().len(), 33); + assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); + assert_eq!(png.raw.palette.as_ref().unwrap().len(), 33); remove_file(output).ok(); } @@ -782,9 +801,9 @@ fn palette_should_be_reduced_with_both() { let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert_eq!(png.ihdr_data.color_type, ColorType::Indexed); - assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); - assert_eq!(png.palette.unwrap().len(), 43); + assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); + assert_eq!(png.raw.palette.as_ref().unwrap().len(), 43); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -801,9 +820,9 @@ fn palette_should_be_reduced_with_both() { } }; - assert_eq!(png.ihdr_data.color_type, ColorType::Indexed); - assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); - assert_eq!(png.palette.unwrap().len(), 33); + assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); + assert_eq!(png.raw.palette.as_ref().unwrap().len(), 33); remove_file(output).ok(); } diff --git a/tests/regression.rs b/tests/regression.rs index c2ebfd07..e53ba734 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -33,8 +33,8 @@ fn test_it_converts( let (output, opts) = custom.unwrap_or_else(|| get_opts(&input)); let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert_eq!(png.ihdr_data.color_type, color_type_in); - assert_eq!(png.ihdr_data.bit_depth, bit_depth_in); + assert_eq!(png.raw.ihdr.color_type, color_type_in, "test file is broken"); + assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in, "test file is broken"); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -51,8 +51,13 @@ fn test_it_converts( } }; - assert_eq!(png.ihdr_data.color_type, color_type_out); - assert_eq!(png.ihdr_data.bit_depth, bit_depth_out); + assert_eq!(png.raw.ihdr.color_type, color_type_out, "optimized to wrong color type"); + assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out, "optimized to wrong bit depth"); + if let Some(palette) = png.raw.palette.as_ref() { + assert!(palette.len() <= 1 << (png.raw.ihdr.bit_depth.as_u8() as usize)); + } else { + assert!(png.raw.ihdr.color_type != ColorType::Indexed); + } remove_file(output).ok(); } @@ -77,9 +82,9 @@ fn issue_42() { let png = PngData::new(&input, opts.fix_errors).unwrap(); - assert_eq!(png.ihdr_data.interlaced, 0); - assert_eq!(png.ihdr_data.color_type, ColorType::GrayscaleAlpha); - assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); + assert_eq!(png.raw.ihdr.interlaced, 0); + assert_eq!(png.raw.ihdr.color_type, ColorType::GrayscaleAlpha); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); match oxipng::optimize(&InFile::Path(input), &output, &opts) { Ok(_) => (), @@ -96,9 +101,9 @@ fn issue_42() { } }; - assert_eq!(png.ihdr_data.interlaced, 1); - assert_eq!(png.ihdr_data.color_type, ColorType::GrayscaleAlpha); - assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight); + assert_eq!(png.raw.ihdr.interlaced, 1); + assert_eq!(png.raw.ihdr.color_type, ColorType::GrayscaleAlpha); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); remove_file(output).ok(); } @@ -572,3 +577,15 @@ fn issue_153() { BitDepth::Eight, ); } + +#[test] +fn issue_159() { + test_it_converts( + "tests/files/issue-159.png", + None, + ColorType::Indexed, + BitDepth::One, + ColorType::Indexed, + BitDepth::One, + ); +}