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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//! Writes typeset pages as SVG
//!
//! Used to show pages before they become a PDF. Letters are drawn with the glyph outlines that
//! suki takes from the fonts, so the pages look the same without the fonts.
use crate::draw::{Mark, Page};
use crate::font::{self, Library};
use std::collections::{BTreeSet, HashMap};
use suki::outline;
use suki::text::number;
/// The path data of each glyph, made once and kept. None for a glyph with nothing to draw
pub type GlyphPath = HashMap<(usize, u16), Option<String>>;
/// Writes one page as SVG
///
/// Each glyph goes in defs once, and the letters refer to it with use. The ids start with
/// prefix, so that pages shown on the same screen keep them apart
pub fn render(page: &Page, library: &Library, prefix: &str, path: &mut GlyphPath) -> String {
let mut used: BTreeSet<(usize, u16)> = BTreeSet::new();
let mut body = String::new();
for mark in &page.mark {
write(mark, library, prefix, path, &mut used, &mut body);
}
let mut out = format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {} {}\" fill=\"currentColor\">",
number(page.width),
number(page.height)
);
if !used.is_empty() {
out.push_str("<defs>");
for (face, glyph) in &used {
if let Some(Some(data)) = path.get(&(*face, *glyph)) {
// Thickened as suki does, so that the letters look as heavy as text
let weight = suki::svg::DARKEN * library.face[*face].units_per_em;
out.push_str(&format!(
"<path id=\"{prefix}-{face}-{glyph}\" d=\"{data}\" stroke=\"currentColor\" \
stroke-width=\"{}\" stroke-linejoin=\"round\"/>",
number(weight)
));
}
}
out.push_str("</defs>");
}
out.push_str(&body);
out.push_str("</svg>");
out
}
/// Writes one mark
fn write(
mark: &Mark,
library: &Library,
prefix: &str,
path: &mut GlyphPath,
used: &mut BTreeSet<(usize, u16)>,
out: &mut String,
) {
match mark {
Mark::Text {face, size, stretch, x, y, letter} => {
let found = &library.face[*face];
let scale = size / found.units_per_em;
let mut pen = *x;
for one in letter {
if drawable(library, *face, one.glyph, path) {
used.insert((*face, one.glyph));
out.push_str(&format!(
"<use href=\"#{prefix}-{face}-{}\" transform=\"matrix({} 0 0 {} {} {})\"/>",
one.glyph,
number(scale),
number(scale * stretch),
number(pen),
number(*y)
));
}
pen += font::advance(found, one.glyph) * size;
}
}
Mark::Rule {x, y, width, height} => out.push_str(&format!(
"<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\"/>",
number(*x),
number(*y),
number(*width),
number(*height)
)),
Mark::Stroke {point, width} => out.push_str(&format!(
"<polyline points=\"{}\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"{}\"/>",
points(point),
number(*width)
)),
Mark::Fill {point} => out.push_str(&format!("<polygon points=\"{}\"/>", points(point))),
Mark::Circle {x, y, radius, width} => out.push_str(&format!(
"<circle cx=\"{}\" cy=\"{}\" r=\"{}\" fill=\"none\" stroke=\"currentColor\" \
stroke-width=\"{}\"/>",
number(*x),
number(*y),
number(*radius),
number(*width)
)),
Mark::Image {x, y, width, height, ..} => out.push_str(&format!(
"<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" fill=\"none\" \
stroke=\"currentColor\" stroke-width=\"0.5\"/>",
number(*x),
number(*y),
number(*width),
number(*height)
)),
}
}
/// Whether a glyph has an outline to draw. Its path is made the first time
fn drawable(library: &Library, face: usize, glyph: u16, path: &mut GlyphPath) -> bool {
if glyph == 0 {
return false;
}
path.entry((face, glyph))
.or_insert_with(|| {
let drawn = suki::font::outline_of(&library.face[face].font, glyph);
if drawn.segment.is_empty() {
None
} else {
Some(outline::path_data(&drawn, 1.0, 0.0, 0.0))
}
})
.is_some()
}
/// Writes points as x,y pairs separated by spaces
fn points(point: &[(f64, f64)]) -> String {
let pair: Vec<String> = point
.iter()
.map(|(x, y)| format!("{},{}", number(*x), number(*y)))
.collect();
pair.join(" ")
}