Also try 4-bit depth for small-depth images

This commit is contained in:
Kornel Lesiński 2019-01-17 16:06:11 +00:00 committed by Kornel
parent de61b7abf9
commit 7ad93edeed
7 changed files with 66 additions and 25 deletions

View file

@ -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 /// The number of bits to be used per channel per pixel
pub enum BitDepth { pub enum BitDepth {
/// One bit per channel per pixel /// One bit per channel per pixel

View file

@ -58,9 +58,9 @@ impl Evaluator {
/// Main loop of evaluation thread /// Main loop of evaluation thread
fn evaluate_images(from_channel: Receiver<(Arc<PngImage>, bool)>) -> Option<PngData> { fn evaluate_images(from_channel: Receiver<(Arc<PngImage>, bool)>) -> Option<PngData> {
let best_candidate_size = AtomicMin::new(None); let best_candidate_size = AtomicMin::new(None);
let best_result = Mutex::new(None); let best_result: Mutex<Option<(PngData, _, _)>> = Mutex::new(None);
// ends when sender is dropped // 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")] #[cfg(feature = "parallel")]
let filters_iter = STD_FILTERS.par_iter().with_max_len(1); let filters_iter = STD_FILTERS.par_iter().with_max_len(1);
#[cfg(not(feature = "parallel"))] #[cfg(not(feature = "parallel"))]
@ -75,13 +75,26 @@ impl Evaluator {
&best_candidate_size, &best_candidate_size,
) { ) {
let mut res = best_result.lock().unwrap(); 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()); best_candidate_size.set_min(idat_data.len());
*res = if is_reduction { *res = if is_reduction {
Some((PngData { Some((PngData {
idat_data, idat_data,
raw: Arc::clone(&image), raw: Arc::clone(&image),
}, f)) }, f, nth))
} else { } else {
None None
}; };
@ -90,7 +103,7 @@ impl Evaluator {
}); });
} }
best_result.into_inner().expect("filters should be done") best_result.into_inner().expect("filters should be done")
.map(|(img, _)| img) .map(|(img, _, _)| img)
} }
} }

View file

@ -20,6 +20,7 @@ use evaluate::Evaluator;
use image::{DynamicImage, GenericImageView, ImageFormat, Pixel}; use image::{DynamicImage, GenericImageView, ImageFormat, Pixel};
use png::PngImage; use png::PngImage;
use png::PngData; use png::PngData;
use colors::BitDepth;
#[cfg(feature = "parallel")] #[cfg(feature = "parallel")]
use rayon::prelude::*; use rayon::prelude::*;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
@ -767,9 +768,17 @@ fn perform_reductions(mut png: Arc<PngImage>, opts: &Options, deadline: &Deadlin
} }
if opts.bit_depth_reduction { 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); png = Arc::new(reduced);
eval.try_image(png.clone()); 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) { if opts.verbosity == Some(1) {
report_reduction(&png); report_reduction(&png);
} }

View file

@ -28,12 +28,12 @@ const FOUR_BIT_PERMUTATIONS: [u8; 11] = [
/// Attempt to reduce the bit depth of the image /// Attempt to reduce the bit depth of the image
/// Returns true if the bit depth was reduced, false otherwise /// Returns true if the bit depth was reduced, false otherwise
#[must_use] #[must_use]
pub fn reduce_bit_depth(png: &PngImage) -> Option<PngImage> { pub fn reduce_bit_depth(png: &PngImage, minimum_bits: usize) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Sixteen { if png.ihdr.bit_depth != BitDepth::Sixteen {
if png.ihdr.color_type == ColorType::Indexed if png.ihdr.color_type == ColorType::Indexed
|| png.ihdr.color_type == ColorType::Grayscale || 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; return None;
} }
@ -75,10 +75,10 @@ pub fn reduce_bit_depth(png: &PngImage) -> Option<PngImage> {
} }
#[must_use] #[must_use]
pub fn reduce_bit_depth_8_or_less(png: &PngImage) -> Option<PngImage> { pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Option<PngImage> {
assert!(minimum_bits >= 1 && minimum_bits < 8);
let mut reduced = BitVec::with_capacity(png.data.len() * 8); let mut reduced = BitVec::with_capacity(png.data.len() * 8);
let bit_depth: usize = png.ihdr.bit_depth.as_u8() as usize; let bit_depth: usize = png.ihdr.bit_depth.as_u8() as usize;
let mut minimum_bits = 1;
if minimum_bits >= bit_depth { if minimum_bits >= bit_depth {
return None; return None;
} }

View file

@ -198,7 +198,7 @@ pub fn reduce_color_type(png: &PngImage) -> Option<PngImage> {
if should_reduce_bit_depth { if should_reduce_bit_depth {
// Some conversions will allow us to perform bit depth reduction that // Some conversions will allow us to perform bit depth reduction that
// wasn't possible before // 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); reduced = Cow::Owned(r);
} }
} }

View file

@ -332,7 +332,7 @@ fn interlacing_1_to_0_small_files() {
assert_eq!(png.raw.ihdr.interlaced, 0); assert_eq!(png.raw.ihdr.interlaced, 0);
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); 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(); remove_file(output).ok();
} }

View file

@ -37,7 +37,7 @@ fn test_it_converts(
let png = PngData::new(&input, opts.fix_errors).unwrap(); let png = PngData::new(&input, opts.fix_errors).unwrap();
assert_eq!(png.raw.ihdr.color_type, color_type_in); 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); assert_eq!(png.raw.ihdr.interlaced, 0);
match oxipng::optimize(&InFile::Path(input), &output, &opts) { match oxipng::optimize(&InFile::Path(input), &output, &opts) {
@ -699,14 +699,33 @@ fn grayscale_8_should_be_grayscale_8() {
#[test] #[test]
fn small_files() { fn small_files() {
test_it_converts( let input = PathBuf::from("tests/files/small_files.png");
"tests/files/small_files.png", let (output, opts) = get_opts(&input);
None,
ColorType::Indexed, let png = PngData::new(&input, opts.fix_errors).unwrap();
BitDepth::Eight,
ColorType::Indexed, assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
BitDepth::One, 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] #[test]