From 7ad93edeed37e54cf75bc7e856b1f891c0df96f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kornel=20Lesin=CC=81ski?= Date: Thu, 17 Jan 2019 16:06:11 +0000 Subject: [PATCH] Also try 4-bit depth for small-depth images --- src/colors.rs | 2 +- src/evaluate.rs | 23 ++++++++++++++++++----- src/lib.rs | 11 ++++++++++- src/reduction/bit_depth.rs | 14 +++++++------- src/reduction/mod.rs | 2 +- tests/flags.rs | 2 +- tests/reduction.rs | 37 ++++++++++++++++++++++++++++--------- 7 files changed, 66 insertions(+), 25 deletions(-) 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/evaluate.rs b/src/evaluate.rs index 2b7c62be..adeef359 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -58,9 +58,9 @@ impl Evaluator { /// Main loop of evaluation thread fn evaluate_images(from_channel: Receiver<(Arc, bool)>) -> Option { let best_candidate_size = AtomicMin::new(None); - let best_result = Mutex::new(None); + let best_result: Mutex> = Mutex::new(None); // ends when sender is dropped - for (image, is_reduction) in from_channel.iter() { + for (nth, (image, 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"))] @@ -75,13 +75,26 @@ impl Evaluator { &best_candidate_size, ) { let mut res = best_result.lock().unwrap(); - if best_candidate_size.get().map_or(true, |len| len >= idat_data.len()) { + if best_candidate_size.get().map_or(true, |best_len| { + // 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 = (idat_data.len(), image.data.len(), image.ihdr.bit_depth, f, nth); + let old = (best_len, old_img.data.len(), old_img.ihdr.bit_depth, res.1, res.2); + new < old + } else if best_len > idat_data.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)) + }, f, nth)) } else { None }; @@ -90,7 +103,7 @@ impl Evaluator { }); } best_result.into_inner().expect("filters should be done") - .map(|(img, _)| img) + .map(|(img, _, _)| img) } } diff --git a/src/lib.rs b/src/lib.rs index 6600bb79..be18e74d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,7 @@ 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}; @@ -767,9 +768,17 @@ fn perform_reductions(mut png: Arc, opts: &Options, deadline: &Deadlin } if opts.bit_depth_reduction { - if let Some(reduced) = reduce_bit_depth(&png) { + 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()); + 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)); + } + } if opts.verbosity == Some(1) { report_reduction(&png); } diff --git a/src/reduction/bit_depth.rs b/src/reduction/bit_depth.rs index 24493f0f..01046311 100644 --- a/src/reduction/bit_depth.rs +++ b/src/reduction/bit_depth.rs @@ -28,12 +28,12 @@ const FOUR_BIT_PERMUTATIONS: [u8; 11] = [ /// 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(png: &PngImage) -> Option { +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); + return reduce_bit_depth_8_or_less(png, minimum_bits); } return None; } @@ -75,18 +75,18 @@ pub fn reduce_bit_depth(png: &PngImage) -> Option { } #[must_use] -pub fn reduce_bit_depth_8_or_less(png: &PngImage) -> Option { +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; - let mut minimum_bits = 1; 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), + 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 { @@ -94,7 +94,7 @@ pub fn reduce_bit_depth_8_or_less(png: &PngImage) -> Option { 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 { diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index 855f5a16..47f0e483 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -198,7 +198,7 @@ pub fn reduce_color_type(png: &PngImage) -> Option { 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) { + if let Some(r) = reduce_bit_depth_8_or_less(&reduced, 1) { reduced = Cow::Owned(r); } } diff --git a/tests/flags.rs b/tests/flags.rs index b75f2e13..9f308854 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -332,7 +332,7 @@ fn interlacing_1_to_0_small_files() { assert_eq!(png.raw.ihdr.interlaced, 0); assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); - assert_eq!(png.raw.ihdr.bit_depth, BitDepth::One); + // 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(); } diff --git a/tests/reduction.rs b/tests/reduction.rs index f10cdade..a9e5f947 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -37,7 +37,7 @@ fn test_it_converts( let png = PngData::new(&input, opts.fix_errors).unwrap(); 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.bit_depth, bit_depth_in, "test file is broken"); assert_eq!(png.raw.ihdr.interlaced, 0); match oxipng::optimize(&InFile::Path(input), &output, &opts) { @@ -699,14 +699,33 @@ 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, - ); + 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.raw.ihdr.color_type, ColorType::Indexed); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); + + 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); + // depth varies depending on zlib implementation used + + remove_file(output).ok(); } #[test]