typst_svg/
lib.rs

1//! Rendering of Typst documents into SVG images.
2
3mod image;
4mod paint;
5mod shape;
6mod text;
7
8pub use image::{convert_image_scaling, convert_image_to_base64_url};
9use rustc_hash::FxHashMap;
10use typst_library::introspection::Introspector;
11use typst_library::model::Destination;
12
13use std::fmt::{self, Display, Formatter, Write};
14
15use ecow::EcoString;
16use typst_library::layout::{
17    Abs, Frame, FrameItem, FrameKind, GroupItem, Page, PagedDocument, Point, Ratio, Size,
18    Transform,
19};
20use typst_library::visualize::{Geometry, Gradient, Tiling};
21use typst_utils::hash128;
22use xmlwriter::XmlWriter;
23
24use crate::paint::{GradientRef, SVGSubGradient, TilingRef};
25use crate::text::RenderedGlyph;
26
27/// Export a frame into a SVG file.
28#[typst_macros::time(name = "svg")]
29pub fn svg(page: &Page) -> String {
30    let mut renderer = SVGRenderer::new();
31    renderer.write_header(page.frame.size());
32
33    let state = State::new(page.frame.size(), Transform::identity());
34    renderer.render_page(&state, Transform::identity(), page);
35    renderer.finalize()
36}
37
38/// Export a frame into a SVG file.
39#[typst_macros::time(name = "svg frame")]
40pub fn svg_frame(frame: &Frame) -> String {
41    let mut renderer = SVGRenderer::new();
42    renderer.write_header(frame.size());
43
44    let state = State::new(frame.size(), Transform::identity());
45    renderer.render_frame(&state, frame);
46    renderer.finalize()
47}
48
49/// Export a frame into an SVG suitable for embedding into HTML.
50#[typst_macros::time(name = "svg html frame")]
51pub fn svg_html_frame(
52    frame: &Frame,
53    text_size: Abs,
54    id: Option<&str>,
55    link_points: &[(Point, EcoString)],
56    introspector: &Introspector,
57) -> String {
58    let mut renderer = SVGRenderer::with_options(
59        xmlwriter::Options {
60            indent: xmlwriter::Indent::None,
61            ..Default::default()
62        },
63        Some(introspector),
64    );
65    renderer.write_header_with_custom_attrs(frame.size(), |xml| {
66        if let Some(id) = id {
67            xml.write_attribute("id", id);
68        }
69        xml.write_attribute("class", "typst-frame");
70        xml.write_attribute_fmt(
71            "style",
72            format_args!(
73                "overflow: visible; width: {}em; height: {}em;",
74                frame.width() / text_size,
75                frame.height() / text_size,
76            ),
77        );
78    });
79
80    let state = State::new(frame.size(), Transform::identity());
81    renderer.render_frame(&state, frame);
82
83    for (pos, id) in link_points {
84        renderer.render_link_point(*pos, id);
85    }
86
87    renderer.finalize()
88}
89
90/// Export a document with potentially multiple pages into a single SVG file.
91///
92/// The padding will be added around and between the individual frames.
93pub fn svg_merged(document: &PagedDocument, padding: Abs) -> String {
94    let width = 2.0 * padding
95        + document
96            .pages
97            .iter()
98            .map(|page| page.frame.width())
99            .max()
100            .unwrap_or_default();
101    let height = padding
102        + document
103            .pages
104            .iter()
105            .map(|page| page.frame.height() + padding)
106            .sum::<Abs>();
107
108    let mut renderer = SVGRenderer::new();
109    renderer.write_header(Size::new(width, height));
110
111    let [x, mut y] = [padding; 2];
112    for page in &document.pages {
113        let ts = Transform::translate(x, y);
114        let state = State::new(page.frame.size(), Transform::identity());
115        renderer.render_page(&state, ts, page);
116        y += page.frame.height() + padding;
117    }
118
119    renderer.finalize()
120}
121
122/// Renders one or multiple frames to an SVG file.
123struct SVGRenderer<'a> {
124    /// The internal XML writer.
125    xml: XmlWriter,
126    /// The document's introspector, if we're writing an HTML frame.
127    introspector: Option<&'a Introspector>,
128    /// Prepared glyphs.
129    glyphs: Deduplicator<RenderedGlyph>,
130    /// Clip paths are used to clip a group. A clip path is a path that defines
131    /// the clipping region. The clip path is referenced by the `clip-path`
132    /// attribute of the group. The clip path is in the format of `M x y L x y C
133    /// x1 y1 x2 y2 x y Z`.
134    clip_paths: Deduplicator<EcoString>,
135    /// Deduplicated gradients with transform matrices. They use a reference
136    /// (`href`) to a "source" gradient instead of being defined inline.
137    /// This saves a lot of space since gradients are often reused but with
138    /// different transforms. Therefore this allows us to reuse the same gradient
139    /// multiple times.
140    gradient_refs: Deduplicator<GradientRef>,
141    /// Deduplicated tilings with transform matrices. They use a reference
142    /// (`href`) to a "source" tiling instead of being defined inline.
143    /// This saves a lot of space since tilings are often reused but with
144    /// different transforms. Therefore this allows us to reuse the same gradient
145    /// multiple times.
146    tiling_refs: Deduplicator<TilingRef>,
147    /// These are the actual gradients being written in the SVG file.
148    /// These gradients are deduplicated because they do not contain the transform
149    /// matrix, allowing them to be reused across multiple invocations.
150    ///
151    /// The `Ratio` is the aspect ratio of the gradient, this is used to correct
152    /// the angle of the gradient.
153    gradients: Deduplicator<(Gradient, Ratio)>,
154    /// These are the actual tilings being written in the SVG file.
155    /// These tilings are deduplicated because they do not contain the transform
156    /// matrix, allowing them to be reused across multiple invocations.
157    ///
158    /// The `String` is the rendered tiling frame.
159    tilings: Deduplicator<Tiling>,
160    /// These are the gradients that compose a conic gradient.
161    conic_subgradients: Deduplicator<SVGSubGradient>,
162}
163
164/// Contextual information for rendering.
165#[derive(Copy, Clone)]
166struct State {
167    /// The transform of the current item.
168    transform: Transform,
169    /// The size of the first hard frame in the hierarchy.
170    size: Size,
171}
172
173impl State {
174    fn new(size: Size, transform: Transform) -> Self {
175        Self { size, transform }
176    }
177
178    /// Pre translate the current item's transform.
179    fn pre_translate(self, pos: Point) -> Self {
180        self.pre_concat(Transform::translate(pos.x, pos.y))
181    }
182
183    /// Pre concat the current item's transform.
184    fn pre_concat(self, transform: Transform) -> Self {
185        Self {
186            transform: self.transform.pre_concat(transform),
187            ..self
188        }
189    }
190
191    /// Sets the size of the first hard frame in the hierarchy.
192    fn with_size(self, size: Size) -> Self {
193        Self { size, ..self }
194    }
195
196    /// Sets the current item's transform.
197    fn with_transform(self, transform: Transform) -> Self {
198        Self { transform, ..self }
199    }
200}
201
202impl<'a> SVGRenderer<'a> {
203    /// Create a new SVG renderer with empty glyph and clip path.
204    fn new() -> Self {
205        Self::with_options(Default::default(), None)
206    }
207
208    /// Create a new SVG renderer with the given configuration.
209    fn with_options(
210        options: xmlwriter::Options,
211        introspector: Option<&'a Introspector>,
212    ) -> Self {
213        SVGRenderer {
214            xml: XmlWriter::new(options),
215            introspector,
216            glyphs: Deduplicator::new('g'),
217            clip_paths: Deduplicator::new('c'),
218            gradient_refs: Deduplicator::new('g'),
219            gradients: Deduplicator::new('f'),
220            conic_subgradients: Deduplicator::new('s'),
221            tiling_refs: Deduplicator::new('p'),
222            tilings: Deduplicator::new('t'),
223        }
224    }
225
226    /// Write the default SVG header, including a `typst-doc` class, the
227    /// `viewBox` and `width` and `height` attributes.
228    fn write_header(&mut self, size: Size) {
229        self.write_header_with_custom_attrs(size, |xml| {
230            xml.write_attribute("class", "typst-doc");
231        });
232    }
233
234    /// Write the SVG header with additional attributes and standard attributes.
235    fn write_header_with_custom_attrs(
236        &mut self,
237        size: Size,
238        write_custom_attrs: impl FnOnce(&mut XmlWriter),
239    ) {
240        self.xml.start_element("svg");
241        write_custom_attrs(&mut self.xml);
242        self.xml.write_attribute_fmt(
243            "viewBox",
244            format_args!("0 0 {} {}", size.x.to_pt(), size.y.to_pt()),
245        );
246        self.xml
247            .write_attribute_fmt("width", format_args!("{}pt", size.x.to_pt()));
248        self.xml
249            .write_attribute_fmt("height", format_args!("{}pt", size.y.to_pt()));
250        self.xml.write_attribute("xmlns", "http://www.w3.org/2000/svg");
251        self.xml
252            .write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
253        self.xml.write_attribute("xmlns:h5", "http://www.w3.org/1999/xhtml");
254    }
255
256    /// Render a page with the given transform.
257    fn render_page(&mut self, state: &State, ts: Transform, page: &Page) {
258        if let Some(fill) = page.fill_or_white() {
259            let shape = Geometry::Rect(page.frame.size()).filled(fill);
260            self.render_shape(state, &shape);
261        }
262
263        if !ts.is_identity() {
264            self.xml.start_element("g");
265            self.xml.write_attribute("transform", &SvgMatrix(ts));
266        }
267
268        self.render_frame(state, &page.frame);
269
270        if !ts.is_identity() {
271            self.xml.end_element();
272        }
273    }
274
275    /// Render a frame with the given transform.
276    fn render_frame(&mut self, state: &State, frame: &Frame) {
277        self.xml.start_element("g");
278
279        for (pos, item) in frame.items() {
280            let state = state.pre_translate(*pos);
281            match item {
282                FrameItem::Group(group) => self.render_group(&state, group),
283                FrameItem::Text(text) => self.render_text(&state, text),
284                FrameItem::Shape(shape, _) => self.render_shape(&state, shape),
285                FrameItem::Image(image, size, _) => {
286                    self.render_image(&state, image, size)
287                }
288                FrameItem::Link(dest, size) => self.render_link(&state, dest, *size),
289                FrameItem::Tag(_) => {}
290            };
291        }
292
293        self.xml.end_element();
294    }
295
296    /// Render a group. If the group has `clips` set to true, a clip path will
297    /// be created.
298    fn render_group(&mut self, state: &State, group: &GroupItem) {
299        self.xml.start_element("g");
300        self.xml.write_attribute("class", "typst-group");
301
302        let state = match group.frame.kind() {
303            FrameKind::Soft => state.pre_concat(group.transform),
304            FrameKind::Hard => {
305                let transform = state.transform.pre_concat(group.transform);
306                if !transform.is_identity() {
307                    self.xml.write_attribute("transform", &SvgMatrix(transform));
308                }
309                state
310                    .with_transform(Transform::identity())
311                    .with_size(group.frame.size())
312            }
313        };
314
315        if let Some(label) = group.label {
316            self.xml.write_attribute("data-typst-label", &label.resolve());
317        }
318
319        if let Some(clip_curve) = &group.clip {
320            let offset = Point::new(state.transform.tx, state.transform.ty);
321            let hash = hash128(&(&clip_curve, &offset));
322            let id = self
323                .clip_paths
324                .insert_with(hash, || shape::convert_curve(offset, clip_curve));
325            self.xml.write_attribute_fmt("clip-path", format_args!("url(#{id})"));
326        }
327
328        self.render_frame(&state, &group.frame);
329        self.xml.end_element();
330    }
331
332    /// Render a link element.
333    fn render_link(&mut self, state: &State, dest: &Destination, size: Size) {
334        self.xml.start_element("a");
335        if !state.transform.is_identity() {
336            self.xml.write_attribute("transform", &SvgMatrix(state.transform));
337        }
338
339        match dest {
340            Destination::Location(loc) => {
341                // TODO: Location links on the same page could also be supported
342                // outside of HTML.
343                if let Some(introspector) = self.introspector
344                    && let Some(id) = introspector.html_id(*loc)
345                {
346                    self.xml.write_attribute_fmt("href", format_args!("#{id}"));
347                    self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
348                }
349            }
350            Destination::Position(_) => {
351                // TODO: Links on the same page could be supported.
352            }
353            Destination::Url(url) => {
354                self.xml.write_attribute("href", url.as_str());
355                self.xml.write_attribute("xlink:href", url.as_str());
356            }
357        }
358
359        self.xml.start_element("rect");
360        self.xml
361            .write_attribute_fmt("width", format_args!("{}", size.x.to_pt()));
362        self.xml
363            .write_attribute_fmt("height", format_args!("{}", size.y.to_pt()));
364        self.xml.write_attribute("fill", "transparent");
365        self.xml.write_attribute("stroke", "none");
366        self.xml.end_element();
367
368        self.xml.end_element();
369    }
370
371    /// Renders a linkable point that can be used to link into an HTML frame.
372    fn render_link_point(&mut self, pos: Point, id: &str) {
373        self.xml.start_element("g");
374        self.xml.write_attribute("id", id);
375        self.xml.write_attribute_fmt(
376            "transform",
377            format_args!("translate({} {})", pos.x.to_pt(), pos.y.to_pt()),
378        );
379        self.xml.end_element();
380    }
381
382    /// Finalize the SVG file. This must be called after all rendering is done.
383    fn finalize(mut self) -> String {
384        self.write_glyph_defs();
385        self.write_clip_path_defs();
386        self.write_gradients();
387        self.write_gradient_refs();
388        self.write_subgradients();
389        self.write_tilings();
390        self.write_tiling_refs();
391        self.xml.end_document()
392    }
393
394    /// Build the clip path definitions.
395    fn write_clip_path_defs(&mut self) {
396        if self.clip_paths.is_empty() {
397            return;
398        }
399
400        self.xml.start_element("defs");
401        self.xml.write_attribute("id", "clip-path");
402
403        for (id, path) in self.clip_paths.iter() {
404            self.xml.start_element("clipPath");
405            self.xml.write_attribute("id", &id);
406            self.xml.start_element("path");
407            self.xml.write_attribute("d", &path);
408            self.xml.end_element();
409            self.xml.end_element();
410        }
411
412        self.xml.end_element();
413    }
414}
415
416/// Deduplicates its elements. It is used to deduplicate glyphs and clip paths.
417/// The `H` is the hash type, and `T` is the value type. The `PREFIX` is the
418/// prefix of the index. This is used to distinguish between glyphs and clip
419/// paths.
420#[derive(Debug, Clone)]
421struct Deduplicator<T> {
422    kind: char,
423    vec: Vec<(u128, T)>,
424    present: FxHashMap<u128, Id>,
425}
426
427impl<T> Deduplicator<T> {
428    fn new(kind: char) -> Self {
429        Self {
430            kind,
431            vec: Vec::new(),
432            present: FxHashMap::default(),
433        }
434    }
435
436    /// Inserts a value into the vector. If the hash is already present, returns
437    /// the index of the existing value and `f` will not be called. Otherwise,
438    /// inserts the value and returns the id of the inserted value.
439    #[must_use = "returns the index of the inserted value"]
440    fn insert_with<F>(&mut self, hash: u128, f: F) -> Id
441    where
442        F: FnOnce() -> T,
443    {
444        *self.present.entry(hash).or_insert_with(|| {
445            let index = self.vec.len();
446            self.vec.push((hash, f()));
447            Id(self.kind, hash, index)
448        })
449    }
450
451    /// Iterate over the elements alongside their ids.
452    fn iter(&self) -> impl Iterator<Item = (Id, &T)> {
453        self.vec
454            .iter()
455            .enumerate()
456            .map(|(i, (id, v))| (Id(self.kind, *id, i), v))
457    }
458
459    /// Returns true if the deduplicator is empty.
460    fn is_empty(&self) -> bool {
461        self.vec.is_empty()
462    }
463}
464
465/// Identifies a `<def>`.
466#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
467struct Id(char, u128, usize);
468
469impl Display for Id {
470    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
471        write!(f, "{}{:0X}", self.0, self.1)
472    }
473}
474
475/// Displays as an SVG matrix.
476struct SvgMatrix(Transform);
477
478impl Display for SvgMatrix {
479    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
480        // Convert a [`Transform`] into a SVG transform string.
481        // See https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
482        write!(
483            f,
484            "matrix({} {} {} {} {} {})",
485            self.0.sx.get(),
486            self.0.ky.get(),
487            self.0.kx.get(),
488            self.0.sy.get(),
489            self.0.tx.to_pt(),
490            self.0.ty.to_pt()
491        )
492    }
493}
494
495/// A builder for SVG path using relative coordinates.
496struct SvgPathBuilder {
497    pub path: EcoString,
498    pub scale: Ratio,
499    pub last_close_point: Point,
500    pub last_point: Point,
501}
502
503impl SvgPathBuilder {
504    fn with_translate(pos: Point) -> Self {
505        // add initial M node to transform the entire path
506        Self {
507            path: EcoString::from(format!("M {} {}", pos.x.to_pt(), pos.y.to_pt())),
508            scale: Ratio::one(),
509            last_close_point: pos,
510            last_point: Point::zero(),
511        }
512    }
513
514    fn with_scale(scale: Ratio) -> Self {
515        Self {
516            path: EcoString::from("M 0 0"),
517            scale,
518            last_close_point: Point::zero(),
519            last_point: Point::zero(),
520        }
521    }
522
523    fn scale(&self) -> f32 {
524        self.scale.get() as f32
525    }
526
527    fn set_point(&mut self, x: f32, y: f32) {
528        let point = Point::new(
529            Abs::pt(f64::from(x * self.scale())),
530            Abs::pt(f64::from(y * self.scale())),
531        );
532
533        self.last_point = point;
534    }
535
536    fn map_x(&self, x: f32) -> f32 {
537        x * self.scale() - self.last_point.x.to_pt() as f32
538    }
539
540    fn map_y(&self, y: f32) -> f32 {
541        y * self.scale() - self.last_point.y.to_pt() as f32
542    }
543
544    /// Create a rectangle path. The rectangle is created with the top-left
545    /// corner at (0, 0). The width and height are the size of the rectangle.
546    fn rect(&mut self, width: f32, height: f32) {
547        self.move_to(0.0, 0.0);
548        self.line_to(0.0, height);
549        self.line_to(width, height);
550        self.line_to(width, 0.0);
551        self.close();
552    }
553
554    /// Creates an arc path.
555    fn arc(
556        &mut self,
557        radius: (f32, f32),
558        x_axis_rot: f32,
559        large_arc_flag: u32,
560        sweep_flag: u32,
561        pos: (f32, f32),
562    ) {
563        let rx = self.map_x(radius.0);
564        let ry = self.map_y(radius.1);
565        let x = self.map_x(pos.0);
566        let y = self.map_y(pos.1);
567        write!(
568            &mut self.path,
569            "a {rx} {ry} {x_axis_rot} {large_arc_flag} {sweep_flag} {x} {y} "
570        )
571        .unwrap();
572
573        self.set_point(x, y);
574    }
575
576    fn move_to(&mut self, x: f32, y: f32) {
577        let _x = self.map_x(x);
578        let _y = self.map_y(y);
579        if _x != 0.0 || _y != 0.0 {
580            write!(&mut self.path, "m {_x} {_y} ").unwrap();
581        }
582
583        self.set_point(x, y);
584        self.last_close_point = self.last_point;
585    }
586
587    fn line_to(&mut self, x: f32, y: f32) {
588        let _x = self.map_x(x);
589        let _y = self.map_y(y);
590
591        if _x != 0.0 && _y != 0.0 {
592            write!(&mut self.path, "l {_x} {_y} ").unwrap();
593        } else if _x != 0.0 {
594            write!(&mut self.path, "h {_x} ").unwrap();
595        } else if _y != 0.0 {
596            write!(&mut self.path, "v {_y} ").unwrap();
597        }
598
599        self.set_point(x, y);
600    }
601
602    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
603        let curve = format!(
604            "c {} {} {} {} {} {} ",
605            self.map_x(x1),
606            self.map_y(y1),
607            self.map_x(x2),
608            self.map_y(y2),
609            self.map_x(x),
610            self.map_y(y)
611        );
612        write!(&mut self.path, "{curve}").unwrap();
613        self.set_point(x, y);
614    }
615
616    fn close(&mut self) {
617        write!(&mut self.path, "Z ").unwrap();
618        self.last_point = self.last_close_point;
619    }
620}
621
622/// A builder for SVG path. This is used to build the path for a glyph.
623impl ttf_parser::OutlineBuilder for SvgPathBuilder {
624    fn move_to(&mut self, x: f32, y: f32) {
625        self.move_to(x, y);
626    }
627
628    fn line_to(&mut self, x: f32, y: f32) {
629        self.line_to(x, y);
630    }
631
632    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
633        let _x1 = self.map_x(x1);
634        let _y1 = self.map_y(y1);
635        let _x = self.map_x(x);
636        let _y = self.map_y(y);
637
638        write!(&mut self.path, "q {_x1} {_y1} {_x} {_y} ").unwrap();
639
640        self.set_point(x, y);
641    }
642
643    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
644        self.curve_to(x1, y1, x2, y2, x, y);
645    }
646
647    fn close(&mut self) {
648        self.close();
649    }
650}
651
652impl Default for SvgPathBuilder {
653    fn default() -> Self {
654        Self {
655            path: Default::default(),
656            scale: Ratio::one(),
657            last_close_point: Point::zero(),
658            last_point: Point::zero(),
659        }
660    }
661}