diff --git a/benches/reductions.rs b/benches/reductions.rs index 46fb8f49..c3040076 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/reduction/bit_depth.rs b/src/reduction/bit_depth.rs index 9e21f73c..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; } diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index d7fae113..1e8666cb 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -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; 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 {