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
pub enum BitDepth {
/// One bit per channel per pixel

View file

@ -58,9 +58,9 @@ impl Evaluator {
/// Main loop of evaluation thread
fn evaluate_images(from_channel: Receiver<(Arc<PngImage>, bool)>) -> Option<PngData> {
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
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)
}
}

View file

@ -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<PngImage>, 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);
}

View file

@ -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<PngImage> {
pub fn reduce_bit_depth(png: &PngImage, minimum_bits: usize) -> Option<PngImage> {
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<PngImage> {
}
#[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 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<PngImage> {
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 {

View file

@ -198,7 +198,7 @@ pub fn reduce_color_type(png: &PngImage) -> Option<PngImage> {
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);
}
}

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.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();
}

View file

@ -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]