oxipng/src/reduction/bit_depth.rs
Luracasmus 33cea5603e
Apply some Clippy lints (#744)
Let me know if I should add these to the `Cargo.toml`.

All `cargo test` tests pass on the latest x86_64-unknown-linux-gnu
nightly.

---------

Co-authored-by: Alejandro González <me@alegon.dev>
2025-12-06 16:06:35 +01:00

230 lines
7.3 KiB
Rust

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<PngImage> {
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<PngImage> {
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<PngImage> {
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<PngImage> {
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
},
})
}