dessin_svg/
lib.rs

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	/// Create a viewport centered around (0, 0), with size (width, height)
35	ManualCentered { width: f32, height: f32 },
36	/// Create a viewport centered around (x, y), with size (width, height)
37	ManualViewport {
38		x: f32,
39		y: f32,
40		width: f32,
41		height: f32,
42	},
43	/// Create a Viewport centered around (0, 0), with auto size that include all [Shapes][`dessin::prelude::Shape`]
44	AutoCentered,
45	#[default]
46	/// Create a Viewport centered around the centered of the shapes, with auto size that include all [Shapes][`dessin::prelude::Shape`]
47	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			)?, // pass [0;1] number to [0;255] for a working CSS code (not needed for alpha)
79
80			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				// creates a base 64 ending font using previous imports
188				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("<", "&lt;").replace(">", "&gt;");
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		)? //Needed to be complete
434	} 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()) // Needed to add StylePosition { fill, stroke } using shape
462}