1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
use std::fmt::Write;
use itertools::Itertools;
use crate::glycan::{
render::element::{Element, TextAnchor, TextBaseline},
RenderedGlycan,
};
impl RenderedGlycan {
/// Render this glycan as SVG. The SVG will be appended to the given buffer.
/// * `output`: the buffer to append the SVG to.
///
/// # Errors
/// If the underlying buffer errors the error is returned.
pub fn to_svg(&self, mut output: impl Write) -> Result<(), std::fmt::Error> {
fn clr(clr: Option<&[u8; 3]>) -> String {
if let Some([r, g, b]) = clr {
format!("rgb({r},{g},{b})")
} else {
"transparent".to_string()
}
}
write!(
output,
"<svg width=\"{}\" height=\"{}\" style=\"--midpoint:{}\" class=\"glycan\">",
self.size.0, self.size.1, self.midpoint
)?;
for element in &self.elements {
match element {
Element::Line {
from,
to,
stroke,
stroke_size,
} => write!(output,
"<line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" stroke=\"{}\" stroke-width=\"{stroke_size}px\"/>",
from.0,
from.1,
to.0,
to.1,
clr(Some(stroke)))?,
Element::Circle {
r,
center,
fill,
stroke,
stroke_size,
svg_header,
} => write!(output,
"<circle r=\"{r}\" cx=\"{}\" cy=\"{}\" fill=\"{}\" stroke=\"{}\" stroke-width=\"{stroke_size}px\"{svg_header}/>",
center.0,
center.1,
clr(fill.as_ref()),
clr(Some(stroke)))?,
Element::Rectangle {
top,
w,
h,
fill,
stroke,
stroke_size,
svg_header,
} => write!(output,
"<rect x=\"{}\" y=\"{}\" width=\"{w}\" height=\"{h}\" fill=\"{}\" stroke=\"{}\" stroke-width=\"{stroke_size}px\"{svg_header}/>",
top.0,
top.1,
clr(Some(fill)),
clr(Some(stroke)))?,
Element::Polygon {
points,
fill,
stroke,
stroke_size,
svg_header,
bevel,
} => write!(output,
"<polygon points=\"{}\" fill=\"{}\" stroke=\"{}\" stroke-width=\"{stroke_size}px\"{svg_header}{}/>",
points.iter().map(|(a, b)| format!("{a} {b}")).join(" "),
clr(Some(fill)),
clr(Some(stroke)),
if *bevel {" stroke-linejoin=\"bevel\""} else {""})?,
Element::Text {
text,
position,
anchor,
baseline,
fill,
size,
italic,
} => write!(output,
"<text x=\"{}\" y=\"{}\" fill=\"{}\" font-size=\"{size}px\" text-anchor=\"{}\" dominant-baseline=\"{}\"{}>{text}</text>",
position.0,
position.1,
clr(Some(fill)),
match anchor {TextAnchor::Start => "start", TextAnchor::Middle => "middle", TextAnchor::End => "End"},
match baseline {TextBaseline::Hanging => "hanging", TextBaseline::Middle => "middle", TextBaseline::Ideographic => "ideographic"},
if *italic {" font-style=\"italic\""} else {""})?,
Element::Curve { start, points, stroke, stroke_size } => write!(output,
"<path d=\"M {} {} {}\" stroke=\"{}\" stroke-width=\"{stroke_size}px\" fill=\"none\"/>",
start.0, start.1,
points.iter().map(|(a, b, c, d)| format!("Q {a} {b} {c} {d}")).join(" "),
clr(Some(stroke)))?,
}
}
write!(output, "</svg>")?;
Ok(())
}
}