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}