pdfrum_doc/ap/mod.rs
1//! Appearance-stream generation: turning an annotation's dictionary into the
2//! content stream a viewer draws.
3//!
4//! # The overlay
5//!
6//! Generating an appearance is conventionally a **mutation of the
7//! document**. A sticky note's `/Rect` is replaced with a 20×20 box; an ink
8//! annotation's is inflated; every annotation touched gains an `/AP /N`
9//! pointing at a new stream and a marker key saying so. Everything that reads
10//! the file afterwards sees the mutated state, which is why a dump reports
11//! 20×20 rectangles for sticky notes whose files say otherwise.
12//!
13//! Parsed objects here are values and the parser's store is immutable, so
14//! [`generate_appearances`] returns an [`AnnotOverlay`] instead: one entry per
15//! `/Annots` index recording the stream it produced and the dictionary edits
16//! it implies. Readers consult the overlay before the dictionary.
17//!
18//! # Which annotations get one
19//!
20//! Thirteen subtypes have a generator, and a widget annotation with no `/AP`
21//! dictionary gets its chrome from [`widget`] besides — plus, when the caller
22//! has fonts to set text with, the field body [`field_body`] lays out.
23//! Generation is refused
24//! outright when the
25//! annotation is hidden, or when `/AP /N` already reads as a dictionary —
26//! and a **stream** answers as its own dictionary, so the ordinary "it
27//! already has an appearance" case is covered by the same test. Only a
28//! missing `/AP`, a missing `/N`, or a scalar `/N` leaves the door open.
29
30mod border;
31pub use border::BorderStyle;
32mod da;
33pub(crate) mod emit;
34pub mod field_body;
35pub(crate) mod fmt;
36pub mod font_map;
37pub mod freetext;
38mod markup;
39pub(crate) mod popup;
40mod shapes;
41pub mod widget;
42
43use kurbo::{Affine, Rect};
44use pdfrum_common::{DiagKind, Diagnostics, Severity};
45use pdfrum_object::{Array, Dict, Name, Object, Resolve, names as obj_names};
46
47use crate::annot::{Subtype, appearance, quad};
48use crate::names;
49use crate::vt;
50
51/// One generated appearance and the dictionary edits it implies.
52///
53/// ```
54/// use pdfrum_common::Diagnostics;
55/// use pdfrum_doc::ap::generate_appearances;
56/// use pdfrum_object::{Array, Dict, Name, NoResolve, Object};
57///
58/// let square = Dict::from_pairs([
59/// (Name::from("Subtype"), Object::Name(Name::from("Square"))),
60/// (
61/// Name::from("Rect"),
62/// Object::Array(Array::of([0, 0, 100, 50])),
63/// ),
64/// ]);
65/// let page = Dict::from_pairs([(
66/// Name::from("Annots"),
67/// Object::Array(Array::of([Object::Dict(square)])),
68/// )]);
69///
70/// let mut diags = Diagnostics::default();
71/// let overlay = generate_appearances(&page, &NoResolve, &mut diags);
72/// let generated = overlay.get(0).expect("a square has a generator");
73///
74/// // The matrix these generators produce is always the identity.
75/// assert_eq!(generated.matrix, kurbo::Affine::IDENTITY);
76/// ```
77#[derive(Debug, Clone, PartialEq)]
78pub struct GeneratedAp {
79 /// The content-stream bytes.
80 pub stream: Vec<u8>,
81 /// The form `XObject`'s bounding box.
82 pub bbox: Rect,
83 /// Its matrix, which these generators always leave as the identity.
84 pub matrix: Affine,
85 /// Its resource dictionary.
86 pub resources: Dict,
87 /// A rewritten `/Rect`, when generation moved one.
88 pub rect_override: Option<Rect>,
89 /// A copied-down `/AS`, for the `/NeedAppearances` widget path.
90 pub as_override: Option<Name>,
91}
92
93/// What an overlay says about one annotation.
94///
95/// Three states, not two, and the third is why this is an enum rather than an
96/// `Option`. "Nothing was generated" and "this annotation draws nothing" are
97/// different instructions: the first falls through to whatever `/AP` the file
98/// carries, the second **suppresses** it. A field whose appearance has been
99/// cleared — focus left it and it went back to drawing nothing — needs the
100/// second, and expressing it as the absence of an entry would make it
101/// indistinguishable from the first.
102///
103/// ```
104/// use pdfrum_doc::{AnnotOverlay, ap::Appearance};
105///
106/// let mut overlay = AnnotOverlay::with_capacity(2);
107/// assert_eq!(overlay.appearance(0), &Appearance::Untouched);
108///
109/// // Suppressed is not the same as untouched: it says "draw nothing",
110/// // even for an annotation the file gave an `/AP`.
111/// overlay.set_appearance(0, Appearance::Suppressed);
112/// assert!(overlay.get(0).is_none());
113/// ```
114#[derive(Debug, Clone, Default, PartialEq)]
115pub enum Appearance {
116 /// Nothing to say. The file's own `/AP` is used, if it has one.
117 #[default]
118 Untouched,
119 /// Draw this instead of the file's `/AP`.
120 Generated(GeneratedAp),
121 /// Draw nothing at all, even if the file carries an `/AP`.
122 ///
123 /// Nothing sets this yet. It exists so a cleared appearance has a
124 /// spelling that is not "absent", which is what keeps the merge below
125 /// able to express one later without changing shape.
126 Suppressed,
127}
128
129/// The shape a focused widget's focus rectangle takes.
130///
131/// A widget being edited has a live control behind it, and what that control
132/// answers when asked for a focus rectangle depends on which control it is —
133/// three answers, not one, and two of the three are *no rectangle at all*:
134///
135/// - A **text field** and a **combo box** — editable or not — answer an empty
136/// rectangle outright, so nothing is stroked over them. This is the common
137/// case and it is why the focused text-field goldens carry a caret and
138/// glyphs but no outline. Editability does **not** enter into it: a caller
139/// that inflates a read-only combo strokes a box that must not be drawn.
140/// - A **check box**, a **radio button** and a **single-select list box**
141/// answer their window rectangle inflated by one unit on every side, which
142/// is [`FocusBox::Inflated`]. A list box falls through to that answer when
143/// it is not multi-select.
144/// - A **multi-select list box** answers the rectangle of the item its caret
145/// sits on, clipped to the client area — a rectangle only the list control's
146/// own scroll and caret state can name, so a caller that has it supplies it
147/// as [`FocusBox::Rect`].
148///
149/// `annot_render`'s own table says the same thing; the two are kept in step
150/// deliberately, because this is the one a `pdfrum-form` caller reads.
151///
152/// [`FocusBox::None`] is the empty answer and the default: a focused entry
153/// that names it is still *focused* — it draws no tint — and simply strokes
154/// nothing.
155///
156/// ```
157/// use pdfrum_doc::{FocusBox, geom};
158///
159/// // A text field and an editable combo box stroke nothing.
160/// assert_eq!(FocusBox::default(), FocusBox::None);
161/// // A multi-select list box names the rectangle only it can compute.
162/// let explicit = FocusBox::Rect(geom::rect(0.0, 0.0, 100.0, 20.0));
163/// assert_ne!(explicit, FocusBox::Inflated);
164/// ```
165#[derive(Debug, Clone, Copy, Default, PartialEq)]
166pub enum FocusBox {
167 /// No rectangle: nothing is stroked. A text field and an editable combo
168 /// box always answer this.
169 #[default]
170 None,
171 /// The annotation's own rectangle, inflated by one unit on every side.
172 Inflated,
173 /// An explicit rectangle in page space, already in its final position.
174 Rect(Rect),
175}
176
177/// Which annotation on the page holds the keyboard focus, and what its focus
178/// rectangle is.
179///
180/// Both halves are needed and they are independent. The *index* alone decides
181/// the tint: a widget with a live control is never tinted, focused or not, and
182/// the focused one is the only widget a live control reaches in a
183/// single-focus session. The *box* decides whether anything is stroked in its
184/// place, which most field types answer with nothing.
185///
186/// ```
187/// use pdfrum_doc::{AnnotOverlay, Focus, FocusBox, geom};
188///
189/// let mut overlay = AnnotOverlay::with_capacity(2);
190/// overlay.set_focus(Focus {
191/// annot: 1,
192/// box_: FocusBox::Rect(geom::rect(0.0, 0.0, 100.0, 20.0)),
193/// });
194/// assert_eq!(overlay.focus().map(|f| f.annot), Some(1));
195/// ```
196#[derive(Debug, Clone, Copy, PartialEq)]
197pub struct Focus {
198 /// The raw `/Annots` index of the focused annotation — the same key space
199 /// [`AnnotOverlay::set`] uses.
200 pub annot: usize,
201 /// The rectangle to stroke, in page space.
202 pub box_: FocusBox,
203}
204
205impl Focus {
206 /// Focus on one annotation with no rectangle to stroke — the answer a
207 /// text field and an editable combo box give.
208 ///
209 /// ```
210 /// use pdfrum_doc::{Focus, FocusBox};
211 ///
212 /// let focus = Focus::at(2);
213 /// assert_eq!(focus.annot, 2);
214 /// // No rectangle to stroke, which is what a text field answers.
215 /// assert_eq!(focus.box_, FocusBox::None);
216 /// ```
217 #[must_use]
218 pub fn at(annot: usize) -> Focus {
219 Focus {
220 annot,
221 box_: FocusBox::None,
222 }
223 }
224}
225
226/// Per-annotation generated appearances, keyed by `/Annots` index.
227///
228/// Besides the per-annotation entries the overlay carries at most one
229/// [`Focus`], because a session focuses one field at a time. It travels here
230/// rather than as another parameter on the annotation pass for two reasons:
231/// it is set by the same session that sets the appearances, from the same
232/// index space, and adding it here left every existing caller compiling
233/// unchanged.
234///
235/// ```
236/// use pdfrum_common::Diagnostics;
237/// use pdfrum_doc::ap::generate_appearances;
238/// use pdfrum_object::{Array, Dict, Name, NoResolve, Object};
239///
240/// let square = Dict::from_pairs([
241/// (Name::from("Subtype"), Object::Name(Name::from("Square"))),
242/// (
243/// Name::from("Rect"),
244/// Object::Array(Array::of([0, 0, 100, 50])),
245/// ),
246/// ]);
247/// let page = Dict::from_pairs([(
248/// Name::from("Annots"),
249/// Object::Array(Array::of([Object::Dict(square)])),
250/// )]);
251///
252/// // Every reader in this crate takes the overlay and consults it
253/// // before the raw dictionary.
254/// let mut diags = Diagnostics::default();
255/// let overlay = generate_appearances(&page, &NoResolve, &mut diags);
256/// assert!(overlay.get(0).is_some());
257/// ```
258#[derive(Debug, Clone, Default, PartialEq)]
259pub struct AnnotOverlay {
260 entries: Vec<Appearance>,
261 focus: Option<Focus>,
262 hover: Option<usize>,
263 live_edit: Option<usize>,
264}
265
266impl AnnotOverlay {
267 /// An overlay with room for `count` annotations and nothing generated.
268 ///
269 /// ```
270 /// use pdfrum_doc::{AnnotOverlay, ap::Appearance};
271 ///
272 /// let overlay = AnnotOverlay::with_capacity(3);
273 /// assert_eq!(overlay.len(), 3);
274 /// assert_eq!(overlay.appearance(0), &Appearance::Untouched);
275 /// ```
276 #[must_use]
277 pub fn with_capacity(count: usize) -> AnnotOverlay {
278 AnnotOverlay {
279 entries: vec![Appearance::Untouched; count],
280 focus: None,
281 hover: None,
282 live_edit: None,
283 }
284 }
285
286 /// Records which annotation holds the focus, and what to stroke over it.
287 ///
288 /// The index is a raw `/Annots` index. It is **not** bounded by the
289 /// overlay's length: an overlay sized for the appearances it carries can
290 /// still name a focused annotation past its end, and the annotation pass
291 /// keys on the index rather than on an entry.
292 ///
293 /// ```
294 /// use pdfrum_doc::{AnnotOverlay, Focus};
295 ///
296 /// let mut overlay = AnnotOverlay::with_capacity(2);
297 /// // The index is a raw `/Annots` index and is not bounded by the length.
298 /// overlay.set_focus(Focus::at(7));
299 /// assert_eq!(overlay.focus().map(|f| f.annot), Some(7));
300 /// ```
301 pub fn set_focus(&mut self, focus: Focus) {
302 self.focus = Some(focus);
303 }
304
305 /// Which annotation holds the focus, if any.
306 ///
307 /// ```
308 /// use pdfrum_doc::{AnnotOverlay, Focus};
309 ///
310 /// let mut overlay = AnnotOverlay::with_capacity(2);
311 /// assert!(overlay.focus().is_none());
312 /// overlay.set_focus(Focus::at(1));
313 /// assert_eq!(overlay.focus().map(|f| f.annot), Some(1));
314 /// ```
315 #[must_use]
316 pub fn focus(&self) -> Option<Focus> {
317 self.focus
318 }
319
320 /// Records which annotation the pointer is inside.
321 ///
322 /// A raw `/Annots` index, like [`Self::set_focus`]'s, and equally
323 /// unbounded by the overlay's length. Hover is a separate fact from focus
324 /// and the two move independently: a pointer resting on an annotation
325 /// leaves the keyboard focus wherever it was, and the annotation under the
326 /// pointer need not be focusable at all — a highlight is the case that
327 /// matters, since it is *only* reachable this way.
328 ///
329 /// What it decides is whether that annotation's synthesized pop-up note is
330 /// **open**. A note card is drawn only while the pointer is inside its
331 /// parent, and nothing a file can say opens one, so this is the whole of
332 /// the signal.
333 ///
334 /// ```
335 /// use pdfrum_doc::AnnotOverlay;
336 ///
337 /// let mut overlay = AnnotOverlay::with_capacity(4);
338 /// overlay.set_hover(1);
339 /// assert_eq!(overlay.hover(), Some(1));
340 /// // Hover and focus move independently.
341 /// assert!(overlay.focus().is_none());
342 /// ```
343 pub fn set_hover(&mut self, annot: usize) {
344 self.hover = Some(annot);
345 }
346
347 /// Which annotation the pointer is inside, if any.
348 ///
349 /// ```
350 /// use pdfrum_doc::AnnotOverlay;
351 ///
352 /// let mut overlay = AnnotOverlay::with_capacity(4);
353 /// assert!(overlay.hover().is_none());
354 /// overlay.set_hover(0);
355 /// assert_eq!(overlay.hover(), Some(0));
356 /// ```
357 #[must_use]
358 pub fn hover(&self) -> Option<usize> {
359 self.hover
360 }
361
362 /// Records that one annotation's supplied appearance is a **live edit's**
363 /// — the field the session is currently typing in.
364 ///
365 /// A raw `/Annots` index, like [`Self::set_focus`]'s and equally unbounded
366 /// by the overlay's length. At most one annotation can be under live edit,
367 /// because a session focuses one field at a time; a second call replaces
368 /// the first rather than accumulating.
369 ///
370 /// It is a separate signal from focus, and the two are **not**
371 /// interchangeable. A field can hold the focus without being edited — it
372 /// was tabbed to and nothing has been typed — in which case the session
373 /// generates no appearance for it and there is nothing to mark. What this
374 /// records is that the appearance carried at this index came from an
375 /// editor, which is what makes the oracle draw its text with `ClearType`.
376 ///
377 /// ```
378 /// use pdfrum_doc::AnnotOverlay;
379 ///
380 /// let mut overlay = AnnotOverlay::with_capacity(4);
381 /// overlay.set_live_edit(1);
382 /// // A second call replaces the first: one field is edited at a time.
383 /// overlay.set_live_edit(2);
384 /// assert_eq!(overlay.live_edit(), Some(2));
385 /// ```
386 pub fn set_live_edit(&mut self, annot: usize) {
387 self.live_edit = Some(annot);
388 }
389
390 /// Which annotation's appearance is a live edit's, if any.
391 ///
392 /// ```
393 /// use pdfrum_doc::AnnotOverlay;
394 ///
395 /// let mut overlay = AnnotOverlay::with_capacity(4);
396 /// assert!(overlay.live_edit().is_none());
397 /// overlay.set_live_edit(3);
398 /// assert_eq!(overlay.live_edit(), Some(3));
399 /// ```
400 #[must_use]
401 pub fn live_edit(&self) -> Option<usize> {
402 self.live_edit
403 }
404
405 /// Whether the appearance at one `/Annots` index came from a live edit.
406 ///
407 /// ```
408 /// use pdfrum_doc::AnnotOverlay;
409 ///
410 /// let mut overlay = AnnotOverlay::with_capacity(4);
411 /// overlay.set_live_edit(1);
412 /// assert!(overlay.is_live_edit(1));
413 /// assert!(!overlay.is_live_edit(0));
414 /// ```
415 #[must_use]
416 pub fn is_live_edit(&self, index: usize) -> bool {
417 self.live_edit == Some(index)
418 }
419
420 /// Records a generated appearance at one `/Annots` index.
421 ///
422 /// ```
423 /// use pdfrum_common::Diagnostics;
424 /// use pdfrum_doc::ap::generate_appearances;
425 /// use pdfrum_object::{Array, Dict, Name, NoResolve, Object};
426 ///
427 /// let square = Dict::from_pairs([
428 /// (Name::from("Subtype"), Object::Name(Name::from("Square"))),
429 /// (
430 /// Name::from("Rect"),
431 /// Object::Array(Array::of([0, 0, 100, 50])),
432 /// ),
433 /// (
434 /// Name::from("IC"),
435 /// Object::Array(Array::of([1, 0, 0])),
436 /// ),
437 /// ]);
438 /// let page = Dict::from_pairs([(
439 /// Name::from("Annots"),
440 /// Object::Array(Array::of([Object::Dict(square)])),
441 /// )]);
442 ///
443 /// let mut diags = Diagnostics::default();
444 /// let overlay = generate_appearances(&page, &NoResolve, &mut diags);
445 ///
446 /// // The walk sets index 0; a caller can set any index the same way.
447 /// let generated = overlay.get(0).expect("a square has a generator").clone();
448 /// let mut mine = pdfrum_doc::AnnotOverlay::with_capacity(2);
449 /// mine.set(1, generated);
450 /// assert!(mine.get(1).is_some());
451 /// ```
452 pub fn set(&mut self, index: usize, generated: GeneratedAp) {
453 self.set_appearance(index, Appearance::Generated(generated));
454 }
455
456 /// Records any of the three states at one `/Annots` index.
457 ///
458 /// ```
459 /// use pdfrum_doc::{AnnotOverlay, ap::Appearance};
460 ///
461 /// let mut overlay = AnnotOverlay::with_capacity(2);
462 /// overlay.set_appearance(0, Appearance::Suppressed);
463 /// assert_eq!(overlay.appearance(0), &Appearance::Suppressed);
464 /// // A suppressed entry has no stream to draw.
465 /// assert!(overlay.get(0).is_none());
466 /// ```
467 pub fn set_appearance(&mut self, index: usize, appearance: Appearance) {
468 if let Some(slot) = self.entries.get_mut(index) {
469 *slot = appearance;
470 }
471 }
472
473 /// What was generated at one `/Annots` index, if anything.
474 ///
475 /// A suppressed entry answers [`None`], the same as an untouched one —
476 /// callers that only want a stream to draw need not distinguish them.
477 /// [`AnnotOverlay::appearance`] is what tells them apart.
478 ///
479 /// ```
480 /// use pdfrum_common::Diagnostics;
481 /// use pdfrum_doc::ap::generate_appearances;
482 /// use pdfrum_object::{Array, Dict, Name, NoResolve, Object};
483 ///
484 /// let square = Dict::from_pairs([
485 /// (Name::from("Subtype"), Object::Name(Name::from("Square"))),
486 /// (
487 /// Name::from("Rect"),
488 /// Object::Array(Array::of([0, 0, 100, 50])),
489 /// ),
490 /// (
491 /// Name::from("IC"),
492 /// Object::Array(Array::of([1, 0, 0])),
493 /// ),
494 /// ]);
495 /// let page = Dict::from_pairs([(
496 /// Name::from("Annots"),
497 /// Object::Array(Array::of([Object::Dict(square)])),
498 /// )]);
499 ///
500 /// let mut diags = Diagnostics::default();
501 /// let overlay = generate_appearances(&page, &NoResolve, &mut diags);
502 ///
503 /// assert!(overlay.get(0).is_some());
504 /// // Past the end is `None`, not a panic.
505 /// assert!(overlay.get(9).is_none());
506 /// ```
507 #[must_use]
508 pub fn get(&self, index: usize) -> Option<&GeneratedAp> {
509 match self.appearance(index) {
510 Appearance::Generated(generated) => Some(generated),
511 Appearance::Untouched | Appearance::Suppressed => None,
512 }
513 }
514
515 /// The full state at one `/Annots` index, suppression included.
516 ///
517 /// An index past the overlay's end reads as [`Appearance::Untouched`],
518 /// which is what makes a short overlay safe to consult for any index.
519 ///
520 /// ```
521 /// use pdfrum_doc::{AnnotOverlay, ap::Appearance};
522 ///
523 /// let overlay = AnnotOverlay::with_capacity(1);
524 /// // An index past the end reads as untouched, so a short overlay is
525 /// // safe to consult for any index.
526 /// assert_eq!(overlay.appearance(99), &Appearance::Untouched);
527 /// ```
528 #[must_use]
529 pub fn appearance(&self, index: usize) -> &Appearance {
530 self.entries.get(index).unwrap_or(&Appearance::Untouched)
531 }
532
533 /// Lays `other`'s entries over this one's.
534 ///
535 /// Every entry `other` has anything to say about — generated **or**
536 /// suppressed — replaces this overlay's, and its [`Appearance::Untouched`]
537 /// entries leave this one's alone. So a caller-supplied overlay wins
538 /// wherever it speaks and defers everywhere else, which is the merge a
539 /// live edit needs: the session has an opinion about the one field being
540 /// edited and none about the rest of the page.
541 ///
542 /// Indices are raw `/Annots` indices in both overlays. An entry of
543 /// `other` past this overlay's end is dropped, because there is no
544 /// annotation for it to apply to.
545 ///
546 /// `other`'s [`Focus`] and its hover each replace this overlay's when it
547 /// has one, and leave it alone when it does not — the same "wins wherever
548 /// it speaks" rule the entries follow. Unlike an entry, either one past
549 /// this overlay's end survives: both name an annotation, not a slot.
550 ///
551 /// ```
552 /// use pdfrum_doc::{AnnotOverlay, ap::Appearance};
553 ///
554 /// let mut page = AnnotOverlay::with_capacity(2);
555 /// page.set_appearance(0, Appearance::Suppressed);
556 ///
557 /// // The session speaks about index 1 only.
558 /// let mut session = AnnotOverlay::with_capacity(2);
559 /// session.set_appearance(1, Appearance::Suppressed);
560 /// page.merge_over(&session);
561 ///
562 /// assert_eq!(page.appearance(0), &Appearance::Suppressed);
563 /// assert_eq!(page.appearance(1), &Appearance::Suppressed);
564 /// ```
565 pub fn merge_over(&mut self, other: &AnnotOverlay) {
566 for (index, entry) in other.entries.iter().enumerate() {
567 if matches!(entry, Appearance::Untouched) {
568 continue;
569 }
570 self.set_appearance(index, entry.clone());
571 }
572 if let Some(focus) = other.focus {
573 self.focus = Some(focus);
574 }
575 if let Some(hover) = other.hover {
576 self.hover = Some(hover);
577 }
578 if let Some(live_edit) = other.live_edit {
579 self.live_edit = Some(live_edit);
580 }
581 }
582
583 /// The rectangle an annotation should be read as having.
584 ///
585 /// ```
586 /// use pdfrum_doc::{AnnotOverlay, geom};
587 ///
588 /// let overlay = AnnotOverlay::with_capacity(1);
589 /// let raw = geom::rect(0.0, 0.0, 100.0, 50.0);
590 /// // Nothing generated: the annotation keeps the rectangle it declared.
591 /// assert_eq!(overlay.rect(0, raw), raw);
592 /// ```
593 #[must_use]
594 pub fn rect(&self, index: usize, raw: Rect) -> Rect {
595 self.get(index)
596 .and_then(|generated| generated.rect_override)
597 .unwrap_or(raw)
598 }
599
600 /// How many annotations the overlay covers.
601 ///
602 /// ```
603 /// use pdfrum_doc::AnnotOverlay;
604 ///
605 /// assert_eq!(AnnotOverlay::with_capacity(3).len(), 3);
606 /// ```
607 #[must_use]
608 pub fn len(&self) -> usize {
609 self.entries.len()
610 }
611
612 /// Whether the overlay covers no annotations at all.
613 ///
614 /// ```
615 /// use pdfrum_doc::AnnotOverlay;
616 ///
617 /// assert!(AnnotOverlay::with_capacity(0).is_empty());
618 /// assert!(!AnnotOverlay::with_capacity(1).is_empty());
619 /// ```
620 #[must_use]
621 pub fn is_empty(&self) -> bool {
622 self.entries.is_empty()
623 }
624}
625
626/// The font a text-bearing generator sets its text with.
627///
628/// Threaded in rather than loaded here, because loading one needs a font
629/// cache the caller already owns, and because the layout engine is a pure
630/// function of these numbers — which is what lets it be tested against a stub.
631///
632/// ```
633/// use pdfrum_doc::ap::FormFonts;
634/// use pdfrum_object::{Dict, NoResolve};
635///
636/// // A catalog with no `/AcroForm` still yields the stock fallback face.
637/// let mut ctx = pdfrum_page::BuildContext::new();
638/// let fonts = FormFonts::load(&Dict::default(), &NoResolve, &mut ctx);
639/// use pdfrum_doc::ap::TextFont;
640///
641/// let font = fonts.face(b"Helv").expect("the fallback face");
642/// let width = |code: u32| TextFont::char_width(font, code);
643/// let text = fonts.text_font(b"Helv", &width).expect("a face to set text with");
644///
645/// // The ascent the layout engine stacks lines by.
646/// assert!(text.metrics.ascent > 0);
647/// ```
648pub struct TextFont<'a> {
649 /// The loaded font.
650 pub font: &'a pdfrum_font::Font,
651 /// Metrics derived from it, for the layout engine.
652 pub metrics: vt::Metrics<'a>,
653}
654
655impl std::fmt::Debug for TextFont<'_> {
656 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
657 f.debug_struct("TextFont")
658 .field("metrics", &self.metrics)
659 .finish_non_exhaustive()
660 }
661}
662
663/// The character code a code point the face cannot map is written as.
664///
665/// A simple font's codes are one byte and `Font::append_char` truncates to
666/// one, so the code the stream ends up carrying is the code point's **low
667/// byte** — and the width has to be looked up under that same byte or the
668/// layout advances by a glyph the stream does not name. A composite font
669/// keeps the whole value, because its CMap decides the width itself.
670fn unmapped_code(font: &pdfrum_font::Font, code: u32) -> pdfrum_font::CharCode {
671 match font {
672 pdfrum_font::Font::Type0(_) => pdfrum_font::CharCode(code),
673 pdfrum_font::Font::Simple(_) | pdfrum_font::Font::Type3(_) => {
674 pdfrum_font::CharCode(code & 0xff)
675 }
676 }
677}
678
679impl TextFont<'_> {
680 /// How one code point is written into a content stream.
681 ///
682 /// A `Symbol` or `ZapfDingbats` font takes the code point's **low byte**
683 /// verbatim, relying on the font's built-in encoding: there is no
684 /// named-glyph table and no `/Encoding` consultation anywhere in this
685 /// path. Anything else goes through the reverse `ToUnicode` mapping.
686 ///
687 /// **A code point the font cannot represent is still written**, as its own
688 /// value taken for a character code. The glyph that draws is whatever that
689 /// code happens to name in the chosen face and is usually wrong — but the
690 /// text object exists, occupies the layout, and is what a reader sees.
691 /// Dropping the character instead loses the object entirely, which on
692 /// `bug_725389` — three Hebrew characters in a `/DA` naming Times-Roman —
693 /// is the difference between six text objects and three.
694 ///
695 /// ```
696 /// use pdfrum_doc::ap::FormFonts;
697 /// use pdfrum_object::{Dict, NoResolve};
698 ///
699 /// // A catalog with no `/AcroForm` still yields the stock fallback face.
700 /// let mut ctx = pdfrum_page::BuildContext::new();
701 /// let fonts = FormFonts::load(&Dict::default(), &NoResolve, &mut ctx);
702 /// use pdfrum_doc::ap::TextFont;
703 ///
704 /// let font = fonts.face(b"Helv").expect("the fallback face");
705 /// let width = |code: u32| TextFont::char_width(font, code);
706 /// let text = fonts.text_font(b"Helv", &width).expect("a face");
707 ///
708 /// // `A` writes as one byte in a simple font.
709 /// assert_eq!(text.encode(u32::from('A')), b"A");
710 /// ```
711 // The oracle reaches the same place by a longer road: CPDF_BAFontMap
712 // first looks for a second face that knows the character, and only
713 // CPWL_EditImpl::GetPDFWordString's fallthrough appends the raw value
714 // when none does. On a hermetic font set no second face is found, so the
715 // fallthrough is the whole of the observable behaviour — which is why no
716 // N-slot map is built here for a result it would not change.
717 #[must_use]
718 pub fn encode(&self, code: u32) -> Vec<u8> {
719 let name = self.font.base_font_name();
720 if name == b"Symbol" || name == b"ZapfDingbats" {
721 return vec![u8::try_from(code & 0xff).unwrap_or(0)];
722 }
723 let mut out = Vec::new();
724 let mapped = char::from_u32(code)
725 .and_then(|ch| self.font.char_code_from_unicode(ch))
726 .unwrap_or_else(|| unmapped_code(self.font, code));
727 self.font.append_char(&mut out, mapped);
728 out
729 }
730
731 /// One code point's width, in thousandths of an em.
732 ///
733 /// The width is the one the face gives whatever [`Self::encode`] wrote, so
734 /// an unrepresentable code point measures the glyph its raw value names
735 /// rather than nothing — the two have to agree or the layout advances past
736 /// characters the stream still contains, and the line comes out the wrong
737 /// length.
738 ///
739 /// A free function rather than a method because [`Self::metrics_of`] wants
740 /// it as a `&dyn Fn` borrowed for the same lifetime as the font, which a
741 /// closure over `self` cannot supply before `self` exists.
742 ///
743 /// ```
744 /// use pdfrum_doc::ap::FormFonts;
745 /// use pdfrum_object::{Dict, NoResolve};
746 ///
747 /// // A catalog with no `/AcroForm` still yields the stock fallback face.
748 /// let mut ctx = pdfrum_page::BuildContext::new();
749 /// let fonts = FormFonts::load(&Dict::default(), &NoResolve, &mut ctx);
750 /// use pdfrum_doc::ap::TextFont;
751 ///
752 /// let font = fonts.face(b"Helv").expect("the fallback face");
753 /// // Thousandths of an em, for whatever `encode` wrote.
754 /// assert!(TextFont::char_width(font, u32::from('A')) > 0);
755 /// ```
756 #[must_use]
757 pub fn char_width(font: &pdfrum_font::Font, code: u32) -> i32 {
758 let charcode = char::from_u32(code)
759 .and_then(|ch| font.char_code_from_unicode(ch))
760 .unwrap_or_else(|| unmapped_code(font, code));
761 #[allow(clippy::cast_possible_truncation)]
762 {
763 font.char_width(charcode) as i32
764 }
765 }
766
767 /// The layout metrics a loaded font supplies.
768 ///
769 /// ```
770 /// use pdfrum_doc::ap::FormFonts;
771 /// use pdfrum_object::{Dict, NoResolve};
772 ///
773 /// // A catalog with no `/AcroForm` still yields the stock fallback face.
774 /// let mut ctx = pdfrum_page::BuildContext::new();
775 /// let fonts = FormFonts::load(&Dict::default(), &NoResolve, &mut ctx);
776 /// use pdfrum_doc::ap::TextFont;
777 ///
778 /// let font = fonts.face(b"Helv").expect("the fallback face");
779 /// let width = |code: u32| TextFont::char_width(font, code);
780 /// let metrics = TextFont::metrics_of(font, &width);
781 /// assert!(metrics.ascent > metrics.descent);
782 /// ```
783 #[must_use]
784 pub fn metrics_of<'a>(
785 font: &'a pdfrum_font::Font,
786 width: &'a dyn Fn(u32) -> i32,
787 ) -> vt::Metrics<'a> {
788 vt::Metrics {
789 width,
790 ascent: font.type_ascent(),
791 descent: font.type_descent(),
792 }
793 }
794}
795
796/// One second face: the resource name it is filed under, the dictionary the
797/// appearance's `/Resources /Font` carries, and the loaded face itself.
798///
799/// A borrow of what [`FormFonts`] already holds. The three travel together
800/// because a generator needs all three to write one character — the alias for
801/// the `Tf`, the face for the width, and the dictionary so the name resolves
802/// when the stream is drawn.
803///
804/// ```
805/// use pdfrum_doc::ap::FormFonts;
806/// use pdfrum_object::{Dict, NoResolve};
807///
808/// // A catalog with no `/AcroForm` still yields the stock fallback face.
809/// let mut ctx = pdfrum_page::BuildContext::new();
810/// let fonts = FormFonts::load(&Dict::default(), &NoResolve, &mut ctx);
811/// use pdfrum_font::Charset;
812///
813/// // A second face for the characters the `/DA` font cannot write.
814/// if let Some(substitute) = fonts.substitute(Charset::ShiftJis) {
815/// // The alias is the `Tf` name and the key in the appearance's
816/// // own `/Resources /Font`.
817/// assert!(!substitute.alias.as_bytes().is_empty());
818/// }
819/// ```
820#[derive(Debug, Clone, Copy)]
821pub struct Substitute<'a> {
822 /// The `Tf` name, and the key in the appearance's font resources.
823 pub alias: &'a Name,
824 /// The font dictionary that key maps to.
825 pub dict: &'a Dict,
826 /// The loaded face, for widths and for the codes it can write.
827 pub font: &'a pdfrum_font::Font,
828}
829
830/// The faces a form's default resources name, loaded once for a page.
831///
832/// # Why the fonts are loaded rather than substituted for
833///
834/// A generator wants *metrics*, and it was tempting to hand every generator
835/// one stock Helvetica on the reasoning that a non-embedded `/DA` font
836/// substitutes to that face anyway. The metrics do not agree with that
837/// reasoning, and the disagreement is visible: an ascent and descent taken
838/// from the base-14 metric tables are 718 and −219, while the ones taken from
839/// the **substituted face** — the size the layout engine actually stacks lines
840/// by — are the face's own, and for the hermetic corpus's metric-compatible
841/// Helvetica that is 905 and −211. On a list box the difference is the row
842/// pitch: 11.24 units per row against 13.39, which is two extra rows in a
843/// thirty-unit box.
844///
845/// So the font a widget's `/DA` names is loaded from the form's `/DR /Font`,
846/// through the same loader and the same substitution options every other font
847/// on the page goes through. A name the resources do not carry gets a stock
848/// Helvetica, which is what the fallback is actually for.
849///
850/// ```
851/// use pdfrum_doc::ap::FormFonts;
852/// use pdfrum_object::{Dict, NoResolve};
853///
854/// // A catalog with no `/AcroForm` still yields the stock fallback face.
855/// let mut ctx = pdfrum_page::BuildContext::new();
856/// let fonts = FormFonts::load(&Dict::default(), &NoResolve, &mut ctx);
857///
858/// // A name the resources do not carry falls back rather than failing.
859/// assert!(fonts.face(b"NoSuchFace").is_some());
860/// ```
861pub struct FormFonts {
862 /// Resource name and the face loaded under it, in `/DR /Font` order with
863 /// the fallback last.
864 entries: Vec<(Name, pdfrum_font::Font)>,
865 /// The faces added for characters no declared font's charset covers, one
866 /// per charset, keyed by the alias they are filed under.
867 ///
868 /// These are not in `/DR`, and no `/DA` names one: they are added when a
869 /// field is asked to write a character its own font cannot, and they go
870 /// into the **appearance stream's** own `/Resources /Font` rather than the
871 /// form's. See [`font_map`].
872 substitutes: Vec<(Name, Dict, pdfrum_font::Font)>,
873}
874
875impl std::fmt::Debug for FormFonts {
876 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
877 f.debug_struct("FormFonts")
878 .field(
879 "names",
880 &self.entries.iter().map(|(n, _)| n).collect::<Vec<_>>(),
881 )
882 .finish_non_exhaustive()
883 }
884}
885
886impl FormFonts {
887 /// Loads every font a document's interactive form declares, plus the
888 /// fallback a name outside them resolves to.
889 ///
890 /// The fallback is loaded unconditionally and stored under an empty name,
891 /// so a `/DA` naming nothing — or naming a font the resources lack — still
892 /// has a face to measure with. That is the same substitution a viewer
893 /// performs; it is only the *metric source* that this fixes.
894 ///
895 /// # Memoized on the context
896 ///
897 /// The faces are a pure function of the form's `/DR /Font`, and building
898 /// them is expensive — the `/DR` walk constructs every font the form
899 /// declares, encoding tables and substitution ladder included. The
900 /// annotation overlay asks for them **once per page per render**, so the
901 /// result is cached in the [`BuildContext`](pdfrum_page::BuildContext)
902 /// beside the rest of the per-document font state. A caller threading one
903 /// context through many renders of one document pays for this once.
904 ///
905 /// Nothing about *what* is built changed when the cache was added, which
906 /// is what makes the appearance streams identical: the fallback still
907 /// goes through the same loader, and the second faces are still loaded
908 /// here rather than where a field discovers it needs one. Only the number
909 /// of times moved.
910 ///
911 /// # What the key names, and why it is not the `/AcroForm`
912 ///
913 /// Building the faces reads exactly one thing out of the document — the
914 /// `/AcroForm`'s `/DR /Font` dictionary — and takes everything else from
915 /// dictionaries written in this crate. So the *faces* are a function of
916 /// that dictionary alone, and keying on the `/AcroForm` instead threw
917 /// away every form written as a direct dictionary, which has no reference
918 /// to name it by.
919 ///
920 /// A direct `/AcroForm` is not the rarity it reads as. An empty
921 /// `<</Fields[]>>` is what a producer writes when it declares a form and
922 /// then puts no fields in it, and six of this corpus's 44 documents carry
923 /// one — none of them a form document. Every one of those was rebuilding
924 /// the fallback face and the substitute face once per page per render, for
925 /// a form with no fields, and on four of them it was the single largest
926 /// line in the render.
927 ///
928 /// ```
929 /// use pdfrum_doc::ap::FormFonts;
930 /// use pdfrum_object::{Dict, NoResolve};
931 ///
932 /// // A catalog with no `/AcroForm` still yields the stock fallback face.
933 /// let mut ctx = pdfrum_page::BuildContext::new();
934 /// let fonts = FormFonts::load(&Dict::default(), &NoResolve, &mut ctx);
935 ///
936 /// // Loaded once per document and shared: a second load hits the cache.
937 /// let again = FormFonts::load(&Dict::default(), &NoResolve, &mut ctx);
938 /// assert!(std::sync::Arc::ptr_eq(&fonts, &again));
939 /// ```
940 #[must_use]
941 pub fn load<R: Resolve>(
942 catalog: &Dict,
943 r: &R,
944 ctx: &mut pdfrum_page::BuildContext,
945 ) -> std::sync::Arc<FormFonts> {
946 ctx.form_fonts(FormFonts::key(catalog, r), |ctx| {
947 FormFonts::build(catalog, r, ctx)
948 })
949 }
950
951 /// The cache slot this catalog's faces belong in.
952 ///
953 /// Split out from [`Self::load`] so the four cases can be asserted
954 /// directly; the walk is `/AcroForm` then `/DR` then `/Font`, resolving
955 /// references at every step except the last, whose *spelling* is the
956 /// answer.
957 fn key<R: Resolve>(catalog: &Dict, r: &R) -> pdfrum_page::FormFontsKey {
958 match catalog.raw(names::ACRO_FORM) {
959 // A real form: the reference names it, as it names every other
960 // per-document cache on the context.
961 Some(Object::Ref(reference)) => return pdfrum_page::FormFontsKey::Form(*reference),
962 Some(_) => {}
963 // No form at all, so no `/DR /Font`: the constants alone.
964 None => return pdfrum_page::FormFontsKey::None,
965 }
966 // A direct `/AcroForm`. Its faces are its `/DR /Font`'s, so that is
967 // what has to be identified — the same walk `build` makes, stopping
968 // one step earlier and reading the spelling rather than the value.
969 let fonts = catalog
970 .dict(names::ACRO_FORM, r)
971 .and_then(|form| form.dict(names::DR, r))
972 .and_then(|resources| resources.raw(names::FONT).cloned());
973 match fonts {
974 // Written as a reference, which is the ordinary spelling even
975 // inside a direct form: as good an identity as the form's own.
976 Some(Object::Ref(reference)) => pdfrum_page::FormFontsKey::DirectResources(reference),
977 // Written out in full. Nothing to key on, so it is rebuilt.
978 Some(_) => pdfrum_page::FormFontsKey::Direct,
979 // No default resources: the constants alone, exactly as a
980 // document with no form at all.
981 None => pdfrum_page::FormFontsKey::None,
982 }
983 }
984
985 /// [`Self::load`] without the cache: the faces, built now.
986 #[must_use]
987 fn build<R: Resolve>(catalog: &Dict, r: &R, ctx: &mut pdfrum_page::BuildContext) -> FormFonts {
988 let (limits, mut diags) = (
989 pdfrum_common::Limits::default(),
990 pdfrum_common::Diagnostics::default(),
991 );
992 let mut load = |dict: &Dict| {
993 pdfrum_font::load_with_options(
994 dict,
995 r,
996 &ctx.fonts,
997 &ctx.substitution,
998 &limits,
999 &mut diags,
1000 )
1001 };
1002
1003 let mut entries = Vec::new();
1004 let fonts = catalog
1005 .dict(names::ACRO_FORM, r)
1006 .and_then(|form| form.dict(names::DR, r))
1007 .and_then(|resources| resources.dict(names::FONT, r));
1008 if let Some(fonts) = fonts {
1009 for key in fonts.keys() {
1010 let Some(dict) = fonts.dict(key, r) else {
1011 continue;
1012 };
1013 if let Some(font) = load(&dict) {
1014 entries.push((key.clone(), font));
1015 }
1016 }
1017 }
1018 // The fallback goes through the **same loader**, not the stock-metrics
1019 // constructor: the point of this type is that the ascent and descent
1020 // come from the face that is actually substituted, and a font built
1021 // from the base-14 tables would answer 718 and −219 where the face
1022 // answers its own. A field with no `/DR` at all is exactly where that
1023 // shows, because there is nothing else for it to measure with.
1024 entries.extend(load(&freetext::fallback_font()).map(|font| (Name::new(Vec::new()), font)));
1025
1026 // The second faces, loaded here rather than where a field discovers it
1027 // needs one: loading needs the page's font cache, and the generators
1028 // are pure functions of the faces they are handed. There is one per
1029 // charset the font map can add for, which is one — see [`font_map`].
1030 let mut substitutes = Vec::new();
1031 for charset in font_map::SUBSTITUTABLE_CHARSETS {
1032 let Some(dict) = font_map::substitute_font_dict(*charset) else {
1033 continue;
1034 };
1035 if let Some(font) = load(&dict) {
1036 substitutes.push((Name::new(font_map::substitute_alias(*charset)), dict, font));
1037 }
1038 }
1039 FormFonts {
1040 entries,
1041 substitutes,
1042 }
1043 }
1044
1045 /// The second face a character of `charset` is written in, with the alias
1046 /// it is filed under and the dictionary that goes into the appearance's
1047 /// own resources.
1048 ///
1049 /// Answers nothing for a charset with no encoding table, and for one whose
1050 /// face would not load — in both cases the caller leaves the character to
1051 /// the `/DA` font, which is the behaviour that predates this.
1052 ///
1053 /// ```
1054 /// use pdfrum_doc::ap::FormFonts;
1055 /// use pdfrum_object::{Dict, NoResolve};
1056 ///
1057 /// // A catalog with no `/AcroForm` still yields the stock fallback face.
1058 /// let mut ctx = pdfrum_page::BuildContext::new();
1059 /// let fonts = FormFonts::load(&Dict::default(), &NoResolve, &mut ctx);
1060 /// use pdfrum_font::Charset;
1061 ///
1062 /// // Nothing for a charset with no encoding table, or whose face will
1063 /// // not load: the caller then leaves the character to the `/DA` font.
1064 /// let _ = fonts.substitute(Charset::ShiftJis);
1065 /// ```
1066 #[must_use]
1067 pub fn substitute(&self, charset: pdfrum_font::Charset) -> Option<Substitute<'_>> {
1068 let alias = font_map::substitute_alias(charset);
1069 self.substitutes
1070 .iter()
1071 .find(|(name, _, _)| name.as_bytes() == alias)
1072 .map(|(name, dict, font)| Substitute {
1073 alias: name,
1074 dict,
1075 font,
1076 })
1077 }
1078
1079 /// The face filed under one resource name, or the fallback.
1080 ///
1081 /// Answers nothing only if the fallback itself is missing, which
1082 /// [`Self::load`] makes impossible — the caller then generates chrome
1083 /// alone rather than being told a face exists that does not.
1084 ///
1085 /// ```
1086 /// use pdfrum_doc::ap::FormFonts;
1087 /// use pdfrum_object::{Dict, NoResolve};
1088 ///
1089 /// // A catalog with no `/AcroForm` still yields the stock fallback face.
1090 /// let mut ctx = pdfrum_page::BuildContext::new();
1091 /// let fonts = FormFonts::load(&Dict::default(), &NoResolve, &mut ctx);
1092 ///
1093 /// assert!(fonts.face(b"Helv").is_some());
1094 /// // Answers the fallback rather than nothing for an unknown name.
1095 /// assert!(fonts.face(b"NoSuchFace").is_some());
1096 /// ```
1097 #[must_use]
1098 pub fn face(&self, name: &[u8]) -> Option<&pdfrum_font::Font> {
1099 self.entries
1100 .iter()
1101 .find(|(key, _)| key.as_bytes() == name)
1102 .or_else(|| self.entries.last())
1103 .map(|(_, font)| font)
1104 }
1105
1106 /// A [`TextFont`] over one resource name, with `width` borrowed for the
1107 /// same lifetime.
1108 ///
1109 /// The width closure cannot live inside the returned value — it has to be
1110 /// borrowed for the font's lifetime, which a closure over `self` cannot
1111 /// supply before `self` exists — so the caller keeps it and passes it in,
1112 /// the same shape [`TextFont::metrics_of`] already has.
1113 ///
1114 /// ```
1115 /// use pdfrum_doc::ap::FormFonts;
1116 /// use pdfrum_object::{Dict, NoResolve};
1117 ///
1118 /// // A catalog with no `/AcroForm` still yields the stock fallback face.
1119 /// let mut ctx = pdfrum_page::BuildContext::new();
1120 /// let fonts = FormFonts::load(&Dict::default(), &NoResolve, &mut ctx);
1121 /// use pdfrum_doc::ap::TextFont;
1122 ///
1123 /// // The width closure is borrowed for the font's lifetime, so the
1124 /// // caller keeps it and passes it in.
1125 /// let font = fonts.face(b"Helv").expect("the fallback face");
1126 /// let width = |code: u32| TextFont::char_width(font, code);
1127 /// assert!(fonts.text_font(b"Helv", &width).is_some());
1128 /// ```
1129 #[must_use]
1130 pub fn text_font<'a>(
1131 &'a self,
1132 name: &[u8],
1133 width: &'a dyn Fn(u32) -> i32,
1134 ) -> Option<TextFont<'a>> {
1135 let font = self.face(name)?;
1136 Some(TextFont {
1137 metrics: TextFont::metrics_of(font, width),
1138 font,
1139 })
1140 }
1141}
1142
1143/// Generates appearances for every annotation on a page that wants one.
1144///
1145/// The walk mirrors what a viewer does when it opens a page, because that
1146/// ordering is what the `--annot` contract describes: pop-ups written into
1147/// the file are skipped, everything else is offered to its generator, and the
1148/// results are keyed by position in `/Annots`.
1149///
1150/// The text-bearing generators are skipped here; [`generate_appearances_with_text`]
1151/// is the walk that enables them.
1152///
1153/// ```
1154/// use pdfrum_common::Diagnostics;
1155/// use pdfrum_doc::ap::generate_appearances;
1156/// use pdfrum_object::{Array, Dict, Name, NoResolve, Object};
1157///
1158/// let square = Dict::from_pairs([
1159/// (Name::from("Subtype"), Object::Name(Name::from("Square"))),
1160/// (
1161/// Name::from("Rect"),
1162/// Object::Array(Array::of([0, 0, 100, 50])),
1163/// ),
1164/// (
1165/// Name::from("IC"),
1166/// Object::Array(Array::of([1, 0, 0])),
1167/// ),
1168/// ]);
1169/// let page = Dict::from_pairs([(
1170/// Name::from("Annots"),
1171/// Object::Array(Array::of([Object::Dict(square)])),
1172/// )]);
1173///
1174/// let mut diags = Diagnostics::default();
1175/// let overlay = generate_appearances(&page, &NoResolve, &mut diags);
1176///
1177/// // One entry per `/Annots` index; the square got a stream.
1178/// assert_eq!(overlay.len(), 1);
1179/// assert!(!overlay.get(0).expect("generated").stream.is_empty());
1180/// ```
1181#[must_use]
1182pub fn generate_appearances<R: Resolve>(
1183 page: &Dict,
1184 r: &R,
1185 diags: &mut Diagnostics,
1186) -> AnnotOverlay {
1187 let Some(annots) = page.array(obj_names::ANNOTS, r) else {
1188 return AnnotOverlay::default();
1189 };
1190 let mut overlay = AnnotOverlay::with_capacity(annots.len());
1191 for index in 0..annots.len() {
1192 let Some(dict) = annots.dict_at(index, r) else {
1193 continue;
1194 };
1195 if crate::annot::is_popup(&dict, r) {
1196 continue;
1197 }
1198 if let Some(generated) = generate_one(&dict, r, diags) {
1199 overlay.set(index, generated);
1200 } else if let Some(generated) = widget::generate(&dict, r) {
1201 // A widget with no appearance dictionary gets its chrome built
1202 // when the page opens, whatever the form says about regenerating
1203 // appearances. See `widget` for how far that goes.
1204 diags.record(Severity::Recovered, DiagKind::AppearanceGenerated, None);
1205 overlay.set(index, generated);
1206 }
1207 }
1208 overlay
1209}
1210
1211/// The same walk, with the text-bearing generators enabled.
1212///
1213/// The generators only produce an appearance when a font is in hand, so a
1214/// caller without one gets the same result as [`generate_appearances`].
1215///
1216/// Each annotation is measured with the face **its own** `/DA` names, looked
1217/// up in the form's default resources — not with one page-wide font. A page
1218/// whose fields name two different faces stacks their lines by two different
1219/// ascents, which is what a viewer does.
1220///
1221/// ```
1222/// use pdfrum_common::Diagnostics;
1223/// use pdfrum_doc::ap::generate_appearances;
1224/// use pdfrum_object::{Array, Dict, Name, NoResolve, Object};
1225///
1226/// let square = Dict::from_pairs([
1227/// (Name::from("Subtype"), Object::Name(Name::from("Square"))),
1228/// (
1229/// Name::from("Rect"),
1230/// Object::Array(Array::of([0, 0, 100, 50])),
1231/// ),
1232/// (
1233/// Name::from("IC"),
1234/// Object::Array(Array::of([1, 0, 0])),
1235/// ),
1236/// ]);
1237/// let page = Dict::from_pairs([(
1238/// Name::from("Annots"),
1239/// Object::Array(Array::of([Object::Dict(square)])),
1240/// )]);
1241///
1242/// let mut diags = Diagnostics::default();
1243/// let overlay = generate_appearances(&page, &NoResolve, &mut diags);
1244/// use pdfrum_doc::ap::generate_appearances_with_text;
1245///
1246/// let catalog = Dict::default();
1247/// // With no fonts in hand the text-bearing generators stay off, so the
1248/// // result matches the plain walk.
1249/// let mut diags = Diagnostics::default();
1250/// let with_text =
1251/// generate_appearances_with_text(&page, &catalog, None, &NoResolve, &mut diags);
1252/// assert_eq!(with_text.get(0), overlay.get(0));
1253/// ```
1254#[must_use]
1255pub fn generate_appearances_with_text<R: Resolve>(
1256 page: &Dict,
1257 catalog: &Dict,
1258 fonts: Option<&FormFonts>,
1259 r: &R,
1260 diags: &mut Diagnostics,
1261) -> AnnotOverlay {
1262 let Some(annots) = page.array(obj_names::ANNOTS, r) else {
1263 return AnnotOverlay::default();
1264 };
1265 let mut overlay = AnnotOverlay::with_capacity(annots.len());
1266 for index in 0..annots.len() {
1267 let Some(dict) = annots.dict_at(index, r) else {
1268 continue;
1269 };
1270 if crate::annot::is_popup(&dict, r) {
1271 continue;
1272 }
1273 let generated = with_text_font(&dict, catalog, fonts, r, |text_font, substitute| {
1274 generate_one(&dict, r, diags)
1275 .or_else(|| generate_text_bearing(&dict, catalog, text_font, r, diags))
1276 .or_else(|| {
1277 // A widget's own body needs the same font the free-text
1278 // generator wanted, so a caller with one gets the field's
1279 // value laid out and a caller without one gets the chrome
1280 // alone.
1281 match text_font {
1282 Some(font) => {
1283 widget::generate_with_text(&dict, catalog, font, substitute, r)
1284 }
1285 None => widget::generate(&dict, r),
1286 }
1287 .inspect(|_| {
1288 diags.record(Severity::Recovered, DiagKind::AppearanceGenerated, None);
1289 })
1290 })
1291 });
1292 if let Some(generated) = generated {
1293 overlay.set(index, generated);
1294 }
1295 }
1296 overlay
1297}
1298
1299/// Resolves the face an annotation's `/DA` names and calls `use_font` with it.
1300///
1301/// The [`TextFont`] borrows a width closure, and that closure has to outlive
1302/// it, so this cannot hand one back — it owns the closure for the length of the
1303/// call and lends the font instead. Both the page-wide generator and the
1304/// form-fill path go through here so the two agree on which face writes a
1305/// field, including the second one that covers what the first one's charset
1306/// does not.
1307///
1308/// `use_font` is called with `None` when there is no usable face, which is the
1309/// signal to draw a widget's chrome alone.
1310pub(crate) fn with_text_font<R: Resolve, T>(
1311 dict: &Dict,
1312 catalog: &Dict,
1313 fonts: Option<&FormFonts>,
1314 r: &R,
1315 use_font: impl FnOnce(Option<&TextFont<'_>>, Option<Substitute<'_>>) -> T,
1316) -> T {
1317 let named = fonts.and_then(|fonts| fonts.face(&font_name_of(dict, catalog, r)));
1318 // A second face, for the characters this one's charset does not cover.
1319 // The **widths** have to know about it as well as the bytes: a run set in
1320 // two faces advances by two faces' metrics, and measuring it all with the
1321 // first gives a line the wrong length wherever the second one writes. So
1322 // the substitute enters through the width closure the layout is built
1323 // from, not only through the encoder.
1324 let da_charset = named.map_or(pdfrum_font::Charset::Ansi, font_map::font_charset);
1325 let substitute = fonts.and_then(|fonts| {
1326 font_map::SUBSTITUTABLE_CHARSETS
1327 .iter()
1328 .find(|charset| **charset != da_charset)
1329 .and_then(|charset| fonts.substitute(*charset))
1330 });
1331 let width = named.map(|font| {
1332 move |code: u32| match substitute {
1333 Some(sub) if !font_map::da_font_writes(font, da_charset, code) => {
1334 font_map::substitute_width(sub.font, code)
1335 }
1336 _ => TextFont::char_width(font, code),
1337 }
1338 });
1339 let text_font = named.zip(width.as_ref()).map(|(font, width)| TextFont {
1340 metrics: TextFont::metrics_of(font, width),
1341 font,
1342 });
1343 use_font(text_font.as_ref(), substitute)
1344}
1345
1346/// The `/DR /Font` resource name one annotation's default appearance names.
1347///
1348/// Falls back to the form's own `/DA`, then to nothing — and nothing resolves
1349/// to [`FormFonts`]'s fallback face rather than declining.
1350fn font_name_of<R: Resolve>(dict: &Dict, catalog: &Dict, r: &R) -> Vec<u8> {
1351 let form = catalog.dict(names::ACRO_FORM, r).unwrap_or_default();
1352 freetext::default_appearance(dict, &form, r)
1353 .map(|appearance| appearance.font_name)
1354 .unwrap_or_default()
1355}
1356
1357/// The free-text generator, when its preconditions and a font allow.
1358fn generate_text_bearing<R: Resolve>(
1359 dict: &Dict,
1360 catalog: &Dict,
1361 text_font: Option<&TextFont<'_>>,
1362 r: &R,
1363 diags: &mut Diagnostics,
1364) -> Option<GeneratedAp> {
1365 if !should_generate(dict, r) {
1366 return None;
1367 }
1368 let subtype = Subtype::from_bytes(&dict.byte_string(obj_names::SUBTYPE, r).unwrap_or_default());
1369 if subtype != Subtype::FreeText {
1370 return None;
1371 }
1372 let font = text_font?;
1373 let generated = freetext::free_text(
1374 dict,
1375 catalog,
1376 r,
1377 &font.metrics,
1378 &|code| font.encode(code),
1379 diags,
1380 )?;
1381 diags.record(Severity::Recovered, DiagKind::AppearanceGenerated, None);
1382 Some(GeneratedAp {
1383 stream: generated.stream,
1384 bbox: dict.rect(obj_names::RECT, r),
1385 matrix: Affine::IDENTITY,
1386 resources: resources_dict(
1387 ext_gstate_dict(dict, false, r),
1388 generated.font_resources.clone(),
1389 ),
1390 rect_override: None,
1391 as_override: None,
1392 })
1393}
1394
1395/// Generates one annotation's appearance, if it should have one.
1396#[must_use]
1397pub(crate) fn generate_one<R: Resolve>(
1398 dict: &Dict,
1399 r: &R,
1400 diags: &mut Diagnostics,
1401) -> Option<GeneratedAp> {
1402 if !should_generate(dict, r) {
1403 return None;
1404 }
1405 let subtype = Subtype::from_bytes(&dict.byte_string(obj_names::SUBTYPE, r).unwrap_or_default());
1406 let generated = match subtype {
1407 Subtype::Caret => markup::caret(dict, r),
1408 Subtype::Circle => markup::circle(dict, r),
1409 Subtype::Highlight => markup::highlight(dict, r),
1410 Subtype::Ink => markup::ink(dict, r, diags)?,
1411 Subtype::Line => markup::line(dict, r)?,
1412 Subtype::Link => markup::link(dict, r),
1413 Subtype::Square => markup::square(dict, r),
1414 Subtype::Squiggly => markup::squiggly(dict, r),
1415 Subtype::StrikeOut => markup::strike_out(dict, r),
1416 Subtype::Text => markup::text(dict, r),
1417 Subtype::Underline => markup::underline(dict, r),
1418 // Everything else has no generator. The two text-bearing subtypes —
1419 // free text and pop-ups — do have one upstream, but it needs the
1420 // layout engine and is built on top of this dispatch rather than
1421 // inside it, so they answer the same way here.
1422 _ => return None,
1423 };
1424 diags.record(Severity::Recovered, DiagKind::AppearanceGenerated, None);
1425
1426 // The bounding box is the annotation's rectangle **as it stands after
1427 // this generator ran** — so a sticky note's is the 20×20 box it just
1428 // produced, not the one the file declared.
1429 let rect = generated
1430 .rect_override
1431 .unwrap_or_else(|| dict.rect(obj_names::RECT, r));
1432 let bbox = if generated.is_text_markup {
1433 quad::bounding_rect_from_quad_points(dict.array(names::QUAD_POINTS, r).as_ref())
1434 } else {
1435 rect
1436 };
1437 Some(GeneratedAp {
1438 stream: generated.stream,
1439 bbox,
1440 matrix: Affine::IDENTITY,
1441 resources: resources_dict(
1442 ext_gstate_dict(dict, generated.blend_multiply, r),
1443 generated.font_resources.clone(),
1444 ),
1445 rect_override: generated.rect_override,
1446 as_override: None,
1447 })
1448}
1449
1450/// Whether an annotation is eligible for a generated appearance.
1451///
1452/// Two gates. A **dictionary-valued** `/AP /N` suppresses generation, and a
1453/// stream answers as its own dictionary — so the common "it already has an
1454/// appearance" case and the multi-state checkbox case are the same test. And
1455/// a hidden annotation never generates.
1456#[must_use]
1457pub(crate) fn should_generate<R: Resolve>(dict: &Dict, r: &R) -> bool {
1458 if appearance::has_appearance(dict, r) {
1459 return false;
1460 }
1461 let flags = crate::annot::AnnotFlags::from_bits(dict.int(names::F, r).unwrap_or(0));
1462 !flags.is_hidden()
1463}
1464
1465/// The graphics-state dictionary a generated appearance names.
1466///
1467/// Both alphas take the annotation's `/CA` when the key is present, whatever
1468/// its type reads as, and one otherwise. Only the highlight generator asks
1469/// for a blend mode other than normal.
1470#[must_use]
1471pub(crate) fn ext_gstate_dict<R: Resolve>(dict: &Dict, multiply: bool, r: &R) -> Dict {
1472 let opacity = if dict.contains_key(names::CA) {
1473 dict.number(names::CA, r).unwrap_or(0.0)
1474 } else {
1475 1.0
1476 };
1477 let blend = if multiply {
1478 names::MULTIPLY
1479 } else {
1480 obj_names::NORMAL
1481 };
1482 let state = Dict::from_pairs([
1483 (
1484 obj_names::TYPE.clone(),
1485 Object::Name(names::EXT_G_STATE.clone()),
1486 ),
1487 (names::CA.clone(), Object::Real(opacity)),
1488 (names::CA_LOWER.clone(), Object::Real(opacity)),
1489 (names::AIS.clone(), Object::Bool(false)),
1490 (names::BM.clone(), Object::Name(blend.clone())),
1491 ]);
1492 Dict::from_pairs([(names::GS.clone(), Object::Dict(state))])
1493}
1494
1495/// The appearance stream's `/Resources`, omitting either half when absent.
1496#[must_use]
1497pub(crate) fn resources_dict(ext_gstate: Dict, font: Option<Dict>) -> Dict {
1498 let mut resources = Dict::new();
1499 resources.push(names::EXT_G_STATE.clone(), Object::Dict(ext_gstate));
1500 if let Some(font) = font {
1501 resources.push(names::FONT.clone(), Object::Dict(font));
1502 }
1503 resources
1504}
1505
1506/// The stream dictionary a generated appearance is stored under.
1507///
1508/// ```
1509/// use pdfrum_common::Diagnostics;
1510/// use pdfrum_doc::ap::generate_appearances;
1511/// use pdfrum_object::{Array, Dict, Name, NoResolve, Object};
1512///
1513/// let square = Dict::from_pairs([
1514/// (Name::from("Subtype"), Object::Name(Name::from("Square"))),
1515/// (
1516/// Name::from("Rect"),
1517/// Object::Array(Array::of([0, 0, 100, 50])),
1518/// ),
1519/// (
1520/// Name::from("IC"),
1521/// Object::Array(Array::of([1, 0, 0])),
1522/// ),
1523/// ]);
1524/// let page = Dict::from_pairs([(
1525/// Name::from("Annots"),
1526/// Object::Array(Array::of([Object::Dict(square)])),
1527/// )]);
1528///
1529/// let mut diags = Diagnostics::default();
1530/// let overlay = generate_appearances(&page, &NoResolve, &mut diags);
1531/// use pdfrum_doc::ap::stream_dict;
1532/// use pdfrum_object::names;
1533///
1534/// let dict = stream_dict(overlay.get(0).expect("generated"));
1535/// assert_eq!(dict.name(names::SUBTYPE).map(|n| n.as_bytes().to_vec()),
1536/// Some(b"Form".to_vec()));
1537/// ```
1538#[must_use]
1539pub fn stream_dict(generated: &GeneratedAp) -> Dict {
1540 Dict::from_pairs([
1541 (names::FORM_TYPE.clone(), Object::Int(1)),
1542 (
1543 obj_names::TYPE.clone(),
1544 Object::Name(names::XOBJECT.clone()),
1545 ),
1546 (
1547 obj_names::SUBTYPE.clone(),
1548 Object::Name(names::FORM.clone()),
1549 ),
1550 (
1551 names::MATRIX.clone(),
1552 Object::Array(matrix_array(generated.matrix)),
1553 ),
1554 (
1555 names::BBOX.clone(),
1556 Object::Array(rect_array(generated.bbox)),
1557 ),
1558 (
1559 names::RESOURCES.clone(),
1560 Object::Dict(generated.resources.clone()),
1561 ),
1562 (
1563 names::LENGTH.clone(),
1564 Object::Int(i64::try_from(generated.stream.len()).unwrap_or(0)),
1565 ),
1566 ])
1567}
1568
1569/// A transform as its six numbers.
1570///
1571/// Narrowed to single precision because that is what a PDF real is; the
1572/// transforms these generators write are all exactly representable anyway.
1573#[allow(clippy::cast_possible_truncation)]
1574fn matrix_array(matrix: Affine) -> Array {
1575 Array::of(
1576 matrix
1577 .as_coeffs()
1578 .into_iter()
1579 .map(|value| Object::Real(value as f32)),
1580 )
1581}
1582
1583/// A rectangle as its four corner numbers, in PDF's ordering.
1584fn rect_array(rect: Rect) -> Array {
1585 use crate::geom;
1586 Array::of(
1587 [
1588 geom::left(rect),
1589 geom::bottom(rect),
1590 geom::right(rect),
1591 geom::top(rect),
1592 ]
1593 .map(Object::Real),
1594 )
1595}
1596
1597#[cfg(test)]
1598mod tests {
1599 use super::{
1600 AnnotOverlay, Appearance, GeneratedAp, ext_gstate_dict, generate_appearances, generate_one,
1601 should_generate,
1602 };
1603 use crate::geom;
1604 use pdfrum_common::Diagnostics;
1605 use pdfrum_object::{Array, ByteSpan, Dict, Name, NoResolve, Object, Resolve, Stream};
1606
1607 fn dict(pairs: &[(&str, Object)]) -> Dict {
1608 Dict::from_pairs(
1609 pairs
1610 .iter()
1611 .map(|(k, v)| (Name::from(*k), v.clone()))
1612 .collect::<Vec<_>>(),
1613 )
1614 }
1615
1616 fn numbers(values: &[f32]) -> Object {
1617 Object::Array(Array::of(values.iter().copied().map(Object::from)))
1618 }
1619
1620 /// A map-backed [`Resolve`], for the key tests: what a font dictionary
1621 /// *resolves to* is irrelevant to the slot it is filed under, but the
1622 /// walk to it goes through `/AcroForm` and `/DR`, so the references on
1623 /// the way have to lead somewhere.
1624 struct Store(std::collections::HashMap<u32, std::sync::Arc<Object>>);
1625
1626 impl Store {
1627 fn of(pairs: impl IntoIterator<Item = (u32, Object)>) -> Store {
1628 Store(
1629 pairs
1630 .into_iter()
1631 .map(|(num, obj)| (num, std::sync::Arc::new(obj)))
1632 .collect(),
1633 )
1634 }
1635 }
1636
1637 impl Resolve for Store {
1638 fn fetch(
1639 &self,
1640 r: pdfrum_object::ObjRef,
1641 ) -> Result<std::sync::Arc<Object>, pdfrum_object::Error> {
1642 self.0
1643 .get(&r.num)
1644 .map(std::sync::Arc::clone)
1645 .ok_or(pdfrum_object::Error::UnresolvedRef(r))
1646 }
1647 }
1648
1649 fn reference(num: u32) -> Object {
1650 Object::Ref(pdfrum_object::ObjRef::new(num, 0))
1651 }
1652
1653 fn sticky_note() -> Dict {
1654 dict(&[
1655 ("Subtype", Object::Name(Name::from("Text"))),
1656 ("Rect", numbers(&[10.0, 20.0, 200.0, 300.0])),
1657 ])
1658 }
1659
1660 #[test]
1661 fn an_annotation_with_an_appearance_stream_generates_nothing() {
1662 let with_ap = dict(&[
1663 ("Subtype", Object::Name(Name::from("Text"))),
1664 (
1665 "AP",
1666 Object::Dict(dict(&[(
1667 "N",
1668 Object::Stream(Box::new(Stream::new(
1669 Dict::new(),
1670 ByteSpan::from(b"x".to_vec()),
1671 ))),
1672 )])),
1673 ),
1674 ]);
1675 assert!(!should_generate(&with_ap, &NoResolve));
1676 assert!(should_generate(&sticky_note(), &NoResolve));
1677 }
1678
1679 #[test]
1680 fn a_hidden_annotation_never_generates() {
1681 let mut hidden = sticky_note();
1682 hidden.push(Name::from("F"), Object::Int(2));
1683 assert!(!should_generate(&hidden, &NoResolve));
1684 }
1685
1686 #[test]
1687 fn a_character_the_face_cannot_map_is_still_written() {
1688 // `bug_725389` shows three Hebrew characters through a `/DA` naming
1689 // Times-Roman, which has no glyph for any of them. Dropping them loses
1690 // the text objects entirely — six become three — where the oracle
1691 // writes the raw code point as a character code and draws whatever it
1692 // names. Wrong glyph, right object count, right layout.
1693 let font = pdfrum_font::Font::load_standard(
1694 pdfrum_font::StandardFont::Times,
1695 &pdfrum_font::FontCache::new(),
1696 );
1697 let width = |code: u32| super::TextFont::char_width(&font, code);
1698 let text = super::TextFont {
1699 metrics: super::TextFont::metrics_of(&font, &width),
1700 font: &font,
1701 };
1702 // Hebrew bet, which no standard Latin face encodes.
1703 assert_eq!(text.encode(0x05D1), vec![0xD1]);
1704 // And a character it does encode still round-trips through the
1705 // `ToUnicode` mapping rather than through the fallthrough.
1706 assert_eq!(text.encode(u32::from('A')), vec![b'A']);
1707 // The width follows whatever `encode` wrote, so the layout advances by
1708 // the same glyph the stream names.
1709 assert_eq!(
1710 super::TextFont::char_width(&font, 0x05D1),
1711 super::TextFont::char_width(&font, 0xD1)
1712 );
1713 }
1714
1715 #[test]
1716 fn a_sticky_notes_rectangle_override_reaches_the_overlay() {
1717 let page = dict(&[(
1718 "Annots",
1719 Object::Array(Array::of([Object::Dict(sticky_note())])),
1720 )]);
1721 let mut diags = Diagnostics::default();
1722 let overlay = generate_appearances(&page, &NoResolve, &mut diags);
1723 assert_eq!(overlay.len(), 1);
1724 let raw = geom::rect(10.0, 20.0, 200.0, 300.0);
1725 assert_eq!(overlay.rect(0, raw), geom::rect(10.0, 20.0, 30.0, 40.0));
1726 // The bounding box follows the rewritten rectangle, not the file's.
1727 assert_eq!(
1728 overlay.get(0).map(|generated| generated.bbox),
1729 Some(geom::rect(10.0, 20.0, 30.0, 40.0))
1730 );
1731 }
1732
1733 #[test]
1734 fn a_pop_up_written_into_the_file_is_skipped_by_the_walk() {
1735 let page = dict(&[(
1736 "Annots",
1737 Object::Array(Array::of([Object::Dict(dict(&[(
1738 "Subtype",
1739 Object::Name(Name::from("Popup")),
1740 )]))])),
1741 )]);
1742 let mut diags = Diagnostics::default();
1743 let overlay = generate_appearances(&page, &NoResolve, &mut diags);
1744 // The slot still exists — indices stay aligned with `/Annots` — but
1745 // nothing was generated into it.
1746 assert_eq!(overlay.len(), 1);
1747 assert!(overlay.get(0).is_none());
1748 }
1749
1750 #[test]
1751 fn a_subtype_with_no_generator_produces_nothing() {
1752 let stamp = dict(&[("Subtype", Object::Name(Name::from("Stamp")))]);
1753 let mut diags = Diagnostics::default();
1754 assert!(generate_one(&stamp, &NoResolve, &mut diags).is_none());
1755 }
1756
1757 #[test]
1758 fn a_text_markup_bounding_box_comes_from_the_quadrilaterals() {
1759 let highlight = dict(&[
1760 ("Subtype", Object::Name(Name::from("Highlight"))),
1761 ("Rect", numbers(&[0.0, 0.0, 5.0, 5.0])),
1762 (
1763 "QuadPoints",
1764 numbers(&[10.0, 20.0, 30.0, 20.0, 10.0, 10.0, 30.0, 10.0]),
1765 ),
1766 ]);
1767 let mut diags = Diagnostics::default();
1768 let got = generate_one(&highlight, &NoResolve, &mut diags).expect("generates");
1769 assert_eq!(got.bbox, geom::rect(10.0, 10.0, 30.0, 20.0));
1770 }
1771
1772 #[test]
1773 fn the_graphics_state_takes_its_alpha_from_the_opacity_key() {
1774 let opaque = ext_gstate_dict(&Dict::new(), false, &NoResolve);
1775 let state = opaque
1776 .dict(&Name::from("GS"), &NoResolve)
1777 .expect("one entry");
1778 assert_eq!(state.number(&Name::from("CA"), &NoResolve), Some(1.0));
1779 assert_eq!(
1780 state.name(&Name::from("BM")).map(Name::as_bytes),
1781 Some(&b"Normal"[..])
1782 );
1783
1784 let half = dict(&[("CA", Object::from(0.5_f32))]);
1785 let state = ext_gstate_dict(&half, true, &NoResolve)
1786 .dict(&Name::from("GS"), &NoResolve)
1787 .expect("one entry");
1788 assert_eq!(state.number(&Name::from("ca"), &NoResolve), Some(0.5));
1789 assert_eq!(
1790 state.name(&Name::from("BM")).map(Name::as_bytes),
1791 Some(&b"Multiply"[..])
1792 );
1793 }
1794
1795 /// A generated appearance, distinguishable by its stream.
1796 fn made(stream: &str) -> GeneratedAp {
1797 GeneratedAp {
1798 stream: stream.as_bytes().to_vec(),
1799 bbox: geom::rect(0.0, 0.0, 1.0, 1.0),
1800 matrix: kurbo::Affine::IDENTITY,
1801 resources: Dict::default(),
1802 rect_override: None,
1803 as_override: None,
1804 }
1805 }
1806
1807 /// The merge's whole contract in one test: the supplied overlay wins
1808 /// where it speaks, defers where it does not, and can say "draw nothing"
1809 /// as a value rather than as an absence.
1810 #[test]
1811 fn a_supplied_overlay_wins_only_where_it_has_something_to_say() {
1812 let mut base = AnnotOverlay::with_capacity(4);
1813 base.set(0, made("base zero"));
1814 base.set(1, made("base one"));
1815 base.set(2, made("base two"));
1816
1817 let mut supplied = AnnotOverlay::with_capacity(4);
1818 supplied.set(1, made("live one"));
1819 supplied.set_appearance(2, Appearance::Suppressed);
1820 // Index 0 and 3 are untouched and must not disturb the base.
1821
1822 base.merge_over(&supplied);
1823
1824 assert_eq!(
1825 base.get(0).map(|g| g.stream.clone()),
1826 Some(b"base zero".to_vec()),
1827 "an untouched entry leaves the generated one alone"
1828 );
1829 assert_eq!(
1830 base.get(1).map(|g| g.stream.clone()),
1831 Some(b"live one".to_vec()),
1832 "a supplied entry replaces the generated one"
1833 );
1834 assert_eq!(
1835 base.appearance(2),
1836 &Appearance::Suppressed,
1837 "suppression survives the merge as a value"
1838 );
1839 assert_eq!(base.get(2), None, "a suppressed entry has no stream");
1840 assert_eq!(base.appearance(3), &Appearance::Untouched);
1841 }
1842
1843 /// Suppression and absence read the same to `get` and differently to
1844 /// `appearance` — which is the distinction the enum exists to carry.
1845 #[test]
1846 fn suppressed_and_untouched_differ_only_where_it_matters() {
1847 let mut overlay = AnnotOverlay::with_capacity(2);
1848 overlay.set_appearance(0, Appearance::Suppressed);
1849 assert_eq!(overlay.get(0), None);
1850 assert_eq!(overlay.get(1), None);
1851 assert_ne!(overlay.appearance(0), overlay.appearance(1));
1852 // An index past the end is untouched rather than a panic, so a short
1853 // overlay is safe to consult for any annotation.
1854 assert_eq!(overlay.appearance(99), &Appearance::Untouched);
1855 }
1856
1857 /// An entry past the end of the overlay being merged into is dropped:
1858 /// there is no annotation for it to apply to.
1859 #[test]
1860 fn a_supplied_entry_past_the_end_is_dropped() {
1861 let mut base = AnnotOverlay::with_capacity(1);
1862 let mut supplied = AnnotOverlay::with_capacity(5);
1863 supplied.set(4, made("nowhere"));
1864 base.merge_over(&supplied);
1865 assert_eq!(base.len(), 1);
1866 assert_eq!(base.get(4), None);
1867 }
1868
1869 #[test]
1870 fn only_the_named_annotation_is_a_live_edit() {
1871 let mut overlay = AnnotOverlay::with_capacity(3);
1872 overlay.set(0, made("a"));
1873 overlay.set(1, made("b"));
1874 assert_eq!(overlay.live_edit(), None);
1875 assert!(!overlay.is_live_edit(0));
1876 overlay.set_live_edit(1);
1877 assert_eq!(overlay.live_edit(), Some(1));
1878 assert!(overlay.is_live_edit(1));
1879 // Every other annotation's appearance is an ordinary one, which is
1880 // what keeps ClearType off the rest of the page.
1881 assert!(!overlay.is_live_edit(0));
1882 assert!(!overlay.is_live_edit(2));
1883 }
1884
1885 #[test]
1886 fn a_session_editing_a_second_field_replaces_the_first() {
1887 // One field is edited at a time, so this is a replacement rather than
1888 // a set: a stale mark would draw a field's committed text with
1889 // ClearType long after the editor left it.
1890 let mut overlay = AnnotOverlay::with_capacity(3);
1891 overlay.set_live_edit(0);
1892 overlay.set_live_edit(2);
1893 assert_eq!(overlay.live_edit(), Some(2));
1894 assert!(!overlay.is_live_edit(0));
1895 }
1896
1897 #[test]
1898 fn merging_carries_the_live_edit_mark_over() {
1899 // The mark has to survive `merge_over` or it would be lost exactly
1900 // where it matters — the supplied overlay is the session's, and the
1901 // base is what the annotation pass generated.
1902 let mut base = AnnotOverlay::with_capacity(2);
1903 let mut supplied = AnnotOverlay::with_capacity(2);
1904 supplied.set(1, made("edited"));
1905 supplied.set_live_edit(1);
1906 base.merge_over(&supplied);
1907 assert!(base.is_live_edit(1));
1908 // And a merge that says nothing about it leaves the mark alone.
1909 let untouched = AnnotOverlay::with_capacity(2);
1910 base.merge_over(&untouched);
1911 assert!(base.is_live_edit(1));
1912 }
1913
1914 #[test]
1915 fn the_form_faces_are_built_once_per_document_and_shared() {
1916 // `FormFonts::load` builds `/DR` fonts, the fallback, and the
1917 // synthesized second faces once per document; the annotation overlay
1918 // calls it once per page per render.
1919 let catalog = dict(&[("AcroForm", Object::Ref(pdfrum_object::ObjRef::new(7, 0)))]);
1920 let mut ctx = pdfrum_page::BuildContext::new();
1921 let first = super::FormFonts::load(&catalog, &NoResolve, &mut ctx);
1922 let second = super::FormFonts::load(&catalog, &NoResolve, &mut ctx);
1923 assert!(
1924 std::sync::Arc::ptr_eq(&first, &second),
1925 "a second load of one document's form must hit the cache"
1926 );
1927 }
1928
1929 #[test]
1930 fn a_catalog_with_no_form_shares_one_set_of_faces() {
1931 // The case that made this a whole-corpus regression rather than a
1932 // forms one: a document with an annotation but no `/AcroForm` paid
1933 // the fallback load and the substitute synthesis on every render.
1934 // With no form there is nothing document-specific to build, so one
1935 // slot serves every such document a context is threaded through.
1936 let mut ctx = pdfrum_page::BuildContext::new();
1937 let first = super::FormFonts::load(&dict(&[]), &NoResolve, &mut ctx);
1938 let second = super::FormFonts::load(
1939 &dict(&[("Type", Object::Name(Name::from("Catalog")))]),
1940 &NoResolve,
1941 &mut ctx,
1942 );
1943 assert!(std::sync::Arc::ptr_eq(&first, &second));
1944 }
1945
1946 #[test]
1947 fn two_documents_do_not_share_one_contexts_form_faces() {
1948 // A `BuildContext` may legitimately be threaded through two
1949 // documents, so the cache keys on the `/AcroForm` reference the way
1950 // every other cache on it keys on the reference that named its value.
1951 let mut ctx = pdfrum_page::BuildContext::new();
1952 let one = super::FormFonts::load(
1953 &dict(&[("AcroForm", Object::Ref(pdfrum_object::ObjRef::new(7, 0)))]),
1954 &NoResolve,
1955 &mut ctx,
1956 );
1957 let two = super::FormFonts::load(
1958 &dict(&[("AcroForm", Object::Ref(pdfrum_object::ObjRef::new(8, 0)))]),
1959 &NoResolve,
1960 &mut ctx,
1961 );
1962 assert!(!std::sync::Arc::ptr_eq(&one, &two));
1963 }
1964
1965 /// A `/DR /Font` written out in full, which is the one spelling with no
1966 /// reference anywhere for the key to name.
1967 fn wholly_direct_form() -> Dict {
1968 dict(&[(
1969 "DR",
1970 Object::Dict(dict(&[(
1971 "Font",
1972 Object::Dict(dict(&[("Helv", Object::Dict(dict(&[])))])),
1973 )])),
1974 )])
1975 }
1976
1977 #[test]
1978 fn a_form_whose_fonts_are_written_out_in_full_is_not_cached() {
1979 // The one case that stays uncached: a direct `/AcroForm` whose
1980 // `/DR /Font` is itself direct has no reference at either level, and
1981 // its content *is* document-specific, so it re-derives rather than
1982 // risking one document's faces standing in for another's.
1983 let catalog = dict(&[("AcroForm", Object::Dict(wholly_direct_form()))]);
1984 let mut ctx = pdfrum_page::BuildContext::new();
1985 let first = super::FormFonts::load(&catalog, &NoResolve, &mut ctx);
1986 let second = super::FormFonts::load(&catalog, &NoResolve, &mut ctx);
1987 assert!(!std::sync::Arc::ptr_eq(&first, &second));
1988 }
1989
1990 #[test]
1991 fn a_direct_form_declaring_no_fonts_is_the_no_form_case() {
1992 // What the corpus actually carries. `<</Fields[]>>` written straight
1993 // into the catalog is what a producer emits when it declares a form
1994 // and puts no fields in it, and six of the 44 benchmark documents
1995 // have one — none of them a form document. It names no `/DR /Font`,
1996 // so its faces are the fallback and the substitutes, built from
1997 // dictionaries this crate writes: the same value a catalog with no
1998 // `/AcroForm` at all gets, and therefore the same slot.
1999 let mut ctx = pdfrum_page::BuildContext::new();
2000 let empty_form = super::FormFonts::load(
2001 &dict(&[(
2002 "AcroForm",
2003 Object::Dict(dict(&[("Fields", Object::Array(Array::default()))])),
2004 )]),
2005 &NoResolve,
2006 &mut ctx,
2007 );
2008 let no_form = super::FormFonts::load(&dict(&[]), &NoResolve, &mut ctx);
2009 assert!(
2010 std::sync::Arc::ptr_eq(&empty_form, &no_form),
2011 "a form with no default resources builds nothing a form-less \
2012 catalog does not"
2013 );
2014 }
2015
2016 #[test]
2017 fn a_direct_form_is_keyed_on_the_font_dictionary_it_names() {
2018 // The ordinary spelling of an unusual case: the `/AcroForm` is
2019 // direct but its `/DR /Font` is a reference, which is as good an
2020 // identity as the form's own reference would have been. Two catalogs
2021 // naming *different* font dictionaries must not share, and one
2022 // catalog asked twice must.
2023 let form = |num: u32| {
2024 dict(&[(
2025 "AcroForm",
2026 Object::Dict(dict(&[(
2027 "DR",
2028 Object::Dict(dict(&[("Font", reference(num))])),
2029 )])),
2030 )])
2031 };
2032 let store = Store::of([(7, Object::Dict(dict(&[]))), (8, Object::Dict(dict(&[])))]);
2033 let mut ctx = pdfrum_page::BuildContext::new();
2034 let first = super::FormFonts::load(&form(7), &store, &mut ctx);
2035 let again = super::FormFonts::load(&form(7), &store, &mut ctx);
2036 let other = super::FormFonts::load(&form(8), &store, &mut ctx);
2037 assert!(
2038 std::sync::Arc::ptr_eq(&first, &again),
2039 "one font dictionary asked twice must hit the cache"
2040 );
2041 assert!(
2042 !std::sync::Arc::ptr_eq(&first, &other),
2043 "two font dictionaries must not share a slot"
2044 );
2045 }
2046
2047 #[test]
2048 fn the_key_reads_the_font_dictionarys_spelling_and_not_its_value() {
2049 // The four cases, asserted directly rather than through the ptr
2050 // identity the three tests above compare. This is the function's
2051 // whole contract: which of the four slots a catalog lands in.
2052 use pdfrum_page::FormFontsKey;
2053 let store = Store::of([(7, Object::Dict(dict(&[])))]);
2054 let key = |catalog: &Dict| super::FormFonts::key(catalog, &store);
2055
2056 assert_eq!(key(&dict(&[])), FormFontsKey::None);
2057 assert_eq!(
2058 key(&dict(&[("AcroForm", reference(7))])),
2059 FormFontsKey::Form(pdfrum_object::ObjRef::new(7, 0))
2060 );
2061 assert_eq!(
2062 key(&dict(&[(
2063 "AcroForm",
2064 Object::Dict(dict(&[("Fields", Object::Array(Array::default()))]))
2065 )])),
2066 FormFontsKey::None,
2067 "a direct form with no `/DR /Font` depends on nothing"
2068 );
2069 assert_eq!(
2070 key(&dict(&[(
2071 "AcroForm",
2072 Object::Dict(dict(&[(
2073 "DR",
2074 Object::Dict(dict(&[("Font", reference(7))]))
2075 )]))
2076 )])),
2077 FormFontsKey::DirectResources(pdfrum_object::ObjRef::new(7, 0))
2078 );
2079 assert_eq!(
2080 key(&dict(&[("AcroForm", Object::Dict(wholly_direct_form()))])),
2081 FormFontsKey::Direct
2082 );
2083 }
2084
2085 #[test]
2086 fn an_overlay_that_marks_no_live_edit_leaves_every_index_ordinary() {
2087 // The default, and the whole corpus outside the form-events rows.
2088 let mut overlay = AnnotOverlay::with_capacity(4);
2089 overlay.set(2, made("generated"));
2090 assert_eq!(overlay.live_edit(), None);
2091 for index in 0..6 {
2092 assert!(!overlay.is_live_edit(index), "index {index}");
2093 }
2094 }
2095}