use alloc::string::String;
use alloc::vec::Vec;
use enough::Stop;
use zencodec::{Cicp, ContentLightLevel, MasteringDisplay};
use zenpixels::PixelBuffer;
use crate::error::PngError;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PhysUnit {
Unknown,
Meter,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct TextChunk {
pub keyword: String,
pub text: String,
pub compressed: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PngBackground {
Indexed(u8),
Gray(u16),
Rgb(u16, u16, u16),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PngTime {
pub year: u16,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SignificantBits {
Gray(u8),
Rgb(u8, u8, u8),
GrayAlpha(u8, u8),
Rgba(u8, u8, u8, u8),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PngChromaticities {
pub white_x: i32,
pub white_y: i32,
pub red_x: i32,
pub red_y: i32,
pub green_x: i32,
pub green_y: i32,
pub blue_x: i32,
pub blue_y: i32,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct PngInfo {
pub width: u32,
pub height: u32,
pub has_alpha: bool,
pub sequence: zencodec::ImageSequence,
pub bit_depth: u8,
pub color_type: u8,
pub icc_profile: Option<Vec<u8>>,
pub exif: Option<Vec<u8>>,
pub xmp: Option<Vec<u8>>,
pub source_gamma: Option<u32>,
pub srgb_intent: Option<u8>,
pub chromaticities: Option<PngChromaticities>,
pub cicp: Option<Cicp>,
pub content_light_level: Option<ContentLightLevel>,
pub mastering_display: Option<MasteringDisplay>,
pub pixels_per_unit_x: Option<u32>,
pub pixels_per_unit_y: Option<u32>,
pub phys_unit: Option<PhysUnit>,
pub text_chunks: Vec<TextChunk>,
pub background: Option<PngBackground>,
pub last_modified: Option<PngTime>,
pub significant_bits: Option<SignificantBits>,
pub interlaced: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PngWarning {
SrgbCicpConflict,
IccpSrgbConflict,
CicpIccpConflict,
CicpChrmConflict,
SrgbGamaMismatch {
actual_gamma: u32,
},
SrgbChrmMismatch,
DecompressionChecksumSkipped,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct PngDecodeOutput {
pub pixels: PixelBuffer,
pub info: PngInfo,
pub warnings: Vec<PngWarning>,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct PngDecodeConfig {
pub max_pixels: Option<u64>,
pub max_memory_bytes: Option<u64>,
pub skip_decompression_checksum: bool,
pub skip_critical_chunk_crc: bool,
}
impl PngDecodeConfig {
pub const DEFAULT_MAX_PIXELS: u64 = 100_000_000;
pub const DEFAULT_MAX_MEMORY: u64 = 4 * 1024 * 1024 * 1024;
#[must_use]
pub const fn none() -> Self {
Self {
max_pixels: None,
max_memory_bytes: None,
skip_decompression_checksum: true,
skip_critical_chunk_crc: true,
}
}
#[must_use]
pub const fn lenient() -> Self {
Self::none()
}
#[must_use]
pub const fn strict() -> Self {
Self {
max_pixels: None,
max_memory_bytes: None,
skip_decompression_checksum: false,
skip_critical_chunk_crc: false,
}
}
#[must_use]
pub const fn with_max_pixels(mut self, max: u64) -> Self {
self.max_pixels = Some(max);
self
}
#[must_use]
pub const fn with_max_memory(mut self, max: u64) -> Self {
self.max_memory_bytes = Some(max);
self
}
#[must_use]
pub const fn with_skip_decompression_checksum(mut self, skip: bool) -> Self {
self.skip_decompression_checksum = skip;
self
}
#[must_use]
pub const fn with_skip_critical_chunk_crc(mut self, skip: bool) -> Self {
self.skip_critical_chunk_crc = skip;
self
}
pub(crate) fn validate(
&self,
width: u32,
height: u32,
bytes_per_pixel: u32,
) -> Result<(), PngError> {
if let Some(max_px) = self.max_pixels {
let pixels = width as u64 * height as u64;
if pixels > max_px {
return Err(PngError::LimitExceeded("pixel count exceeds limit".into()));
}
}
if let Some(max_mem) = self.max_memory_bytes {
let estimated = width as u64 * height as u64 * bytes_per_pixel as u64;
if estimated > max_mem {
return Err(PngError::LimitExceeded(
"estimated memory exceeds limit".into(),
));
}
}
Ok(())
}
}
impl Default for PngDecodeConfig {
fn default() -> Self {
Self {
max_pixels: Some(Self::DEFAULT_MAX_PIXELS),
max_memory_bytes: Some(Self::DEFAULT_MAX_MEMORY),
skip_decompression_checksum: true,
skip_critical_chunk_crc: true,
}
}
}
#[deprecated(note = "renamed to PngDecodeConfig")]
pub type PngLimits = PngDecodeConfig;
pub fn probe(data: &[u8]) -> crate::error::Result<PngInfo> {
Ok(crate::decoder::probe_png(data)?)
}
pub fn decode(
data: &[u8],
config: &PngDecodeConfig,
cancel: &dyn Stop,
) -> crate::error::Result<PngDecodeOutput> {
Ok(crate::decoder::decode_png(data, config, cancel)?)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ApngFrameInfo {
pub delay_num: u16,
pub delay_den: u16,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct ApngFrame {
pub pixels: PixelBuffer,
pub frame_info: ApngFrameInfo,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct ApngDecodeOutput {
pub frames: Vec<ApngFrame>,
pub info: PngInfo,
pub num_plays: u32,
pub warnings: Vec<PngWarning>,
}
pub fn decode_apng(
data: &[u8],
config: &PngDecodeConfig,
cancel: &dyn Stop,
) -> crate::error::Result<ApngDecodeOutput> {
let probe_info = crate::decoder::probe_png(data)?;
if !probe_info.sequence.is_animation() {
let output = crate::decoder::decode_png(data, config, cancel)?;
let frame = ApngFrame {
pixels: output.pixels,
frame_info: ApngFrameInfo {
delay_num: 0,
delay_den: 100,
},
};
return Ok(ApngDecodeOutput {
frames: vec![frame],
info: output.info,
num_plays: 0,
warnings: output.warnings,
});
}
let result = crate::decoder::apng::decode_apng_composed(data, config, cancel)?;
let info = crate::decoder::build_png_info(&result.ihdr, &result.ancillary);
Ok(ApngDecodeOutput {
frames: result.frames,
info,
num_plays: result.num_plays,
warnings: result.warnings,
})
}
const SRGB_CHRM: [i32; 8] = [
31270, 32900, 64000, 33000, 30000, 60000, 15000, 6000, ];
pub(crate) fn detect_color_warnings(
srgb_intent: Option<u8>,
gamma: Option<u32>,
chrm: Option<&[i32; 8]>,
cicp: Option<&[u8; 4]>,
icc_profile: Option<&[u8]>,
) -> Vec<PngWarning> {
let mut warnings = Vec::new();
let has_srgb = srgb_intent.is_some();
let has_cicp = cicp.is_some();
let has_iccp = icc_profile.is_some();
if has_srgb && has_cicp {
warnings.push(PngWarning::SrgbCicpConflict);
}
if has_iccp && has_srgb {
warnings.push(PngWarning::IccpSrgbConflict);
}
if has_cicp && has_iccp {
warnings.push(PngWarning::CicpIccpConflict);
}
if has_cicp && chrm.is_some() {
warnings.push(PngWarning::CicpChrmConflict);
}
if has_srgb {
if let Some(g) = gamma
&& g != 45455
{
warnings.push(PngWarning::SrgbGamaMismatch { actual_gamma: g });
}
if let Some(c) = chrm
&& c != &SRGB_CHRM
{
warnings.push(PngWarning::SrgbChrmMismatch);
}
}
warnings
}
#[cfg(test)]
mod tests {
use super::*;
fn craft_ihdr_png(width: u32, height: u32, color_type: u8, bit_depth: u8) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&[137, 80, 78, 71, 13, 10, 26, 10]);
buf.extend_from_slice(&13u32.to_be_bytes());
let ihdr_type = b"IHDR";
buf.extend_from_slice(ihdr_type);
buf.extend_from_slice(&width.to_be_bytes());
buf.extend_from_slice(&height.to_be_bytes());
buf.push(bit_depth);
buf.push(color_type);
buf.push(0); buf.push(0); buf.push(0); let crc = zenflate::crc32(zenflate::crc32(0, ihdr_type), &buf[16..29]);
buf.extend_from_slice(&crc.to_be_bytes());
let idat_data: &[u8] = &[];
buf.extend_from_slice(&0u32.to_be_bytes());
let idat_type = b"IDAT";
buf.extend_from_slice(idat_type);
let crc = zenflate::crc32(zenflate::crc32(0, idat_type), idat_data);
buf.extend_from_slice(&crc.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
let iend_type = b"IEND";
buf.extend_from_slice(iend_type);
let crc = zenflate::crc32(0, iend_type);
buf.extend_from_slice(&crc.to_be_bytes());
buf
}
#[test]
fn limits_default_rejects_oversized() {
let png = craft_ihdr_png(65535, 65535, 6, 8);
let result = decode(&png, &PngDecodeConfig::default(), &enough::Unstoppable);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
matches!(err.error(), PngError::LimitExceeded(_)),
"expected LimitExceeded, got: {err:?}"
);
}
#[test]
fn limits_none_skips_checks() {
let png = craft_ihdr_png(65535, 65535, 6, 8);
let result = decode(&png, &PngDecodeConfig::none(), &enough::Unstoppable);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
!matches!(err.error(), PngError::LimitExceeded(_)),
"expected non-limits error, got: {err:?}"
);
}
#[test]
fn limits_custom_pixel_threshold() {
let png = craft_ihdr_png(100, 100, 6, 8);
let config = PngDecodeConfig::none().with_max_pixels(5_000);
let result = decode(&png, &config, &enough::Unstoppable);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err().error(),
PngError::LimitExceeded(_)
));
}
#[test]
fn limits_custom_memory_threshold() {
let png = craft_ihdr_png(100, 100, 6, 8);
let config = PngDecodeConfig::none().with_max_memory(20_000);
let result = decode(&png, &config, &enough::Unstoppable);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err().error(),
PngError::LimitExceeded(_)
));
}
#[test]
fn default_skips_checksums() {
let config = PngDecodeConfig::default();
assert_eq!(config.max_pixels, Some(100_000_000));
assert_eq!(config.max_memory_bytes, Some(4 * 1024 * 1024 * 1024));
assert!(config.skip_decompression_checksum);
assert!(config.skip_critical_chunk_crc);
}
#[test]
fn none_has_no_limits_and_skips_checksums() {
let config = PngDecodeConfig::none();
assert!(config.max_pixels.is_none());
assert!(config.max_memory_bytes.is_none());
assert!(config.skip_decompression_checksum);
assert!(config.skip_critical_chunk_crc);
}
#[test]
fn lenient_has_no_limits_and_skips_checksums() {
let config = PngDecodeConfig::lenient();
assert!(config.max_pixels.is_none());
assert!(config.max_memory_bytes.is_none());
assert!(config.skip_decompression_checksum);
assert!(config.skip_critical_chunk_crc);
}
#[test]
fn strict_verifies_checksums() {
let config = PngDecodeConfig::strict();
assert!(config.max_pixels.is_none());
assert!(config.max_memory_bytes.is_none());
assert!(!config.skip_decompression_checksum);
assert!(!config.skip_critical_chunk_crc);
}
#[test]
fn detect_srgb_cicp_conflict() {
let w = detect_color_warnings(Some(0), None, None, Some(&[1, 13, 0, 1]), None);
assert!(w.contains(&PngWarning::SrgbCicpConflict));
}
#[test]
fn detect_iccp_srgb_conflict() {
let w = detect_color_warnings(Some(0), None, None, None, Some(&[0]));
assert!(w.contains(&PngWarning::IccpSrgbConflict));
}
#[test]
fn detect_srgb_gama_mismatch() {
let w = detect_color_warnings(Some(0), Some(50000), None, None, None);
assert!(w.contains(&PngWarning::SrgbGamaMismatch {
actual_gamma: 50000
}));
}
#[test]
fn detect_srgb_gama_correct() {
let w = detect_color_warnings(Some(0), Some(45455), None, None, None);
assert!(
!w.iter()
.any(|w| matches!(w, PngWarning::SrgbGamaMismatch { .. }))
);
}
#[test]
fn detect_srgb_chrm_mismatch() {
let bad_chrm: [i32; 8] = [31270, 32900, 64000, 33000, 30000, 60000, 15000, 7000];
let w = detect_color_warnings(Some(0), None, Some(&bad_chrm), None, None);
assert!(w.contains(&PngWarning::SrgbChrmMismatch));
}
#[test]
fn detect_srgb_chrm_correct() {
let w = detect_color_warnings(Some(0), None, Some(&SRGB_CHRM), None, None);
assert!(!w.contains(&PngWarning::SrgbChrmMismatch));
}
#[test]
fn no_warnings_when_clean() {
let w = detect_color_warnings(Some(0), Some(45455), Some(&SRGB_CHRM), None, None);
assert!(w.is_empty());
}
#[test]
fn with_skip_decompression_checksum_builder() {
let config = PngDecodeConfig::strict().with_skip_decompression_checksum(true);
assert!(config.skip_decompression_checksum);
let config2 = PngDecodeConfig::none().with_skip_decompression_checksum(false);
assert!(!config2.skip_decompression_checksum);
}
#[test]
fn with_skip_critical_chunk_crc_builder() {
let config = PngDecodeConfig::strict().with_skip_critical_chunk_crc(true);
assert!(config.skip_critical_chunk_crc);
let config2 = PngDecodeConfig::none().with_skip_critical_chunk_crc(false);
assert!(!config2.skip_critical_chunk_crc);
}
fn craft_png_with_chunks(
width: u32,
height: u32,
color_type: u8,
bit_depth: u8,
chunks: &[(&[u8; 4], &[u8])],
) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&[137, 80, 78, 71, 13, 10, 26, 10]);
let ihdr_start = buf.len();
buf.extend_from_slice(&13u32.to_be_bytes());
buf.extend_from_slice(b"IHDR");
buf.extend_from_slice(&width.to_be_bytes());
buf.extend_from_slice(&height.to_be_bytes());
buf.push(bit_depth);
buf.push(color_type);
buf.push(0);
buf.push(0);
buf.push(0); let crc = zenflate::crc32(
zenflate::crc32(0, b"IHDR"),
&buf[ihdr_start + 8..ihdr_start + 8 + 13],
);
buf.extend_from_slice(&crc.to_be_bytes());
for &(ctype, cdata) in chunks {
buf.extend_from_slice(&(cdata.len() as u32).to_be_bytes());
buf.extend_from_slice(ctype);
buf.extend_from_slice(cdata);
let crc = zenflate::crc32(zenflate::crc32(0, ctype), cdata);
buf.extend_from_slice(&crc.to_be_bytes());
}
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(b"IDAT");
let crc = zenflate::crc32(zenflate::crc32(0, b"IDAT"), &[]);
buf.extend_from_slice(&crc.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(b"IEND");
let crc = zenflate::crc32(0, b"IEND");
buf.extend_from_slice(&crc.to_be_bytes());
buf
}
#[test]
fn probe_phys_meter() {
let mut phys_data = [0u8; 9];
phys_data[0..4].copy_from_slice(&3780u32.to_be_bytes());
phys_data[4..8].copy_from_slice(&3780u32.to_be_bytes());
phys_data[8] = 1;
let png = craft_png_with_chunks(4, 4, 2, 8, &[(b"pHYs", &phys_data)]);
let info = probe(&png).unwrap();
assert_eq!(info.pixels_per_unit_x, Some(3780));
assert_eq!(info.pixels_per_unit_y, Some(3780));
assert_eq!(info.phys_unit, Some(PhysUnit::Meter));
}
#[test]
fn probe_phys_unknown() {
let mut phys_data = [0u8; 9];
phys_data[0..4].copy_from_slice(&1u32.to_be_bytes());
phys_data[4..8].copy_from_slice(&2u32.to_be_bytes());
phys_data[8] = 0;
let png = craft_png_with_chunks(4, 4, 2, 8, &[(b"pHYs", &phys_data)]);
let info = probe(&png).unwrap();
assert_eq!(info.pixels_per_unit_x, Some(1));
assert_eq!(info.pixels_per_unit_y, Some(2));
assert_eq!(info.phys_unit, Some(PhysUnit::Unknown));
}
#[test]
fn probe_text_chunks() {
let mut text1 = Vec::new();
text1.extend_from_slice(b"Author");
text1.push(0);
text1.extend_from_slice(b"Alice");
let mut text2 = Vec::new();
text2.extend_from_slice(b"Comment");
text2.push(0);
text2.extend_from_slice(b"test");
let png = craft_png_with_chunks(4, 4, 2, 8, &[(b"tEXt", &text1), (b"tEXt", &text2)]);
let info = probe(&png).unwrap();
assert_eq!(info.text_chunks.len(), 2);
assert_eq!(info.text_chunks[0].keyword, "Author");
assert_eq!(info.text_chunks[0].text, "Alice");
assert!(!info.text_chunks[0].compressed);
assert_eq!(info.text_chunks[1].keyword, "Comment");
assert_eq!(info.text_chunks[1].text, "test");
}
#[test]
fn probe_bkgd_rgb() {
let mut bkgd = [0u8; 6];
bkgd[0..2].copy_from_slice(&255u16.to_be_bytes());
bkgd[2..4].copy_from_slice(&128u16.to_be_bytes());
bkgd[4..6].copy_from_slice(&0u16.to_be_bytes());
let png = craft_png_with_chunks(4, 4, 2, 8, &[(b"bKGD", &bkgd)]);
let info = probe(&png).unwrap();
assert_eq!(info.background, Some(PngBackground::Rgb(255, 128, 0)));
}
#[test]
fn probe_bkgd_gray() {
let bkgd = 42u16.to_be_bytes();
let png = craft_png_with_chunks(4, 4, 0, 8, &[(b"bKGD", &bkgd)]);
let info = probe(&png).unwrap();
assert_eq!(info.background, Some(PngBackground::Gray(42)));
}
#[test]
fn probe_time() {
let mut time_data = [0u8; 7];
time_data[0..2].copy_from_slice(&2026u16.to_be_bytes());
time_data[2] = 3;
time_data[3] = 18;
time_data[4] = 14;
time_data[5] = 30;
time_data[6] = 45;
let png = craft_png_with_chunks(4, 4, 2, 8, &[(b"tIME", &time_data)]);
let info = probe(&png).unwrap();
let t = info.last_modified.unwrap();
assert_eq!(t.year, 2026);
assert_eq!(t.month, 3);
assert_eq!(t.day, 18);
assert_eq!(t.hour, 14);
assert_eq!(t.minute, 30);
assert_eq!(t.second, 45);
}
#[test]
fn probe_sbit_rgb() {
let png = craft_png_with_chunks(4, 4, 2, 8, &[(b"sBIT", &[5, 6, 5])]);
let info = probe(&png).unwrap();
assert_eq!(info.significant_bits, Some(SignificantBits::Rgb(5, 6, 5)));
}
#[test]
fn probe_sbit_rgba() {
let png = craft_png_with_chunks(4, 4, 6, 8, &[(b"sBIT", &[5, 6, 5, 8])]);
let info = probe(&png).unwrap();
assert_eq!(
info.significant_bits,
Some(SignificantBits::Rgba(5, 6, 5, 8))
);
}
#[test]
fn probe_no_ancillary_defaults() {
let png = craft_png_with_chunks(4, 4, 2, 8, &[]);
let info = probe(&png).unwrap();
assert!(info.pixels_per_unit_x.is_none());
assert!(info.pixels_per_unit_y.is_none());
assert!(info.phys_unit.is_none());
assert!(info.text_chunks.is_empty());
assert!(info.background.is_none());
assert!(info.last_modified.is_none());
assert!(info.significant_bits.is_none());
}
#[test]
fn roundtrip_phys_text_time() {
use imgref::ImgVec;
use rgb::Rgb;
let pixels = ImgVec::new(
vec![
Rgb {
r: 128u8,
g: 64,
b: 32
};
16
],
4,
4,
);
let config = crate::EncodeConfig::default()
.with_phys(3780, 3780, PhysUnit::Meter)
.with_text("Author", "zenpng test")
.with_text("Comment", "roundtrip")
.with_last_modified(PngTime {
year: 2026,
month: 3,
day: 18,
hour: 15,
minute: 0,
second: 0,
});
let encoded = crate::encode_rgb8(
pixels.as_ref(),
None,
&config,
&enough::Unstoppable,
&enough::Unstoppable,
)
.unwrap();
let info = probe(&encoded).unwrap();
assert_eq!(info.pixels_per_unit_x, Some(3780));
assert_eq!(info.pixels_per_unit_y, Some(3780));
assert_eq!(info.phys_unit, Some(PhysUnit::Meter));
assert_eq!(info.text_chunks.len(), 2);
assert_eq!(info.text_chunks[0].keyword, "Author");
assert_eq!(info.text_chunks[0].text, "zenpng test");
assert_eq!(info.text_chunks[1].keyword, "Comment");
assert_eq!(info.text_chunks[1].text, "roundtrip");
let t = info.last_modified.unwrap();
assert_eq!(t.year, 2026);
assert_eq!(t.month, 3);
assert_eq!(t.day, 18);
assert_eq!(t.hour, 15);
assert_eq!(t.minute, 0);
assert_eq!(t.second, 0);
}
#[test]
fn roundtrip_phys_unknown_unit() {
use imgref::ImgVec;
use rgb::Rgb;
let pixels = ImgVec::new(vec![Rgb { r: 0u8, g: 0, b: 0 }; 4], 2, 2);
let config = crate::EncodeConfig::default().with_phys(1, 2, PhysUnit::Unknown);
let encoded = crate::encode_rgb8(
pixels.as_ref(),
None,
&config,
&enough::Unstoppable,
&enough::Unstoppable,
)
.unwrap();
let info = probe(&encoded).unwrap();
assert_eq!(info.pixels_per_unit_x, Some(1));
assert_eq!(info.pixels_per_unit_y, Some(2));
assert_eq!(info.phys_unit, Some(PhysUnit::Unknown));
}
}