Skip to main content

pdfrum_render/
ctx.rs

1//! The render session's context and caches.
2//!
3//! [`RenderCtx`] is the state a nested render inherits, split into parts by
4//! concern: the borrowed [`RunCtx`] (stop conditions, region geometry),
5//! recursion [`Depth`], [`Inherited`] colours, the [`Type3Ancestry`] and the
6//! [`Nesting`] of transparency groups. [`RenderCaches`] is owned by the
7//! session rather than by a global.
8
9use pdfrum_common::Deadline;
10use pdfrum_font::{FontId, GlyphCache};
11use pdfrum_page::Transparency;
12
13use crate::color::Argb;
14use crate::options::RenderOptions;
15
16/// The render-recursion cap.
17///
18/// Upstream keeps this in a *process-global* counter, shared across
19/// concurrent renders in the same process; ours is a field, which is strictly
20/// more correct and never less permissive. The cap counts form, char-proc,
21/// pattern and soft-mask recursion, independently of the page layer's own
22/// parse-time form guard.
23pub const MAX_RECURSION_DEPTH: u32 = 64;
24
25/// How a type-3 glyph procedure's colour is decided.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Type3Colour {
28    /// The procedure declared only a width (`d1`): every drawing operation
29    /// inside it takes the frame's colour.
30    Imposed,
31    /// The procedure declared its own colour (`d0`): an operation with a
32    /// colour of its own keeps it.
33    Declared,
34}
35
36impl Type3Colour {
37    /// The colour mode a glyph metrics record's `colored` flag names.
38    #[must_use]
39    pub fn from_declared(declared: bool) -> Self {
40        if declared {
41            Self::Declared
42        } else {
43            Self::Imposed
44        }
45    }
46}
47
48/// A type-3 char proc's imposed colour and the char it is drawing.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct Type3Frame {
51    /// The colour every uncoloured drawing operation inside the proc takes.
52    pub fill: Argb,
53    /// Whether the glyph procedure declared its own colour.
54    pub colour: Type3Colour,
55}
56
57/// The render's stop conditions: its deadline and the caller's cancel.
58///
59/// Both are borrowed from the [`RenderSession`](crate::RenderSession), so
60/// the record is two pointers and not two deadlines. `None` is no limit.
61#[derive(Debug, Clone, Copy, Default)]
62pub struct Stop<'a> {
63    /// The run's deadline.
64    pub deadline: Option<&'a Deadline>,
65    /// The caller's own stop for this one render, borrowed from
66    /// [`RenderSession::cancel`](crate::RenderSession::cancel). Read beside
67    /// `deadline`; either passing ends the walk.
68    pub cancel: Option<&'a Deadline>,
69}
70
71impl Stop<'_> {
72    /// Whether the deadline or cancel is set and has passed — the per-object
73    /// read, kept to a branch when both are unset.
74    #[must_use]
75    pub fn passed(&self) -> bool {
76        self.deadline.is_some_and(Deadline::passed) || self.cancel.is_some_and(Deadline::passed)
77    }
78}
79
80/// What is fixed for one whole render, whatever the nesting: the stop
81/// conditions and the region geometry. Borrowed by every [`RenderCtx`]
82/// rather than copied into each.
83#[derive(Debug, Clone, Copy, Default)]
84pub struct RunCtx<'a> {
85    /// When the render must give up.
86    pub stop: Stop<'a>,
87    /// Device pixels the cull test grows the device box by. Zero for a whole
88    /// page; a region render's tile is not bounded by the page's edge, so an
89    /// object just outside it can still paint into it.
90    pub cull_margin: f64,
91}
92
93/// How deep the render recursion is, capped at [`MAX_RECURSION_DEPTH`].
94#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
95pub struct Depth(pub u32);
96
97impl Depth {
98    /// Whether another level of recursion is permitted.
99    #[must_use]
100    pub fn may_recurse(self) -> bool {
101        self.0 < MAX_RECURSION_DEPTH
102    }
103
104    /// One level deeper.
105    #[must_use]
106    pub fn deeper(self) -> Self {
107        Self(self.0.saturating_add(1))
108    }
109}
110
111/// The enclosing state's colours, which an object with none of its own
112/// inherits (`initial_states_`).
113#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
114pub struct Inherited {
115    /// The fill colour.
116    pub fill: Option<Argb>,
117    /// The same for strokes.
118    pub stroke: Option<Argb>,
119}
120
121impl Inherited {
122    /// Nothing inherited: the clean slate a group starts from
123    /// (`Initialize(null, null)`).
124    pub const NONE: Self = Self {
125        fill: None,
126        stroke: None,
127    };
128
129    /// One colour imposed on both fills and strokes.
130    #[must_use]
131    pub fn both(colour: Argb) -> Self {
132        Self {
133            fill: Some(colour),
134            stroke: Some(colour),
135        }
136    }
137}
138
139/// The type-3 ancestry: the frame being drawn and the fonts above it.
140#[derive(Debug, Clone, Copy, Default)]
141pub struct Type3Ancestry<'a> {
142    /// The type-3 frame, when inside a glyph procedure.
143    pub frame: Option<Type3Frame>,
144    /// The fonts already on the ancestry.
145    ///
146    /// A **set**, not a depth counter: a font may not appear twice anywhere
147    /// above the current procedure, which is what stops a glyph that draws
148    /// itself.
149    pub fonts: &'a [FontId],
150}
151
152impl Type3Ancestry<'_> {
153    /// Whether a font is already on the ancestry, which is what stops a glyph
154    /// procedure recursing into its own font.
155    #[must_use]
156    pub fn is_active(&self, font: FontId) -> bool {
157        self.fonts.contains(&font)
158    }
159}
160
161/// Whether a context is already inside a transparency group.
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum GroupNesting {
164    /// Not inside a group.
165    Outside,
166    /// Inside one, which stops a nested group re-applying the enclosing
167    /// group's alpha.
168    Inside,
169}
170
171/// The transparency state a nested context composites under.
172#[derive(Debug, Clone, Copy)]
173pub struct Nesting {
174    /// The enclosing holder's group flags. Note that a group composites back
175    /// under the *enclosing* transparency with `group` forced on, not under
176    /// its own.
177    pub transparency: Transparency,
178    /// Whether this context is already inside a transparency group.
179    pub group: GroupNesting,
180}
181
182impl Nesting {
183    /// The state inside a group whose own flags are `transparency`.
184    #[must_use]
185    pub fn in_group(transparency: Transparency) -> Self {
186        Self {
187            transparency,
188            group: GroupNesting::Inside,
189        }
190    }
191}
192
193/// The mutable state a render carries down through nested forms, patterns,
194/// glyph procedures and soft masks, split by concern. What is the same for
195/// the whole render lives in the borrowed [`RunCtx`], so a nested context
196/// clones a pointer to it rather than its fields.
197#[derive(Debug, Clone)]
198pub struct RenderCtx<'a> {
199    /// The render-wide stop and region geometry.
200    pub run: &'a RunCtx<'a>,
201    /// The caller's options, plus whatever a nested context forced on.
202    pub opts: RenderOptions,
203    /// How deep the render recursion is.
204    pub depth: Depth,
205    /// The colours an object with none of its own takes.
206    pub inherited: Inherited,
207    /// The type-3 frame and the fonts above it.
208    pub type3: Type3Ancestry<'a>,
209    /// The transparency this context composites under.
210    pub nesting: Nesting,
211}
212
213impl<'a> RenderCtx<'a> {
214    /// A fresh top-level context.
215    #[must_use]
216    pub fn new(run: &'a RunCtx<'a>, opts: RenderOptions, transparency: Transparency) -> Self {
217        Self {
218            run,
219            opts,
220            depth: Depth::default(),
221            inherited: Inherited::NONE,
222            type3: Type3Ancestry::default(),
223            nesting: Nesting {
224                transparency,
225                group: GroupNesting::Outside,
226            },
227        }
228    }
229
230    /// The same context one level deeper.
231    #[must_use]
232    pub fn deeper(&self) -> Self {
233        // A `RenderCtx` clone is a `RenderOptions` clone plus a handful of
234        // `Copy` fields, and `RenderOptions` is `Copy`-shaped but derives only
235        // `Clone` — so the interesting question is how often this runs, not
236        // how many bytes it moves. Counted at one byte per field-set so the
237        // count is the number and the byte column is not read as heap traffic.
238        crate::walkprofile::alloc_items(
239            crate::walkprofile::Site::CtxClone,
240            1,
241            core::mem::size_of::<Self>(),
242        );
243        Self {
244            depth: self.depth.deeper(),
245            ..self.clone()
246        }
247    }
248}
249
250/// Caches owned by one render session, passed down by the owner.
251///
252/// Scoping the glyph cache here rather than to a process makes the type-3
253/// blue-zone snapping deterministic: it is order-dependent by design, so a
254/// shared cache would make output depend on what else had been rendered.
255#[derive(Debug, Default)]
256pub struct RenderCaches {
257    /// Glyph outlines, keyed as `pdfrum-font` keys them.
258    ///
259    /// Feeds the *path* side of text: display type above the size threshold,
260    /// a stroked or pattern-coloured run, and a caller who asked for fractional
261    /// placement.
262    pub(crate) glyphs: GlyphCache,
263    /// Glyph bitmaps, keyed by the outline key plus the quantised device
264    /// matrix ([`crate::glyph::BitmapKey`]).
265    ///
266    /// Feeds the ordinary small-text path, which is most of the text in the
267    /// corpus. It is a second cache rather than a second field on the first
268    /// because the two are keyed differently — a bitmap depends on the size it
269    /// is drawn at and an outline does not — and because the outline cache
270    /// lives in `pdfrum-font`, which has no notion of a device.
271    pub(crate) glyph_bitmaps: crate::glyph::BitmapCache,
272    /// Rendered images: decoded samples converted to a premultiplied pixmap
273    /// and box-reduced toward their device footprint, keyed by the `XObject`
274    /// they came from and the shape of the request
275    /// (`crate::imagecache::PixmapRequest`).
276    ///
277    /// A third cache rather than a field on either of the others because it
278    /// is keyed by neither's key and holds neither's kind of thing: the
279    /// decoded samples upstream of it are `pdfrum-page`'s to cache, and what
280    /// is cached here is the two pure functions *downstream* of them.
281    pub(crate) images: crate::imagecache::RenderedImageCache,
282    /// The degenerate-sub-path scan's working buffers.
283    ///
284    /// Not a cache — nothing is remembered between paths, and it would be
285    /// wrong to remember anything, since the scan's answer depends on the
286    /// path. What is reused is the *memory*: the scan runs on every fill-only
287    /// path object, building a point list and a result list to answer
288    /// "nothing degenerate here" on the overwhelming majority of them.
289    pub(crate) zero_area: crate::zero_area::Scratch,
290    /// One text object's placed glyphs, refilled per object.
291    ///
292    /// The same kind of thing as [`Self::zero_area`] and for the same reason:
293    /// one `Vec<PlacedGlyph>` per text object, otherwise allocated afresh
294    /// thousands of times per render. Nothing is remembered between objects;
295    /// the memory is.
296    pub(crate) placed_glyphs: Vec<crate::text::PlacedGlyph>,
297    /// One glyph blit's two working buffers, refilled per glyph.
298    ///
299    /// The same kind of thing as [`Self::placed_glyphs`], one level finer.
300    /// The blit runs per glyph *occurrence* — tens of thousands of times on a
301    /// text-heavy page, against a few hundred distinct glyphs — and each
302    /// occurrence built a coverage `Vec` and a premultiplied `Pixmap` of its
303    /// own. Neither is a cache: the bytes depend on the glyph and the fill
304    /// colour and are rewritten in full every time. Only the memory is reused.
305    pub(crate) glyph_blit: GlyphBlitScratch,
306    /// Type 3 baseline snapping, keyed by the linear device matrix.
307    ///
308    /// Order-dependent by design, so it lives on the session rather than in
309    /// a process-wide cache: a shared one would make baselines depend on
310    /// what else had been drawn.
311    pub(crate) type3_blues: crate::type3::BlueCache,
312}
313
314/// The per-occurrence buffers a glyph blit fills.
315///
316/// Held on [`RenderCaches`] rather than built per glyph. Both are fully
317/// rewritten on every use, so nothing carries between glyphs but the
318/// allocation.
319#[derive(Debug)]
320pub(crate) struct GlyphBlitScratch {
321    /// [`crate::glyph::LcdBitmap::gray_coverage_into`]'s output.
322    pub(crate) coverage: Vec<u8>,
323    /// The premultiplied pixmap that coverage is recoloured into.
324    pub(crate) pixels: crate::Pixmap,
325}
326
327impl Default for GlyphBlitScratch {
328    fn default() -> Self {
329        Self {
330            coverage: Vec::new(),
331            // Zero-sized: `Pixmap::new` allocates nothing at this size, and
332            // the first glyph resizes it to its own. `Pixmap` is public and
333            // deliberately has no `Default`, so this is spelt out rather than
334            // derived.
335            pixels: crate::Pixmap::new(0, 0),
336        }
337    }
338}
339
340impl RenderCaches {
341    /// Empty caches for a new session.
342    #[must_use]
343    pub fn new() -> Self {
344        Self::default()
345    }
346
347    /// How much room [`Self::placed_glyphs`] is holding, for the integration
348    /// test that checks the walk hands the buffer back.
349    ///
350    /// Hidden rather than public: it is a fact about an internal buffer and
351    /// no caller has a use for it. It exists because the property it pins —
352    /// the walk `mem::take`s the buffer and must put it back — is invisible
353    /// to every pixel test, since output is byte-identical either way.
354    #[doc(hidden)]
355    #[must_use]
356    pub fn glyph_buffer_capacity(&self) -> usize {
357        self.placed_glyphs.capacity()
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    #[test]
366    fn depth_cap_is_64() {
367        let run = RunCtx::default();
368        let mut ctx = RenderCtx::new(&run, RenderOptions::default(), Transparency::default());
369        for _ in 0..MAX_RECURSION_DEPTH {
370            assert!(ctx.depth.may_recurse());
371            ctx = ctx.deeper();
372        }
373        assert!(!ctx.depth.may_recurse(), "the 65th level is refused");
374    }
375
376    #[test]
377    fn deeper_keeps_everything_but_the_depth() {
378        let run = RunCtx::default();
379        let ctx = RenderCtx {
380            inherited: Inherited::both(Argb::opaque(1, 2, 3)),
381            nesting: Nesting::in_group(Transparency::default()),
382            ..RenderCtx::new(&run, RenderOptions::default(), Transparency::default())
383        };
384        let child = ctx.deeper();
385        assert_eq!(child.inherited, ctx.inherited);
386        assert_eq!(child.nesting.group, GroupNesting::Inside);
387        assert_eq!(child.depth, Depth(1));
388    }
389
390    #[test]
391    fn the_type3_guard_is_a_set_not_a_depth() {
392        let run = RunCtx::default();
393        let fonts = [FontId(7), FontId(9)];
394        let ctx = RenderCtx {
395            type3: Type3Ancestry {
396                fonts: &fonts,
397                ..Type3Ancestry::default()
398            },
399            ..RenderCtx::new(&run, RenderOptions::default(), Transparency::default())
400        };
401        assert!(ctx.type3.is_active(FontId(7)));
402        assert!(ctx.type3.is_active(FontId(9)));
403        assert!(!ctx.type3.is_active(FontId(8)));
404    }
405}