use crate::{
colors::{BitDepth, ColorType},
headers::IhdrData,
png::PngImage,
};
#[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);
}
if png.data.chunks_exact(2).any(|pair| pair[0] != pair[1]) {
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
},
})
}
#[must_use]
pub fn scaled_bit_depth_16_to_8(png: &PngImage) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Sixteen {
return None;
}
let data = png
.data
.chunks_exact(2)
.map(|pair| {
if pair[0] == pair[1] {
return pair[0];
}
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
},
})
}
#[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 {
match palette.len() {
0..=2 => 1,
3..=4 => 2,
5..=16 => 4,
_ => return None,
}
} else {
let mut minimum_bits = 1;
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.rotate_left(minimum_bits as u32);
let compare = byte & mask;
for _ in divisions.clone() {
byte = byte.rotate_left(minimum_bits as u32);
if byte & mask != compare {
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) {
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;
new_byte |= (byte & mask) << shift;
}
reduced.push(new_byte);
}
}
let color_type = if let ColorType::Grayscale {
transparent_shade: Some(trans),
} = png.ihdr.color_type
{
let reduced_trans = (trans & 0xFF) >> (8 - minimum_bits);
let mut check = reduced_trans;
let mut bits = minimum_bits;
while bits < 8 {
check = (check << bits) | check;
bits <<= 1;
}
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
},
})
}
#[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;
}
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 {
for _ in 0..ppb {
byte = byte.rotate_left(bit_depth);
let mut val = byte & mask;
if is_gray {
let mut bits = bit_depth;
while bits < 8 {
val = (val << bits) | val;
bits <<= 1;
}
}
reduced.push(val);
}
}
length += line.num_pixels;
reduced.truncate(length);
}
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
},
})
}