Simplify depth handling in reductions

This commit is contained in:
Andrew 2023-06-02 18:47:02 +12:00
parent 22e8101442
commit 58a1e76ebe
4 changed files with 58 additions and 129 deletions

View file

@ -31,7 +31,7 @@ fn reductions_8_to_4_bits(b: &mut Bencher) {
)); ));
let png = PngData::new(&input, &Options::default()).unwrap(); 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] #[bench]
@ -41,7 +41,7 @@ fn reductions_8_to_2_bits(b: &mut Bencher) {
)); ));
let png = PngData::new(&input, &Options::default()).unwrap(); 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] #[bench]
@ -51,7 +51,7 @@ fn reductions_8_to_1_bits(b: &mut Bencher) {
)); ));
let png = PngData::new(&input, &Options::default()).unwrap(); 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] #[bench]
@ -61,7 +61,7 @@ fn reductions_4_to_2_bits(b: &mut Bencher) {
)); ));
let png = PngData::new(&input, &Options::default()).unwrap(); 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] #[bench]
@ -71,7 +71,7 @@ fn reductions_4_to_1_bits(b: &mut Bencher) {
)); ));
let png = PngData::new(&input, &Options::default()).unwrap(); 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] #[bench]
@ -81,7 +81,7 @@ fn reductions_2_to_1_bits(b: &mut Bencher) {
)); ));
let png = PngData::new(&input, &Options::default()).unwrap(); 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] #[bench]
@ -91,7 +91,7 @@ fn reductions_grayscale_8_to_4_bits(b: &mut Bencher) {
)); ));
let png = PngData::new(&input, &Options::default()).unwrap(); 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] #[bench]
@ -101,7 +101,7 @@ fn reductions_grayscale_8_to_2_bits(b: &mut Bencher) {
)); ));
let png = PngData::new(&input, &Options::default()).unwrap(); 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] #[bench]
@ -111,7 +111,7 @@ fn reductions_grayscale_8_to_1_bits(b: &mut Bencher) {
)); ));
let png = PngData::new(&input, &Options::default()).unwrap(); 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] #[bench]
@ -121,7 +121,7 @@ fn reductions_grayscale_4_to_2_bits(b: &mut Bencher) {
)); ));
let png = PngData::new(&input, &Options::default()).unwrap(); 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] #[bench]
@ -131,7 +131,7 @@ fn reductions_grayscale_4_to_1_bits(b: &mut Bencher) {
)); ));
let png = PngData::new(&input, &Options::default()).unwrap(); 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] #[bench]
@ -141,7 +141,7 @@ fn reductions_grayscale_2_to_1_bits(b: &mut Bencher) {
)); ));
let png = PngData::new(&input, &Options::default()).unwrap(); 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] #[bench]

View file

@ -61,60 +61,48 @@ pub fn scaled_bit_depth_16_to_8(png: &PngImage) -> Option<PngImage> {
}) })
} }
/// 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] #[must_use]
pub fn reduced_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Option<PngImage> { pub fn reduced_bit_depth_8_or_less(png: &PngImage) -> Option<PngImage> {
assert!((1..8).contains(&minimum_bits)); if png.ihdr.bit_depth != BitDepth::Eight || png.channels_per_pixel() != 1 {
let bit_depth = png.ihdr.bit_depth as usize;
if minimum_bits >= bit_depth || bit_depth > 8 || png.channels_per_pixel() != 1 {
return None; 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 { if let ColorType::Indexed { palette } = &png.ihdr.color_type {
// We can easily determine minimum depth by the palette size // We can easily determine minimum depth by the palette size
let required_bits = match palette.len() { minimum_bits = match palette.len() {
0..=2 => 1, 0..=2 => 1,
3..=4 => 2, 3..=4 => 2,
5..=16 => 4, 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 { } else {
// Finding minimum depth for grayscale is much more complicated // Finding minimum depth for grayscale is much more complicated
let mut mask = (1 << minimum_bits) - 1; let mut mask = 1;
let mut divisions = 1..(bit_depth / minimum_bits); let mut divisions = 1..8;
for &b in &png.data { for &b in &png.data {
if b == 0 || b == 255 { if b == 0 || b == 255 {
continue; continue;
} }
'try_depth: loop { 'try_depth: loop {
let mut byte = b; // Align the first pixel division with the mask
// Loop over each pixel in the byte let mut byte = b.rotate_left(minimum_bits as u32);
for _ in 0..ppb { // Each potential division of this pixel must be identical to successfully reduce
// Align the first pixel division with the mask let compare = byte & mask;
for _ in divisions.clone() {
// Align the next division with the mask
byte = byte.rotate_left(minimum_bits as u32); byte = byte.rotate_left(minimum_bits as u32);
// Each potential division of this pixel must be identical to successfully reduce if byte & mask != compare {
let compare = byte & mask; // This depth is not possible, try the next one up
for _ in divisions.clone() { minimum_bits <<= 1;
// Align the next division with the mask if minimum_bits == 8 {
byte = byte.rotate_left(minimum_bits as u32); return None;
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;
} }
mask = (1 << minimum_bits) - 1;
divisions = 1..(8 / minimum_bits);
continue 'try_depth;
} }
} }
break; 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; let mask = (1 << minimum_bits) - 1;
for line in png.scan_lines(false) { for line in png.scan_lines(false) {
// Loop over the data in chunks that will produce 1 byte of output // 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 new_byte = 0;
let mut shift = 8; let mut shift = 8;
for &(mut byte) in chunk { for byte in chunk {
// Loop over each pixel in the byte shift -= minimum_bits;
for _ in 0..ppb { // Take the low bits of the pixel and shift them into the output byte
// Align the current pixel with the mask new_byte |= (byte & mask) << shift;
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;
}
} }
reduced.push(new_byte); 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), transparent_shade: Some(trans),
} = png.ihdr.color_type } = 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 // Verify the reduction is valid by restoring back to original bit depth
let mut check = reduced_trans; let mut check = reduced_trans;
let mut bits = minimum_bits; let mut bits = minimum_bits;
while bits < bit_depth { while bits < 8 {
check = check << bits | check; check = check << bits | check;
bits <<= 1; bits <<= 1;
} }

View file

@ -128,8 +128,8 @@ pub(crate) fn perform_reductions(
if opts.bit_depth_reduction && !deadline.passed() { if opts.bit_depth_reduction && !deadline.passed() {
// Try reducing the previous png, falling back to the indexed one if it exists // 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 // This allows a grayscale depth reduction to be preferred over an indexed depth reduction
let reduced = 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, 1))); .or_else(|| indexed.and_then(|png| reduced_bit_depth_8_or_less(&png)));
if let Some(reduced) = reduced { if let Some(reduced) = reduced {
eval.try_image(Arc::new(reduced)); eval.try_image(Arc::new(reduced));
evaluation_added = true; evaluation_added = true;

View file

@ -7,16 +7,22 @@ use rgb::RGBA8;
/// Attempt to reduce the number of colors in the palette, returning the reduced image if successful /// Attempt to reduce the number of colors in the palette, returning the reduced image if successful
#[must_use] #[must_use]
pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option<PngImage> { pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight {
return None;
}
let palette = match &png.ihdr.color_type { let palette = match &png.ihdr.color_type {
ColorType::Indexed { palette } if palette.len() > 1 => palette, ColorType::Indexed { palette } if palette.len() > 1 => palette,
_ => return None, _ => 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 black = RGBA8::new(0, 0, 0, 255);
let mut condensed = IndexSet::with_capacity(palette.len()); 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; let mut did_change = false;
for (i, used) in used.iter().enumerate() { for (i, used) in used.iter().enumerate() {
if !used { if !used {
@ -24,15 +30,14 @@ pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option<PngImage>
} }
// There are invalid files that use pixel indices beyond palette size // There are invalid files that use pixel indices beyond palette size
let color = *palette.get(i).unwrap_or(&black); let color = *palette.get(i).unwrap_or(&black);
palette_map[i] = add_color_to_set(color, &mut condensed, optimize_alpha); byte_map[i] = add_color_to_set(color, &mut condensed, optimize_alpha);
if palette_map[i] as usize != i { if byte_map[i] as usize != i {
did_change = true; did_change = true;
} }
} }
let data = if did_change { let data = if did_change {
// Reassign data bytes to new indices // 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() png.data.iter().map(|b| byte_map[*b as usize]).collect()
} else if condensed.len() < palette.len() { } else if condensed.len() < palette.len() {
// Data is unchanged but palette will be truncated // Data is unchanged but palette will be truncated
@ -64,68 +69,10 @@ fn add_color_to_set(mut color: RGBA8, set: &mut IndexSet<RGBA8>, optimize_alpha:
idx as u8 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 /// Attempt to sort the colors in the palette, returning the sorted image if successful
#[must_use] #[must_use]
pub fn sorted_palette(png: &PngImage) -> Option<PngImage> { pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
if png.ihdr.bit_depth == BitDepth::One { if png.ihdr.bit_depth != BitDepth::Eight {
// Don't bother trying to sort a 1-bit image
return None; return None;
} }
let palette = match &png.ihdr.color_type { let palette = match &png.ihdr.color_type {
@ -154,12 +101,11 @@ pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
return None; return None;
} }
// Construct the palette and byte maps and convert the data // Construct the new mapping and convert the data
let mut new_map = [0; 256]; let mut byte_map = [0; 256];
for (i, &v) in old_map.iter().enumerate() { 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(); let data = png.data.iter().map(|&b| byte_map[b as usize]).collect();
Some(PngImage { Some(PngImage {