1use ::image::ImageFormat;
2use dessin::{
3 export::{Export, Exporter},
4 font::FontRef,
5 prelude::*,
6};
7use nalgebra::{Scale2, Transform2};
8use std::{
9 collections::HashSet,
10 fmt::{self, Write},
11 io::Cursor,
12 sync::{atomic::AtomicU32, LazyLock},
13};
14
15#[derive(Debug)]
16pub enum SVGError {
17 WriteError(fmt::Error),
18 CurveHasNoStartingPoint(CurvePosition),
19}
20impl fmt::Display for SVGError {
21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 write!(f, "{self:?}")
23 }
24}
25impl From<fmt::Error> for SVGError {
26 fn from(value: fmt::Error) -> Self {
27 SVGError::WriteError(value)
28 }
29}
30impl std::error::Error for SVGError {}
31
32#[derive(Default, Clone, Copy, PartialEq)]
33pub enum ViewPort {
34 ManualCentered { width: f32, height: f32 },
36 ManualViewport {
38 x: f32,
39 y: f32,
40 width: f32,
41 height: f32,
42 },
43 AutoCentered,
45 #[default]
46 AutoBoundingBox,
48}
49
50#[derive(Default, Clone)]
51pub struct SVGOptions {
52 pub viewport: ViewPort,
53 pub skip_svg_tag: bool,
54}
55
56pub struct SVGExporter {
57 acc: String,
58 used_font: HashSet<(FontRef, FontWeight)>,
59}
60
61impl SVGExporter {
62 pub fn new() -> Self {
63 let acc = String::new();
64 let used_font: HashSet<(FontRef, FontWeight)> = HashSet::default();
65
66 SVGExporter { acc, used_font }
67 }
68
69 fn write_style(&mut self, style: StylePosition) -> Result<(), SVGError> {
70 match style.fill {
71 Some(Fill::Solid { color }) => write!(
72 self.acc,
73 "fill='rgb({} {} {} / {:.3})' ",
74 (color.red * 255.) as u32,
75 (color.green * 255.) as u32,
76 (color.blue * 255.) as u32,
77 color.alpha
78 )?, None => write!(self.acc, "fill='none' ")?,
81 }
82
83 match style.stroke {
84 Some(Stroke::Dashed {
85 color,
86 width,
87 on,
88 off,
89 }) => write!(
90 self.acc,
91 "stroke='rgb({} {} {} / {:.3})' stroke-width='{width}' stroke-dasharray='{on},{off}' ",
92 (color.red * 255.) as u32,
93 (color.green * 255.) as u32,
94 (color.blue * 255.) as u32,
95 color.alpha
96 )?,
97 Some(Stroke::Solid { color, width }) => write!(
98 self.acc,
99 "stroke='rgb({} {} {} / {:.3})' stroke-width='{width}' ",
100 (color.red * 255.) as u32,
101 (color.green * 255.) as u32,
102 (color.blue * 255.) as u32,
103 color.alpha
104 )?,
105
106 None => {}
107 }
108
109 Ok(())
110 }
111
112 #[allow(unused)]
113 fn write_curve(&mut self, curve: CurvePosition) -> Result<(), SVGError> {
114 let mut has_start = false;
115
116 for keypoint in &curve.keypoints {
117 match keypoint {
118 KeypointPosition::Point(p) => {
119 if has_start {
120 write!(self.acc, "L ")?;
121 } else {
122 write!(self.acc, "M ")?;
123 has_start = true;
124 }
125 write!(self.acc, "{} {} ", p.x, p.y)?;
126 }
127 KeypointPosition::Bezier(b) => {
128 if has_start {
129 if let Some(v) = b.start {
130 write!(self.acc, "L {} {} ", v.x, v.y)?;
131 }
132 } else {
133 if let Some(v) = b.start {
134 write!(self.acc, "M {} {} ", v.x, v.y)?;
135 has_start = true;
136 } else {
137 return Err(SVGError::CurveHasNoStartingPoint(curve));
138 }
139 }
140
141 write!(
142 self.acc,
143 "C {start_ctrl_x} {start_ctrl_y} {end_ctrl_x} {end_ctrl_y} {end_x} {end_y} ",
144 start_ctrl_x = b.start_control.x,
145 start_ctrl_y = b.start_control.y,
146 end_ctrl_x = b.end_control.x,
147 end_ctrl_y = b.end_control.y,
148 end_x = b.end.x,
149 end_y = b.end.y,
150 )?;
151 }
152 }
153
154 has_start = true;
155 }
156
157 if curve.closed {
158 write!(self.acc, "Z",)?;
159 }
160
161 Ok(())
162 }
163
164 pub fn finish(
165 self,
166 svg_start_tag: impl fmt::Display,
167 svg_end_tag: impl fmt::Display,
168 ) -> String {
169 let return_fonts = self
170 .used_font
171 .into_iter()
172 .map(move |(font_ref, font_weight)| {
173 let font_group = font::get(&font_ref);
174 let (mime, bytes) = match font_group.get(font_weight) {
175 dessin::font::Font::OTF(bytes) => ("font/otf", bytes),
176 dessin::font::Font::TTF(bytes) => ("font/ttf", bytes),
177 };
178 let font_name = &*font_ref;
179
180 let styles = match font_weight {
181 FontWeight::Regular => "font-weight:normal;font-style:normal;",
182 FontWeight::Bold => "font-weight:bold;font-style:normal;",
183 FontWeight::Italic => "font-weight:normal;font-style:italic;",
184 FontWeight::BoldItalic => "font-weight:bold;font-style:italic;",
185 };
186
187 let encoded_font_bytes = data_encoding::BASE64.encode(&bytes);
189 format!(
190 r#"@font-face{{font-family:{font_name};src:url("data:{mime};base64,{encoded_font_bytes}");{styles}}}"#
191 )
192 })
193 .collect::<String>();
194
195 let fonts = (!return_fonts.is_empty())
196 .then(|| format!("<defs><style>{return_fonts}</style></defs>"))
197 .unwrap_or_default();
198 let content = self.acc;
199 format!("{svg_start_tag}{fonts}{content}{svg_end_tag}")
200 }
201}
202
203impl Exporter for SVGExporter {
204 type Error = SVGError;
205
206 const CAN_EXPORT_ELLIPSE: bool = true;
207
208 fn start_style(&mut self, style: StylePosition) -> Result<(), Self::Error> {
209 write!(self.acc, "<g ")?;
210 self.write_style(style)?;
211 write!(self.acc, ">")?;
212
213 Ok(())
214 }
215
216 fn end_style(&mut self) -> Result<(), Self::Error> {
217 write!(self.acc, "</g>")?;
218 Ok(())
219 }
220
221 fn start_block(&mut self, _metadata: &[(String, String)]) -> Result<(), Self::Error> {
222 if !_metadata.is_empty() {
223 write!(self.acc, "<g ")?;
224 for (key, value) in _metadata {
225 write!(self.acc, r#"{key}={value} "#)?;
226 }
227 write!(self.acc, ">")?;
228 }
229
230 Ok(())
231 }
232
233 fn end_block(&mut self, _metadata: &[(String, String)]) -> Result<(), Self::Error> {
234 if !_metadata.is_empty() {
235 write!(self.acc, "</g>")?;
236 }
237 Ok(())
238 }
239
240 fn export_image(
241 &mut self,
242 ImagePosition {
243 top_left: _,
244 top_right: _,
245 bottom_right: _,
246 bottom_left: _,
247 center,
248 width,
249 height,
250 rotation,
251 image,
252 }: ImagePosition,
253 ) -> Result<(), Self::Error> {
254 let mut raw_image = Cursor::new(vec![]);
255 image.write_to(&mut raw_image, ImageFormat::Png).unwrap();
256
257 let data = data_encoding::BASE64.encode(&raw_image.into_inner());
258
259 write!(
260 self.acc,
261 r#"<image width="{width}" height="{height}" x="{x}" y="{y}" "#,
262 x = center.x - width / 2.,
263 y = center.y - height / 2.,
264 )?;
265
266 if rotation.abs() > 10e-6 {
267 write!(
268 self.acc,
269 r#" transform="rotate({rot})" "#,
270 rot = (-rotation.to_degrees() + 360.) % 360.
271 )?;
272 }
273
274 write!(self.acc, r#"href="data:image/png;base64,{data}"/>"#,)?;
275
276 Ok(())
277 }
278
279 fn export_ellipse(
280 &mut self,
281 EllipsePosition {
282 center,
283 semi_major_axis,
284 semi_minor_axis,
285 rotation,
286 }: EllipsePosition,
287 _: StylePosition,
288 ) -> Result<(), Self::Error> {
289 write!(
290 self.acc,
291 r#"<ellipse rx="{semi_major_axis}" ry="{semi_minor_axis}" transform=""#,
292 )?;
293
294 write!(
295 self.acc,
296 r#"translate({cx} {cy}) "#,
297 cx = center.x,
298 cy = center.y
299 )?;
300
301 if rotation.abs() > 10e-6 {
302 write!(self.acc, r#"rotate({rot}) "#, rot = -rotation.to_degrees())?;
303 }
304
305 write!(self.acc, r#""/>"#)?;
306
307 Ok(())
308 }
309
310 fn export_curve(&mut self, curve: CurvePosition, _: StylePosition) -> Result<(), Self::Error> {
311 write!(self.acc, r#"<path d=""#)?;
312 self.write_curve(curve)?;
313 write!(self.acc, r#""/>"#)?;
314
315 Ok(())
316 }
317
318 fn export_text(
319 &mut self,
320 TextPosition {
321 text,
322 align,
323 font_weight,
324 on_curve,
325 font_size,
326 reference_start,
327 direction,
328 font,
329 }: TextPosition,
330 _: StylePosition,
331 ) -> Result<(), Self::Error> {
332 static ID: LazyLock<AtomicU32> = LazyLock::new(|| AtomicU32::new(0));
333 let id = ID.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
334
335 let weight = match font_weight {
336 FontWeight::Bold | FontWeight::BoldItalic => "bold",
337 _ => "normal",
338 };
339 let text_style = match font_weight {
340 FontWeight::Italic | FontWeight::BoldItalic => "italic",
341 _ => "normal",
342 };
343 let align = match align {
344 TextAlign::Center => "middle",
345 TextAlign::Left => "start",
346 TextAlign::Right => "end",
347 };
348
349 let text = text.replace("<", "<").replace(">", ">");
350 let font = font.clone().unwrap_or(FontRef::default());
351 self.used_font.insert((font.clone(), font_weight));
352
353 write!(
354 self.acc,
355 r#"<text font-family="{font}" text-anchor="{align}" font-size="{font_size}px" font-weight="{weight}" text-style="{text_style}" transform=""#,
356 )?;
357
358 write!(
359 self.acc,
360 r#"translate({cx} {cy}) "#,
361 cx = reference_start.x,
362 cy = reference_start.y
363 )?;
364
365 let rotation = direction.y.atan2(direction.x);
366 if rotation.abs() > 10e-6 {
367 write!(self.acc, r#"rotate({rot}) "#, rot = rotation.to_degrees())?;
368 }
369
370 write!(self.acc, r#"">"#)?;
371
372 if let Some(curve) = on_curve {
373 write!(self.acc, r#"<path id="{id}" d=""#)?;
374 self.write_curve(curve)?;
375 write!(self.acc, r#""/>"#)?;
376
377 write!(self.acc, r##"<textPath href="#{id}">{text}</textPath>"##)?;
378 } else {
379 write!(self.acc, "{text}")?;
380 }
381 write!(self.acc, r#"</text>"#)?;
382
383 Ok(())
384 }
385}
386
387pub fn to_string_with_options(shape: &Shape, options: SVGOptions) -> Result<String, SVGError> {
388 let (min_x, min_y, span_x, span_y) = match options.viewport {
389 ViewPort::ManualCentered { width, height } => (-width / 2., -height / 2., width, height),
390 ViewPort::ManualViewport {
391 x,
392 y,
393 width,
394 height,
395 } => (x - width / 2., y - height / 2., width, height),
396 ViewPort::AutoCentered => {
397 let bb = shape.local_bounding_box().straigthen();
398
399 let mirror_bb = bb
400 .transform(&nalgebra::convert::<_, Transform2<f32>>(Scale2::new(
401 -1., -1.,
402 )))
403 .into_straight();
404
405 let overall_bb = bb.join(mirror_bb);
406
407 (
408 -overall_bb.width() / 2.,
409 -overall_bb.height() / 2.,
410 overall_bb.width(),
411 overall_bb.height(),
412 )
413 }
414 ViewPort::AutoBoundingBox => {
415 let bb = shape.local_bounding_box().straigthen();
416
417 (bb.top_left().x, -bb.top_left().y, bb.width(), bb.height())
418 }
419 };
420
421 let mut exporter = SVGExporter::new();
422
423 let parent_transform = nalgebra::convert(Scale2::new(1., -1.));
424
425 if let Shape::Style { fill, stroke, .. } = shape {
426 shape.write_into_exporter(
427 &mut exporter,
428 &parent_transform,
429 StylePosition {
430 fill: *fill,
431 stroke: *stroke,
432 },
433 )? } else {
435 shape.write_into_exporter(
436 &mut exporter,
437 &parent_transform,
438 StylePosition {
439 fill: None,
440 stroke: None,
441 },
442 )?
443 }
444
445 let (start, end) = if options.skip_svg_tag {
446 (String::new(), "")
447 } else {
448 const SCHEME: &str =
449 r#"xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink""#;
450
451 (
452 format!(r#"<svg viewBox="{min_x} {min_y} {span_x} {span_y}" {SCHEME}>"#),
453 "</svg>",
454 )
455 };
456
457 Ok(exporter.finish(start, end))
458}
459
460pub fn to_string(shape: &Shape) -> Result<String, SVGError> {
461 to_string_with_options(shape, SVGOptions::default()) }