1pub mod error;
39#[cfg(feature = "rasterize")]
40pub mod rasterize;
41pub mod svg;
42
43pub use error::RenderError;
44#[cfg(feature = "rasterize")]
45pub use rasterize::{init_font_database, svg_to_png};
46pub use svg::element_to_svg;
47
48#[cfg(feature = "rasterize")]
49use chartml_core::ChartML;
50
51#[cfg(feature = "rasterize")]
53const DEFAULT_PADDING: u32 = 16;
54
55#[cfg(feature = "rasterize")]
69fn strip_dashoffset_for_static(svg: &str) -> String {
70 const ATTR: &str = " stroke-dashoffset=\"";
71 if !svg.contains(ATTR) {
72 return svg.to_owned();
73 }
74 let mut out = String::with_capacity(svg.len());
75 let mut rest = svg;
76 while let Some(pos) = rest.find(ATTR) {
77 out.push_str(&rest[..pos]);
78 let after = &rest[pos + ATTR.len()..];
79 match after.find('"') {
80 Some(end) => rest = &after[end + 1..],
81 None => {
82 rest = after;
83 break;
84 }
85 }
86 }
87 out.push_str(rest);
88 out
89}
90
91#[cfg(feature = "rasterize")]
93const WHITE: [u8; 3] = [255, 255, 255];
94
95#[cfg(feature = "rasterize")]
107pub fn render_to_png(
108 chartml: &ChartML,
109 yaml: &str,
110 width: u32,
111 height: u32,
112 density: u32,
113) -> Result<Vec<u8>, RenderError> {
114 render_to_png_with_background(chartml, yaml, width, height, density, WHITE)
115}
116
117#[cfg(feature = "rasterize")]
133pub fn render_to_png_with_background(
134 chartml: &ChartML,
135 yaml: &str,
136 width: u32,
137 height: u32,
138 density: u32,
139 background: [u8; 3],
140) -> Result<Vec<u8>, RenderError> {
141 let element = chartml.render_from_yaml_with_size(
142 yaml,
143 Some(width as f64),
144 Some(height as f64),
145 )?;
146
147 let svg_str = element_to_svg(&element, width as f64, height as f64);
148 let svg_str = strip_dashoffset_for_static(&svg_str);
149 svg_to_png(&svg_str, width, height, density, DEFAULT_PADDING, background)
150}
151
152#[cfg(feature = "rasterize")]
164pub async fn render_to_png_async(
165 chartml: &ChartML,
166 yaml: &str,
167 width: u32,
168 height: u32,
169 density: u32,
170) -> Result<Vec<u8>, RenderError> {
171 render_to_png_with_background_async(chartml, yaml, width, height, density, WHITE).await
172}
173
174#[cfg(feature = "rasterize")]
188pub async fn render_to_png_with_background_async(
189 chartml: &ChartML,
190 yaml: &str,
191 width: u32,
192 height: u32,
193 density: u32,
194 background: [u8; 3],
195) -> Result<Vec<u8>, RenderError> {
196 let element = chartml.render_from_yaml_with_params_async(
197 yaml,
198 Some(width as f64),
199 Some(height as f64),
200 None,
201 ).await?;
202
203 let svg_str = element_to_svg(&element, width as f64, height as f64);
204 let svg_str = strip_dashoffset_for_static(&svg_str);
205 svg_to_png(&svg_str, width, height, density, DEFAULT_PADDING, background)
206}
207
208#[cfg(feature = "rasterize")]
212pub fn element_to_png(
213 element: &chartml_core::ChartElement,
214 width: u32,
215 height: u32,
216 density: u32,
217) -> Result<Vec<u8>, RenderError> {
218 element_to_png_with_background(element, width, height, density, WHITE)
219}
220
221#[cfg(feature = "rasterize")]
224pub fn element_to_png_with_background(
225 element: &chartml_core::ChartElement,
226 width: u32,
227 height: u32,
228 density: u32,
229 background: [u8; 3],
230) -> Result<Vec<u8>, RenderError> {
231 let svg_str = element_to_svg(element, width as f64, height as f64);
232 let svg_str = strip_dashoffset_for_static(&svg_str);
233 svg_to_png(&svg_str, width, height, density, DEFAULT_PADDING, background)
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn strip_dashoffset_removes_animation_attrs() {
242 let svg = r##"<path d="M0,0L100,50" stroke="#D97706" stroke-width="2" stroke-dasharray="112" stroke-dashoffset="112" class="series-line"/>"##;
243 let result = strip_dashoffset_for_static(svg);
244 assert!(!result.contains("stroke-dashoffset"));
245 assert!(result.contains(r#"stroke-dasharray="112""#));
246 assert!(result.contains(r##"stroke="#D97706""##));
247 }
248
249 #[test]
250 fn strip_dashoffset_preserves_dashed_lines() {
251 let svg = r#"<path d="M0,0L100,50" stroke-dasharray="8 4" class="dashed"/>"#;
252 let result = strip_dashoffset_for_static(svg);
253 assert_eq!(result, svg);
254 }
255
256 #[test]
257 fn strip_dashoffset_handles_multiple_paths() {
258 let svg = r#"<path stroke-dashoffset="200"/><path stroke-dashoffset="300"/>"#;
259 let result = strip_dashoffset_for_static(svg);
260 assert!(!result.contains("stroke-dashoffset"));
261 assert_eq!(result, r#"<path/><path/>"#);
262 }
263
264 #[test]
265 fn strip_dashoffset_no_op_without_attr() {
266 let svg = r#"<circle cx="50" cy="50" r="4" fill="red"/>"#;
267 let result = strip_dashoffset_for_static(svg);
268 assert_eq!(result, svg);
269 }
270
271 #[cfg(feature = "rasterize")]
272 #[test]
273 fn element_to_png_with_background_fills_canvas() {
274 let element = chartml_core::ChartElement::Svg {
275 viewbox: chartml_core::element::ViewBox {
276 x: 0.0,
277 y: 0.0,
278 width: 100.0,
279 height: 50.0,
280 },
281 width: Some(100.0),
282 height: Some(50.0),
283 class: String::new(),
284 children: vec![],
285 };
286
287 let bg = [36u8, 32, 30]; let png_bytes =
289 element_to_png_with_background(&element, 100, 50, 72, bg).expect("render succeeds");
290
291 let decoder = png::Decoder::new(&png_bytes[..]);
292 let mut reader = decoder.read_info().expect("valid PNG");
293 let mut buf = vec![0u8; reader.output_buffer_size()];
294 let info = reader.next_frame(&mut buf).expect("decodable frame");
295
296 assert_eq!(info.width, 100 + 2 * DEFAULT_PADDING);
298 assert_eq!(info.height, 50 + 2 * DEFAULT_PADDING);
299
300 assert_eq!(&buf[0..3], &bg);
303 let last = buf.len() - 4;
304 assert_eq!(&buf[last..last + 3], &bg);
305 }
306
307 #[cfg(feature = "rasterize")]
308 #[test]
309 fn element_to_png_defaults_to_white_background() {
310 let element = chartml_core::ChartElement::Svg {
311 viewbox: chartml_core::element::ViewBox {
312 x: 0.0,
313 y: 0.0,
314 width: 100.0,
315 height: 50.0,
316 },
317 width: Some(100.0),
318 height: Some(50.0),
319 class: String::new(),
320 children: vec![],
321 };
322
323 let png_bytes = element_to_png(&element, 100, 50, 72).expect("render succeeds");
324
325 let decoder = png::Decoder::new(&png_bytes[..]);
326 let mut reader = decoder.read_info().expect("valid PNG");
327 let mut buf = vec![0u8; reader.output_buffer_size()];
328 reader.next_frame(&mut buf).expect("decodable frame");
329
330 assert_eq!(&buf[0..3], &WHITE);
331 }
332}