use std::io;
use std::path::Path;
use uzor::render::{RenderContext, TextAlign, TextBaseline};
use crate::render::MultiLegRender;
const HEADER_HEIGHT: u32 = 28;
const PANEL_GAP: u32 = 8;
const GRID_COLS: u32 = 3;
const GRID_ROWS: u32 = 2;
const COMPOSITE_BG: &str = "#14161c";
const HEADER_LABEL_COLOR: &str = "#e8e8ee";
#[derive(Debug)]
pub enum CompositeError {
Io(io::Error),
Encode(String),
}
impl std::fmt::Display for CompositeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CompositeError::Io(e) => write!(f, "I/O error writing composite PNG: {e}"),
CompositeError::Encode(msg) => write!(f, "PNG encode error: {msg}"),
}
}
}
impl std::error::Error for CompositeError {}
impl From<io::Error> for CompositeError {
fn from(e: io::Error) -> Self {
CompositeError::Io(e)
}
}
fn render_header_label(panel_w: u32, label: &str) -> Vec<u8> {
let mut ctx = uzor_render_tiny_skia::TinySkiaCpuRenderContext::new(panel_w, HEADER_HEIGHT, 1.0);
let ctx_dyn: &mut dyn RenderContext = &mut ctx;
ctx_dyn.set_fill_color(COMPOSITE_BG);
ctx_dyn.fill_rect(0.0, 0.0, panel_w as f64, HEADER_HEIGHT as f64);
ctx_dyn.set_fill_color(HEADER_LABEL_COLOR);
ctx_dyn.set_font("bold 15px sans-serif");
ctx_dyn.set_text_align(TextAlign::Center);
ctx_dyn.set_text_baseline(TextBaseline::Middle);
ctx_dyn.fill_text(label, panel_w as f64 / 2.0, HEADER_HEIGHT as f64 / 2.0);
ctx.pixels().to_vec()
}
fn blit(dst: &mut [u8], dst_w: u32, dst_x: u32, dst_y: u32, src: &[u8], src_w: u32, src_h: u32) {
for row in 0..src_h {
let src_start = (row * src_w * 4) as usize;
let src_end = src_start + (src_w * 4) as usize;
let dst_row_start = (((dst_y + row) * dst_w + dst_x) * 4) as usize;
let dst_row_end = dst_row_start + (src_w * 4) as usize;
dst[dst_row_start..dst_row_end].copy_from_slice(&src[src_start..src_end]);
}
}
fn unpremultiply(buf: &mut [u8]) {
for px in buf.chunks_exact_mut(4) {
let a = px[3];
if a == 0 || a == 255 {
continue;
}
let unmul = |c: u8| -> u8 { ((c as u32 * 255 + (a as u32) / 2) / (a as u32)).min(255) as u8 };
px[0] = unmul(px[0]);
px[1] = unmul(px[1]);
px[2] = unmul(px[2]);
}
}
pub fn write_composite_png(render: &MultiLegRender, path: &Path) -> Result<(), CompositeError> {
let panel_w = render.width;
let panel_h = render.height;
let total_w = panel_w * GRID_COLS + PANEL_GAP * (GRID_COLS - 1);
let row_h = HEADER_HEIGHT + panel_h;
let total_h = row_h * GRID_ROWS + PANEL_GAP * (GRID_ROWS - 1);
let grid: [[Option<(&str, &[u8])>; 2]; 3] = [
[Some(("tiny-skia", render.tiny_skia.as_slice())), None],
[Some(("vello-cpu", render.vello_cpu.as_slice())), render.vello_gpu.as_deref().map(|px| ("vello-gpu", px))],
[Some(("urx-cpu", render.urx_cpu.as_slice())), render.urx_gpu.as_ref().map(|r| ("urx-gpu", r.pixels.as_slice()))],
];
let mut composite = vec![0u8; (total_w * total_h * 4) as usize];
let bg = parse_hex_rgb(COMPOSITE_BG);
for px in composite.chunks_exact_mut(4) {
px.copy_from_slice(&[bg[0], bg[1], bg[2], 255]);
}
for (col_idx, col) in grid.iter().enumerate() {
let panel_x = col_idx as u32 * (panel_w + PANEL_GAP);
for (row_idx, cell) in col.iter().enumerate() {
let Some((label, pixels)) = cell else { continue };
let panel_y = row_idx as u32 * (row_h + PANEL_GAP);
let header = render_header_label(panel_w, label);
blit(&mut composite, total_w, panel_x, panel_y, &header, panel_w, HEADER_HEIGHT);
blit(&mut composite, total_w, panel_x, panel_y + HEADER_HEIGHT, pixels, panel_w, panel_h);
}
}
unpremultiply(&mut composite);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = std::fs::File::create(path)?;
let mut encoder = png::Encoder::new(io::BufWriter::new(file), total_w, total_h);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header().map_err(|e| CompositeError::Encode(e.to_string()))?;
writer.write_image_data(&composite).map_err(|e| CompositeError::Encode(e.to_string()))?;
Ok(())
}
fn parse_hex_rgb(hex: &str) -> [u8; 3] {
let h = hex.trim_start_matches('#');
let byte = |s: &str| u8::from_str_radix(s, 16).unwrap_or(0);
if h.len() >= 6 {
[byte(&h[0..2]), byte(&h[2..4]), byte(&h[4..6])]
} else {
[0, 0, 0]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::MultiLegRender;
#[test]
fn write_composite_png_produces_a_correctly_sized_valid_png() {
let render = MultiLegRender::capture(6, 6, |ctx| {
ctx.set_fill_color("#3366ff");
ctx.fill_rect(0.0, 0.0, 6.0, 6.0);
});
let dir = std::env::temp_dir().join("uzor-proof-harness-tests");
let path = dir.join("composite_smoke.png");
write_composite_png(&render, &path).expect("composite PNG should write");
let bytes = std::fs::read(&path).expect("composite PNG should be readable back");
let decoder = png::Decoder::new(bytes.as_slice());
let reader = decoder.read_info().expect("valid PNG header");
let info = reader.info();
assert_eq!(info.width, 6 * GRID_COLS + PANEL_GAP * (GRID_COLS - 1));
assert_eq!(info.height, (HEADER_HEIGHT + 6) * GRID_ROWS + PANEL_GAP * (GRID_ROWS - 1));
let _ = std::fs::remove_file(&path);
}
#[test]
fn parse_hex_rgb_parses_the_composite_background_constant() {
assert_eq!(parse_hex_rgb(COMPOSITE_BG), [0x14, 0x16, 0x1c]);
}
}