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();
|
||||
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
pub fn reduced_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Option<PngImage> {
|
||||
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<PngImage> {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<PngImage> {
|
||||
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<PngImage>
|
|||
}
|
||||
// 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<RGBA8>, 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<PngImage> {
|
||||
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<PngImage> {
|
|||
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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue