typst_svg/
lib.rs

1//! Rendering of Typst documents into SVG images.
2
3mod 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/// Export a frame into a SVG file.
25#[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/// Export a frame into a SVG file.
36#[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
46/// Export a document with potentially multiple pages into a single SVG file.
47///
48/// The padding will be added around and between the individual frames.
49pub 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
78/// Renders one or multiple frames to an SVG file.
79struct SVGRenderer {
80    /// The internal XML writer.
81    xml: XmlWriter,
82    /// Prepared glyphs.
83    glyphs: Deduplicator<RenderedGlyph>,
84    /// Clip paths are used to clip a group. A clip path is a path that defines
85    /// the clipping region. The clip path is referenced by the `clip-path`
86    /// attribute of the group. The clip path is in the format of `M x y L x y C
87    /// x1 y1 x2 y2 x y Z`.
88    clip_paths: Deduplicator<EcoString>,
89    /// Deduplicated gradients with transform matrices. They use a reference
90    /// (`href`) to a "source" gradient instead of being defined inline.
91    /// This saves a lot of space since gradients are often reused but with
92    /// different transforms. Therefore this allows us to reuse the same gradient
93    /// multiple times.
94    gradient_refs: Deduplicator<GradientRef>,
95    /// Deduplicated tilings with transform matrices. They use a reference
96    /// (`href`) to a "source" tiling instead of being defined inline.
97    /// This saves a lot of space since tilings are often reused but with
98    /// different transforms. Therefore this allows us to reuse the same gradient
99    /// multiple times.
100    tiling_refs: Deduplicator<TilingRef>,
101    /// These are the actual gradients being written in the SVG file.
102    /// These gradients are deduplicated because they do not contain the transform
103    /// matrix, allowing them to be reused across multiple invocations.
104    ///
105    /// The `Ratio` is the aspect ratio of the gradient, this is used to correct
106    /// the angle of the gradient.
107    gradients: Deduplicator<(Gradient, Ratio)>,
108    /// These are the actual tilings being written in the SVG file.
109    /// These tilings are deduplicated because they do not contain the transform
110    /// matrix, allowing them to be reused across multiple invocations.
111    ///
112    /// The `String` is the rendered tiling frame.
113    tilings: Deduplicator<Tiling>,
114    /// These are the gradients that compose a conic gradient.
115    conic_subgradients: Deduplicator<SVGSubGradient>,
116}
117
118/// Contextual information for rendering.
119#[derive(Clone, Copy)]
120struct State {
121    /// The transform of the current item.
122    transform: Transform,
123    /// The size of the first hard frame in the hierarchy.
124    size: Size,
125}
126
127impl State {
128    fn new(size: Size, transform: Transform) -> Self {
129        Self { size, transform }
130    }
131
132    /// Pre translate the current item's transform.
133    fn pre_translate(self, pos: Point) -> Self {
134        self.pre_concat(Transform::translate(pos.x, pos.y))
135    }
136
137    /// Pre concat the current item's transform.
138    fn pre_concat(self, transform: Transform) -> Self {
139        Self {
140            transform: self.transform.pre_concat(transform),
141            ..self
142        }
143    }
144
145    /// Sets the size of the first hard frame in the hierarchy.
146    fn with_size(self, size: Size) -> Self {
147        Self { size, ..self }
148    }
149
150    /// Sets the current item's transform.
151    fn with_transform(self, transform: Transform) -> Self {
152        Self { transform, ..self }
153    }
154}
155
156impl SVGRenderer {
157    /// Create a new SVG renderer with empty glyph and clip path.
158    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    /// Write the SVG header, including the `viewBox` and `width` and `height`
172    /// attributes.
173    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    /// Render a page with the given transform.
191    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    /// Render a frame with the given transform.
201    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            // File size optimization.
209            // TODO: SVGs could contain links, couldn't they?
210            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    /// Render a group. If the group has `clips` set to true, a clip path will
242    /// be created.
243    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    /// Finalize the SVG file. This must be called after all rendering is done.
270    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    /// Build the clip path definitions.
282    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/// Deduplicates its elements. It is used to deduplicate glyphs and clip paths.
304/// The `H` is the hash type, and `T` is the value type. The `PREFIX` is the
305/// prefix of the index. This is used to distinguish between glyphs and clip
306/// paths.
307#[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    /// Inserts a value into the vector. If the hash is already present, returns
320    /// the index of the existing value and `f` will not be called. Otherwise,
321    /// inserts the value and returns the id of the inserted value.
322    #[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    /// Iterate over the elements alongside their ids.
335    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    /// Returns true if the deduplicator is empty.
343    fn is_empty(&self) -> bool {
344        self.vec.is_empty()
345    }
346}
347
348/// Identifies a `<def>`.
349#[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
358/// Displays as an SVG matrix.
359struct SvgMatrix(Transform);
360
361impl Display for SvgMatrix {
362    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
363        // Convert a [`Transform`] into a SVG transform string.
364        // See https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
365        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
378/// A builder for SVG path.
379struct 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    /// Create a rectangle path. The rectangle is created with the top-left
391    /// corner at (0, 0). The width and height are the size of the rectangle.
392    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    /// Creates an arc path.
401    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
428/// A builder for SVG path. This is used to build the path for a glyph.
429impl 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}