1use zenith_core::{AssetProvider, FontProvider};
4use zenith_scene::Scene;
5
6use crate::backend::{RasterBackend, RasterImage};
7use crate::error::RenderError;
8use crate::tiny_skia::TinySkiaBackend;
9
10pub fn render_png(
26 scene: &Scene,
27 fonts: &dyn FontProvider,
28 assets: &dyn AssetProvider,
29) -> Result<Vec<u8>, RenderError> {
30 let backend = TinySkiaBackend;
31 let image = backend.rasterize(scene, fonts, assets)?;
32 backend.encode_png(&image)
33}
34
35pub fn render_spread_png(
49 left: &Scene,
50 right: &Scene,
51 gutter_px: u32,
52 fonts: &dyn FontProvider,
53 assets: &dyn AssetProvider,
54) -> Result<Vec<u8>, RenderError> {
55 let backend = TinySkiaBackend;
56 let left_img = backend.rasterize(left, fonts, assets)?;
57 let right_img = backend.rasterize(right, fonts, assets)?;
58 let spread = composite_spread(&left_img, &right_img, gutter_px)?;
59 backend.encode_png(&spread)
60}
61
62pub fn composite_spread(
80 left: &RasterImage,
81 right: &RasterImage,
82 gutter_px: u32,
83) -> Result<RasterImage, RenderError> {
84 let width = left
85 .width
86 .checked_add(gutter_px)
87 .and_then(|w| w.checked_add(right.width))
88 .ok_or_else(|| {
89 RenderError::new(format!(
90 "spread width overflow: {} + {} + {} exceeds u32",
91 left.width, gutter_px, right.width
92 ))
93 })?;
94 let height = left.height.max(right.height);
95
96 let stride = (width as usize)
97 .checked_mul(4)
98 .ok_or_else(|| RenderError::new(format!("spread row stride overflow for width {width}")))?;
99 let total = stride.checked_mul(height as usize).ok_or_else(|| {
100 RenderError::new(format!("spread buffer size overflow ({width}×{height})"))
101 })?;
102
103 let mut rgba = vec![0u8; total];
106
107 blit(&mut rgba, stride, left, 0);
108 blit(&mut rgba, stride, right, (left.width + gutter_px) as usize);
109
110 Ok(RasterImage {
111 width,
112 height,
113 rgba,
114 })
115}
116
117fn blit(dst: &mut [u8], dst_stride: usize, src: &RasterImage, x_offset: usize) {
122 let src_stride = src.width as usize * 4;
123 let byte_offset = x_offset * 4;
124 for row in 0..src.height as usize {
125 let src_start = row * src_stride;
126 let src_row = match src.rgba.get(src_start..src_start + src_stride) {
127 Some(r) => r,
128 None => break, };
130 let dst_start = row * dst_stride + byte_offset;
131 if let Some(dst_row) = dst.get_mut(dst_start..dst_start + src_stride) {
132 dst_row.copy_from_slice(src_row);
133 }
134 }
135}
136
137pub fn render_image(
151 scene: &Scene,
152 fonts: &dyn FontProvider,
153 assets: &dyn AssetProvider,
154) -> Result<RasterImage, RenderError> {
155 let backend = TinySkiaBackend;
156 backend.rasterize(scene, fonts, assets)
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 fn solid(w: u32, h: u32, rgba: [u8; 4]) -> RasterImage {
165 let mut buf = Vec::with_capacity((w * h * 4) as usize);
166 for _ in 0..(w * h) {
167 buf.extend_from_slice(&rgba);
168 }
169 RasterImage {
170 width: w,
171 height: h,
172 rgba: buf,
173 }
174 }
175
176 #[test]
177 fn composite_spread_width_is_sum() {
178 let left = solid(30, 20, [255, 0, 0, 255]);
179 let right = solid(40, 20, [0, 0, 255, 255]);
180 let out = composite_spread(&left, &right, 0).expect("composite");
181 assert_eq!(out.width, 70, "spread width must be wA + wB");
182 assert_eq!(out.height, 20, "spread height must be max(hA, hB)");
183 assert_eq!(out.rgba.len(), (70 * 20 * 4) as usize);
184 }
185
186 #[test]
187 fn composite_spread_height_is_max() {
188 let left = solid(10, 50, [1, 2, 3, 255]);
189 let right = solid(10, 30, [4, 5, 6, 255]);
190 let out = composite_spread(&left, &right, 0).expect("composite");
191 assert_eq!(out.height, 50, "height must be the taller of the two");
192 }
193
194 #[test]
195 fn composite_spread_places_pages_side_by_side() {
196 let left = solid(2, 1, [10, 20, 30, 255]);
197 let right = solid(3, 1, [40, 50, 60, 255]);
198 let out = composite_spread(&left, &right, 0).expect("composite");
199 assert_eq!(&out.rgba[0..4], &[10, 20, 30, 255], "x=0 is left page");
201 assert_eq!(&out.rgba[4..8], &[10, 20, 30, 255], "x=1 is left page");
202 assert_eq!(&out.rgba[8..12], &[40, 50, 60, 255], "x=2 is right page");
203 assert_eq!(&out.rgba[12..16], &[40, 50, 60, 255], "x=3 is right page");
204 assert_eq!(&out.rgba[16..20], &[40, 50, 60, 255], "x=4 is right page");
205 }
206
207 #[test]
208 fn composite_spread_short_page_leaves_transparent_gap() {
209 let left = solid(1, 2, [9, 9, 9, 255]);
211 let right = solid(1, 1, [8, 8, 8, 255]);
212 let out = composite_spread(&left, &right, 0).expect("composite");
213 assert_eq!(out.width, 2);
214 assert_eq!(out.height, 2);
215 let stride = 2 * 4;
217 let row1_right = &out.rgba[stride + 4..stride + 8];
218 assert_eq!(
219 row1_right,
220 &[0, 0, 0, 0],
221 "gap below short page is transparent"
222 );
223 }
224
225 #[test]
228 fn composite_spread_gutter_zero_is_byte_identical_to_no_gutter() {
229 let left = solid(5, 3, [1, 2, 3, 255]);
230 let right = solid(7, 3, [4, 5, 6, 255]);
231 let width = (left.width + right.width) as usize;
233 let height = left.height.max(right.height) as usize;
234 let mut expected = vec![0u8; width * height * 4];
235 let stride = width * 4;
236 for row in 0..left.height as usize {
237 for col in 0..left.width as usize {
238 let dst = row * stride + col * 4;
239 expected[dst..dst + 4].copy_from_slice(&[1, 2, 3, 255]);
240 }
241 }
242 for row in 0..right.height as usize {
243 for col in 0..right.width as usize {
244 let dst = row * stride + (left.width as usize + col) * 4;
245 expected[dst..dst + 4].copy_from_slice(&[4, 5, 6, 255]);
246 }
247 }
248 let out = composite_spread(&left, &right, 0).expect("composite gutter=0");
249 assert_eq!(out.width as usize, width, "width with gutter=0");
250 assert_eq!(
251 out.rgba, expected,
252 "gutter=0 output must be byte-identical to no-gutter"
253 );
254 }
255
256 #[test]
259 fn composite_spread_gutter_shifts_right_page_and_leaves_transparent_columns() {
260 let left = solid(10, 1, [11, 22, 33, 255]);
261 let right = solid(20, 1, [44, 55, 66, 255]);
262 let gutter: u32 = 40;
263 let out = composite_spread(&left, &right, gutter).expect("composite gutter=40");
264 assert_eq!(out.width, 10 + 40 + 20, "width = left + gutter + right");
265 assert_eq!(
267 &out.rgba[9 * 4..10 * 4],
268 &[11, 22, 33, 255],
269 "x=9 is left page"
270 );
271 for x in 10usize..50 {
273 let pixel = &out.rgba[x * 4..(x + 1) * 4];
274 assert_eq!(
275 pixel,
276 &[0, 0, 0, 0],
277 "gutter column {x} must be transparent"
278 );
279 }
280 assert_eq!(
282 &out.rgba[50 * 4..51 * 4],
283 &[44, 55, 66, 255],
284 "x=50 is right page"
285 );
286 }
287}