1use crate::error::RenderError;
26use std::sync::{Arc, OnceLock};
27
28static FONT_DB: OnceLock<Arc<fontdb::Database>> = OnceLock::new();
35
36pub fn init_font_database(extra_fonts: Vec<Vec<u8>>) -> bool {
54 let mut db = fontdb::Database::new();
55 db.load_system_fonts();
56 for font_bytes in extra_fonts {
57 db.load_font_data(font_bytes);
58 }
59 FONT_DB.set(Arc::new(db)).is_ok()
60}
61
62fn get_font_db() -> Arc<fontdb::Database> {
64 FONT_DB
65 .get_or_init(|| {
66 let mut db = fontdb::Database::new();
67 db.load_system_fonts();
68 Arc::new(db)
69 })
70 .clone()
71}
72
73pub fn svg_to_png(
83 svg: &str,
84 width: u32,
85 height: u32,
86 density: u32,
87 padding: u32,
88 background: [u8; 3],
89) -> Result<Vec<u8>, RenderError> {
90 let scale = density as f32 / 72.0;
91
92 let options = usvg::Options {
94 font_family: "Inter, Liberation Sans, Arial, sans-serif".to_string(),
95 font_size: 12.0,
96 dpi: density as f32,
97 fontdb: get_font_db(),
98 ..Default::default()
99 };
100 let tree = usvg::Tree::from_str(svg, &options)
101 .map_err(|e| RenderError::SvgParse(e.to_string()))?;
102
103 let total_width = ((width + 2 * padding) as f32 * scale).ceil() as u32;
105 let total_height = ((height + 2 * padding) as f32 * scale).ceil() as u32;
106
107 let mut pixmap = tiny_skia::Pixmap::new(total_width, total_height)
109 .ok_or_else(|| RenderError::Rasterize("Failed to create pixmap".into()))?;
110
111 let bg = tiny_skia::Color::from_rgba8(background[0], background[1], background[2], 255);
113 pixmap.fill(bg);
114
115 let padding_px = padding as f32 * scale;
117 let chart_width = width as f32 * scale;
118 let chart_height = height as f32 * scale;
119
120 let svg_size = tree.size();
122 let scale_x = chart_width / svg_size.width();
123 let scale_y = chart_height / svg_size.height();
124
125 let transform = tiny_skia::Transform::from_translate(padding_px, padding_px)
126 .post_scale(scale_x, scale_y);
127
128 resvg::render(&tree, transform, &mut pixmap.as_mut());
129
130 let png_data = pixmap.encode_png()
132 .map_err(|e| RenderError::PngEncode(e.to_string()))?;
133
134 Ok(png_data)
135}
136
137#[cfg(test)]
138mod tests {
139 #![allow(clippy::unwrap_used)]
140 use super::*;
141
142 fn simple_svg() -> String {
143 concat!(
144 r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100" width="200" height="100">"#,
145 r#"<rect x="10" y="10" width="180" height="80" fill="steelblue"/>"#,
146 r#"</svg>"#,
147 ).to_string()
148 }
149
150 fn circle_svg() -> String {
151 concat!(
152 r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">"#,
153 r#"<circle cx="50" cy="50" r="40" fill="red"/>"#,
154 r#"</svg>"#,
155 ).to_string()
156 }
157
158 #[test]
159 fn renders_simple_svg_to_png() {
160 let svg = simple_svg();
161 let result = svg_to_png(&svg, 200, 100, 72, 16, [255, 255, 255]);
162 assert!(result.is_ok(), "render failed: {:?}", result.err());
163 let png = result.unwrap();
164 assert_eq!(&png[0..4], &[0x89, 0x50, 0x4E, 0x47]);
166 assert!(png.len() > 100);
167 }
168
169 #[test]
170 fn density_2x_produces_larger_output() {
171 let svg = circle_svg();
172 let png_1x = svg_to_png(&svg, 100, 100, 72, 0, [255, 255, 255]).unwrap();
173 let png_2x = svg_to_png(&svg, 100, 100, 144, 0, [255, 255, 255]).unwrap();
174 assert!(png_2x.len() > png_1x.len());
175 }
176
177 #[test]
178 fn padding_increases_dimensions() {
179 let svg = circle_svg();
180 let no_pad = svg_to_png(&svg, 100, 100, 72, 0, [255, 255, 255]).unwrap();
181 let with_pad = svg_to_png(&svg, 100, 100, 72, 16, [255, 255, 255]).unwrap();
182 assert!(with_pad.len() > no_pad.len());
183 }
184
185 #[test]
186 fn invalid_svg_returns_error() {
187 let result = svg_to_png("not valid svg", 100, 100, 72, 0, [255, 255, 255]);
188 assert!(result.is_err());
189 }
190
191 #[test]
192 fn svg_with_text_renders() {
193 let svg = concat!(
194 r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100" width="200" height="100">"#,
195 r#"<text x="100" y="50" text-anchor="middle" font-family="Liberation Sans, Arial, sans-serif" font-size="16">Hello World</text>"#,
196 r#"</svg>"#,
197 );
198 let result = svg_to_png(svg, 200, 100, 72, 0, [255, 255, 255]);
199 assert!(result.is_ok(), "text render failed: {:?}", result.err());
200 }
201}