Skip to main content

pdfrum_render/
ctx.rs

1//! The render session's context and caches.
2//!
3//! [`RenderCtx`] is a record every field of which some free function reads
4//! and none of which is read by all; [`RenderCaches`] is owned by the session
5//! rather than by a global.
6
7use pdfrum_common::Deadline;
8use pdfrum_font::{FontId, GlyphCache};
9use pdfrum_page::Transparency;
10
11use crate::color::Argb;
12use crate::options::RenderOptions;
13
14/// The render-recursion cap.
15///
16/// Upstream keeps this in a *process-global* counter, shared across
17/// concurrent renders in the same process; ours is a field, which is strictly
18/// more correct and never less permissive. The cap counts form, char-proc,
19/// pattern and soft-mask recursion, independently of the page layer's own
20/// parse-time form guard.
21pub const MAX_RECURSION_DEPTH: u32 = 64;
22
23/// A type-3 char proc's imposed colour and the char it is drawing.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct Type3Frame {
26    /// The colour every uncoloured drawing operation inside the proc takes.
27    pub fill: Argb,
28    /// Whether the glyph procedure declared its own colour (`d0`) rather
29    /// than only a width (`d1`).
30    pub colored: bool,
31}
32
33/// The mutable state a render carries down through nested forms, patterns,
34/// glyph procedures and soft masks.
35#[derive(Debug, Clone)]
36pub struct RenderCtx<'a> {
37    /// The caller's options, plus whatever a nested context forced on.
38    pub opts: RenderOptions,
39    /// How deep the render recursion is, capped at [`MAX_RECURSION_DEPTH`].
40    pub depth: u32,
41    /// The enclosing state's colours, which an object with none of its own
42    /// inherits (`initial_states_`).
43    pub initial_fill: Option<Argb>,
44    /// The same for strokes.
45    pub initial_stroke: Option<Argb>,
46    /// The type-3 frame, when inside a glyph procedure.
47    pub type3: Option<Type3Frame>,
48    /// The fonts already on the type-3 ancestry.
49    ///
50    /// A **set**, not a depth counter: a font may not appear twice anywhere
51    /// above the current procedure, which is what stops a glyph that draws
52    /// itself.
53    pub type3_fonts: &'a [FontId],
54    /// The enclosing holder's group flags. Note that a group composites back
55    /// under the *enclosing* transparency with `group` forced on, not under
56    /// its own.
57    pub transparency: Transparency,
58    /// Whether this context is already inside a transparency group, which
59    /// stops a nested group re-applying the enclosing group's alpha.
60    pub in_group: bool,
61    /// The run's deadline, borrowed from the [`RenderSession`](crate::RenderSession)
62    /// so that the record every nested context clones grows by a pointer and
63    /// not by the deadline itself. `None` is no limit.
64    pub deadline: Option<&'a Deadline>,
65}
66
67impl RenderCtx<'_> {
68    /// A fresh top-level context.
69    #[must_use]
70    pub fn new(opts: RenderOptions, transparency: Transparency) -> Self {
71        Self {
72            opts,
73            depth: 0,
74            initial_fill: None,
75            initial_stroke: None,
76            type3: None,
77            type3_fonts: &[],
78            transparency,
79            in_group: false,
80            deadline: None,
81        }
82    }
83
84    /// Whether the run's deadline is set and has passed — the per-object
85    /// read, kept to a branch when unset.
86    #[must_use]
87    pub fn out_of_time(&self) -> bool {
88        self.deadline.is_some_and(Deadline::passed)
89    }
90
91    /// Whether another level of recursion is permitted.
92    #[must_use]
93    pub fn may_recurse(&self) -> bool {
94        self.depth < MAX_RECURSION_DEPTH
95    }
96
97    /// The same context one level deeper.
98    #[must_use]
99    pub fn deeper(&self) -> Self {
100        // A `RenderCtx` clone is a `RenderOptions` clone plus a handful of
101        // `Copy` fields, and `RenderOptions` is `Copy`-shaped but derives only
102        // `Clone` — so the interesting question is how often this runs, not
103        // how many bytes it moves. Counted at one byte per field-set so the
104        // count is the number and the byte column is not read as heap traffic.
105        crate::walkprofile::alloc_items(
106            crate::walkprofile::Site::CtxClone,
107            1,
108            core::mem::size_of::<Self>(),
109        );
110        Self {
111            depth: self.depth.saturating_add(1),
112            ..self.clone()
113        }
114    }
115
116    /// Whether a font is already on the type-3 ancestry, which is what stops
117    /// a glyph procedure recursing into its own font.
118    #[must_use]
119    pub fn type3_font_is_active(&self, font: FontId) -> bool {
120        self.type3_fonts.contains(&font)
121    }
122}
123
124/// Caches owned by one render session, passed down by the owner.
125///
126/// Scoping the glyph cache here rather than to a process makes the type-3
127/// blue-zone snapping deterministic: it is order-dependent by design, so a
128/// shared cache would make output depend on what else had been rendered.
129#[derive(Debug, Default)]
130pub struct RenderCaches {
131    /// Glyph outlines, keyed as `pdfrum-font` keys them.
132    ///
133    /// Feeds the *path* side of text: display type above the size threshold,
134    /// a stroked or pattern-coloured run, and a caller who asked for fractional
135    /// placement.
136    pub(crate) glyphs: GlyphCache,
137    /// Glyph bitmaps, keyed by the outline key plus the quantised device
138    /// matrix ([`crate::glyph::BitmapKey`]).
139    ///
140    /// Feeds the ordinary small-text path, which is most of the text in the
141    /// corpus. It is a second cache rather than a second field on the first
142    /// because the two are keyed differently — a bitmap depends on the size it
143    /// is drawn at and an outline does not — and because the outline cache
144    /// lives in `pdfrum-font`, which has no notion of a device.
145    pub(crate) glyph_bitmaps: crate::glyph::BitmapCache,
146    /// Rendered images: decoded samples converted to a premultiplied pixmap
147    /// and box-reduced toward their device footprint, keyed by the `XObject`
148    /// they came from and the shape of the request
149    /// (`crate::imagecache::PixmapRequest`).
150    ///
151    /// A third cache rather than a field on either of the others because it
152    /// is keyed by neither's key and holds neither's kind of thing: the
153    /// decoded samples upstream of it are `pdfrum-page`'s to cache, and what
154    /// is cached here is the two pure functions *downstream* of them.
155    pub(crate) images: crate::imagecache::RenderedImageCache,
156    /// The degenerate-sub-path scan's working buffers.
157    ///
158    /// Not a cache — nothing is remembered between paths, and it would be
159    /// wrong to remember anything, since the scan's answer depends on the
160    /// path. What is reused is the *memory*: the scan runs on every fill-only
161    /// path object, building a point list and a result list to answer
162    /// "nothing degenerate here" on the overwhelming majority of them.
163    pub(crate) zero_area: crate::zero_area::Scratch,
164    /// One text object's placed glyphs, refilled per object.
165    ///
166    /// The same kind of thing as [`Self::zero_area`] and for the same reason:
167    /// one `Vec<PlacedGlyph>` per text object, otherwise allocated afresh
168    /// thousands of times per render. Nothing is remembered between objects;
169    /// the memory is.
170    pub(crate) placed_glyphs: Vec<crate::text::PlacedGlyph>,
171    /// One glyph blit's two working buffers, refilled per glyph.
172    ///
173    /// The same kind of thing as [`Self::placed_glyphs`], one level finer.
174    /// The blit runs per glyph *occurrence* — tens of thousands of times on a
175    /// text-heavy page, against a few hundred distinct glyphs — and each
176    /// occurrence built a coverage `Vec` and a premultiplied `Pixmap` of its
177    /// own. Neither is a cache: the bytes depend on the glyph and the fill
178    /// colour and are rewritten in full every time. Only the memory is reused.
179    pub(crate) glyph_blit: GlyphBlitScratch,
180    /// Type 3 baseline snapping, keyed by the linear device matrix.
181    ///
182    /// Order-dependent by design, so it lives on the session rather than in
183    /// a process-wide cache: a shared one would make baselines depend on
184    /// what else had been drawn.
185    pub(crate) type3_blues: crate::type3::BlueCache,
186}
187
188/// The per-occurrence buffers a glyph blit fills.
189///
190/// Held on [`RenderCaches`] rather than built per glyph. Both are fully
191/// rewritten on every use, so nothing carries between glyphs but the
192/// allocation.
193#[derive(Debug)]
194pub(crate) struct GlyphBlitScratch {
195    /// [`crate::glyph::LcdBitmap::gray_coverage_into`]'s output.
196    pub(crate) coverage: Vec<u8>,
197    /// The premultiplied pixmap that coverage is recoloured into.
198    pub(crate) pixels: crate::Pixmap,
199}
200
201impl Default for GlyphBlitScratch {
202    fn default() -> Self {
203        Self {
204            coverage: Vec::new(),
205            // Zero-sized: `Pixmap::new` allocates nothing at this size, and
206            // the first glyph resizes it to its own. `Pixmap` is public and
207            // deliberately has no `Default`, so this is spelt out rather than
208            // derived.
209            pixels: crate::Pixmap::new(0, 0),
210        }
211    }
212}
213
214impl RenderCaches {
215    /// Empty caches for a new session.
216    #[must_use]
217    pub fn new() -> Self {
218        Self::default()
219    }
220
221    /// How much room [`Self::placed_glyphs`] is holding, for the integration
222    /// test that checks the walk hands the buffer back.
223    ///
224    /// Hidden rather than public: it is a fact about an internal buffer and
225    /// no caller has a use for it. It exists because the property it pins —
226    /// the walk `mem::take`s the buffer and must put it back — is invisible
227    /// to every pixel test, since output is byte-identical either way.
228    #[doc(hidden)]
229    #[must_use]
230    pub fn glyph_buffer_capacity(&self) -> usize {
231        self.placed_glyphs.capacity()
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn depth_cap_is_64() {
241        let mut ctx = RenderCtx::new(RenderOptions::default(), Transparency::default());
242        for _ in 0..MAX_RECURSION_DEPTH {
243            assert!(ctx.may_recurse());
244            ctx = ctx.deeper();
245        }
246        assert!(!ctx.may_recurse(), "the 65th level is refused");
247    }
248
249    #[test]
250    fn deeper_keeps_everything_but_the_depth() {
251        let ctx = RenderCtx {
252            initial_fill: Some(Argb::opaque(1, 2, 3)),
253            in_group: true,
254            ..RenderCtx::new(RenderOptions::default(), Transparency::default())
255        };
256        let child = ctx.deeper();
257        assert_eq!(child.initial_fill, ctx.initial_fill);
258        assert!(child.in_group);
259        assert_eq!(child.depth, 1);
260    }
261
262    #[test]
263    fn the_type3_guard_is_a_set_not_a_depth() {
264        let fonts = [FontId(7), FontId(9)];
265        let ctx = RenderCtx {
266            type3_fonts: &fonts,
267            ..RenderCtx::new(RenderOptions::default(), Transparency::default())
268        };
269        assert!(ctx.type3_font_is_active(FontId(7)));
270        assert!(ctx.type3_font_is_active(FontId(9)));
271        assert!(!ctx.type3_font_is_active(FontId(8)));
272    }
273}