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//! 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}