Skip to main content

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_layout::{Page, PagedDocument};
10use typst_library::layout::{
11    Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Sides, Size, Transform,
12};
13use typst_library::visualize::{Color, Geometry, Paint};
14use typst_utils::Scalar;
15
16/// Export a page into a raster image.
17///
18/// This renders the page at the given number of pixels per point and returns
19/// the resulting `tiny-skia` pixel buffer.
20#[typst_macros::time(name = "render")]
21pub fn render(page: &Page, opts: &RenderOptions) -> sk::Pixmap {
22    let bleed = if opts.render_bleed { page.bleed } else { Sides::default() };
23
24    let size = page.frame.size() + bleed.sum_by_axis();
25    let pixel_per_pt = opts.pixel_per_pt.get() as f32;
26    let pxw = (pixel_per_pt * size.x.to_f32()).round().max(1.0) as u32;
27    let pxh = (pixel_per_pt * size.y.to_f32()).round().max(1.0) as u32;
28
29    let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt);
30    let state = State::new(size, ts, pixel_per_pt);
31
32    let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap();
33
34    if let Some(fill) = page.fill_or_white() {
35        if let Paint::Solid(color) = fill {
36            canvas.fill(paint::to_sk_color(color.to_process()));
37        } else {
38            let rect = Geometry::Rect(size).filled(fill);
39            shape::render_shape(&mut canvas, state, &rect);
40        }
41    }
42
43    let state = state.pre_translate(Point { x: bleed.left, y: bleed.top });
44
45    render_frame(&mut canvas, state, &page.frame);
46
47    canvas
48}
49
50/// Export a document with potentially multiple pages into a single raster image.
51pub fn render_merged(
52    document: &PagedDocument,
53    opts: &RenderOptions,
54    gap: Abs,
55    fill: Option<Color>,
56) -> sk::Pixmap {
57    let pixmaps: Vec<_> =
58        document.pages().iter().map(|page| render(page, opts)).collect();
59
60    let pixel_per_pt = opts.pixel_per_pt.get() as f32;
61    let gap = (pixel_per_pt * gap.to_f32()).round() as u32;
62    let pxw = pixmaps.iter().map(sk::Pixmap::width).max().unwrap_or_default();
63    let pxh = pixmaps.iter().map(|pixmap| pixmap.height()).sum::<u32>()
64        + gap * pixmaps.len().saturating_sub(1) as u32;
65
66    let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap();
67    if let Some(fill) = fill {
68        canvas.fill(paint::to_sk_color(fill.to_process()));
69    }
70
71    let mut y = 0;
72    for pixmap in pixmaps {
73        canvas.draw_pixmap(
74            0,
75            y as i32,
76            pixmap.as_ref(),
77            &sk::PixmapPaint::default(),
78            sk::Transform::identity(),
79            None,
80        );
81
82        y += pixmap.height() + gap;
83    }
84
85    canvas
86}
87
88/// Settings for raster image export.
89#[derive(Debug, Clone, Eq, PartialEq, Hash)]
90pub struct RenderOptions {
91    /// Controls the scale of the rendered output in pixels per typographic
92    /// point. By default, a value of `1.0` is used, meaning one pixel is
93    /// generated per point. Increasing this value produces higher-resolution
94    /// images, while lower values reduce the output size and rendering cost.
95    /// This can be useful when adjusting the final image quality for display or
96    /// printing purposes.
97    pub pixel_per_pt: Scalar,
98    /// By default, rendered pages are bounded to the page size. In some
99    /// circumstances, such as when preparing documents for print, it may be
100    /// desirable to include content beyond these bounds to account for bleed
101    /// margins. This field allows expanding the rendered area to include such
102    /// bleed.
103    pub render_bleed: bool,
104}
105
106impl Default for RenderOptions {
107    fn default() -> Self {
108        Self {
109            pixel_per_pt: Scalar::new(2.0),
110            render_bleed: false,
111        }
112    }
113}
114
115/// Additional metadata carried through the rendering process.
116#[derive(Default, Copy, Clone)]
117struct State<'a> {
118    /// The transform of the current item.
119    transform: sk::Transform,
120    /// The transform of the first hard frame in the hierarchy.
121    container_transform: sk::Transform,
122    /// The mask of the current item.
123    mask: Option<&'a sk::Mask>,
124    /// The pixel per point ratio.
125    pixel_per_pt: f32,
126    /// The size of the first hard frame in the hierarchy.
127    size: Size,
128}
129
130impl<'a> State<'a> {
131    fn new(size: Size, transform: sk::Transform, pixel_per_pt: f32) -> Self {
132        Self {
133            size,
134            transform,
135            container_transform: transform,
136            pixel_per_pt,
137            ..Default::default()
138        }
139    }
140
141    /// Pre translate the current item's transform.
142    fn pre_translate(self, pos: Point) -> Self {
143        Self {
144            transform: self.transform.pre_translate(pos.x.to_f32(), pos.y.to_f32()),
145            ..self
146        }
147    }
148
149    fn pre_scale(self, scale: Axes<Abs>) -> Self {
150        Self {
151            transform: self.transform.pre_scale(scale.x.to_f32(), scale.y.to_f32()),
152            ..self
153        }
154    }
155
156    /// Pre concat the current item's transform.
157    fn pre_concat(self, transform: sk::Transform) -> Self {
158        Self {
159            transform: self.transform.pre_concat(transform),
160            ..self
161        }
162    }
163
164    /// Sets the current mask.
165    ///
166    /// If no mask is provided, the parent mask is used.
167    fn with_mask(self, mask: Option<&'a sk::Mask>) -> State<'a> {
168        State { mask: mask.or(self.mask), ..self }
169    }
170
171    /// Sets the size of the first hard frame in the hierarchy.
172    fn with_size(self, size: Size) -> Self {
173        Self { size, ..self }
174    }
175
176    /// Pre concat the container's transform.
177    fn pre_concat_container(self, transform: sk::Transform) -> Self {
178        Self {
179            container_transform: self.container_transform.pre_concat(transform),
180            ..self
181        }
182    }
183}
184
185/// Render a frame into the canvas.
186fn render_frame(canvas: &mut sk::Pixmap, state: State, frame: &Frame) {
187    for (pos, item) in frame.items() {
188        match item {
189            FrameItem::Group(group) => {
190                render_group(canvas, state, *pos, group);
191            }
192            FrameItem::Text(text) => {
193                text::render_text(canvas, state.pre_translate(*pos), text);
194            }
195            FrameItem::Shape(shape, _) => {
196                shape::render_shape(canvas, state.pre_translate(*pos), shape);
197            }
198            FrameItem::Image(image, size, _) => {
199                image::render_image(canvas, state.pre_translate(*pos), image, *size);
200            }
201            FrameItem::Link(_, _) => {}
202            FrameItem::Tag(_) => {}
203        }
204    }
205}
206
207/// Render a group frame with optional transform and clipping into the canvas.
208fn render_group(canvas: &mut sk::Pixmap, state: State, pos: Point, group: &GroupItem) {
209    let sk_transform = to_sk_transform(&group.transform);
210    let state = match group.frame.kind() {
211        FrameKind::Soft => state.pre_translate(pos).pre_concat(sk_transform),
212        FrameKind::Hard => state
213            .pre_translate(pos)
214            .pre_concat(sk_transform)
215            .pre_concat_container(
216                state
217                    .transform
218                    .post_concat(state.container_transform.invert().unwrap()),
219            )
220            .pre_concat_container(to_sk_transform(&Transform::translate(pos.x, pos.y)))
221            .pre_concat_container(sk_transform)
222            .with_size(group.frame.size()),
223    };
224
225    let mut mask = state.mask;
226    let storage;
227    if let Some(clip_curve) = group.clip.as_ref()
228        && let Some(path) = shape::convert_curve(clip_curve)
229            .and_then(|path| path.transform(state.transform))
230    {
231        if let Some(mask) = mask {
232            let mut mask = mask.clone();
233            mask.intersect_path(
234                &path,
235                sk::FillRule::default(),
236                true,
237                sk::Transform::default(),
238            );
239            storage = mask;
240        } else {
241            let pxw = canvas.width();
242            let pxh = canvas.height();
243            let Some(mut mask) = sk::Mask::new(pxw, pxh) else {
244                // Fails if clipping rect is empty. In that case we just
245                // clip everything by returning.
246                return;
247            };
248
249            mask.fill_path(
250                &path,
251                sk::FillRule::default(),
252                true,
253                sk::Transform::default(),
254            );
255            storage = mask;
256        };
257
258        mask = Some(&storage);
259    }
260
261    render_frame(canvas, state.with_mask(mask), &group.frame);
262}
263
264fn to_sk_transform(transform: &Transform) -> sk::Transform {
265    let Transform { sx, ky, kx, sy, tx, ty } = *transform;
266    sk::Transform::from_row(
267        sx.get() as _,
268        ky.get() as _,
269        kx.get() as _,
270        sy.get() as _,
271        tx.to_f32(),
272        ty.to_f32(),
273    )
274}
275
276/// Additional methods for [`Abs`].
277trait AbsExt {
278    /// Convert to a number of points as f32.
279    fn to_f32(self) -> f32;
280}
281
282impl AbsExt for Abs {
283    fn to_f32(self) -> f32 {
284        self.to_pt() as f32
285    }
286}