use crate::error::IconError;
const fn make_crc_table() -> [u32; 256] {
let mut table = [0u32; 256];
let mut n = 0u32;
while n < 256 {
let mut c = n;
let mut k = 0;
while k < 8 {
c = if c & 1 != 0 { 0xEDB88320 ^ (c >> 1) } else { c >> 1 };
k += 1;
}
table[n as usize] = c;
n += 1;
}
table
}
static CRC_TABLE: [u32; 256] = make_crc_table();
fn crc32_update(mut crc: u32, data: &[u8]) -> u32 {
for &b in data {
crc = CRC_TABLE[((crc ^ b as u32) & 0xFF) as usize] ^ (crc >> 8);
}
crc
}
fn write_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) {
out.extend_from_slice(&(data.len() as u32).to_be_bytes());
out.extend_from_slice(chunk_type);
out.extend_from_slice(data);
let crc = crc32_update(crc32_update(0xFFFF_FFFF, chunk_type), data) ^ 0xFFFF_FFFF;
out.extend_from_slice(&crc.to_be_bytes());
}
#[derive(Clone, Copy, Debug, Default)]
pub enum PngFilter {
#[default]
None,
Sub,
}
#[derive(Clone, Copy, Debug)]
pub struct PngOptions {
pub filter: PngFilter,
pub compression_level: u8,
}
impl Default for PngOptions {
fn default() -> Self {
Self { filter: PngFilter::None, compression_level: 6 }
}
}
impl PngOptions {
pub fn best_quality() -> Self {
Self { filter: PngFilter::None, compression_level: 10 }
}
}
pub fn encode_png(rgba: &[u8], width: u32, height: u32) -> Result<Vec<u8>, IconError> {
encode_png_with(rgba, width, height, &PngOptions::default())
}
pub fn encode_png_with(
rgba: &[u8], width: u32, height: u32, opts: &PngOptions,
) -> Result<Vec<u8>, IconError> {
let expected = (width as usize) * (height as usize) * 4;
if rgba.len() != expected {
return Err(IconError::Encode(format!(
"RGBA buffer size mismatch: expected {expected}, got {}", rgba.len()
)));
}
let mut out = Vec::with_capacity(expected / 2);
out.extend_from_slice(&[137, 80, 78, 71, 13, 10, 26, 10]);
let mut ihdr = [0u8; 13];
ihdr[0..4].copy_from_slice(&width.to_be_bytes());
ihdr[4..8].copy_from_slice(&height.to_be_bytes());
ihdr[8] = 8; ihdr[9] = 6; write_chunk(&mut out, b"IHDR", &ihdr);
let stride = (width as usize) * 4;
let mut raw = Vec::with_capacity(height as usize * (1 + stride));
match opts.filter {
PngFilter::None => {
for row in rgba.chunks_exact(stride) {
raw.push(0);
raw.extend_from_slice(row);
}
}
PngFilter::Sub => {
for row in rgba.chunks_exact(stride) {
raw.push(1);
raw.extend_from_slice(&row[..4]);
for i in 4..stride {
raw.push(row[i].wrapping_sub(row[i - 4]));
}
}
}
}
let level = opts.compression_level.min(10);
let compressed = miniz_oxide::deflate::compress_to_vec_zlib(&raw, level);
write_chunk(&mut out, b"IDAT", &compressed);
write_chunk(&mut out, b"IEND", &[]);
Ok(out)
}