Skip to main content

day_vector/
raster.rs

1// Copyright © The Daybrite Project
2// SPDX-License-Identifier: MPL-2.0
3
4//! SVG parsing + PNG rasterization — the render path both `day icon` and vector staging share.
5
6use resvg::tiny_skia;
7use resvg::usvg;
8
9/// Parse SVG bytes into a usvg tree. Text is not compiled in (see the crate docs): a `<text>`
10/// element parses but renders as nothing, so callers that care (day-cli) probe the raw XML for
11/// `<text` and refuse with an "outline your text" message before calling this.
12pub fn parse(data: &[u8]) -> Result<usvg::Tree, String> {
13    usvg::Tree::from_data(data, &usvg::Options::default()).map_err(|e| e.to_string())
14}
15
16/// Render the tree into a `px`×`px` PNG, scaled uniformly to fit and centered. Transparent
17/// background; the caller composites/flattens where a format demands opacity (e.g. the iOS
18/// 1024 icon).
19pub fn render_png(tree: &usvg::Tree, px: u32) -> Result<Vec<u8>, String> {
20    render_png_padded(tree, px, 0.0)
21}
22
23/// Like [`render_png`], with `pad` (a fraction of the edge, e.g. `0.1` = 10 %) of transparent
24/// margin on every side — the macOS icon convention (art inset on the 1024 canvas).
25pub fn render_png_padded(tree: &usvg::Tree, px: u32, pad: f32) -> Result<Vec<u8>, String> {
26    if px == 0 {
27        return Err("zero-size render".into());
28    }
29    let mut pixmap =
30        tiny_skia::Pixmap::new(px, px).ok_or_else(|| "pixmap allocation failed".to_string())?;
31    let size = tree.size();
32    let (w, h) = (size.width(), size.height());
33    if w <= 0.0 || h <= 0.0 {
34        return Err("SVG has a zero-sized viewport".into());
35    }
36    let inner = px as f32 * (1.0 - 2.0 * pad.clamp(0.0, 0.45));
37    let scale = inner / w.max(h);
38    let tx = (px as f32 - w * scale) / 2.0;
39    let ty = (px as f32 - h * scale) / 2.0;
40    let ts = tiny_skia::Transform::from_row(scale, 0.0, 0.0, scale, tx, ty);
41    resvg::render(tree, ts, &mut pixmap.as_mut());
42    pixmap.encode_png().map_err(|e| e.to_string())
43}
44
45/// The tree content's absolute bounding box (strokes included), or `None` for empty art — the
46/// input for safe-zone validation (an Android adaptive foreground overflowing the 66/108 zone).
47pub fn content_bbox(tree: &usvg::Tree) -> Option<tiny_skia::Rect> {
48    let b = tree.root().abs_layer_bounding_box();
49    tiny_skia::Rect::from_xywh(b.x(), b.y(), b.width(), b.height())
50}