1mod image;
4mod paint;
5mod shape;
6mod text;
7
8use std::collections::HashMap;
9use std::fmt::{self, Display, Formatter, Write};
10
11use ecow::EcoString;
12use ttf_parser::OutlineBuilder;
13use typst_library::layout::{
14 Abs, Frame, FrameItem, FrameKind, GroupItem, Page, PagedDocument, Point, Ratio, Size,
15 Transform,
16};
17use typst_library::visualize::{Geometry, Gradient, Tiling};
18use typst_utils::hash128;
19use xmlwriter::XmlWriter;
20
21use crate::paint::{GradientRef, SVGSubGradient, TilingRef};
22use crate::text::RenderedGlyph;
23
24#[typst_macros::time(name = "svg")]
26pub fn svg(page: &Page) -> String {
27 let mut renderer = SVGRenderer::new();
28 renderer.write_header(page.frame.size());
29
30 let state = State::new(page.frame.size(), Transform::identity());
31 renderer.render_page(state, Transform::identity(), page);
32 renderer.finalize()
33}
34
35#[typst_macros::time(name = "svg frame")]
37pub fn svg_frame(frame: &Frame) -> String {
38 let mut renderer = SVGRenderer::new();
39 renderer.write_header(frame.size());
40
41 let state = State::new(frame.size(), Transform::identity());
42 renderer.render_frame(state, Transform::identity(), frame);
43 renderer.finalize()
44}
45
46pub fn svg_merged(document: &PagedDocument, padding: Abs) -> String {
50 let width = 2.0 * padding
51 + document
52 .pages
53 .iter()
54 .map(|page| page.frame.width())
55 .max()
56 .unwrap_or_default();
57 let height = padding
58 + document
59 .pages
60 .iter()
61 .map(|page| page.frame.height() + padding)
62 .sum::<Abs>();
63
64 let mut renderer = SVGRenderer::new();
65 renderer.write_header(Size::new(width, height));
66
67 let [x, mut y] = [padding; 2];
68 for page in &document.pages {
69 let ts = Transform::translate(x, y);
70 let state = State::new(page.frame.size(), Transform::identity());
71 renderer.render_page(state, ts, page);
72 y += page.frame.height() + padding;
73 }
74
75 renderer.finalize()
76}
77
78struct SVGRenderer {
80 xml: XmlWriter,
82 glyphs: Deduplicator<RenderedGlyph>,
84 clip_paths: Deduplicator<EcoString>,
89 gradient_refs: Deduplicator<GradientRef>,
95 tiling_refs: Deduplicator<TilingRef>,
101 gradients: Deduplicator<(Gradient, Ratio)>,
108 tilings: Deduplicator<Tiling>,
114 conic_subgradients: Deduplicator<SVGSubGradient>,
116}
117
118#[derive(Clone, Copy)]
120struct State {
121 transform: Transform,
123 size: Size,
125}
126
127impl State {
128 fn new(size: Size, transform: Transform) -> Self {
129 Self { size, transform }
130 }
131
132 fn pre_translate(self, pos: Point) -> Self {
134 self.pre_concat(Transform::translate(pos.x, pos.y))
135 }
136
137 fn pre_concat(self, transform: Transform) -> Self {
139 Self {
140 transform: self.transform.pre_concat(transform),
141 ..self
142 }
143 }
144
145 fn with_size(self, size: Size) -> Self {
147 Self { size, ..self }
148 }
149
150 fn with_transform(self, transform: Transform) -> Self {
152 Self { transform, ..self }
153 }
154}
155
156impl SVGRenderer {
157 fn new() -> Self {
159 SVGRenderer {
160 xml: XmlWriter::new(xmlwriter::Options::default()),
161 glyphs: Deduplicator::new('g'),
162 clip_paths: Deduplicator::new('c'),
163 gradient_refs: Deduplicator::new('g'),
164 gradients: Deduplicator::new('f'),
165 conic_subgradients: Deduplicator::new('s'),
166 tiling_refs: Deduplicator::new('p'),
167 tilings: Deduplicator::new('t'),
168 }
169 }
170
171 fn write_header(&mut self, size: Size) {
174 self.xml.start_element("svg");
175 self.xml.write_attribute("class", "typst-doc");
176 self.xml.write_attribute_fmt(
177 "viewBox",
178 format_args!("0 0 {} {}", size.x.to_pt(), size.y.to_pt()),
179 );
180 self.xml
181 .write_attribute_fmt("width", format_args!("{}pt", size.x.to_pt()));
182 self.xml
183 .write_attribute_fmt("height", format_args!("{}pt", size.y.to_pt()));
184 self.xml.write_attribute("xmlns", "http://www.w3.org/2000/svg");
185 self.xml
186 .write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
187 self.xml.write_attribute("xmlns:h5", "http://www.w3.org/1999/xhtml");
188 }
189
190 fn render_page(&mut self, state: State, ts: Transform, page: &Page) {
192 if let Some(fill) = page.fill_or_white() {
193 let shape = Geometry::Rect(page.frame.size()).filled(fill);
194 self.render_shape(state, &shape);
195 }
196
197 self.render_frame(state, ts, &page.frame);
198 }
199
200 fn render_frame(&mut self, state: State, ts: Transform, frame: &Frame) {
202 self.xml.start_element("g");
203 if !ts.is_identity() {
204 self.xml.write_attribute("transform", &SvgMatrix(ts));
205 }
206
207 for (pos, item) in frame.items() {
208 if matches!(item, FrameItem::Link(_, _) | FrameItem::Tag(_)) {
211 continue;
212 }
213
214 let x = pos.x.to_pt();
215 let y = pos.y.to_pt();
216 self.xml.start_element("g");
217 self.xml
218 .write_attribute_fmt("transform", format_args!("translate({x} {y})"));
219
220 match item {
221 FrameItem::Group(group) => {
222 self.render_group(state.pre_translate(*pos), group)
223 }
224 FrameItem::Text(text) => {
225 self.render_text(state.pre_translate(*pos), text)
226 }
227 FrameItem::Shape(shape, _) => {
228 self.render_shape(state.pre_translate(*pos), shape)
229 }
230 FrameItem::Image(image, size, _) => self.render_image(image, size),
231 FrameItem::Link(_, _) => unreachable!(),
232 FrameItem::Tag(_) => unreachable!(),
233 };
234
235 self.xml.end_element();
236 }
237
238 self.xml.end_element();
239 }
240
241 fn render_group(&mut self, state: State, group: &GroupItem) {
244 let state = match group.frame.kind() {
245 FrameKind::Soft => state.pre_concat(group.transform),
246 FrameKind::Hard => state
247 .with_transform(Transform::identity())
248 .with_size(group.frame.size()),
249 };
250
251 self.xml.start_element("g");
252 self.xml.write_attribute("class", "typst-group");
253
254 if let Some(label) = group.label {
255 self.xml.write_attribute("data-typst-label", &label.resolve());
256 }
257
258 if let Some(clip_curve) = &group.clip {
259 let hash = hash128(&group);
260 let id =
261 self.clip_paths.insert_with(hash, || shape::convert_curve(clip_curve));
262 self.xml.write_attribute_fmt("clip-path", format_args!("url(#{id})"));
263 }
264
265 self.render_frame(state, group.transform, &group.frame);
266 self.xml.end_element();
267 }
268
269 fn finalize(mut self) -> String {
271 self.write_glyph_defs();
272 self.write_clip_path_defs();
273 self.write_gradients();
274 self.write_gradient_refs();
275 self.write_subgradients();
276 self.write_tilings();
277 self.write_tiling_refs();
278 self.xml.end_document()
279 }
280
281 fn write_clip_path_defs(&mut self) {
283 if self.clip_paths.is_empty() {
284 return;
285 }
286
287 self.xml.start_element("defs");
288 self.xml.write_attribute("id", "clip-path");
289
290 for (id, path) in self.clip_paths.iter() {
291 self.xml.start_element("clipPath");
292 self.xml.write_attribute("id", &id);
293 self.xml.start_element("path");
294 self.xml.write_attribute("d", &path);
295 self.xml.end_element();
296 self.xml.end_element();
297 }
298
299 self.xml.end_element();
300 }
301}
302
303#[derive(Debug, Clone)]
308struct Deduplicator<T> {
309 kind: char,
310 vec: Vec<(u128, T)>,
311 present: HashMap<u128, Id>,
312}
313
314impl<T> Deduplicator<T> {
315 fn new(kind: char) -> Self {
316 Self { kind, vec: Vec::new(), present: HashMap::new() }
317 }
318
319 #[must_use = "returns the index of the inserted value"]
323 fn insert_with<F>(&mut self, hash: u128, f: F) -> Id
324 where
325 F: FnOnce() -> T,
326 {
327 *self.present.entry(hash).or_insert_with(|| {
328 let index = self.vec.len();
329 self.vec.push((hash, f()));
330 Id(self.kind, hash, index)
331 })
332 }
333
334 fn iter(&self) -> impl Iterator<Item = (Id, &T)> {
336 self.vec
337 .iter()
338 .enumerate()
339 .map(|(i, (id, v))| (Id(self.kind, *id, i), v))
340 }
341
342 fn is_empty(&self) -> bool {
344 self.vec.is_empty()
345 }
346}
347
348#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
350struct Id(char, u128, usize);
351
352impl Display for Id {
353 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
354 write!(f, "{}{:0X}", self.0, self.1)
355 }
356}
357
358struct SvgMatrix(Transform);
360
361impl Display for SvgMatrix {
362 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
363 write!(
366 f,
367 "matrix({} {} {} {} {} {})",
368 self.0.sx.get(),
369 self.0.ky.get(),
370 self.0.kx.get(),
371 self.0.sy.get(),
372 self.0.tx.to_pt(),
373 self.0.ty.to_pt()
374 )
375 }
376}
377
378struct SvgPathBuilder(pub EcoString, pub Ratio);
380
381impl SvgPathBuilder {
382 fn with_scale(scale: Ratio) -> Self {
383 Self(EcoString::new(), scale)
384 }
385
386 fn scale(&self) -> f32 {
387 self.1.get() as f32
388 }
389
390 fn rect(&mut self, width: f32, height: f32) {
393 self.move_to(0.0, 0.0);
394 self.line_to(0.0, height);
395 self.line_to(width, height);
396 self.line_to(width, 0.0);
397 self.close();
398 }
399
400 fn arc(
402 &mut self,
403 radius: (f32, f32),
404 x_axis_rot: f32,
405 large_arc_flag: u32,
406 sweep_flag: u32,
407 pos: (f32, f32),
408 ) {
409 let scale = self.scale();
410 write!(
411 &mut self.0,
412 "A {rx} {ry} {x_axis_rot} {large_arc_flag} {sweep_flag} {x} {y} ",
413 rx = radius.0 * scale,
414 ry = radius.1 * scale,
415 x = pos.0 * scale,
416 y = pos.1 * scale,
417 )
418 .unwrap();
419 }
420}
421
422impl Default for SvgPathBuilder {
423 fn default() -> Self {
424 Self(Default::default(), Ratio::one())
425 }
426}
427
428impl ttf_parser::OutlineBuilder for SvgPathBuilder {
430 fn move_to(&mut self, x: f32, y: f32) {
431 let scale = self.scale();
432 write!(&mut self.0, "M {} {} ", x * scale, y * scale).unwrap();
433 }
434
435 fn line_to(&mut self, x: f32, y: f32) {
436 let scale = self.scale();
437 write!(&mut self.0, "L {} {} ", x * scale, y * scale).unwrap();
438 }
439
440 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
441 let scale = self.scale();
442 write!(
443 &mut self.0,
444 "Q {} {} {} {} ",
445 x1 * scale,
446 y1 * scale,
447 x * scale,
448 y * scale
449 )
450 .unwrap();
451 }
452
453 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
454 let scale = self.scale();
455 write!(
456 &mut self.0,
457 "C {} {} {} {} {} {} ",
458 x1 * scale,
459 y1 * scale,
460 x2 * scale,
461 y2 * scale,
462 x * scale,
463 y * scale
464 )
465 .unwrap();
466 }
467
468 fn close(&mut self) {
469 write!(&mut self.0, "Z ").unwrap();
470 }
471}