use crate::{ colors::{BitDepth, ColorType}, headers::IhdrData, png::PngImage, }; /// Attempt to reduce a 16-bit image to 8-bit, returning the reduced image if successful #[must_use] pub fn reduced_bit_depth_16_to_8(png: &PngImage, force_scale: bool) -> Option { if png.ihdr.bit_depth != BitDepth::Sixteen { return None; } if force_scale { return scaled_bit_depth_16_to_8(png); } // Reduce from 16 to 8 bits per channel per pixel if png.data.chunks_exact(2).any(|pair| pair[0] != pair[1]) { // Can't reduce return None; } Some(PngImage { data: png.data.chunks_exact(2).map(|pair| pair[0]).collect(), ihdr: IhdrData { color_type: png.ihdr.color_type.clone(), bit_depth: BitDepth::Eight, ..png.ihdr }, }) } /// Forcibly reduce a 16-bit image to 8-bit by scaling, returning the reduced image if successful #[must_use] pub fn scaled_bit_depth_16_to_8(png: &PngImage) -> Option { if png.ihdr.bit_depth != BitDepth::Sixteen { return None; } // Reduce from 16 to 8 bits per channel per pixel by scaling when necessary let data = png .data .chunks_exact(2) .map(|pair| { if pair[0] == pair[1] { return pair[0]; } // See: http://www.libpng.org/pub/png/spec/1.2/PNG-Decoders.html#D.Sample-depth-rescaling // This allows values such as 0x00FF to be rounded to 0x01 rather than truncated to 0x00 let val = f32::from(u16::from_be_bytes([pair[0], pair[1]])); (val * (255.0 / 65535.0)).round() as u8 }) .collect(); Some(PngImage { data, ihdr: IhdrData { color_type: png.ihdr.color_type.clone(), bit_depth: BitDepth::Eight, ..png.ihdr }, }) } /// 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) -> Option { if png.ihdr.bit_depth != BitDepth::Eight || png.channels_per_pixel() != 1 { return None; } let minimum_bits = if let ColorType::Indexed { palette } = &png.ihdr.color_type { // We can easily determine minimum depth by the palette size match palette.len() { 0..=2 => 1, 3..=4 => 2, 5..=16 => 4, _ => return None, } } else { let mut minimum_bits = 1; // Finding minimum depth for grayscale is much more complicated let mut mask = 1; let mut divisions = 1..8; for &b in &png.data { if b == 0 || b == 255 { continue; } 'try_depth: loop { // 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); 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; } } minimum_bits }; let mut reduced = Vec::with_capacity(png.data.len()); 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(8 / minimum_bits) { let mut new_byte = 0; let mut shift = 8; 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); } } // If the image is grayscale we also need to reduce the transparency pixel let color_type = if let ColorType::Grayscale { transparent_shade: Some(trans), } = png.ihdr.color_type { 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 < 8 { check = (check << bits) | check; bits <<= 1; } // If the transparency doesn't fit the new bit depth it is therefore unused - set it to None ColorType::Grayscale { transparent_shade: if trans == check { Some(reduced_trans) } else { None }, } } else { png.ihdr.color_type.clone() }; Some(PngImage { data: reduced, ihdr: IhdrData { color_type, bit_depth: (minimum_bits as u8).try_into().unwrap(), ..png.ihdr }, }) } /// Expand a 1/2/4-bit image to 8-bit, returning the expanded image if successful #[must_use] pub fn expanded_bit_depth_to_8(png: &PngImage) -> Option { let bit_depth = png.ihdr.bit_depth as u32; if bit_depth >= 8 { return None; } // Calculate the current number of pixels per byte let ppb = 8 / bit_depth; let is_gray = matches!(png.ihdr.color_type, ColorType::Grayscale { .. }); let mut reduced = Vec::with_capacity((png.ihdr.width * png.ihdr.height) as usize); let mut length = 0; let mask = (1 << bit_depth) - 1; for line in png.scan_lines(false) { for &(mut byte) in line.data { // Loop over each pixel in the byte for _ in 0..ppb { // Align the current pixel with the mask byte = byte.rotate_left(bit_depth); let mut val = byte & mask; if is_gray { // Expand gray by repeating the bits let mut bits = bit_depth; while bits < 8 { val = (val << bits) | val; bits <<= 1; } } reduced.push(val); } } // Trim any overflow length += line.num_pixels; reduced.truncate(length); } // If the image is grayscale we also need to expand the transparency pixel let color_type = if let ColorType::Grayscale { transparent_shade: Some(mut trans), } = png.ihdr.color_type { let mut bits = bit_depth; while bits < 8 { trans = (trans << bits) | trans; bits <<= 1; } ColorType::Grayscale { transparent_shade: Some(trans), } } else { png.ihdr.color_type.clone() }; Some(PngImage { data: reduced, ihdr: IhdrData { color_type, bit_depth: BitDepth::Eight, ..png.ihdr }, }) }