Simplify depth handling in reductions
This commit is contained in:
parent
22e8101442
commit
58a1e76ebe
4 changed files with 58 additions and 129 deletions
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue