use zenith_core::{AssetProvider, FontProvider};
use zenith_scene::Scene;
use crate::backend::{RasterBackend, RasterImage};
use crate::error::RenderError;
use crate::tiny_skia::TinySkiaBackend;
pub fn render_png(
scene: &Scene,
fonts: &dyn FontProvider,
assets: &dyn AssetProvider,
) -> Result<Vec<u8>, RenderError> {
let backend = TinySkiaBackend;
let image = backend.rasterize(scene, fonts, assets)?;
backend.encode_png(&image)
}
pub fn render_spread_png(
left: &Scene,
right: &Scene,
gutter_px: u32,
fonts: &dyn FontProvider,
assets: &dyn AssetProvider,
) -> Result<Vec<u8>, RenderError> {
let backend = TinySkiaBackend;
let left_img = backend.rasterize(left, fonts, assets)?;
let right_img = backend.rasterize(right, fonts, assets)?;
let spread = composite_spread(&left_img, &right_img, gutter_px)?;
backend.encode_png(&spread)
}
pub fn composite_spread(
left: &RasterImage,
right: &RasterImage,
gutter_px: u32,
) -> Result<RasterImage, RenderError> {
let width = left
.width
.checked_add(gutter_px)
.and_then(|w| w.checked_add(right.width))
.ok_or_else(|| {
RenderError::new(format!(
"spread width overflow: {} + {} + {} exceeds u32",
left.width, gutter_px, right.width
))
})?;
let height = left.height.max(right.height);
let stride = (width as usize)
.checked_mul(4)
.ok_or_else(|| RenderError::new(format!("spread row stride overflow for width {width}")))?;
let total = stride.checked_mul(height as usize).ok_or_else(|| {
RenderError::new(format!("spread buffer size overflow ({width}×{height})"))
})?;
let mut rgba = vec![0u8; total];
blit(&mut rgba, stride, left, 0);
blit(&mut rgba, stride, right, (left.width + gutter_px) as usize);
Ok(RasterImage {
width,
height,
rgba,
})
}
fn blit(dst: &mut [u8], dst_stride: usize, src: &RasterImage, x_offset: usize) {
let src_stride = src.width as usize * 4;
let byte_offset = x_offset * 4;
for row in 0..src.height as usize {
let src_start = row * src_stride;
let src_row = match src.rgba.get(src_start..src_start + src_stride) {
Some(r) => r,
None => break, };
let dst_start = row * dst_stride + byte_offset;
if let Some(dst_row) = dst.get_mut(dst_start..dst_start + src_stride) {
dst_row.copy_from_slice(src_row);
}
}
}
pub fn render_image(
scene: &Scene,
fonts: &dyn FontProvider,
assets: &dyn AssetProvider,
) -> Result<RasterImage, RenderError> {
let backend = TinySkiaBackend;
backend.rasterize(scene, fonts, assets)
}
#[cfg(test)]
mod tests {
use super::*;
fn solid(w: u32, h: u32, rgba: [u8; 4]) -> RasterImage {
let mut buf = Vec::with_capacity((w * h * 4) as usize);
for _ in 0..(w * h) {
buf.extend_from_slice(&rgba);
}
RasterImage {
width: w,
height: h,
rgba: buf,
}
}
#[test]
fn composite_spread_width_is_sum() {
let left = solid(30, 20, [255, 0, 0, 255]);
let right = solid(40, 20, [0, 0, 255, 255]);
let out = composite_spread(&left, &right, 0).expect("composite");
assert_eq!(out.width, 70, "spread width must be wA + wB");
assert_eq!(out.height, 20, "spread height must be max(hA, hB)");
assert_eq!(out.rgba.len(), (70 * 20 * 4) as usize);
}
#[test]
fn composite_spread_height_is_max() {
let left = solid(10, 50, [1, 2, 3, 255]);
let right = solid(10, 30, [4, 5, 6, 255]);
let out = composite_spread(&left, &right, 0).expect("composite");
assert_eq!(out.height, 50, "height must be the taller of the two");
}
#[test]
fn composite_spread_places_pages_side_by_side() {
let left = solid(2, 1, [10, 20, 30, 255]);
let right = solid(3, 1, [40, 50, 60, 255]);
let out = composite_spread(&left, &right, 0).expect("composite");
assert_eq!(&out.rgba[0..4], &[10, 20, 30, 255], "x=0 is left page");
assert_eq!(&out.rgba[4..8], &[10, 20, 30, 255], "x=1 is left page");
assert_eq!(&out.rgba[8..12], &[40, 50, 60, 255], "x=2 is right page");
assert_eq!(&out.rgba[12..16], &[40, 50, 60, 255], "x=3 is right page");
assert_eq!(&out.rgba[16..20], &[40, 50, 60, 255], "x=4 is right page");
}
#[test]
fn composite_spread_short_page_leaves_transparent_gap() {
let left = solid(1, 2, [9, 9, 9, 255]);
let right = solid(1, 1, [8, 8, 8, 255]);
let out = composite_spread(&left, &right, 0).expect("composite");
assert_eq!(out.width, 2);
assert_eq!(out.height, 2);
let stride = 2 * 4;
let row1_right = &out.rgba[stride + 4..stride + 8];
assert_eq!(
row1_right,
&[0, 0, 0, 0],
"gap below short page is transparent"
);
}
#[test]
fn composite_spread_gutter_zero_is_byte_identical_to_no_gutter() {
let left = solid(5, 3, [1, 2, 3, 255]);
let right = solid(7, 3, [4, 5, 6, 255]);
let width = (left.width + right.width) as usize;
let height = left.height.max(right.height) as usize;
let mut expected = vec![0u8; width * height * 4];
let stride = width * 4;
for row in 0..left.height as usize {
for col in 0..left.width as usize {
let dst = row * stride + col * 4;
expected[dst..dst + 4].copy_from_slice(&[1, 2, 3, 255]);
}
}
for row in 0..right.height as usize {
for col in 0..right.width as usize {
let dst = row * stride + (left.width as usize + col) * 4;
expected[dst..dst + 4].copy_from_slice(&[4, 5, 6, 255]);
}
}
let out = composite_spread(&left, &right, 0).expect("composite gutter=0");
assert_eq!(out.width as usize, width, "width with gutter=0");
assert_eq!(
out.rgba, expected,
"gutter=0 output must be byte-identical to no-gutter"
);
}
#[test]
fn composite_spread_gutter_shifts_right_page_and_leaves_transparent_columns() {
let left = solid(10, 1, [11, 22, 33, 255]);
let right = solid(20, 1, [44, 55, 66, 255]);
let gutter: u32 = 40;
let out = composite_spread(&left, &right, gutter).expect("composite gutter=40");
assert_eq!(out.width, 10 + 40 + 20, "width = left + gutter + right");
assert_eq!(
&out.rgba[9 * 4..10 * 4],
&[11, 22, 33, 255],
"x=9 is left page"
);
for x in 10usize..50 {
let pixel = &out.rgba[x * 4..(x + 1) * 4];
assert_eq!(
pixel,
&[0, 0, 0, 0],
"gutter column {x} must be transparent"
);
}
assert_eq!(
&out.rgba[50 * 4..51 * 4],
&[44, 55, 66, 255],
"x=50 is right page"
);
}
}