typst_render/
lib.rs

1//! Rendering of Typst documents into raster images.
2
3mod image;
4mod paint;
5mod shape;
6mod text;
7
8use tiny_skia as sk;
9use typst_library::layout::{
10    Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Page, PagedDocument, Point, Size,
11    Transform,
12};
13use typst_library::visualize::{Color, Geometry, Paint};
14
15/// Export a page into a raster image.
16///
17/// This renders the page at the given number of pixels per point and returns
18/// the resulting `tiny-skia` pixel buffer.
19#[typst_macros::time(name = "render")]
20pub fn render(page: &Page, pixel_per_pt: f32) -> sk::Pixmap {
21    let size = page.frame.size();
22    let pxw = (pixel_per_pt * size.x.to_f32()).round().max(1.0) as u32;
23    let pxh = (pixel_per_pt * size.y.to_f32()).round().max(1.0) as u32;
24
25    let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt);
26    let state = State::new(size, ts, pixel_per_pt);
27
28    let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap();
29
30    if let Some(fill) = page.fill_or_white() {
31        if let Paint::Solid(color) = fill {
32            canvas.fill(paint::to_sk_color(color));
33        } else {
34            let rect = Geometry::Rect(page.frame.size()).filled(fill);
35            shape::render_shape(&mut canvas, state, &rect);
36        }
37    }
38
39    render_frame(&mut canvas, state, &page.frame);
40
41    canvas
42}
43
44/// Export a document with potentially multiple pages into a single raster image.
45pub fn render_merged(
46    document: &PagedDocument,
47    pixel_per_pt: f32,
48    gap: Abs,
49    fill: Option<Color>,
50) -> sk::Pixmap {
51    let pixmaps: Vec<_> =
52        document.pages.iter().map(|page| render(page, pixel_per_pt)).collect();
53
54    let gap = (pixel_per_pt * gap.to_f32()).round() as u32;
55    let pxw = pixmaps.iter().map(sk::Pixmap::width).max().unwrap_or_default();
56    let pxh = pixmaps.iter().map(|pixmap| pixmap.height()).sum::<u32>()
57        + gap * pixmaps.len().saturating_sub(1) as u32;
58
59    let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap();
60    if let Some(fill) = fill {
61        canvas.fill(paint::to_sk_color(fill));
62    }
63
64    let mut y = 0;
65    for pixmap in pixmaps {
66        canvas.draw_pixmap(
67            0,
68            y as i32,
69            pixmap.as_ref(),
70            &sk::PixmapPaint::default(),
71            sk::Transform::identity(),
72            None,
73        );
74
75        y += pixmap.height() + gap;
76    }
77
78    canvas
79}
80
81/// Additional metadata carried through the rendering process.
82#[derive(Clone, Copy, Default)]
83struct State<'a> {
84    /// The transform of the current item.
85    transform: sk::Transform,
86    /// The transform of the first hard frame in the hierarchy.
87    container_transform: sk::Transform,
88    /// The mask of the current item.
89    mask: Option<&'a sk::Mask>,
90    /// The pixel per point ratio.
91    pixel_per_pt: f32,
92    /// The size of the first hard frame in the hierarchy.
93    size: Size,
94}
95
96impl State<'_> {
97    fn new(size: Size, transform: sk::Transform, pixel_per_pt: f32) -> Self {
98        Self {
99            size,
100            transform,
101            container_transform: transform,
102            pixel_per_pt,
103            ..Default::default()
104        }
105    }
106
107    /// Pre translate the current item's transform.
108    fn pre_translate(self, pos: Point) -> Self {
109        Self {
110            transform: self.transform.pre_translate(pos.x.to_f32(), pos.y.to_f32()),
111            ..self
112        }
113    }
114
115    fn pre_scale(self, scale: Axes<Abs>) -> Self {
116        Self {
117            transform: self.transform.pre_scale(scale.x.to_f32(), scale.y.to_f32()),
118            ..self
119        }
120    }
121
122    /// Pre concat the current item's transform.
123    fn pre_concat(self, transform: sk::Transform) -> Self {
124        Self {
125            transform: self.transform.pre_concat(transform),
126            ..self
127        }
128    }
129
130    /// Sets the current mask.
131    fn with_mask(self, mask: Option<&sk::Mask>) -> State<'_> {
132        // Ensure that we're using the parent's mask if we don't have one.
133        if mask.is_some() {
134            State { mask, ..self }
135        } else {
136            State { mask: None, ..self }
137        }
138    }
139
140    /// Sets the size of the first hard frame in the hierarchy.
141    fn with_size(self, size: Size) -> Self {
142        Self { size, ..self }
143    }
144
145    /// Pre concat the container's transform.
146    fn pre_concat_container(self, transform: sk::Transform) -> Self {
147        Self {
148            container_transform: self.container_transform.pre_concat(transform),
149            ..self
150        }
151    }
152}
153
154/// Render a frame into the canvas.
155fn render_frame(canvas: &mut sk::Pixmap, state: State, frame: &Frame) {
156    for (pos, item) in frame.items() {
157        match item {
158            FrameItem::Group(group) => {
159                render_group(canvas, state, *pos, group);
160            }
161            FrameItem::Text(text) => {
162                text::render_text(canvas, state.pre_translate(*pos), text);
163            }
164            FrameItem::Shape(shape, _) => {
165                shape::render_shape(canvas, state.pre_translate(*pos), shape);
166            }
167            FrameItem::Image(image, size, _) => {
168                image::render_image(canvas, state.pre_translate(*pos), image, *size);
169            }
170            FrameItem::Link(_, _) => {}
171            FrameItem::Tag(_) => {}
172        }
173    }
174}
175
176/// Render a group frame with optional transform and clipping into the canvas.
177fn render_group(canvas: &mut sk::Pixmap, state: State, pos: Point, group: &GroupItem) {
178    let sk_transform = to_sk_transform(&group.transform);
179    let state = match group.frame.kind() {
180        FrameKind::Soft => state.pre_translate(pos).pre_concat(sk_transform),
181        FrameKind::Hard => state
182            .pre_translate(pos)
183            .pre_concat(sk_transform)
184            .pre_concat_container(
185                state
186                    .transform
187                    .post_concat(state.container_transform.invert().unwrap()),
188            )
189            .pre_concat_container(to_sk_transform(&Transform::translate(pos.x, pos.y)))
190            .pre_concat_container(sk_transform)
191            .with_size(group.frame.size()),
192    };
193
194    let mut mask = state.mask;
195    let storage;
196    if let Some(clip_curve) = group.clip.as_ref() {
197        if let Some(path) = shape::convert_curve(clip_curve)
198            .and_then(|path| path.transform(state.transform))
199        {
200            if let Some(mask) = mask {
201                let mut mask = mask.clone();
202                mask.intersect_path(
203                    &path,
204                    sk::FillRule::default(),
205                    false,
206                    sk::Transform::default(),
207                );
208                storage = mask;
209            } else {
210                let pxw = canvas.width();
211                let pxh = canvas.height();
212                let Some(mut mask) = sk::Mask::new(pxw, pxh) else {
213                    // Fails if clipping rect is empty. In that case we just
214                    // clip everything by returning.
215                    return;
216                };
217
218                mask.fill_path(
219                    &path,
220                    sk::FillRule::default(),
221                    false,
222                    sk::Transform::default(),
223                );
224                storage = mask;
225            };
226
227            mask = Some(&storage);
228        }
229    }
230
231    render_frame(canvas, state.with_mask(mask), &group.frame);
232}
233
234fn to_sk_transform(transform: &Transform) -> sk::Transform {
235    let Transform { sx, ky, kx, sy, tx, ty } = *transform;
236    sk::Transform::from_row(
237        sx.get() as _,
238        ky.get() as _,
239        kx.get() as _,
240        sy.get() as _,
241        tx.to_f32(),
242        ty.to_f32(),
243    )
244}
245
246/// Additional methods for [`Abs`].
247trait AbsExt {
248    /// Convert to a number of points as f32.
249    fn to_f32(self) -> f32;
250}
251
252impl AbsExt for Abs {
253    fn to_f32(self) -> f32 {
254        self.to_pt() as f32
255    }
256}