Skip to main content

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