pub(crate) mod apng;
pub(crate) mod compress;
pub(crate) mod filter;
pub(crate) mod metadata;
use alloc::string::ToString;
use alloc::vec;
use alloc::vec::Vec;
use enough::Stop;
use crate::chunk::PNG_SIGNATURE;
use crate::chunk::write::write_chunk;
use crate::error::PngError;
#[allow(unused_imports)]
use whereat::at;
pub(crate) use self::compress::compress_filtered;
pub(crate) use self::metadata::{PngWriteMetadata, metadata_size_estimate, write_all_metadata};
pub(crate) struct CompressOptions<'a> {
pub parallel: bool,
pub cancel: &'a dyn Stop,
pub deadline: &'a dyn Stop,
#[allow(dead_code)] pub remaining_ns: Option<&'a dyn Fn() -> Option<u64>>,
pub max_threads: usize,
}
#[derive(Clone, Debug)]
#[doc(hidden)]
#[allow(dead_code)]
pub struct PhaseStat {
pub name: alloc::string::String,
pub duration_ns: u64,
pub best_size: usize,
pub evaluations: u32,
}
#[derive(Clone, Debug, Default)]
#[doc(hidden)]
pub struct PhaseStats {
pub phases: Vec<PhaseStat>,
pub raw_size: usize,
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn write_indexed_png(
indices: &[u8],
width: u32,
height: u32,
palette_rgb: &[u8],
palette_alpha: Option<&[u8]>,
write_meta: &PngWriteMetadata<'_>,
effort: u32,
opts: CompressOptions<'_>,
) -> crate::error::Result<Vec<u8>> {
let w = width as usize;
let h = height as usize;
let n_colors = palette_rgb.len() / 3;
if n_colors == 0 || n_colors > 256 {
return Err(at!(PngError::InvalidInput(alloc::format!(
"palette must have 1-256 entries, got {n_colors}"
))));
}
if indices.len() < w * h {
return Err(at!(PngError::InvalidInput(
"index buffer too small for dimensions".to_string(),
)));
}
let bit_depth = select_bit_depth(n_colors);
let packed_rows = pack_all_rows(indices, w, h, bit_depth);
let row_bytes = packed_row_bytes(w, bit_depth);
let compressed = compress_filtered(&packed_rows, row_bytes, h, 1, effort, opts, None)?;
let trns_data = truncate_trns(palette_alpha);
let est = 8
+ 25
+ (12 + n_colors * 3)
+ trns_data.as_ref().map_or(0, |t| 12 + t.len())
+ (12 + compressed.len())
+ 12
+ metadata_size_estimate(write_meta);
let mut out = Vec::with_capacity(est);
out.extend_from_slice(&PNG_SIGNATURE);
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] = bit_depth;
ihdr[9] = 3; write_chunk(&mut out, b"IHDR", &ihdr);
write_all_metadata(&mut out, write_meta)?;
write_chunk(&mut out, b"PLTE", &palette_rgb[..n_colors * 3]);
if let Some(trns) = &trns_data {
write_chunk(&mut out, b"tRNS", trns);
}
write_chunk(&mut out, b"IDAT", &compressed);
write_chunk(&mut out, b"IEND", &[]);
Ok(out)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn write_truecolor_png(
pixel_bytes: &[u8],
width: u32,
height: u32,
color_type: u8,
bit_depth: u8,
trns: Option<&[u8]>,
write_meta: &PngWriteMetadata<'_>,
effort: u32,
opts: CompressOptions<'_>,
) -> crate::error::Result<Vec<u8>> {
let w = width as usize;
let h = height as usize;
if color_type == 0 && bit_depth < 8 {
let expected_samples = w * h;
if pixel_bytes.len() < expected_samples {
return Err(at!(PngError::InvalidInput(alloc::format!(
"pixel buffer too small: need {expected_samples} samples, got {}",
pixel_bytes.len()
))));
}
let packed = pack_all_rows(pixel_bytes, w, h, bit_depth);
let row_bytes = packed_row_bytes(w, bit_depth);
let compressed = compress_filtered(&packed, row_bytes, h, 1, effort, opts, None)?;
let trns_size = trns.map_or(0, |t| 12 + t.len());
let est =
8 + 25 + trns_size + (12 + compressed.len()) + 12 + metadata_size_estimate(write_meta);
let mut out = Vec::with_capacity(est);
out.extend_from_slice(&PNG_SIGNATURE);
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] = bit_depth;
ihdr[9] = color_type;
write_chunk(&mut out, b"IHDR", &ihdr);
write_all_metadata(&mut out, write_meta)?;
if let Some(trns_data) = trns {
write_chunk(&mut out, b"tRNS", trns_data);
}
write_chunk(&mut out, b"IDAT", &compressed);
write_chunk(&mut out, b"IEND", &[]);
return Ok(out);
}
let channels: usize = match color_type {
0 => 1, 2 => 3, 4 => 2, 6 => 4, _ => {
return Err(at!(PngError::InvalidInput(alloc::format!(
"unsupported PNG color type: {color_type}"
))));
}
};
let bytes_per_channel = bit_depth as usize / 8;
let bpp = channels * bytes_per_channel;
let row_bytes = w * bpp;
let expected_len = row_bytes * h;
if pixel_bytes.len() < expected_len {
return Err(at!(PngError::InvalidInput(alloc::format!(
"pixel buffer too small: need {expected_len}, got {}",
pixel_bytes.len()
))));
}
if effort == 0 {
let filtered_row = row_bytes + 1;
let total_filtered = filtered_row * h;
let num_blocks = if total_filtered == 0 {
1
} else {
total_filtered.div_ceil(65535)
};
let idat_data_len = 2 + 5 * num_blocks + total_filtered + 4;
let trns_size = trns.map_or(0, |t| 12 + t.len());
let est =
8 + 25 + trns_size + (12 + idat_data_len) + 12 + metadata_size_estimate(write_meta);
let mut out = Vec::with_capacity(est);
out.extend_from_slice(&PNG_SIGNATURE);
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] = bit_depth;
ihdr[9] = color_type;
write_chunk(&mut out, b"IHDR", &ihdr);
write_all_metadata(&mut out, write_meta)?;
if let Some(trns_data) = trns {
write_chunk(&mut out, b"tRNS", trns_data);
}
out.extend_from_slice(&(idat_data_len as u32).to_be_bytes());
out.extend_from_slice(b"IDAT");
let idat_start = out.len();
compress::write_zlib_stored_inline(&mut out, &pixel_bytes[..expected_len], row_bytes, h);
let crc = zenflate::crc32(zenflate::crc32(0, b"IDAT"), &out[idat_start..]);
out.extend_from_slice(&crc.to_be_bytes());
write_chunk(&mut out, b"IEND", &[]);
return Ok(out);
}
let compressed = compress_filtered(
&pixel_bytes[..expected_len],
row_bytes,
h,
bpp,
effort,
opts,
None,
)?;
let trns_size = trns.map_or(0, |t| 12 + t.len());
let est =
8 + 25 + trns_size + (12 + compressed.len()) + 12 + metadata_size_estimate(write_meta);
let mut out = Vec::with_capacity(est);
out.extend_from_slice(&PNG_SIGNATURE);
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] = bit_depth;
ihdr[9] = color_type;
write_chunk(&mut out, b"IHDR", &ihdr);
write_all_metadata(&mut out, write_meta)?;
if let Some(trns_data) = trns {
write_chunk(&mut out, b"tRNS", trns_data);
}
write_chunk(&mut out, b"IDAT", &compressed);
write_chunk(&mut out, b"IEND", &[]);
Ok(out)
}
#[cfg(feature = "_dev")]
#[allow(clippy::too_many_arguments)]
pub(crate) fn write_truecolor_png_with_stats(
pixel_bytes: &[u8],
width: u32,
height: u32,
color_type: u8,
bit_depth: u8,
trns: Option<&[u8]>,
write_meta: &PngWriteMetadata<'_>,
effort: u32,
opts: CompressOptions<'_>,
stats: &mut PhaseStats,
) -> crate::error::Result<Vec<u8>> {
let w = width as usize;
let h = height as usize;
if color_type == 0 && bit_depth < 8 {
let expected_samples = w * h;
if pixel_bytes.len() < expected_samples {
return Err(at!(PngError::InvalidInput(alloc::format!(
"pixel buffer too small: need {expected_samples} samples, got {}",
pixel_bytes.len()
))));
}
let packed = pack_all_rows(pixel_bytes, w, h, bit_depth);
let row_bytes = packed_row_bytes(w, bit_depth);
let compressed = compress_filtered(&packed, row_bytes, h, 1, effort, opts, Some(stats))?;
let trns_size = trns.map_or(0, |t| 12 + t.len());
let est =
8 + 25 + trns_size + (12 + compressed.len()) + 12 + metadata_size_estimate(write_meta);
let mut out = Vec::with_capacity(est);
out.extend_from_slice(&PNG_SIGNATURE);
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] = bit_depth;
ihdr[9] = color_type;
write_chunk(&mut out, b"IHDR", &ihdr);
write_all_metadata(&mut out, write_meta)?;
if let Some(trns_data) = trns {
write_chunk(&mut out, b"tRNS", trns_data);
}
write_chunk(&mut out, b"IDAT", &compressed);
write_chunk(&mut out, b"IEND", &[]);
return Ok(out);
}
let channels: usize = match color_type {
0 => 1,
2 => 3,
4 => 2,
6 => 4,
_ => {
return Err(at!(PngError::InvalidInput(alloc::format!(
"unsupported PNG color type: {color_type}"
))));
}
};
let bytes_per_channel = bit_depth as usize / 8;
let bpp = channels * bytes_per_channel;
let row_bytes = w * bpp;
let expected_len = row_bytes * h;
if pixel_bytes.len() < expected_len {
return Err(at!(PngError::InvalidInput(alloc::format!(
"pixel buffer too small: need {expected_len}, got {}",
pixel_bytes.len()
))));
}
let compressed = compress_filtered(
&pixel_bytes[..expected_len],
row_bytes,
h,
bpp,
effort,
opts,
Some(stats),
)?;
let trns_size = trns.map_or(0, |t| 12 + t.len());
let est =
8 + 25 + trns_size + (12 + compressed.len()) + 12 + metadata_size_estimate(write_meta);
let mut out = Vec::with_capacity(est);
out.extend_from_slice(&PNG_SIGNATURE);
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] = bit_depth;
ihdr[9] = color_type;
write_chunk(&mut out, b"IHDR", &ihdr);
write_all_metadata(&mut out, write_meta)?;
if let Some(trns_data) = trns {
write_chunk(&mut out, b"tRNS", trns_data);
}
write_chunk(&mut out, b"IDAT", &compressed);
write_chunk(&mut out, b"IEND", &[]);
Ok(out)
}
pub(crate) fn select_bit_depth(n_colors: usize) -> u8 {
if n_colors <= 2 {
1
} else if n_colors <= 4 {
2
} else if n_colors <= 16 {
4
} else {
8
}
}
pub(crate) fn packed_row_bytes(width: usize, bit_depth: u8) -> usize {
match bit_depth {
8 => width,
4 => width.div_ceil(2),
2 => width.div_ceil(4),
1 => width.div_ceil(8),
_ => width,
}
}
pub(crate) fn pack_all_rows(indices: &[u8], width: usize, height: usize, bit_depth: u8) -> Vec<u8> {
if bit_depth == 8 {
return indices[..width * height].to_vec();
}
let row_bytes = packed_row_bytes(width, bit_depth);
let mut packed = vec![0u8; row_bytes * height];
let ppb = 8 / bit_depth as usize;
let mask = (1u8 << bit_depth) - 1;
for y in 0..height {
let src_row = &indices[y * width..y * width + width];
let dst_row = &mut packed[y * row_bytes..y * row_bytes + row_bytes];
for (x, &idx) in src_row.iter().enumerate() {
let byte_pos = x / ppb;
let bit_offset = (ppb - 1 - x % ppb) * bit_depth as usize;
dst_row[byte_pos] |= (idx & mask) << bit_offset;
}
}
packed
}
pub(crate) fn truncate_trns(palette_alpha: Option<&[u8]>) -> Option<Vec<u8>> {
let alpha = palette_alpha?;
let last_non_opaque = alpha.iter().rposition(|&a| a != 255)?;
Some(alpha[..=last_non_opaque].to_vec())
}
#[cfg(test)]
mod tests {
use super::*;
use enough::Unstoppable;
fn default_opts() -> CompressOptions<'static> {
CompressOptions {
parallel: false,
cancel: &Unstoppable,
deadline: &Unstoppable,
remaining_ns: None,
max_threads: 0,
}
}
#[test]
fn select_bit_depth_boundaries() {
assert_eq!(select_bit_depth(1), 1);
assert_eq!(select_bit_depth(2), 1);
assert_eq!(select_bit_depth(3), 2);
assert_eq!(select_bit_depth(4), 2);
assert_eq!(select_bit_depth(5), 4);
assert_eq!(select_bit_depth(16), 4);
assert_eq!(select_bit_depth(17), 8);
assert_eq!(select_bit_depth(256), 8);
}
#[test]
fn packed_row_bytes_all_depths() {
assert_eq!(packed_row_bytes(8, 8), 8);
assert_eq!(packed_row_bytes(8, 4), 4);
assert_eq!(packed_row_bytes(8, 2), 2);
assert_eq!(packed_row_bytes(8, 1), 1);
assert_eq!(packed_row_bytes(3, 4), 2); assert_eq!(packed_row_bytes(5, 2), 2); assert_eq!(packed_row_bytes(9, 1), 2); }
#[test]
fn pack_8bit_is_identity() {
let indices = vec![0u8, 1, 2, 3, 4, 5];
let packed = pack_all_rows(&indices, 3, 2, 8);
assert_eq!(packed, indices);
}
#[test]
fn pack_4bit() {
let indices = vec![0, 15, 3, 12];
let packed = pack_all_rows(&indices, 4, 1, 4);
assert_eq!(packed, vec![0x0F, 0x3C]);
}
#[test]
fn pack_2bit() {
let indices = vec![0, 1, 2, 3];
let packed = pack_all_rows(&indices, 4, 1, 2);
assert_eq!(packed, vec![0x1B]);
}
#[test]
fn pack_1bit() {
let indices = vec![0, 1, 0, 1, 0, 1, 0, 1];
let packed = pack_all_rows(&indices, 8, 1, 1);
assert_eq!(packed, vec![0x55]);
}
#[test]
fn truncate_trns_none() {
assert!(truncate_trns(None).is_none());
}
#[test]
fn truncate_trns_all_opaque() {
assert!(truncate_trns(Some(&[255, 255, 255])).is_none());
}
#[test]
fn truncate_trns_truncates_trailing_opaque() {
let alpha = [0, 128, 255, 255, 255];
let result = truncate_trns(Some(&alpha)).unwrap();
assert_eq!(result, vec![0, 128]);
}
#[test]
fn truncate_trns_single_transparent() {
let alpha = [0, 255, 255];
let result = truncate_trns(Some(&alpha)).unwrap();
assert_eq!(result, vec![0]);
}
#[test]
fn indexed_png_empty_palette_error() {
let result = write_indexed_png(
&[0; 4],
2,
2,
&[], None,
&PngWriteMetadata::from_metadata(None),
1,
default_opts(),
);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("1-256"));
}
#[test]
fn indexed_png_oversized_palette_error() {
let palette = vec![0u8; 257 * 3]; let result = write_indexed_png(
&[0; 4],
2,
2,
&palette,
None,
&PngWriteMetadata::from_metadata(None),
1,
default_opts(),
);
assert!(result.is_err());
}
#[test]
fn indexed_png_buffer_too_small_error() {
let palette = vec![0u8; 3]; let result = write_indexed_png(
&[0; 2], 2,
2,
&palette,
None,
&PngWriteMetadata::from_metadata(None),
1,
default_opts(),
);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too small"));
}
#[test]
fn indexed_png_valid_roundtrip() {
let palette = vec![255, 0, 0, 0, 255, 0]; let indices = vec![0, 1, 1, 0];
let result = write_indexed_png(
&indices,
2,
2,
&palette,
None,
&PngWriteMetadata::from_metadata(None),
1,
default_opts(),
);
assert!(result.is_ok());
let png = result.unwrap();
assert!(png[..8] == [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
}
#[test]
fn truecolor_png_unsupported_color_type() {
let result = write_truecolor_png(
&[0; 12],
2,
2,
5, 8,
None,
&PngWriteMetadata::from_metadata(None),
1,
default_opts(),
);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("unsupported"));
}
#[test]
fn truecolor_png_buffer_too_small() {
let result = write_truecolor_png(
&[0; 4], 2,
2,
2, 8,
None,
&PngWriteMetadata::from_metadata(None),
1,
default_opts(),
);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too small"));
}
#[test]
fn truecolor_png_effort_0_store_path() {
let pixels = vec![128u8; 2 * 2 * 3]; let result = write_truecolor_png(
&pixels,
2,
2,
2, 8,
None,
&PngWriteMetadata::from_metadata(None),
0,
default_opts(),
);
let png = result.unwrap();
assert!(png[..8] == [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
let decoded = crate::decode(&png, &crate::PngDecodeConfig::strict(), &Unstoppable).unwrap();
assert_eq!(decoded.info.width, 2);
}
#[test]
fn truecolor_png_16bit_path() {
let pixels = vec![0u8; 2 * 2 * 6]; let result = write_truecolor_png(
&pixels,
2,
2,
2, 16, None,
&PngWriteMetadata::from_metadata(None),
1,
default_opts(),
);
assert!(result.is_ok());
}
#[test]
fn truecolor_png_grayscale_alpha() {
let pixels = vec![128u8; 2 * 2 * 2]; let result = write_truecolor_png(
&pixels,
2,
2,
4, 8,
None,
&PngWriteMetadata::from_metadata(None),
1,
default_opts(),
);
assert!(result.is_ok());
}
#[test]
fn truecolor_png_with_trns() {
let pixels = vec![0u8; 2 * 2 * 3]; let trns = [0u8, 0, 0, 0, 0, 0]; let result = write_truecolor_png(
&pixels,
2,
2,
2, 8,
Some(&trns),
&PngWriteMetadata::from_metadata(None),
1,
default_opts(),
);
assert!(result.is_ok());
}
#[test]
fn subbyte_grayscale_4bit_roundtrip() {
let pixels: Vec<u8> = (0..16).collect();
let result = write_truecolor_png(
&pixels,
4,
4,
0, 4, None,
&PngWriteMetadata::from_metadata(None),
1,
default_opts(),
);
assert!(result.is_ok());
}
#[test]
fn subbyte_grayscale_buffer_too_small() {
let result = write_truecolor_png(
&[0; 2], 4,
4,
0, 4, None,
&PngWriteMetadata::from_metadata(None),
1,
default_opts(),
);
assert!(result.is_err());
}
}