diff --git a/src/reduction/bit_depth.rs b/src/reduction/bit_depth.rs index 003dd433..d48e0f58 100644 --- a/src/reduction/bit_depth.rs +++ b/src/reduction/bit_depth.rs @@ -1,28 +1,6 @@ use crate::colors::{BitDepth, ColorType}; use crate::headers::IhdrData; 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 /// 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 pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Option { assert!((1..8).contains(&minimum_bits)); 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; } + // Calculate the current number of pixels per byte + let ppb = 8 / bit_depth; + if png.ihdr.color_type == ColorType::Indexed { for line in png.scan_lines(false) { let line_max = line @@ -91,37 +72,60 @@ pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Op } } } else { - for &byte in &png.data { - while minimum_bits < bit_depth { - let permutations: &[u8] = if minimum_bits == 1 { - &ONE_BIT_PERMUTATIONS - } else if minimum_bits == 2 { - &TWO_BIT_PERMUTATIONS - } else if minimum_bits == 4 { - &FOUR_BIT_PERMUTATIONS - } else { - return None; - }; - if permutations.iter().any(|perm| *perm == byte) { - break; + // Checking for grayscale depth reduction is quite different than for indexed + // Note: In rare cases, padding bits in the data may cause this to incorrectly return None + let mut mask = (1 << minimum_bits) - 1; + let mut divisions = 1..(bit_depth / minimum_bits); + 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 + 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::::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) { - let bit_vec = line.data.view_bits::(); - for (i, bit) in bit_vec.iter().by_vals().enumerate() { - let bit_index = bit_depth - (i % bit_depth); - if bit_index <= minimum_bits { - reduced.push(bit); + // Loop over the data in chunks that will produce 1 byte of output + for chunk in line.data.chunks(bit_depth / 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; + } } - } - // Pad end of line to get 8 bits per byte - while reduced.len() % 8 != 0 { - reduced.push(false); + reduced.push(new_byte); } } @@ -148,7 +152,7 @@ pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Op } Some(PngImage { - data: reduced.as_raw_slice().to_vec(), + data: reduced, ihdr: IhdrData { bit_depth: BitDepth::from_u8(minimum_bits as u8), ..png.ihdr