use alloc::vec::Vec;
use zencodec::{Cicp, ContentLightLevel, MasteringDisplay, Metadata};
use zenflate::{CompressionLevel, Compressor, Unstoppable};
use crate::chunk::write::write_chunk;
use crate::decode::{PhysUnit, PngChromaticities, PngTime, TextChunk};
use crate::error::PngError;
#[allow(unused_imports)]
use whereat::at;
pub(crate) struct PngWriteMetadata<'a> {
pub generic: Option<&'a Metadata>,
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 last_modified: Option<PngTime>,
}
impl<'a> PngWriteMetadata<'a> {
pub fn from_metadata(meta: Option<&'a Metadata>) -> Self {
let (cicp, content_light_level, mastering_display) = meta
.map(|m| (m.cicp, m.content_light_level, m.mastering_display))
.unwrap_or((None, None, None));
Self {
generic: meta,
source_gamma: None,
srgb_intent: None,
chromaticities: None,
cicp,
content_light_level,
mastering_display,
pixels_per_unit_x: None,
pixels_per_unit_y: None,
phys_unit: None,
text_chunks: Vec::new(),
last_modified: None,
}
}
}
pub(crate) fn write_all_metadata(
out: &mut Vec<u8>,
meta: &PngWriteMetadata<'_>,
) -> crate::error::Result<()> {
let has_cicp = meta.cicp.is_some();
let has_iccp = meta.generic.is_some_and(|g| g.icc_profile.is_some());
let has_srgb = meta.srgb_intent.is_some();
let write_srgb = has_srgb && !has_cicp && !has_iccp;
let write_gama_chrm = !has_cicp && !has_iccp && !has_srgb;
if write_srgb && let Some(intent) = meta.srgb_intent {
write_srgb_chunk(out, intent);
}
if write_gama_chrm && let Some(gamma) = meta.source_gamma {
write_gama_chunk(out, gamma);
}
if write_gama_chrm && let Some(chrm) = &meta.chromaticities {
write_chrm_chunk(out, chrm);
}
if let Some(generic) = meta.generic
&& let Some(icc) = &generic.icc_profile
{
write_iccp_chunk(out, icc)?;
}
if let Some(cicp) = &meta.cicp {
write_cicp_chunk(out, cicp);
}
if let Some(mdcv) = &meta.mastering_display {
write_mdcv_chunk(out, mdcv);
}
if let Some(clli) = &meta.content_light_level {
write_clli_chunk(out, clli);
}
if let Some(generic) = meta.generic
&& let Some(exif) = &generic.exif
{
write_exif_chunk(out, exif);
}
if let Some(generic) = meta.generic
&& let Some(xmp) = &generic.xmp
{
let xmp_str = core::str::from_utf8(xmp).unwrap_or_default();
if !xmp_str.is_empty() {
write_itxt_chunk(out, "XML:com.adobe.xmp", xmp_str);
}
}
if let (Some(ppux), Some(ppuy)) = (meta.pixels_per_unit_x, meta.pixels_per_unit_y) {
let unit_byte = match meta.phys_unit {
Some(PhysUnit::Meter) => 1u8,
_ => 0u8,
};
write_phys_chunk(out, ppux, ppuy, unit_byte);
}
for tc in &meta.text_chunks {
write_text_chunk(out, &tc.keyword, &tc.text);
}
if let Some(ref t) = meta.last_modified {
write_time_chunk(out, t);
}
Ok(())
}
fn write_srgb_chunk(out: &mut Vec<u8>, intent: u8) {
write_chunk(out, b"sRGB", &[intent]);
}
fn write_gama_chunk(out: &mut Vec<u8>, gamma: u32) {
write_chunk(out, b"gAMA", &gamma.to_be_bytes());
}
fn write_chrm_chunk(out: &mut Vec<u8>, chrm: &PngChromaticities) {
let mut data = [0u8; 32];
data[0..4].copy_from_slice(&chrm.white_x.to_be_bytes());
data[4..8].copy_from_slice(&chrm.white_y.to_be_bytes());
data[8..12].copy_from_slice(&chrm.red_x.to_be_bytes());
data[12..16].copy_from_slice(&chrm.red_y.to_be_bytes());
data[16..20].copy_from_slice(&chrm.green_x.to_be_bytes());
data[20..24].copy_from_slice(&chrm.green_y.to_be_bytes());
data[24..28].copy_from_slice(&chrm.blue_x.to_be_bytes());
data[28..32].copy_from_slice(&chrm.blue_y.to_be_bytes());
write_chunk(out, b"cHRM", &data);
}
fn write_cicp_chunk(out: &mut Vec<u8>, cicp: &Cicp) {
let data = [
cicp.color_primaries,
cicp.transfer_characteristics,
cicp.matrix_coefficients,
if cicp.full_range { 1 } else { 0 },
];
write_chunk(out, b"cICP", &data);
}
fn write_mdcv_chunk(out: &mut Vec<u8>, mdcv: &MasteringDisplay) {
let mut data = [0u8; 24];
let to_u16 = |v: f32| (v / 0.00002).round() as u16;
let to_u32 = |v: f32| (v / 0.0001).round() as u32;
for (i, &[x, y]) in mdcv.primaries_xy.iter().enumerate() {
data[i * 4..i * 4 + 2].copy_from_slice(&to_u16(x).to_be_bytes());
data[i * 4 + 2..i * 4 + 4].copy_from_slice(&to_u16(y).to_be_bytes());
}
data[12..14].copy_from_slice(&to_u16(mdcv.white_point_xy[0]).to_be_bytes());
data[14..16].copy_from_slice(&to_u16(mdcv.white_point_xy[1]).to_be_bytes());
data[16..20].copy_from_slice(&to_u32(mdcv.max_luminance).to_be_bytes());
data[20..24].copy_from_slice(&to_u32(mdcv.min_luminance).to_be_bytes());
write_chunk(out, b"mDCV", &data);
}
fn write_clli_chunk(out: &mut Vec<u8>, clli: &ContentLightLevel) {
let max_cll = clli.max_content_light_level as u32 * 10000;
let max_fall = clli.max_frame_average_light_level as u32 * 10000;
let mut data = [0u8; 8];
data[0..4].copy_from_slice(&max_cll.to_be_bytes());
data[4..8].copy_from_slice(&max_fall.to_be_bytes());
write_chunk(out, b"cLLI", &data);
}
fn write_iccp_chunk(out: &mut Vec<u8>, icc_profile: &[u8]) -> crate::error::Result<()> {
let keyword = b"ICC Profile\0";
let compression_method = [0u8];
let level = CompressionLevel::new(9);
let mut compressor = Compressor::new(level);
let bound = Compressor::zlib_compress_bound(icc_profile.len());
let mut compressed = vec![0u8; bound];
let compressed_len = compressor
.zlib_compress(icc_profile, &mut compressed, Unstoppable)
.map_err(|e| {
at!(PngError::InvalidInput(alloc::format!(
"ICC compression failed: {e}"
)))
})?;
let mut chunk_data = Vec::with_capacity(keyword.len() + 1 + compressed_len);
chunk_data.extend_from_slice(keyword);
chunk_data.extend_from_slice(&compression_method);
chunk_data.extend_from_slice(&compressed[..compressed_len]);
write_chunk(out, b"iCCP", &chunk_data);
Ok(())
}
fn write_exif_chunk(out: &mut Vec<u8>, exif: &[u8]) {
write_chunk(out, b"eXIf", exif);
}
fn write_phys_chunk(out: &mut Vec<u8>, ppux: u32, ppuy: u32, unit: u8) {
let mut data = [0u8; 9];
data[0..4].copy_from_slice(&ppux.to_be_bytes());
data[4..8].copy_from_slice(&ppuy.to_be_bytes());
data[8] = unit;
write_chunk(out, b"pHYs", &data);
}
fn write_text_chunk(out: &mut Vec<u8>, keyword: &str, text: &str) {
let mut chunk_data = Vec::with_capacity(keyword.len() + 1 + text.len());
chunk_data.extend_from_slice(keyword.as_bytes());
chunk_data.push(0);
chunk_data.extend_from_slice(text.as_bytes());
write_chunk(out, b"tEXt", &chunk_data);
}
fn write_time_chunk(out: &mut Vec<u8>, t: &PngTime) {
let mut data = [0u8; 7];
data[0..2].copy_from_slice(&t.year.to_be_bytes());
data[2] = t.month;
data[3] = t.day;
data[4] = t.hour;
data[5] = t.minute;
data[6] = t.second;
write_chunk(out, b"tIME", &data);
}
fn write_itxt_chunk(out: &mut Vec<u8>, keyword: &str, text: &str) {
let mut chunk_data = Vec::with_capacity(keyword.len() + 5 + text.len());
chunk_data.extend_from_slice(keyword.as_bytes());
chunk_data.push(0); chunk_data.push(0); chunk_data.push(0); chunk_data.push(0); chunk_data.push(0); chunk_data.extend_from_slice(text.as_bytes());
write_chunk(out, b"iTXt", &chunk_data);
}
pub(crate) fn metadata_size_estimate(meta: &PngWriteMetadata<'_>) -> usize {
let mut size = 0;
let has_cicp = meta.cicp.is_some();
let has_iccp = meta.generic.is_some_and(|g| g.icc_profile.is_some());
let has_srgb = meta.srgb_intent.is_some();
if let Some(generic) = meta.generic {
if let Some(ref icc) = generic.icc_profile {
size += 12 + 13 + icc.len() / 2;
}
if let Some(ref exif) = generic.exif {
size += 12 + exif.len();
}
if let Some(ref xmp) = generic.xmp {
size += 12 + 25 + xmp.len();
}
}
if has_srgb && !has_cicp && !has_iccp {
size += 13; }
if !has_cicp && !has_iccp && !has_srgb {
if meta.source_gamma.is_some() {
size += 16; }
if meta.chromaticities.is_some() {
size += 44; }
}
if has_cicp {
size += 16; }
if meta.mastering_display.is_some() {
size += 36;
}
if meta.content_light_level.is_some() {
size += 20;
}
if meta.pixels_per_unit_x.is_some() && meta.pixels_per_unit_y.is_some() {
size += 12 + 9; }
for tc in &meta.text_chunks {
size += 12 + tc.keyword.len() + 1 + tc.text.len();
}
if meta.last_modified.is_some() {
size += 12 + 7; }
size
}