Faster depth reduction

This commit is contained in:
Andrew 2022-12-15 20:17:14 +13:00
parent e3d82db96b
commit 825ae6a9b2

View file

@ -1,28 +1,6 @@
use crate::colors::{BitDepth, ColorType}; use crate::colors::{BitDepth, ColorType};
use crate::headers::IhdrData; use crate::headers::IhdrData;
use crate::png::PngImage; use crate::png::PngImage;
use bitvec::prelude::*;
const ONE_BIT_PERMUTATIONS: [u8; 2] = [0b0000_0000, 0b1111_1111];
const TWO_BIT_PERMUTATIONS: [u8; 4] = [0b0000_0000, 0b0101_0101, 0b1010_1010, 0b1111_1111];
const FOUR_BIT_PERMUTATIONS: [u8; 16] = [
0b0000_0000,
0b0001_0001,
0b0010_0010,
0b0011_0011,
0b0100_0100,
0b0101_0101,
0b0110_0110,
0b0111_0111,
0b1000_1000,
0b1001_1001,
0b1010_1010,
0b1011_1011,
0b1100_1100,
0b1101_1101,
0b1110_1110,
0b1111_1111,
];
/// Attempt to reduce the bit depth of the image /// Attempt to reduce the bit depth of the image
/// Returns true if the bit depth was reduced, false otherwise /// Returns true if the bit depth was reduced, false otherwise
@ -58,9 +36,12 @@ pub fn reduce_bit_depth(png: &PngImage, minimum_bits: usize) -> Option<PngImage>
pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Option<PngImage> { pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Option<PngImage> {
assert!((1..8).contains(&minimum_bits)); assert!((1..8).contains(&minimum_bits));
let bit_depth: usize = png.ihdr.bit_depth.as_u8() as usize; let bit_depth: usize = png.ihdr.bit_depth.as_u8() as usize;
if minimum_bits >= bit_depth { if minimum_bits >= bit_depth || bit_depth > 8 {
return None; return None;
} }
// Calculate the current number of pixels per byte
let ppb = 8 / bit_depth;
if png.ihdr.color_type == ColorType::Indexed { if png.ihdr.color_type == ColorType::Indexed {
for line in png.scan_lines(false) { for line in png.scan_lines(false) {
let line_max = line let line_max = line
@ -91,37 +72,60 @@ pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Op
} }
} }
} else { } else {
for &byte in &png.data { // Checking for grayscale depth reduction is quite different than for indexed
while minimum_bits < bit_depth { // Note: In rare cases, padding bits in the data may cause this to incorrectly return None
let permutations: &[u8] = if minimum_bits == 1 { let mut mask = (1 << minimum_bits) - 1;
&ONE_BIT_PERMUTATIONS let mut divisions = 1..(bit_depth / minimum_bits);
} else if minimum_bits == 2 { for &b in &png.data {
&TWO_BIT_PERMUTATIONS if b == 0 || b == 255 {
} else if minimum_bits == 4 { continue;
&FOUR_BIT_PERMUTATIONS }
} else { 'try_depth: loop {
return None; let mut byte = b;
}; // Loop over each pixel in the byte
if permutations.iter().any(|perm| *perm == byte) { for _ in 0..ppb {
break; // Align the first pixel 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;
}
}
} }
minimum_bits <<= 1; break;
} }
} }
} }
let mut reduced = BitVec::<u8, Msb0>::with_capacity(png.data.len() * 8); let mut reduced = Vec::with_capacity(png.data.len());
let mask = (1 << minimum_bits) - 1;
for line in png.scan_lines(false) { for line in png.scan_lines(false) {
let bit_vec = line.data.view_bits::<Msb0>(); // Loop over the data in chunks that will produce 1 byte of output
for (i, bit) in bit_vec.iter().by_vals().enumerate() { for chunk in line.data.chunks(bit_depth / minimum_bits) {
let bit_index = bit_depth - (i % bit_depth); let mut new_byte = 0;
if bit_index <= minimum_bits { let mut shift = 8;
reduced.push(bit); 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;
}
} }
} reduced.push(new_byte);
// Pad end of line to get 8 bits per byte
while reduced.len() % 8 != 0 {
reduced.push(false);
} }
} }
@ -148,7 +152,7 @@ pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Op
} }
Some(PngImage { Some(PngImage {
data: reduced.as_raw_slice().to_vec(), data: reduced,
ihdr: IhdrData { ihdr: IhdrData {
bit_depth: BitDepth::from_u8(minimum_bits as u8), bit_depth: BitDepth::from_u8(minimum_bits as u8),
..png.ihdr ..png.ihdr