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}