Also try 4-bit depth for small-depth images
This commit is contained in:
parent
de61b7abf9
commit
7ad93edeed
7 changed files with 66 additions and 25 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
11
src/lib.rs
11
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<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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue