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 let element = chartml.render_from_yaml_with_size(
115 yaml,
116 Some(width as f64),
117 Some(height as f64),
118 )?;
119
120 let svg_str = element_to_svg(&element, width as f64, height as f64);
121 let svg_str = strip_dashoffset_for_static(&svg_str);
122 svg_to_png(&svg_str, width, height, density, DEFAULT_PADDING, WHITE)
123}
124
125#[cfg(feature = "rasterize")]
137pub async fn render_to_png_async(
138 chartml: &ChartML,
139 yaml: &str,
140 width: u32,
141 height: u32,
142 density: u32,
143) -> Result<Vec<u8>, RenderError> {
144 let element = chartml.render_from_yaml_with_params_async(
145 yaml,
146 Some(width as f64),
147 Some(height as f64),
148 None,
149 ).await?;
150
151 let svg_str = element_to_svg(&element, width as f64, height as f64);
152 let svg_str = strip_dashoffset_for_static(&svg_str);
153 svg_to_png(&svg_str, width, height, density, DEFAULT_PADDING, WHITE)
154}
155
156#[cfg(feature = "rasterize")]
160pub fn element_to_png(
161 element: &chartml_core::ChartElement,
162 width: u32,
163 height: u32,
164 density: u32,
165) -> Result<Vec<u8>, RenderError> {
166 let svg_str = element_to_svg(element, width as f64, height as f64);
167 let svg_str = strip_dashoffset_for_static(&svg_str);
168 svg_to_png(&svg_str, width, height, density, DEFAULT_PADDING, WHITE)
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn strip_dashoffset_removes_animation_attrs() {
177 let svg = r##"<path d="M0,0L100,50" stroke="#D97706" stroke-width="2" stroke-dasharray="112" stroke-dashoffset="112" class="series-line"/>"##;
178 let result = strip_dashoffset_for_static(svg);
179 assert!(!result.contains("stroke-dashoffset"));
180 assert!(result.contains(r#"stroke-dasharray="112""#));
181 assert!(result.contains(r##"stroke="#D97706""##));
182 }
183
184 #[test]
185 fn strip_dashoffset_preserves_dashed_lines() {
186 let svg = r#"<path d="M0,0L100,50" stroke-dasharray="8 4" class="dashed"/>"#;
187 let result = strip_dashoffset_for_static(svg);
188 assert_eq!(result, svg);
189 }
190
191 #[test]
192 fn strip_dashoffset_handles_multiple_paths() {
193 let svg = r#"<path stroke-dashoffset="200"/><path stroke-dashoffset="300"/>"#;
194 let result = strip_dashoffset_for_static(svg);
195 assert!(!result.contains("stroke-dashoffset"));
196 assert_eq!(result, r#"<path/><path/>"#);
197 }
198
199 #[test]
200 fn strip_dashoffset_no_op_without_attr() {
201 let svg = r#"<circle cx="50" cy="50" r="4" fill="red"/>"#;
202 let result = strip_dashoffset_for_static(svg);
203 assert_eq!(result, svg);
204 }
205}