diff --git a/benches/reductions.rs b/benches/reductions.rs index be9b6e06..dc97effb 100644 --- a/benches/reductions.rs +++ b/benches/reductions.rs @@ -12,7 +12,7 @@ fn reductions_16_to_8_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_8.png")); let png = PngData::new(&input, false).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_16_to_8(&png.raw)); } #[bench] @@ -22,7 +22,7 @@ fn reductions_8_to_4_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -32,7 +32,7 @@ fn reductions_8_to_2_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -42,7 +42,7 @@ fn reductions_8_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -52,7 +52,7 @@ fn reductions_4_to_2_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -62,7 +62,7 @@ fn reductions_4_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -72,7 +72,7 @@ fn reductions_2_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -82,7 +82,7 @@ fn reductions_grayscale_8_to_4_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -92,7 +92,7 @@ fn reductions_grayscale_8_to_2_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -102,7 +102,7 @@ fn reductions_grayscale_8_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -112,7 +112,7 @@ fn reductions_grayscale_4_to_2_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -122,7 +122,7 @@ fn reductions_grayscale_4_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -132,7 +132,7 @@ fn reductions_grayscale_2_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); } #[bench] @@ -140,7 +140,7 @@ fn reductions_rgba_to_rgb_16(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgba_16_should_be_rgb_16.png")); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| alpha::reduced_alpha_channel(&png.raw, false)); } #[bench] @@ -148,7 +148,7 @@ fn reductions_rgba_to_rgb_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_rgb_8.png")); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| alpha::reduced_alpha_channel(&png.raw, false)); } #[bench] @@ -158,7 +158,7 @@ fn reductions_rgba_to_grayscale_alpha_16(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_rgb_to_grayscale(&png.raw)); } #[bench] @@ -168,7 +168,7 @@ fn reductions_rgba_to_grayscale_alpha_8(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_rgb_to_grayscale(&png.raw)); } #[bench] @@ -178,7 +178,10 @@ fn reductions_rgba_to_grayscale_16(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| { + color::reduce_rgb_to_grayscale(&png.raw) + .and_then(|r| alpha::reduced_alpha_channel(&r, false)) + }); } #[bench] @@ -188,7 +191,10 @@ fn reductions_rgba_to_grayscale_8(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| { + color::reduce_rgb_to_grayscale(&png.raw) + .and_then(|r| alpha::reduced_alpha_channel(&r, false)) + }); } #[bench] @@ -198,7 +204,7 @@ fn reductions_rgb_to_grayscale_16(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_rgb_to_grayscale(&png.raw)); } #[bench] @@ -206,7 +212,7 @@ fn reductions_rgb_to_grayscale_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_grayscale_8.png")); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_rgb_to_grayscale(&png.raw)); } #[bench] @@ -214,7 +220,7 @@ fn reductions_rgba_to_palette_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_palette_8.png")); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_to_palette(&png.raw)); } #[bench] @@ -222,7 +228,7 @@ fn reductions_rgb_to_palette_8(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_palette_8.png")); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduce_color_type(&png.raw, true, false)); + b.iter(|| color::reduce_to_palette(&png.raw)); } #[bench] @@ -232,7 +238,7 @@ fn reductions_palette_duplicate_reduction(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduced_palette(&png.raw, false)); + b.iter(|| palette::optimized_palette(&png.raw, false)); } #[bench] @@ -242,7 +248,7 @@ fn reductions_palette_unused_reduction(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduced_palette(&png.raw, false)); + b.iter(|| palette::optimized_palette(&png.raw, false)); } #[bench] @@ -252,7 +258,7 @@ fn reductions_palette_full_reduction(b: &mut Bencher) { )); let png = PngData::new(&input, false).unwrap(); - b.iter(|| reduced_palette(&png.raw, false)); + b.iter(|| palette::optimized_palette(&png.raw, false)); } #[bench] diff --git a/src/colors.rs b/src/colors.rs index 77ee9818..59a0c360 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -74,6 +74,15 @@ impl ColorType { pub fn has_alpha(&self) -> bool { matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA) } + + #[inline] + pub fn has_trns(&self) -> bool { + match self { + ColorType::Grayscale { transparent_shade } => transparent_shade.is_some(), + ColorType::RGB { transparent_color } => transparent_color.is_some(), + _ => false, + } + } } #[repr(u8)] diff --git a/src/lib.rs b/src/lib.rs index d331c6dc..99a0d6fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,6 @@ extern crate rayon; mod rayon; use crate::atomicmin::AtomicMin; -use crate::colors::BitDepth; use crate::deflate::{crc32, inflate}; use crate::evaluate::Evaluator; use crate::png::PngData; @@ -477,23 +476,6 @@ fn optimize_png( perform_strip(png, opts); let stripped_png = png.clone(); - // Interlacing is not part of the evaluator trials but must be done first to evaluate the rest correctly - let mut reduction_occurred = false; - if let Some(interlacing) = opts.interlace { - if let Some(reduced) = png.raw.change_interlacing(interlacing) { - png.raw = Arc::new(reduced); - reduction_occurred = true; - } - } - - // If alpha optimization is enabled, perform a black alpha reduction before evaluating reductions - // This can allow reductions from alpha to indexed which may not have been possible otherwise - if opts.optimize_alpha { - if let Some(reduced) = cleaned_alpha_channel(&png.raw) { - png.raw = Arc::new(reduced); - } - } - // Must use normal (lazy) compression, as faster ones (greedy) are not representative let eval_compression = 5; // None and Bigrams work well together, especially for alpha reductions @@ -505,7 +487,9 @@ fn optimize_png( eval_compression, false, ); - perform_reductions(png.raw.clone(), opts, &deadline, &eval); + let (baseline, mut reduction_occurred) = + perform_reductions(png.raw.clone(), opts, &deadline, &eval); + png.raw = baseline; let mut eval_filter = if let Some(result) = eval.get_best_candidate() { *png = result.image; if result.is_reduction { @@ -664,66 +648,6 @@ fn optimize_png( Ok(output) } -fn perform_reductions( - mut png: Arc, - opts: &Options, - deadline: &Deadline, - eval: &Evaluator, -) { - // The eval baseline will be set from the original png only if we attempt any reductions - let baseline = png.clone(); - let mut reduction_occurred = false; - - if opts.palette_reduction { - if let Some(reduced) = reduced_palette(&png, opts.optimize_alpha) { - png = Arc::new(reduced); - eval.try_image(png.clone()); - reduction_occurred = true; - } - if deadline.passed() { - return; - } - } - - if opts.bit_depth_reduction { - 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)); - } - } - reduction_occurred = true; - } - if deadline.passed() { - return; - } - } - - if opts.color_type_reduction { - if let Some(reduced) = - reduce_color_type(&png, opts.grayscale_reduction, opts.optimize_alpha) - { - png = Arc::new(reduced); - eval.try_image(png.clone()); - reduction_occurred = true; - } - if deadline.passed() { - return; - } - } - - if reduction_occurred { - eval.set_baseline(baseline); - } -} - /// Execute a compression trial fn perform_trial( filtered: &[u8], diff --git a/src/reduction/bit_depth.rs b/src/reduction/bit_depth.rs index 948de473..9213c88c 100644 --- a/src/reduction/bit_depth.rs +++ b/src/reduction/bit_depth.rs @@ -2,13 +2,10 @@ use crate::colors::{BitDepth, ColorType}; use crate::headers::IhdrData; use crate::png::PngImage; -/// Attempt to reduce the bit depth of the image, returning the reduced image if successful +/// Attempt to reduce a 16-bit image to 8-bit, returning the reduced image if successful #[must_use] -pub fn reduce_bit_depth(png: &PngImage, minimum_bits: usize) -> Option { +pub fn reduced_bit_depth_16_to_8(png: &PngImage) -> Option { if png.ihdr.bit_depth != BitDepth::Sixteen { - if png.channels_per_pixel() == 1 { - return reduce_bit_depth_8_or_less(png, minimum_bits); - } return None; } @@ -29,11 +26,12 @@ pub fn reduce_bit_depth(png: &PngImage, minimum_bits: usize) -> Option }) } +/// Attempt to reduce an 8/4/2-bit image to a lower bit depth, returning the reduced image if successful #[must_use] -pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Option { +pub fn reduced_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Option { assert!((1..8).contains(&minimum_bits)); let bit_depth = png.ihdr.bit_depth as usize; - if minimum_bits >= bit_depth || bit_depth > 8 { + if minimum_bits >= bit_depth || bit_depth > 8 || png.channels_per_pixel() != 1 { return None; } // Calculate the current number of pixels per byte diff --git a/src/reduction/color.rs b/src/reduction/color.rs index e8d5e73f..6eb9700a 100644 --- a/src/reduction/color.rs +++ b/src/reduction/color.rs @@ -35,7 +35,7 @@ where #[must_use] pub fn reduce_to_palette(png: &PngImage) -> Option { - if png.ihdr.bit_depth != BitDepth::Eight { + if png.ihdr.bit_depth != BitDepth::Eight || png.channels_per_pixel() == 1 { return None; } let mut raw_data = Vec::with_capacity(png.data.len()); @@ -98,22 +98,36 @@ pub fn reduce_to_palette(png: &PngImage) -> Option { let mut aux_headers = png.aux_headers.clone(); if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") { - if bkgd_header.len() != 6 { - // malformed chunk? - return None; - } - // In bKGD 16-bit values are used even for 8-bit images - let bg = RGBA8::new(bkgd_header[1], bkgd_header[3], bkgd_header[5], 255); - let entry = if let Some(&entry) = palette.get(&bg) { - entry - } else if palette.len() < 256 { - let entry = palette.len() as u8; - palette.insert(bg, entry); - entry + let bg = if png.ihdr.color_type.is_rgb() && bkgd_header.len() == 6 { + // In bKGD 16-bit values are used even for 8-bit images + Some(RGBA8::new( + bkgd_header[1], + bkgd_header[3], + bkgd_header[5], + 255, + )) + } else if png.ihdr.color_type == ColorType::GrayscaleAlpha && bkgd_header.len() == 2 { + Some(RGBA8::new( + bkgd_header[1], + bkgd_header[1], + bkgd_header[1], + 255, + )) } else { - return None; // No space in palette to store the bg as an index + None }; - aux_headers.insert(*b"bKGD", vec![entry]); + if let Some(bg) = bg { + let entry = if let Some(&entry) = palette.get(&bg) { + entry + } else if palette.len() < 256 { + let entry = palette.len() as u8; + palette.insert(bg, entry); + entry + } else { + return None; // No space in palette to store the bg as an index + }; + aux_headers.insert(*b"bKGD", vec![entry]); + } } if let Some(sbit_header) = png.aux_headers.get(b"sBIT") { @@ -140,6 +154,10 @@ pub fn reduce_to_palette(png: &PngImage) -> Option { #[must_use] pub fn reduce_rgb_to_grayscale(png: &PngImage) -> Option { + if !png.ihdr.color_type.is_rgb() { + return None; + } + let mut reduced = Vec::with_capacity(png.data.len()); let byte_depth = png.bytes_per_channel(); let bpp = png.channels_per_pixel() * byte_depth; diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index 8154189c..e6888216 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -1,247 +1,115 @@ -use crate::colors::{BitDepth, ColorType}; -use crate::headers::IhdrData; +use crate::evaluate::Evaluator; use crate::png::PngImage; -use indexmap::map::{Entry::*, IndexMap}; -use rgb::RGBA8; -use std::borrow::Cow; +use crate::Deadline; +use crate::Options; +use std::sync::Arc; pub mod alpha; -use crate::alpha::reduced_alpha_channel; +use crate::alpha::*; pub mod bit_depth; -use crate::bit_depth::reduce_bit_depth_8_or_less; +use crate::bit_depth::*; pub mod color; use crate::color::*; +pub mod palette; +use crate::palette::*; -pub(crate) use crate::alpha::cleaned_alpha_channel; -pub(crate) use crate::bit_depth::reduce_bit_depth; +pub(crate) fn perform_reductions( + mut png: Arc, + opts: &Options, + deadline: &Deadline, + eval: &Evaluator, +) -> (Arc, bool) { + let mut reduction_occurred = false; + let mut evaluation_added = false; -/// Attempt to reduce the number of colors in the palette -/// Returns `None` if palette hasn't changed -pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option { - let palette = match &png.ihdr.color_type { - ColorType::Indexed { palette } => palette, - // Can't reduce if there is no palette - _ => return None, - }; - if png.ihdr.bit_depth == BitDepth::One { - // Gains from 1-bit images will be at most 1 byte - // Not worth the CPU time - return None; - } - - let mut palette_map = [None; 256]; - let mut used = [false; 256]; - { - // Find palette entries that are never used - match png.ihdr.bit_depth { - BitDepth::Eight => { - for &byte in &png.data { - used[byte as usize] = true; - } - } - BitDepth::Four => { - for &byte in &png.data { - used[(byte & 0x0F) as usize] = true; - used[(byte >> 4) as usize] = true; - } - } - BitDepth::Two => { - for &byte in &png.data { - used[(byte & 0x03) as usize] = true; - used[((byte >> 2) & 0x03) as usize] = true; - used[((byte >> 4) & 0x03) as usize] = true; - used[(byte >> 6) as usize] = true; - } - } - _ => unreachable!(), - } - - let mut used_enumerated: Vec<(usize, &bool)> = used.iter().enumerate().collect(); - used_enumerated.sort_by(|a, b| { - //Sort by ascending alpha and descending luma. - let color_val = |i| { - let color = palette - .get(i) - .copied() - .unwrap_or_else(|| RGBA8::new(0, 0, 0, 255)); - ((color.a as i32) << 18) - // These are coefficients for standard sRGB to luma conversion - - i32::from(color.r) * 299 - - i32::from(color.g) * 587 - - i32::from(color.b) * 114 - }; - color_val(a.0).cmp(&color_val(b.0)) - }); - - // Make sure the background is also included, but only after sorting since it may not be used in idat - if let Some(&idx) = png.aux_headers.get(b"bKGD").and_then(|b| b.first()) { - if !used[idx as usize] { - used_enumerated.push((idx as usize, &true)); - } - } - - let mut next_index = 0_u16; - let mut seen = IndexMap::with_capacity(palette.len()); - for (i, used) in used_enumerated.iter().cloned() { - if !used { - continue; - } - // There are invalid files that use pixel indices beyond palette size - let mut color = palette - .get(i) - .cloned() - .unwrap_or_else(|| RGBA8::new(0, 0, 0, 255)); - // If there are multiple fully transparent entries, reduce them into one - if optimize_alpha && color.a == 0 { - color.r = 0; - color.g = 0; - color.b = 0; - } - match seen.entry(color) { - Vacant(new) => { - palette_map[i] = Some(next_index as u8); - new.insert(next_index as u8); - next_index += 1; - } - Occupied(remap_to) => palette_map[i] = Some(*remap_to.get()), - } + // Interlacing must be processed first in order to evaluate the rest correctly + if let Some(interlacing) = opts.interlace { + if let Some(reduced) = png.change_interlacing(interlacing) { + png = Arc::new(reduced); + reduction_occurred = true; } } - do_palette_reduction(png, palette, &palette_map) -} - -#[must_use] -fn do_palette_reduction( - png: &PngImage, - palette: &[RGBA8], - palette_map: &[Option; 256], -) -> Option { - let byte_map = palette_map_to_byte_map(png, palette_map)?; - - // Reassign data bytes to new indices - let raw_data = png.data.iter().map(|b| byte_map[*b as usize]).collect(); - - let mut aux_headers = png.aux_headers.clone(); - if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") { - if let Some(Some(map_to)) = bkgd_header - .first() - .and_then(|&idx| palette_map.get(idx as usize)) - { - aux_headers.insert(*b"bKGD", vec![*map_to]); + // If alpha optimization is enabled, clean the alpha channel before continuing + // This can allow some color type reductions which may not have been possible otherwise + if opts.optimize_alpha && !deadline.passed() { + if let Some(reduced) = cleaned_alpha_channel(&png) { + png = Arc::new(reduced); + // This does not count as a reduction } } - Some(PngImage { - ihdr: IhdrData { - color_type: ColorType::Indexed { - palette: reordered_palette(palette, palette_map), - }, - ..png.ihdr - }, - data: raw_data, - aux_headers, - }) -} - -fn palette_map_to_byte_map(png: &PngImage, palette_map: &[Option; 256]) -> Option<[u8; 256]> { - if (0..256).all(|i| palette_map[i].map_or(true, |to| to == i as u8)) { - // No reduction necessary - return None; - } - - let mut byte_map = [0_u8; 256]; - - // low bit-depths can be pre-computed for every byte value - match png.ihdr.bit_depth { - BitDepth::Eight => { - for byte in 0..=255usize { - byte_map[byte] = palette_map[byte].unwrap_or(0) - } - } - BitDepth::Four => { - for byte in 0..=255usize { - byte_map[byte] = palette_map[byte & 0x0F].unwrap_or(0) - | (palette_map[byte >> 4].unwrap_or(0) << 4); - } - } - BitDepth::Two => { - for byte in 0..=255usize { - byte_map[byte] = palette_map[byte & 0x03].unwrap_or(0) - | (palette_map[(byte >> 2) & 0x03].unwrap_or(0) << 2) - | (palette_map[(byte >> 4) & 0x03].unwrap_or(0) << 4) - | (palette_map[byte >> 6].unwrap_or(0) << 6); - } - } - _ => {} - } - - Some(byte_map) -} - -fn reordered_palette(palette: &[RGBA8], palette_map: &[Option; 256]) -> Vec { - let max_index = palette_map.iter().cloned().flatten().max().unwrap_or(0) as usize; - let mut new_palette = vec![RGBA8::new(0, 0, 0, 255); max_index + 1]; - for (&color, &map_to) in palette.iter().zip(palette_map.iter()) { - if let Some(map_to) = map_to { - new_palette[map_to as usize] = color; + // Attempt to reduce 16-bit to 8-bit + // This is just removal of bytes and does not need to be evaluated + if opts.bit_depth_reduction && !deadline.passed() { + if let Some(reduced) = reduced_bit_depth_16_to_8(&png) { + png = Arc::new(reduced); + reduction_occurred = true; } } - new_palette -} -/// Attempt to reduce the color type of the image, returning the reduced image if successful -pub fn reduce_color_type( - png: &PngImage, - grayscale_reduction: bool, - optimize_alpha: bool, -) -> Option { - let was_single_channel = png.channels_per_pixel() == 1; - let mut reduced = Cow::Borrowed(png); - - // Go down one step at a time - maybe not the most efficient, but it's safe // Attempt to reduce RGB to grayscale - if grayscale_reduction && reduced.ihdr.color_type.is_rgb() { - if let Some(r) = reduce_rgb_to_grayscale(&reduced) { - reduced = Cow::Owned(r); + // This is just removal of bytes and does not need to be evaluated + if opts.color_type_reduction && !deadline.passed() { + if let Some(reduced) = reduce_rgb_to_grayscale(&png) { + png = Arc::new(reduced); + reduction_occurred = true; } } - // Attempt grayscale alpha reduction before palette, as grayscale will typically be smaller than indexed - if reduced.ihdr.color_type == ColorType::GrayscaleAlpha { - if let Some(r) = reduced_alpha_channel(&reduced, optimize_alpha) { - reduced = Cow::Owned(r); - } - } + // Now retain the current png for the evaluator baseline + // It will only be entered into the evaluator if there are also others to evaluate + let mut baseline = png.clone(); - // Attempt to reduce to palette, if not already a single channel - if reduced.channels_per_pixel() != 1 { - if let Some(r) = reduce_to_palette(&reduced) { - reduced = Cow::Owned(r); - - // Make sure that palette gets sorted. Ideally, this should be done within reduce_to_palette. - if let Some(r) = reduced_palette(&reduced, optimize_alpha) { - reduced = Cow::Owned(r); + // Attempt alpha removal + if opts.color_type_reduction && !deadline.passed() { + if let Some(reduced) = reduced_alpha_channel(&png, opts.optimize_alpha) { + png = Arc::new(reduced); + // If the reduction requires a tRNS chunk, enter this into the evaluator + // Otherwise it is just removal of bytes and should become the baseline + if png.ihdr.color_type.has_trns() { + eval.try_image(png.clone()); + evaluation_added = true; + } else { + baseline = png.clone(); + reduction_occurred = true; } } } - // Attempt RGBA alpha reduction after palette, so it can be skipped if palette was successful - if reduced.ihdr.color_type == ColorType::RGBA { - if let Some(r) = reduced_alpha_channel(&reduced, optimize_alpha) { - reduced = Cow::Owned(r); + // Attempt to reduce the palette size + if opts.palette_reduction && !deadline.passed() { + if let Some(reduced) = optimized_palette(&png, opts.optimize_alpha) { + png = Arc::new(reduced); + eval.try_image(png.clone()); + evaluation_added = true; } } - // Some conversions will allow us to perform bit depth reduction that wasn't possible before - if !was_single_channel && reduced.channels_per_pixel() == 1 { - if let Some(r) = reduce_bit_depth_8_or_less(&reduced, 1) { - reduced = Cow::Owned(r); + // Attempt to reduce to palette + if opts.color_type_reduction && !deadline.passed() { + if let Some(reduced) = reduce_to_palette(&png) { + png = Arc::new(reduced); + // Make sure the palette gets sorted (ideally, this should be done within reduce_to_palette) + if let Some(reduced) = optimized_palette(&png, opts.optimize_alpha) { + png = Arc::new(reduced); + } + eval.try_image(png.clone()); + evaluation_added = true; } } - match reduced { - Cow::Owned(r) => Some(r), - _ => None, + // Attempt to reduce to a lower bit depth + if opts.bit_depth_reduction && !deadline.passed() { + if let Some(reduced) = reduced_bit_depth_8_or_less(&png, 1) { + png = Arc::new(reduced); + eval.try_image(png.clone()); + evaluation_added = true; + } } + + if evaluation_added { + eval.set_baseline(baseline.clone()); + } + (baseline, reduction_occurred) } diff --git a/src/reduction/palette.rs b/src/reduction/palette.rs new file mode 100644 index 00000000..081d43db --- /dev/null +++ b/src/reduction/palette.rs @@ -0,0 +1,180 @@ +use crate::colors::{BitDepth, ColorType}; +use crate::headers::IhdrData; +use crate::png::PngImage; +use indexmap::map::{Entry::*, IndexMap}; +use rgb::RGBA8; + +/// Attempt to shrink and sort the palette, returning the optimized image if successful +#[must_use] +pub fn optimized_palette(png: &PngImage, optimize_alpha: bool) -> Option { + let palette = match &png.ihdr.color_type { + ColorType::Indexed { palette } => palette, + // Can't reduce if there is no palette + _ => return None, + }; + if png.ihdr.bit_depth == BitDepth::One { + // Gains from 1-bit images will be at most 1 byte + // Not worth the CPU time + return None; + } + + let mut palette_map = [None; 256]; + let mut used = [false; 256]; + { + // Find palette entries that are never used + match png.ihdr.bit_depth { + BitDepth::Eight => { + for &byte in &png.data { + used[byte as usize] = true; + } + } + BitDepth::Four => { + for &byte in &png.data { + used[(byte & 0x0F) as usize] = true; + used[(byte >> 4) as usize] = true; + } + } + BitDepth::Two => { + for &byte in &png.data { + used[(byte & 0x03) as usize] = true; + used[((byte >> 2) & 0x03) as usize] = true; + used[((byte >> 4) & 0x03) as usize] = true; + used[(byte >> 6) as usize] = true; + } + } + _ => unreachable!(), + } + + let mut used_enumerated: Vec<(usize, &bool)> = used.iter().enumerate().collect(); + used_enumerated.sort_by(|a, b| { + //Sort by ascending alpha and descending luma. + let color_val = |i| { + let color = palette + .get(i) + .copied() + .unwrap_or_else(|| RGBA8::new(0, 0, 0, 255)); + ((color.a as i32) << 18) + // These are coefficients for standard sRGB to luma conversion + - i32::from(color.r) * 299 + - i32::from(color.g) * 587 + - i32::from(color.b) * 114 + }; + color_val(a.0).cmp(&color_val(b.0)) + }); + + // Make sure the background is also included, but only after sorting since it may not be used in idat + if let Some(&idx) = png.aux_headers.get(b"bKGD").and_then(|b| b.first()) { + if !used[idx as usize] { + used_enumerated.push((idx as usize, &true)); + } + } + + let mut next_index = 0_u16; + let mut seen = IndexMap::with_capacity(palette.len()); + for (i, used) in used_enumerated.iter().cloned() { + if !used { + continue; + } + // There are invalid files that use pixel indices beyond palette size + let mut color = palette + .get(i) + .cloned() + .unwrap_or_else(|| RGBA8::new(0, 0, 0, 255)); + // If there are multiple fully transparent entries, reduce them into one + if optimize_alpha && color.a == 0 { + color.r = 0; + color.g = 0; + color.b = 0; + } + match seen.entry(color) { + Vacant(new) => { + palette_map[i] = Some(next_index as u8); + new.insert(next_index as u8); + next_index += 1; + } + Occupied(remap_to) => palette_map[i] = Some(*remap_to.get()), + } + } + } + + do_palette_reduction(png, palette, &palette_map) +} + +#[must_use] +fn do_palette_reduction( + png: &PngImage, + palette: &[RGBA8], + palette_map: &[Option; 256], +) -> Option { + let byte_map = palette_map_to_byte_map(png, palette_map)?; + + // Reassign data bytes to new indices + let raw_data = png.data.iter().map(|b| byte_map[*b as usize]).collect(); + + let mut aux_headers = png.aux_headers.clone(); + if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") { + if let Some(Some(map_to)) = bkgd_header + .first() + .and_then(|&idx| palette_map.get(idx as usize)) + { + aux_headers.insert(*b"bKGD", vec![*map_to]); + } + } + + Some(PngImage { + ihdr: IhdrData { + color_type: ColorType::Indexed { + palette: reordered_palette(palette, palette_map), + }, + ..png.ihdr + }, + data: raw_data, + aux_headers, + }) +} + +fn palette_map_to_byte_map(png: &PngImage, palette_map: &[Option; 256]) -> Option<[u8; 256]> { + if (0..256).all(|i| palette_map[i].map_or(true, |to| to == i as u8)) { + // No reduction necessary + return None; + } + + let mut byte_map = [0_u8; 256]; + + // low bit-depths can be pre-computed for every byte value + match png.ihdr.bit_depth { + BitDepth::Eight => { + for byte in 0..=255usize { + byte_map[byte] = palette_map[byte].unwrap_or(0) + } + } + BitDepth::Four => { + for byte in 0..=255usize { + byte_map[byte] = palette_map[byte & 0x0F].unwrap_or(0) + | (palette_map[byte >> 4].unwrap_or(0) << 4); + } + } + BitDepth::Two => { + for byte in 0..=255usize { + byte_map[byte] = palette_map[byte & 0x03].unwrap_or(0) + | (palette_map[(byte >> 2) & 0x03].unwrap_or(0) << 2) + | (palette_map[(byte >> 4) & 0x03].unwrap_or(0) << 4) + | (palette_map[byte >> 6].unwrap_or(0) << 6); + } + } + _ => {} + } + + Some(byte_map) +} + +fn reordered_palette(palette: &[RGBA8], palette_map: &[Option; 256]) -> Vec { + let max_index = palette_map.iter().cloned().flatten().max().unwrap_or(0) as usize; + let mut new_palette = vec![RGBA8::new(0, 0, 0, 255); max_index + 1]; + for (&color, &map_to) in palette.iter().zip(palette_map.iter()) { + if let Some(map_to) = map_to { + new_palette[map_to as usize] = color; + } + } + new_palette +} diff --git a/tests/files/grayscale_16_should_be_grayscale_1.png b/tests/files/grayscale_16_should_be_grayscale_1.png new file mode 100644 index 00000000..66fe12e3 Binary files /dev/null and b/tests/files/grayscale_16_should_be_grayscale_1.png differ diff --git a/tests/reduction.rs b/tests/reduction.rs index 040375af..a7fe5e39 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -712,6 +712,18 @@ fn grayscale_16_should_be_grayscale_8() { ); } +#[test] +fn grayscale_16_should_be_grayscale_1() { + test_it_converts( + "tests/files/grayscale_16_should_be_grayscale_1.png", + false, + GRAYSCALE, + BitDepth::Sixteen, + GRAYSCALE, + BitDepth::One, + ); +} + #[test] fn grayscale_8_should_be_grayscale_8() { test_it_converts( diff --git a/tests/regression.rs b/tests/regression.rs index a161a3bf..c24c8373 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -171,8 +171,8 @@ fn issue_52_04() { None, RGBA, BitDepth::Eight, - INDEXED, - BitDepth::One, + RGB, + BitDepth::Eight, ); }