1mod primitive;
27
28use rusty_mermaid_core::{Color, Renderer, Scene, Theme};
29
30#[derive(Debug, Clone)]
32pub struct RasterConfig {
33 pub scale: f64,
35 pub theme: Theme,
37}
38
39impl Default for RasterConfig {
40 fn default() -> Self {
41 Self {
42 scale: 2.0,
43 theme: Theme::default(),
44 }
45 }
46}
47
48pub struct RasterRenderer {
50 pub config: RasterConfig,
51}
52
53impl RasterRenderer {
54 pub fn new() -> Self {
55 Self {
56 config: RasterConfig::default(),
57 }
58 }
59
60 pub fn with_config(config: RasterConfig) -> Self {
61 Self { config }
62 }
63}
64
65impl Default for RasterRenderer {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl Renderer for RasterRenderer {
72 type Output = Vec<u8>;
73
74 fn render(&self, scene: &Scene) -> Vec<u8> {
75 let theme = &self.config.theme;
76 let padding = theme.padding;
77 let scale = self.config.scale;
78 let w = ((scene.width + padding * 2.0) * scale).ceil() as u32;
79 let h = ((scene.height + padding * 2.0) * scale).ceil() as u32;
80
81 let mut pixmap = tiny_skia::Pixmap::new(w, h).expect("pixmap dimensions must be > 0");
82
83 let bg = to_skia_color(theme.background);
85 pixmap.fill(bg);
86
87 let offset = tiny_skia::Transform::from_scale(scale as f32, scale as f32)
89 .post_translate(padding as f32 * scale as f32, padding as f32 * scale as f32);
90
91 for elem in scene.elements() {
92 primitive::render_primitive(&mut pixmap, &elem.primitive, offset, theme);
93 }
94
95 encode_png(&pixmap)
96 }
97}
98
99fn to_skia_color(c: Color) -> tiny_skia::Color {
100 tiny_skia::Color::from_rgba8(c.r, c.g, c.b, c.a)
101}
102
103fn encode_png(pixmap: &tiny_skia::Pixmap) -> Vec<u8> {
104 let mut buf = Vec::new();
105 let mut encoder = png::Encoder::new(&mut buf, pixmap.width(), pixmap.height());
106 encoder.set_color(png::ColorType::Rgba);
107 encoder.set_depth(png::BitDepth::Eight);
108 let mut writer = encoder.write_header().expect("PNG header write failed");
109 writer
110 .write_image_data(pixmap.data())
111 .expect("PNG data write failed");
112 writer.finish().expect("PNG finish failed");
113 buf
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use rusty_mermaid_core::{BBox, Point, Primitive, Style};
120
121 #[test]
122 fn render_empty_scene() {
123 let renderer = RasterRenderer::new();
124 let scene = Scene::new(100.0, 50.0);
125 let png = renderer.render(&scene);
126 assert_eq!(&png[..4], &[0x89, b'P', b'N', b'G']);
128 }
129
130 #[test]
131 fn render_rect() {
132 let renderer = RasterRenderer::new();
133 let mut scene = Scene::new(200.0, 100.0);
134 scene.push(Primitive::Rect {
135 bbox: BBox::new(100.0, 50.0, 80.0, 40.0),
136 rx: 5.0,
137 ry: 5.0,
138 style: Style {
139 fill: Some(Color::rgb(236, 236, 255)),
140 stroke: Some(Color::rgb(147, 112, 219)),
141 stroke_width: Some(1.5),
142 ..Default::default()
143 },
144 });
145 let png = renderer.render(&scene);
146 assert_eq!(&png[..4], &[0x89, b'P', b'N', b'G']);
147 assert!(png.len() > 100); }
149
150 #[test]
151 fn render_circle() {
152 let renderer = RasterRenderer::new();
153 let mut scene = Scene::new(100.0, 100.0);
154 scene.push(Primitive::Circle {
155 center: Point::new(50.0, 50.0),
156 radius: 20.0,
157 style: Style {
158 fill: Some(Color::rgb(51, 51, 51)),
159 stroke: Some(Color::rgb(51, 51, 51)),
160 ..Default::default()
161 },
162 });
163 let png = renderer.render(&scene);
164 assert_eq!(&png[..4], &[0x89, b'P', b'N', b'G']);
165 }
166
167 #[test]
168 fn render_path() {
169 use rusty_mermaid_core::PathSegment;
170 let renderer = RasterRenderer::new();
171 let mut scene = Scene::new(200.0, 200.0);
172 scene.push(Primitive::Path {
173 segments: vec![
174 PathSegment::MoveTo(Point::new(10.0, 10.0)),
175 PathSegment::LineTo(Point::new(190.0, 190.0)),
176 ],
177 style: Style {
178 stroke: Some(Color::rgb(51, 51, 51)),
179 stroke_width: Some(1.5),
180 ..Default::default()
181 },
182 marker_start: None,
183 marker_end: None,
184 });
185 let png = renderer.render(&scene);
186 assert_eq!(&png[..4], &[0x89, b'P', b'N', b'G']);
187 }
188
189 #[test]
190 fn scale_affects_pixel_dimensions() {
191 let renderer_1x = RasterRenderer::with_config(RasterConfig {
192 scale: 1.0,
193 ..Default::default()
194 });
195 let renderer_2x = RasterRenderer::with_config(RasterConfig {
196 scale: 2.0,
197 ..Default::default()
198 });
199 let scene = Scene::new(100.0, 50.0);
200 let png_1x = renderer_1x.render(&scene);
201 let png_2x = renderer_2x.render(&scene);
202 assert!(png_2x.len() > png_1x.len());
204 }
205
206 #[test]
207 fn render_polygon() {
208 let renderer = RasterRenderer::new();
209 let mut scene = Scene::new(100.0, 100.0);
210 scene.push(Primitive::Polygon {
211 points: vec![
212 Point::new(50.0, 10.0),
213 Point::new(90.0, 90.0),
214 Point::new(10.0, 90.0),
215 ],
216 style: Style {
217 fill: Some(Color::rgb(200, 200, 255)),
218 stroke: Some(Color::rgb(100, 100, 200)),
219 stroke_width: Some(2.0),
220 ..Default::default()
221 },
222 });
223 let png = renderer.render(&scene);
224 assert_eq!(&png[..4], &[0x89, b'P', b'N', b'G']);
225 }
226
227 #[test]
228 fn render_text() {
229 use rusty_mermaid_core::{TextAnchor, TextStyle};
230 let renderer = RasterRenderer::new();
231 let mut scene = Scene::new(200.0, 50.0);
232 scene.push(Primitive::Text {
233 position: Point::new(100.0, 25.0),
234 content: String::from("Hello"),
235 anchor: TextAnchor::Middle,
236 style: TextStyle {
237 font_size: 14.0,
238 fill: Some(Color::rgb(51, 51, 51)),
239 ..Default::default()
240 },
241 });
242 let png = renderer.render(&scene);
243 assert_eq!(&png[..4], &[0x89, b'P', b'N', b'G']);
244 let empty_png = RasterRenderer::new().render(&Scene::new(200.0, 50.0));
246 assert_ne!(png, empty_png);
247 }
248
249 #[test]
250 fn render_text_multiline() {
251 use rusty_mermaid_core::{TextAnchor, TextStyle};
252 let renderer = RasterRenderer::new();
253 let mut scene = Scene::new(200.0, 100.0);
254 scene.push(Primitive::Text {
255 position: Point::new(100.0, 50.0),
256 content: String::from("Line 1\nLine 2"),
257 anchor: TextAnchor::Middle,
258 style: TextStyle::default(),
259 });
260 let png = renderer.render(&scene);
261 assert_eq!(&png[..4], &[0x89, b'P', b'N', b'G']);
262 }
263
264 #[test]
265 fn render_full_diagram_to_file() {
266 use rusty_mermaid_core::Renderer;
267 let scene = rusty_mermaid_diagrams::render_to_scene(
268 "stateDiagram-v2\n [*] --> Active\n Active --> Paused : pause\n Paused --> Active : resume\n Active --> [*] : done",
269 &rusty_mermaid_core::Theme::default(),
270 ).unwrap();
271 let renderer = RasterRenderer::new();
272 let png = renderer.render(&scene);
273 let path = std::env::temp_dir().join("rusty_mermaid_state.png");
274 std::fs::write(&path, &png).unwrap();
275 eprintln!("wrote PNG to {}", path.display());
276 assert!(png.len() > 1000);
277 }
278
279 #[test]
280 fn render_flowchart_to_file() {
281 use rusty_mermaid_core::Renderer;
282 let scene = rusty_mermaid_diagrams::render_to_scene(
283 "flowchart TD\n A[Start] --> B{Decision}\n B -->|Yes| C[OK]\n B -->|No| D[Fail]",
284 &rusty_mermaid_core::Theme::default(),
285 ).unwrap();
286 let renderer = RasterRenderer::new();
287 let png = renderer.render(&scene);
288 let path = std::env::temp_dir().join("rusty_mermaid_flowchart.png");
289 std::fs::write(&path, &png).unwrap();
290 eprintln!("wrote PNG to {}", path.display());
291 assert!(png.len() > 1000);
292 }
293}