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
|
/// 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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
11
src/lib.rs
11
src/lib.rs
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue