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}
181
182/// The per-occurrence buffers a glyph blit fills.
183///
184/// Held on [`RenderCaches`] rather than built per glyph. Both are fully
185/// rewritten on every use, so nothing carries between glyphs but the
186/// allocation.
187#[derive(Debug)]
188pub(crate) struct GlyphBlitScratch {
189 /// [`crate::glyph::LcdBitmap::gray_coverage_into`]'s output.
190 pub(crate) coverage: Vec<u8>,
191 /// The premultiplied pixmap that coverage is recoloured into.
192 pub(crate) pixels: crate::Pixmap,
193}
194
195impl Default for GlyphBlitScratch {
196 fn default() -> Self {
197 Self {
198 coverage: Vec::new(),
199 // Zero-sized: `Pixmap::new` allocates nothing at this size, and
200 // the first glyph resizes it to its own. `Pixmap` is public and
201 // deliberately has no `Default`, so this is spelt out rather than
202 // derived.
203 pixels: crate::Pixmap::new(0, 0),
204 }
205 }
206}
207
208impl RenderCaches {
209 /// Empty caches for a new session.
210 #[must_use]
211 pub fn new() -> Self {
212 Self::default()
213 }
214
215 /// How much room [`Self::placed_glyphs`] is holding, for the integration
216 /// test that checks the walk hands the buffer back.
217 ///
218 /// Hidden rather than public: it is a fact about an internal buffer and
219 /// no caller has a use for it. It exists because the property it pins —
220 /// the walk `mem::take`s the buffer and must put it back — is invisible
221 /// to every pixel test, since output is byte-identical either way.
222 #[doc(hidden)]
223 #[must_use]
224 pub fn glyph_buffer_capacity(&self) -> usize {
225 self.placed_glyphs.capacity()
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn depth_cap_is_64() {
235 let mut ctx = RenderCtx::new(RenderOptions::default(), Transparency::default());
236 for _ in 0..MAX_RECURSION_DEPTH {
237 assert!(ctx.may_recurse());
238 ctx = ctx.deeper();
239 }
240 assert!(!ctx.may_recurse(), "the 65th level is refused");
241 }
242
243 #[test]
244 fn deeper_keeps_everything_but_the_depth() {
245 let ctx = RenderCtx {
246 initial_fill: Some(Argb::opaque(1, 2, 3)),
247 in_group: true,
248 ..RenderCtx::new(RenderOptions::default(), Transparency::default())
249 };
250 let child = ctx.deeper();
251 assert_eq!(child.initial_fill, ctx.initial_fill);
252 assert!(child.in_group);
253 assert_eq!(child.depth, 1);
254 }
255
256 #[test]
257 fn the_type3_guard_is_a_set_not_a_depth() {
258 let fonts = [FontId(7), FontId(9)];
259 let ctx = RenderCtx {
260 type3_fonts: &fonts,
261 ..RenderCtx::new(RenderOptions::default(), Transparency::default())
262 };
263 assert!(ctx.type3_font_is_active(FontId(7)));
264 assert!(ctx.type3_font_is_active(FontId(9)));
265 assert!(!ctx.type3_font_is_active(FontId(8)));
266 }
267}