use super::icc_profile;
use crate::color::ColorSpace;
use image::{ExtendedColorType, ImageEncoder, codecs::png::PngEncoder};
pub fn encode_png_with_icc(
rgba: &[u8],
width: u32,
height: u32,
color_space: ColorSpace,
) -> Result<Vec<u8>, String> {
if rgba.len() != (width as usize) * (height as usize) * 4 {
return Err("Internal error: pixel buffer size did not match dimensions.".to_string());
}
let mut bytes = Vec::new();
let mut encoder = PngEncoder::new(&mut bytes);
if color_space != ColorSpace::Srgb {
encoder
.set_icc_profile(icc_profile::build(color_space))
.map_err(|e| e.to_string())?;
}
encoder
.write_image(rgba, width, height, ExtendedColorType::Rgba8)
.map_err(|e| e.to_string())?;
Ok(bytes)
}
#[must_use]
pub fn encode_preview_png(
width: u32,
height: u32,
accum: &[glam::Vec3],
samples: u32,
) -> Option<Vec<u8>> {
if accum.len() != (width as usize) * (height as usize) {
return None;
}
let rgba = crate::renderer::tonemap::tonemap_to_rgba(accum, 1.0 / samples.max(1) as f32);
encode_png_with_icc(&rgba, width, height, ColorSpace::Srgb).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn preview_png_is_the_srgb_tone_map_of_the_averaged_sum() {
let accum: Vec<glam::Vec3> = (0..12)
.map(|i| glam::Vec3::new(0.2, 0.3, 0.1) * (i as f32 + 1.0) * 8.0)
.collect();
let png = encode_preview_png(4, 3, &accum, 8).expect("a well-formed sum encodes");
let rgba = crate::renderer::tonemap::tonemap_to_rgba(&accum, 1.0 / 8.0);
let expected = encode_png_with_icc(&rgba, 4, 3, ColorSpace::Srgb).unwrap();
assert_eq!(png, expected);
assert_eq!(&png[..8], b"\x89PNG\r\n\x1a\n");
}
#[test]
fn preview_png_refuses_a_wrongly_sized_sum() {
assert!(encode_preview_png(4, 3, &[glam::Vec3::ZERO; 11], 1).is_none());
}
fn fixed_rgba(width: u32, height: u32) -> Vec<u8> {
(0..width * height * 4)
.map(|i| (i * 37 + 11).to_le_bytes()[0])
.collect()
}
#[test]
fn rejects_a_mismatched_buffer_size() {
let err = encode_png_with_icc(&[0, 0, 0, 0], 2, 2, ColorSpace::Srgb).unwrap_err();
assert!(err.contains("did not match dimensions"));
}
#[test]
fn srgb_and_display_p3_encode_to_different_bytes_for_the_same_pixels() {
let rgba = fixed_rgba(4, 3);
let srgb = encode_png_with_icc(&rgba, 4, 3, ColorSpace::Srgb).unwrap();
let p3 = encode_png_with_icc(&rgba, 4, 3, ColorSpace::DisplayP3).unwrap();
assert_ne!(srgb, p3, "an embedded ICC profile must change the bytes");
}
#[test]
fn encoding_is_deterministic_for_identical_input() {
let rgba = fixed_rgba(5, 5);
let a = encode_png_with_icc(&rgba, 5, 5, ColorSpace::Rec2020).unwrap();
let b = encode_png_with_icc(&rgba, 5, 5, ColorSpace::Rec2020).unwrap();
assert_eq!(a, b);
}
}