diff --git a/benches/reductions.rs b/benches/reductions.rs index a5a7b7d8..fedecf5f 100644 --- a/benches/reductions.rs +++ b/benches/reductions.rs @@ -31,7 +31,7 @@ fn reductions_8_to_4_bits(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw)); } #[bench] @@ -41,7 +41,7 @@ fn reductions_8_to_2_bits(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw)); } #[bench] @@ -51,7 +51,7 @@ fn reductions_8_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw)); } #[bench] @@ -61,7 +61,7 @@ fn reductions_4_to_2_bits(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw)); } #[bench] @@ -71,7 +71,7 @@ fn reductions_4_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw)); } #[bench] @@ -81,7 +81,7 @@ fn reductions_2_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw)); } #[bench] @@ -91,7 +91,7 @@ fn reductions_grayscale_8_to_4_bits(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw)); } #[bench] @@ -101,7 +101,7 @@ fn reductions_grayscale_8_to_2_bits(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw)); } #[bench] @@ -111,7 +111,7 @@ fn reductions_grayscale_8_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw)); } #[bench] @@ -121,7 +121,7 @@ fn reductions_grayscale_4_to_2_bits(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw)); } #[bench] @@ -131,7 +131,7 @@ fn reductions_grayscale_4_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw)); } #[bench] @@ -141,7 +141,7 @@ fn reductions_grayscale_2_to_1_bits(b: &mut Bencher) { )); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1)); + b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw)); } #[bench] diff --git a/src/evaluate.rs b/src/evaluate.rs index 8c05fde7..0d187198 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -25,7 +25,6 @@ pub struct Candidate { pub idat_data: Vec, pub filtered: Vec, pub filter: RowFilter, - pub is_reduction: bool, // first wins tie-breaker nth: usize, } @@ -95,11 +94,6 @@ impl Evaluator { self.eval_best_candidate.into_inner() } - /// Set baseline image. It will be used only to measure minimum compression level required - pub fn set_baseline(&self, image: Arc) { - self.try_image_inner(image, false) - } - /// Set best size, if known in advance pub fn set_best_size(&self, size: usize) { self.best_candidate_size.set_min(size); @@ -107,10 +101,6 @@ impl Evaluator { /// Check if the image is smaller than others pub fn try_image(&self, image: Arc) { - self.try_image_inner(image, true) - } - - fn try_image_inner(&self, image: Arc, is_reduction: bool) { let nth = self.nth.fetch_add(1, SeqCst); // These clones are only cheap refcounts let deadline = self.deadline.clone(); @@ -150,7 +140,6 @@ impl Evaluator { idat_data, filtered, filter, - is_reduction, nth, }; diff --git a/src/lib.rs b/src/lib.rs index bbb9919e..d870da55 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -615,7 +615,7 @@ fn optimize_png( /// Perform optimization on the input image data using the options provided fn optimize_raw( - mut png: Arc, + image: Arc, opts: &Options, deadline: Arc, max_size: Option, @@ -631,16 +631,14 @@ fn optimize_raw( eval_compression, false, ); - let (baseline, mut reduction_occurred) = - perform_reductions(png.clone(), opts, &deadline, &eval); - png = baseline; + let mut png = perform_reductions(image.clone(), opts, &deadline, &eval); let mut eval_result = eval.get_best_candidate(); if let Some(ref result) = eval_result { - if result.is_reduction { - png = result.image.clone(); - reduction_occurred = true; - } + png = result.image.clone(); } + let reduction_occurred = png.ihdr.color_type != image.ihdr.color_type + || png.ihdr.bit_depth != image.ihdr.bit_depth + || png.ihdr.interlaced != image.ihdr.interlaced; if reduction_occurred { report_format("Reducing image to ", &png); diff --git a/src/png/scan_lines.rs b/src/png/scan_lines.rs index 1c612f89..0ea33ae1 100644 --- a/src/png/scan_lines.rs +++ b/src/png/scan_lines.rs @@ -25,7 +25,7 @@ impl<'a> Iterator for ScanLines<'a> { type Item = ScanLine<'a>; #[inline] fn next(&mut self) -> Option { - self.iter.next().map(|(len, pass)| { + self.iter.next().map(|(len, pass, num_pixels)| { let (data, rest) = self.raw_data.split_at(len); self.raw_data = rest; let (&filter, data) = if self.has_filter { @@ -33,7 +33,12 @@ impl<'a> Iterator for ScanLines<'a> { } else { (&0, data) }; - ScanLine { filter, data, pass } + ScanLine { + filter, + data, + pass, + num_pixels, + } }) } } @@ -68,7 +73,7 @@ impl ScanLineRanges { } impl Iterator for ScanLineRanges { - type Item = (usize, Option); + type Item = (usize, Option, usize); fn next(&mut self) -> Option { if self.left == 0 { return None; @@ -149,7 +154,7 @@ impl Iterator for ScanLineRanges { len += 1; } self.left = self.left.checked_sub(len)?; - Some((len, current_pass)) + Some((len, current_pass, pixels_per_line as usize)) } } @@ -162,4 +167,6 @@ pub struct ScanLine<'a> { pub data: &'a [u8], /// The current pass if the image is interlaced pub pass: Option, + /// The number of pixels in the current scan line + pub num_pixels: usize, } diff --git a/src/reduction/bit_depth.rs b/src/reduction/bit_depth.rs index 7a4a1ca5..e36536b7 100644 --- a/src/reduction/bit_depth.rs +++ b/src/reduction/bit_depth.rs @@ -61,60 +61,48 @@ pub fn scaled_bit_depth_16_to_8(png: &PngImage) -> Option { }) } -/// Attempt to reduce an 8/4/2-bit image to a lower bit depth, returning the reduced image if successful +/// Attempt to reduce an 8-bit image to a lower bit depth, returning the reduced image if successful #[must_use] -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 || png.channels_per_pixel() != 1 { +pub fn reduced_bit_depth_8_or_less(png: &PngImage) -> Option { + if png.ihdr.bit_depth != BitDepth::Eight || png.channels_per_pixel() != 1 { return None; } - // Calculate the current number of pixels per byte - let ppb = 8 / bit_depth; + + let mut minimum_bits = 1; if let ColorType::Indexed { palette } = &png.ihdr.color_type { // We can easily determine minimum depth by the palette size - let required_bits = match palette.len() { + minimum_bits = match palette.len() { 0..=2 => 1, 3..=4 => 2, 5..=16 => 4, - _ => 8, + _ => return None, }; - if required_bits >= bit_depth { - // Not reducable - return None; - } else if required_bits > minimum_bits { - minimum_bits = required_bits; - } } else { // Finding minimum depth for grayscale is much more complicated - let mut mask = (1 << minimum_bits) - 1; - let mut divisions = 1..(bit_depth / minimum_bits); + let mut mask = 1; + let mut divisions = 1..8; for &b in &png.data { if b == 0 || b == 255 { continue; } 'try_depth: loop { - let mut byte = b; - // Loop over each pixel in the byte - for _ in 0..ppb { - // Align the first pixel division with the mask + // Align the first pixel division with the mask + let mut byte = b.rotate_left(minimum_bits as u32); + // Each potential division of this pixel must be identical to successfully reduce + let compare = byte & mask; + for _ in divisions.clone() { + // Align the next division with the mask byte = byte.rotate_left(minimum_bits as u32); - // Each potential division of this pixel must be identical to successfully reduce - let compare = byte & mask; - for _ in divisions.clone() { - // Align the next division with the mask - byte = byte.rotate_left(minimum_bits as u32); - if byte & mask != compare { - // This depth is not possible, try the next one up - minimum_bits <<= 1; - if minimum_bits == bit_depth { - return None; - } - mask = (1 << minimum_bits) - 1; - divisions = 1..(bit_depth / minimum_bits); - continue 'try_depth; + if byte & mask != compare { + // This depth is not possible, try the next one up + minimum_bits <<= 1; + if minimum_bits == 8 { + return None; } + mask = (1 << minimum_bits) - 1; + divisions = 1..(8 / minimum_bits); + continue 'try_depth; } } break; @@ -126,18 +114,13 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> O let mask = (1 << minimum_bits) - 1; for line in png.scan_lines(false) { // Loop over the data in chunks that will produce 1 byte of output - for chunk in line.data.chunks(bit_depth / minimum_bits) { + for chunk in line.data.chunks(8 / minimum_bits) { let mut new_byte = 0; let mut shift = 8; - for &(mut byte) in chunk { - // Loop over each pixel in the byte - for _ in 0..ppb { - // Align the current pixel with the mask - byte = byte.rotate_left(bit_depth as u32); - shift -= minimum_bits; - // Take the low bits of the pixel and shift them into the output byte - new_byte |= (byte & mask) << shift; - } + for byte in chunk { + shift -= minimum_bits; + // Take the low bits of the pixel and shift them into the output byte + new_byte |= (byte & mask) << shift; } reduced.push(new_byte); } @@ -148,11 +131,11 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> O transparent_shade: Some(trans), } = png.ihdr.color_type { - let reduced_trans = (trans & 0xFF) >> (bit_depth - minimum_bits); + let reduced_trans = (trans & 0xFF) >> (8 - minimum_bits); // Verify the reduction is valid by restoring back to original bit depth let mut check = reduced_trans; let mut bits = minimum_bits; - while bits < bit_depth { + while bits < 8 { check = check << bits | check; bits <<= 1; } @@ -177,3 +160,67 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> O }, }) } + +/// Expand a 1/2/4-bit image to 8-bit, returning the expanded image if successful +#[must_use] +pub fn expanded_bit_depth_to_8(png: &PngImage) -> Option { + let bit_depth = png.ihdr.bit_depth as u32; + if bit_depth >= 8 { + return None; + } + // Calculate the current number of pixels per byte + let ppb = 8 / bit_depth; + let is_gray = matches!(png.ihdr.color_type, ColorType::Grayscale { .. }); + + let mut reduced = Vec::with_capacity((png.ihdr.width * png.ihdr.height) as usize); + let mut length = 0; + let mask = (1 << bit_depth) - 1; + for line in png.scan_lines(false) { + for &(mut byte) in line.data { + // Loop over each pixel in the byte + for _ in 0..ppb { + // Align the current pixel with the mask + byte = byte.rotate_left(bit_depth); + let mut val = byte & mask; + if is_gray { + // Expand gray by repeating the bits + let mut bits = bit_depth; + while bits < 8 { + val = val << bits | val; + bits <<= 1; + } + } + reduced.push(val); + } + } + // Trim any overflow + length += line.num_pixels; + reduced.truncate(length); + } + + // If the image is grayscale we also need to expand the transparency pixel + let color_type = if let ColorType::Grayscale { + transparent_shade: Some(mut trans), + } = png.ihdr.color_type + { + let mut bits = bit_depth; + while bits < 8 { + trans = trans << bits | trans; + bits <<= 1; + } + ColorType::Grayscale { + transparent_shade: Some(trans), + } + } else { + png.ihdr.color_type.clone() + }; + + Some(PngImage { + data: reduced, + ihdr: IhdrData { + color_type, + bit_depth: BitDepth::Eight, + ..png.ihdr + }, + }) +} diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index a5ea3c90..bb464739 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -18,15 +18,13 @@ pub(crate) fn perform_reductions( opts: &Options, deadline: &Deadline, eval: &Evaluator, -) -> (Arc, bool) { - let mut reduction_occurred = false; +) -> Arc { let mut evaluation_added = false; // 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; } } @@ -35,7 +33,6 @@ pub(crate) fn perform_reductions( 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 } } @@ -44,7 +41,6 @@ pub(crate) fn perform_reductions( if opts.bit_depth_reduction && !deadline.passed() { if let Some(reduced) = reduced_bit_depth_16_to_8(&png, opts.scale_16) { png = Arc::new(reduced); - reduction_occurred = true; } } @@ -53,7 +49,14 @@ pub(crate) fn perform_reductions( if opts.color_type_reduction && opts.grayscale_reduction && !deadline.passed() { if let Some(reduced) = reduced_rgb_to_grayscale(&png) { png = Arc::new(reduced); - reduction_occurred = true; + } + } + + // Attempt to expand the bit depth to 8 + // This does need to be evaluated but will be done so later when it gets reduced again + if opts.bit_depth_reduction && !deadline.passed() { + if let Some(reduced) = expanded_bit_depth_to_8(&png) { + png = Arc::new(reduced); } } @@ -62,7 +65,6 @@ pub(crate) fn perform_reductions( if opts.palette_reduction && !deadline.passed() { if let Some(reduced) = reduced_palette(&png, opts.optimize_alpha) { png = Arc::new(reduced); - reduction_occurred = true; } } @@ -81,7 +83,6 @@ pub(crate) fn perform_reductions( evaluation_added = true; } else { baseline = png.clone(); - reduction_occurred = true; } } } @@ -118,7 +119,6 @@ pub(crate) fn perform_reductions( evaluation_added = true; } else { baseline = new.clone(); - reduction_occurred = true; } indexed = Some(new); } @@ -128,8 +128,8 @@ pub(crate) fn perform_reductions( if opts.bit_depth_reduction && !deadline.passed() { // Try reducing the previous png, falling back to the indexed one if it exists // This allows a grayscale depth reduction to be preferred over an indexed depth reduction - let reduced = reduced_bit_depth_8_or_less(&png, 1) - .or_else(|| indexed.and_then(|png| reduced_bit_depth_8_or_less(&png, 1))); + let reduced = reduced_bit_depth_8_or_less(&png) + .or_else(|| indexed.and_then(|png| reduced_bit_depth_8_or_less(&png))); if let Some(reduced) = reduced { eval.try_image(Arc::new(reduced)); evaluation_added = true; @@ -137,7 +137,7 @@ pub(crate) fn perform_reductions( } if evaluation_added { - eval.set_baseline(baseline.clone()); + eval.try_image(baseline.clone()); } - (baseline, reduction_occurred) + baseline } diff --git a/src/reduction/palette.rs b/src/reduction/palette.rs index 8c610d03..08340013 100644 --- a/src/reduction/palette.rs +++ b/src/reduction/palette.rs @@ -7,16 +7,22 @@ use rgb::RGBA8; /// Attempt to reduce the number of colors in the palette, returning the reduced image if successful #[must_use] pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option { + if png.ihdr.bit_depth != BitDepth::Eight { + return None; + } let palette = match &png.ihdr.color_type { ColorType::Indexed { palette } if palette.len() > 1 => palette, _ => return None, }; - let used = get_used_entries(png); + let mut used = [false; 256]; + for &byte in &png.data { + used[byte as usize] = true; + } let black = RGBA8::new(0, 0, 0, 255); let mut condensed = IndexSet::with_capacity(palette.len()); - let mut palette_map = [0; 256]; + let mut byte_map = [0; 256]; let mut did_change = false; for (i, used) in used.iter().enumerate() { if !used { @@ -24,15 +30,14 @@ pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option } // There are invalid files that use pixel indices beyond palette size let color = *palette.get(i).unwrap_or(&black); - palette_map[i] = add_color_to_set(color, &mut condensed, optimize_alpha); - if palette_map[i] as usize != i { + byte_map[i] = add_color_to_set(color, &mut condensed, optimize_alpha); + if byte_map[i] as usize != i { did_change = true; } } let data = if did_change { // Reassign data bytes to new indices - let byte_map = palette_map_to_byte_map(png.ihdr.bit_depth, &palette_map); png.data.iter().map(|b| byte_map[*b as usize]).collect() } else if condensed.len() < palette.len() { // Data is unchanged but palette will be truncated @@ -64,68 +69,10 @@ fn add_color_to_set(mut color: RGBA8, set: &mut IndexSet, optimize_alpha: idx as u8 } -fn get_used_entries(png: &PngImage) -> [bool; 256] { - let mut used = [false; 256]; - 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; - } - } - BitDepth::One => { - // Only two options, don't bother checking which are actually used - used[0] = true; - used[1] = true; - } - _ => unreachable!(), - }; - used -} - -fn palette_map_to_byte_map(bit_depth: BitDepth, palette_map: &[u8; 256]) -> [u8; 256] { - // Low bit-depths can be pre-computed for every byte value - match bit_depth { - BitDepth::Eight => *palette_map, - BitDepth::Four => { - let mut byte_map = [0_u8; 256]; - for byte in 0..256 { - byte_map[byte] = palette_map[byte & 0x0F] | (palette_map[byte >> 4] << 4); - } - byte_map - } - BitDepth::Two => { - let mut byte_map = [0_u8; 256]; - for byte in 0..256 { - byte_map[byte] = palette_map[byte & 0x03] - | (palette_map[(byte >> 2) & 0x03] << 2) - | (palette_map[(byte >> 4) & 0x03] << 4) - | (palette_map[byte >> 6] << 6); - } - byte_map - } - _ => unreachable!(), - } -} - /// Attempt to sort the colors in the palette, returning the sorted image if successful #[must_use] pub fn sorted_palette(png: &PngImage) -> Option { - if png.ihdr.bit_depth == BitDepth::One { - // Don't bother trying to sort a 1-bit image + if png.ihdr.bit_depth != BitDepth::Eight { return None; } let palette = match &png.ihdr.color_type { @@ -154,12 +101,11 @@ pub fn sorted_palette(png: &PngImage) -> Option { return None; } - // Construct the palette and byte maps and convert the data - let mut new_map = [0; 256]; + // Construct the new mapping and convert the data + let mut byte_map = [0; 256]; for (i, &v) in old_map.iter().enumerate() { - new_map[v] = i as u8; + byte_map[v] = i as u8; } - let byte_map = palette_map_to_byte_map(png.ihdr.bit_depth, &new_map); let data = png.data.iter().map(|&b| byte_map[b as usize]).collect(); Some(PngImage { diff --git a/tests/flags.rs b/tests/flags.rs index 7d146a31..2df4ea0b 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -604,6 +604,57 @@ fn fix_errors() { remove_file(output).ok(); } +#[test] +fn no_color_type_change() { + let input = PathBuf::from("tests/files/palette_8_should_be_rgb.png"); + let (output, mut opts) = get_opts(&input); + opts.color_type_reduction = false; + + test_it_converts( + input, + &output, + &opts, + INDEXED, + BitDepth::Eight, + INDEXED, + BitDepth::One, + ); +} + +#[test] +fn no_grayscale_change() { + let input = PathBuf::from("tests/files/rgb_8_should_be_grayscale_8.png"); + let (output, mut opts) = get_opts(&input); + opts.grayscale_reduction = false; + + test_it_converts( + input, + &output, + &opts, + RGB, + BitDepth::Eight, + INDEXED, + BitDepth::Eight, + ); +} + +#[test] +fn no_bit_depth_change() { + let input = PathBuf::from("tests/files/palette_4_should_be_palette_2.png"); + let (output, mut opts) = get_opts(&input); + opts.bit_depth_reduction = false; + + test_it_converts( + input, + &output, + &opts, + INDEXED, + BitDepth::Four, + INDEXED, + BitDepth::Four, + ); +} + #[test] fn scale_16() { let input = PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"); diff --git a/tests/regression.rs b/tests/regression.rs index c84a25dd..9382f27a 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -262,7 +262,7 @@ fn issue_82() { INDEXED, BitDepth::Four, INDEXED, - BitDepth::Four, + BitDepth::Eight, ); }