Skip to main content

pdfboss_core/
annotation.rs

1//! Annotations (ISO 32000-1 §12.5) and actions (§12.6), read as data: the
2//! entries every annotation dictionary shares, the markup entries, the
3//! state a reply sets, a link's destination, an attachment's file
4//! specification, and action dictionaries with their `/Next` chains and
5//! the trigger events that name them. No action is ever executed.
6
7use crate::article::rectangle;
8use crate::date::Date;
9use crate::destination::{destination_with, named_destination_with, Destination};
10use crate::document::Page;
11use crate::embedded_file::{bytes_entry, file_spec_with, FileSpec};
12use crate::geom::Rect;
13use crate::hash::FastSet;
14use crate::object::{decode_text_string, Dict, ObjRef, Object};
15use crate::source::AsyncObjectSource;
16use crate::tree::{resolved_dict, Entries};
17
18/// Maximum action dictionaries one `/A` or `/AA` entry reads, every
19/// `/Next` branch included; past it the rest of the tree is dropped.
20pub const MAX_ACTIONS: usize = 256;
21
22/// Maximum nesting of target dictionaries (`/T` inside `/T`) an embedded
23/// go-to action reads.
24pub const MAX_TARGET_DEPTH: usize = 32;
25
26/// The subtypes Table 169 marks as markup annotations, whose dictionaries
27/// carry the Table 170 entries.
28const MARKUP_SUBTYPES: [&str; 17] = [
29    "Text",
30    "FreeText",
31    "Line",
32    "Square",
33    "Circle",
34    "Polygon",
35    "PolyLine",
36    "Highlight",
37    "Underline",
38    "Squiggly",
39    "StrikeOut",
40    "Stamp",
41    "Caret",
42    "Ink",
43    "FileAttachment",
44    "Sound",
45    "Redact",
46];
47
48/// The `/F` flag word of an annotation (ISO 32000-1 §12.5.3, Table 165),
49/// bit 1 being the lowest.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub struct AnnotationFlags(pub u32);
52
53impl AnnotationFlags {
54    /// Whether the flag at `position` (1-based, as Table 165 numbers them)
55    /// is set.
56    ///
57    /// Covers ISO 32000-1 §12.5.3.
58    pub fn bit(self, position: u32) -> bool {
59        (1..=32).contains(&position) && self.0 & (1 << (position - 1)) != 0
60    }
61
62    /// Bit 1: do not show an annotation of an unknown subtype.
63    pub fn invisible(self) -> bool {
64        self.bit(1)
65    }
66
67    /// Bit 2: never show or print the annotation.
68    pub fn hidden(self) -> bool {
69        self.bit(2)
70    }
71
72    /// Bit 3: print the annotation with the page.
73    pub fn print(self) -> bool {
74        self.bit(3)
75    }
76
77    /// Bit 4: keep the appearance at its size whatever the magnification.
78    pub fn no_zoom(self) -> bool {
79        self.bit(4)
80    }
81
82    /// Bit 5: keep the appearance upright whatever the page rotation.
83    pub fn no_rotate(self) -> bool {
84        self.bit(5)
85    }
86
87    /// Bit 6: do not show the annotation on screen or let it interact.
88    pub fn no_view(self) -> bool {
89        self.bit(6)
90    }
91
92    /// Bit 7: do not let the user interact with the annotation.
93    pub fn read_only(self) -> bool {
94        self.bit(7)
95    }
96
97    /// Bit 8: do not let the user delete or move the annotation.
98    pub fn locked(self) -> bool {
99        self.bit(8)
100    }
101
102    /// Bit 9: invert the NoView flag for certain events.
103    pub fn toggle_no_view(self) -> bool {
104        self.bit(9)
105    }
106
107    /// Bit 10: do not let the user change the annotation's contents.
108    pub fn locked_contents(self) -> bool {
109        self.bit(10)
110    }
111}
112
113/// An annotation's `/Border` array (ISO 32000-1 §12.5.2, Table 164): the
114/// corner radii and width of the border, with the dash array PDF 1.1
115/// allows as a fourth element.
116#[derive(Debug, Clone, PartialEq)]
117pub struct Border {
118    pub horizontal_radius: f32,
119    pub vertical_radius: f32,
120    pub width: f32,
121    /// The dash array in the form of the graphics state's line dash
122    /// pattern (§8.4.3.6); `None` for a solid border.
123    pub dash: Option<Vec<f32>>,
124}
125
126/// How a reply relates to the annotation it replies to (`/RT`, ISO
127/// 32000-1 §12.5.6.2, Table 170).
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
129pub enum ReplyType {
130    /// `R`: a reply, shown threaded under the original (the default).
131    #[default]
132    Reply,
133    /// `Group`: a subordinate annotation of a group the original leads.
134    Group,
135}
136
137impl ReplyType {
138    fn from_name(name: &str) -> Option<ReplyType> {
139        match name {
140            "R" => Some(ReplyType::Reply),
141            "Group" => Some(ReplyType::Group),
142            _ => None,
143        }
144    }
145}
146
147/// The entries every markup annotation may carry (ISO 32000-1 §12.5.6.2,
148/// Table 170).
149#[derive(Debug, Clone, PartialEq)]
150pub struct Markup {
151    /// `/T`: the label of the pop-up window's title bar, by convention the
152    /// author.
153    pub title: Option<String>,
154    /// `/Popup`: the pop-up annotation that shows the text, by reference.
155    pub popup: Option<ObjRef>,
156    /// `/CA`: the constant opacity the annotation is painted with when it
157    /// has no appearance stream; 1 by default.
158    pub opacity: f32,
159    /// `/RC`: the rich text shown in the pop-up window, a text string or a
160    /// text stream, decoded.
161    pub rich_contents: Option<String>,
162    /// `/CreationDate`, parsed.
163    pub created: Option<Date>,
164    /// `/IRT`: the annotation this one replies to, by reference.
165    pub in_reply_to: Option<ObjRef>,
166    /// `/Subj`: a short description of the subject.
167    pub subject: Option<String>,
168    /// `/RT`: how this annotation relates to `in_reply_to`.
169    pub reply_type: ReplyType,
170    /// `/IT`: the intent that refines the subtype's behaviour, as written;
171    /// `None` when absent.
172    pub intent: Option<String>,
173}
174
175/// The state a text annotation sets on the annotation it replies to (ISO
176/// 32000-1 §12.5.6.3, Tables 171 and 172).
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct AnnotationState {
179    /// `/StateModel`: `Marked` or `Review`, as written; `None` when the
180    /// annotation names a state without its model.
181    pub model: Option<String>,
182    /// `/State`, or the model's default when absent: `Unmarked` under
183    /// `Marked`, `None` under `Review`.
184    pub state: String,
185}
186
187/// The event that fires an action in an additional-actions dictionary
188/// (ISO 32000-1 §12.6.3, Tables 194 to 197).
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum Trigger {
191    /// `E`: the cursor enters the annotation's active area.
192    CursorEnter,
193    /// `X`: the cursor leaves the annotation's active area.
194    CursorExit,
195    /// `D`: the mouse button goes down inside the active area.
196    MouseDown,
197    /// `U`: the mouse button comes up inside the active area.
198    MouseUp,
199    /// `Fo`: a widget receives the input focus.
200    Focus,
201    /// `Bl`: a widget loses the input focus.
202    Blur,
203    /// `PO`: the page holding the annotation opens.
204    PageOpen,
205    /// `PC`: the page holding the annotation closes.
206    PageClose,
207    /// `PV`: the page holding the annotation becomes visible.
208    PageVisible,
209    /// `PI`: the page holding the annotation stops being visible.
210    PageInvisible,
211    /// `K` on a field: a keystroke changes a text or choice field.
212    Keystroke,
213    /// `F` on a field: the field is about to be formatted for display.
214    Format,
215    /// `V` on a field: the field's value changed.
216    Validate,
217    /// `C` on a field: another field's value changed.
218    Calculate,
219    /// `O` on a page: the page opens.
220    Open,
221    /// `C` on a page: the page closes.
222    Close,
223    /// `WC` on the catalog: the document is about to close.
224    WillClose,
225    /// `WS` on the catalog: the document is about to be saved.
226    WillSave,
227    /// `DS` on the catalog: the document was saved.
228    DidSave,
229    /// `WP` on the catalog: the document is about to be printed.
230    WillPrint,
231    /// `DP` on the catalog: the document was printed.
232    DidPrint,
233}
234
235/// The keys of an annotation's additional-actions dictionary (Table 194),
236/// with the field triggers (Table 196) a merged widget and field
237/// dictionary also carries.
238const ANNOTATION_TRIGGERS: [(&str, Trigger); 14] = [
239    ("E", Trigger::CursorEnter),
240    ("X", Trigger::CursorExit),
241    ("D", Trigger::MouseDown),
242    ("U", Trigger::MouseUp),
243    ("Fo", Trigger::Focus),
244    ("Bl", Trigger::Blur),
245    ("PO", Trigger::PageOpen),
246    ("PC", Trigger::PageClose),
247    ("PV", Trigger::PageVisible),
248    ("PI", Trigger::PageInvisible),
249    ("K", Trigger::Keystroke),
250    ("F", Trigger::Format),
251    ("V", Trigger::Validate),
252    ("C", Trigger::Calculate),
253];
254
255/// The keys of a page's additional-actions dictionary (Table 195).
256const PAGE_TRIGGERS: [(&str, Trigger); 2] = [("O", Trigger::Open), ("C", Trigger::Close)];
257
258/// The keys of the catalog's additional-actions dictionary (Table 197).
259const DOCUMENT_TRIGGERS: [(&str, Trigger); 5] = [
260    ("WC", Trigger::WillClose),
261    ("WS", Trigger::WillSave),
262    ("DS", Trigger::DidSave),
263    ("WP", Trigger::WillPrint),
264    ("DP", Trigger::DidPrint),
265];
266
267/// One entry of an additional-actions dictionary: the event and the action
268/// it fires.
269#[derive(Debug, Clone, PartialEq)]
270pub struct TriggeredAction {
271    pub trigger: Trigger,
272    pub action: Action,
273}
274
275/// Whether a target document is the parent or a child of the current one
276/// (`/R`, ISO 32000-1 §12.6.4.4, Table 202).
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278pub enum Relationship {
279    Parent,
280    Child,
281}
282
283/// The page a target dictionary's `/P` names: a 0-based page number, or a
284/// named destination that resolves to one.
285#[derive(Debug, Clone, PartialEq, Eq)]
286pub enum TargetPage {
287    Number(u32),
288    Named(Vec<u8>),
289}
290
291/// The file attachment annotation a target dictionary's `/A` names: its
292/// 0-based index in the page's `/Annots`, or its `/NM` name.
293#[derive(Debug, Clone, PartialEq, Eq)]
294pub enum TargetAnnotation {
295    Index(u32),
296    Name(String),
297}
298
299/// One step of the path from the current document to the document an
300/// embedded go-to action jumps into (ISO 32000-1 §12.6.4.4, Table 202).
301#[derive(Debug, Clone, PartialEq)]
302pub struct Target {
303    /// `/R`: whether this step goes to the parent document or to a child.
304    pub relationship: Relationship,
305    /// `/N`: the child's name in the `/EmbeddedFiles` name tree.
306    pub name: Option<Vec<u8>>,
307    /// `/P`: the page whose file attachment annotation holds the child.
308    pub page: Option<TargetPage>,
309    /// `/A`: which annotation of that page holds the child.
310    pub annotation: Option<TargetAnnotation>,
311    /// `/T`: the next step; `None` when this step's document is the target.
312    pub next: Option<Box<Target>>,
313}
314
315/// The Windows launch parameters of a launch action (ISO 32000-1
316/// §12.6.4.5, Table 204).
317#[derive(Debug, Clone, PartialEq, Eq)]
318pub struct WindowsLaunch {
319    /// `/F`: the application or document, a plain Windows path.
320    pub file: Vec<u8>,
321    /// `/D`: the default directory.
322    pub directory: Option<Vec<u8>>,
323    /// `/O`: `open` (the default) or `print`.
324    pub operation: String,
325    /// `/P`: the parameter string passed to the application.
326    pub parameters: Option<Vec<u8>>,
327}
328
329/// What one action dictionary asks for (ISO 32000-1 §12.6.4, Table 198).
330/// The go-to family, launch, URI, named and JavaScript actions are read
331/// into their own entries; every other type keeps its dictionary.
332#[derive(Debug, Clone, PartialEq)]
333pub enum ActionKind {
334    /// `GoTo` (§12.6.4.2): a destination in this document. A named `/D` is
335    /// looked up; `named_destination` keeps the name either way.
336    GoTo {
337        destination: Option<Destination>,
338        named_destination: Option<Vec<u8>>,
339    },
340    /// `GoToR` (§12.6.4.3): a destination in another PDF file, whose
341    /// explicit form numbers pages from 0 and whose named form cannot be
342    /// looked up here.
343    GoToR {
344        file: Option<FileSpec>,
345        destination: Option<Destination>,
346        named_destination: Option<Vec<u8>>,
347        new_window: Option<bool>,
348    },
349    /// `GoToE` (§12.6.4.4): a destination in an embedded PDF file reached
350    /// through `target`.
351    GoToE {
352        file: Option<FileSpec>,
353        destination: Option<Destination>,
354        named_destination: Option<Vec<u8>>,
355        new_window: Option<bool>,
356        target: Option<Target>,
357    },
358    /// `Launch` (§12.6.4.5): an application to start or a document to
359    /// open or print.
360    Launch {
361        file: Option<FileSpec>,
362        windows: Option<WindowsLaunch>,
363        new_window: Option<bool>,
364    },
365    /// `URI` (§12.6.4.7): a resource to resolve.
366    Uri {
367        /// `/URI`, decoded as a text string (7-bit ASCII as the clause
368        /// requires, so the decoding is the identity).
369        uri: String,
370        /// `/IsMap`: whether the mouse position is appended to the URI.
371        is_map: bool,
372    },
373    /// `Named` (§12.6.4.11): a viewer action by name (`/N`).
374    Named { name: String },
375    /// `JavaScript` (§12.6.4.16): the script (`/JS`), a text string or a
376    /// text stream, decoded and never run.
377    JavaScript { script: Option<String> },
378    /// Any other `/S` value, its dictionary kept as written.
379    Other { kind: String, entries: Dict },
380}
381
382/// One action dictionary (ISO 32000-1 §12.6.2, Table 193) and the actions
383/// its `/Next` entry chains after it, in order.
384#[derive(Debug, Clone, PartialEq)]
385pub struct Action {
386    /// The dictionary's reference; `None` for a direct dictionary.
387    pub object: Option<ObjRef>,
388    pub kind: ActionKind,
389    /// `/Next`: the actions performed after this one, a single dictionary
390    /// or an array, each with its own chain. A dictionary already read in
391    /// this tree is not entered again.
392    pub next: Vec<Action>,
393}
394
395/// One annotation of a page (ISO 32000-1 §12.5.2, Table 164), with the
396/// entries of the subtypes pdfboss reads beyond the common ones: the
397/// markup entries (§12.5.6.2), a reply's state (§12.5.6.3), a text or
398/// pop-up annotation's open flag and icon, a link's destination
399/// (§12.5.6.5), an attachment's file (§12.5.6.15), and the `/A` and `/AA`
400/// actions (§12.6).
401#[derive(Debug, Clone, PartialEq)]
402pub struct Annotation {
403    /// The dictionary's reference in the page's `/Annots`; `None` for a
404    /// direct dictionary.
405    pub object: Option<ObjRef>,
406    /// `/Subtype`, as written.
407    pub subtype: String,
408    /// `/Rect`, normalized; `None` when the entry is missing or malformed.
409    pub rect: Option<Rect>,
410    /// `/Contents`: the annotation's text, or an alternate description.
411    pub contents: Option<String>,
412    /// `/P`: the page the annotation belongs to, by reference.
413    pub page: Option<ObjRef>,
414    /// `/NM`: the name unique among the page's annotations.
415    pub name: Option<String>,
416    /// `/M`: when the annotation last changed, as written.
417    pub modified: Option<String>,
418    /// `/M` parsed as a date (§7.9.4); `None` when it is in another format.
419    pub modified_date: Option<Date>,
420    /// `/F`, 0 when absent.
421    pub flags: AnnotationFlags,
422    /// Whether an `/AP` dictionary with a normal appearance is present.
423    pub has_appearance: bool,
424    /// `/AS`: the appearance state that picks the appearance stream.
425    pub appearance_state: Option<String>,
426    /// `/Border`, as written; `None` when absent (the clause's default is
427    /// `[0 0 1]`) or malformed.
428    pub border: Option<Border>,
429    /// `/C`: the colour of the icon background, pop-up title bar or link
430    /// border, 0 (transparent), 1, 3 or 4 components as written.
431    pub color: Option<Vec<f32>>,
432    /// `/StructParent`: the key under which the structure tree's parent
433    /// tree lists the annotation (§14.7.4.4).
434    pub struct_parent: Option<i64>,
435    /// `/OC`: the optional content group or membership dictionary that
436    /// controls the annotation's visibility, by reference.
437    pub optional_content: Option<ObjRef>,
438    /// The Table 170 entries; `Some` for the markup subtypes of Table 169.
439    pub markup: Option<Markup>,
440    /// The state a text annotation sets on the annotation it replies to;
441    /// `Some` when `/StateModel` or `/State` is present.
442    pub state: Option<AnnotationState>,
443    /// `/Open` of a text or pop-up annotation.
444    pub open: Option<bool>,
445    /// `/Name`: the icon an annotation shows without an appearance stream,
446    /// as written; text, file attachment and sound annotations name one.
447    pub icon: Option<String>,
448    /// `/Parent`: the markup annotation a pop-up belongs to, by reference.
449    pub parent: Option<ObjRef>,
450    /// `/FS`: the file a file attachment annotation carries.
451    pub file: Option<FileSpec>,
452    /// A link's `/Dest`, explicit or looked up by name.
453    pub destination: Option<Destination>,
454    /// `/A`: the action performed when the annotation is activated, with
455    /// its `/Next` chain.
456    pub action: Option<Action>,
457    /// `/AA`: the actions the annotation's trigger events fire, in the
458    /// order of Tables 194 and 196.
459    pub additional_actions: Vec<TriggeredAction>,
460}
461
462/// The annotations of `page`'s `/Annots` array in order, each read as
463/// [`Annotation`]. An item that is no dictionary or names no `/Subtype` is
464/// skipped; a missing or malformed `/Annots` reads as empty. `trailer` is
465/// the document trailer, needed to look up named destinations.
466///
467/// Covers ISO 32000-1 §12.5.2.
468pub async fn annotations_with<S: AsyncObjectSource>(
469    src: &S,
470    trailer: &Dict,
471    page: &Page,
472) -> Vec<Annotation> {
473    let Some(entry) = page.dict().get("Annots") else {
474        return Vec::new();
475    };
476    let Ok(Object::Array(items)) = src.resolve(entry).await else {
477        return Vec::new();
478    };
479    let mut annotations = Vec::new();
480    for item in &items {
481        let Some(dict) = resolved_dict(src, item).await else {
482            continue;
483        };
484        let Some(subtype) = dict.get_name("Subtype") else {
485            continue;
486        };
487        let subtype = subtype.0.clone();
488        annotations.push(read_annotation(src, trailer, item.as_ref(), &dict, subtype).await);
489    }
490    annotations
491}
492
493/// The Table 164 entries of one annotation dictionary plus the subtype
494/// entries pdfboss reads.
495///
496/// Covers ISO 32000-1 §12.5.2, §12.5.6.4, §12.5.6.5, §12.5.6.14 and §12.5.6.15.
497async fn read_annotation<S: AsyncObjectSource>(
498    src: &S,
499    trailer: &Dict,
500    object: Option<ObjRef>,
501    dict: &Dict,
502    subtype: String,
503) -> Annotation {
504    let entries = Entries { src, dict };
505    let modified = entries.text("M").await;
506    let modified_date = modified.as_deref().and_then(Date::parse_pdf);
507    let flags = entries
508        .value("F")
509        .await
510        .and_then(|f| f.as_int())
511        .and_then(|f| u32::try_from(f).ok())
512        .map_or(AnnotationFlags::default(), AnnotationFlags);
513    let has_appearance = match dict.get("AP") {
514        Some(ap) => resolved_dict(src, ap)
515            .await
516            .is_some_and(|ap| ap.get("N").is_some()),
517        None => false,
518    };
519    let appearance_state = entries.named("AS", |n| Some(n.to_string())).await;
520    let markup = if MARKUP_SUBTYPES.contains(&subtype.as_str()) {
521        Some(read_markup(src, dict).await)
522    } else {
523        None
524    };
525    let file = match dict.get("FS") {
526        Some(fs) if subtype == "FileAttachment" => file_spec_with(src, fs).await,
527        _ => None,
528    };
529    let destination = match dict.get("Dest") {
530        Some(dest) if subtype == "Link" => {
531            let (explicit, _) = destination_entry(src, trailer, dest, false).await;
532            explicit
533        }
534        _ => None,
535    };
536    let action = match dict.get("A") {
537        Some(a) => action_with(src, trailer, a).await,
538        None => None,
539    };
540    let additional_actions = triggered_actions(src, trailer, dict, &ANNOTATION_TRIGGERS).await;
541    Annotation {
542        object,
543        subtype,
544        rect: rectangle(src, dict.get("Rect")).await,
545        contents: entries.text("Contents").await,
546        page: dict.get("P").and_then(Object::as_ref),
547        name: entries.text("NM").await,
548        modified,
549        modified_date,
550        flags,
551        has_appearance,
552        appearance_state,
553        border: read_border(src, dict.get("Border")).await,
554        color: number_array(src, dict.get("C")).await,
555        struct_parent: entries.value("StructParent").await.and_then(|v| v.as_int()),
556        optional_content: dict.get("OC").and_then(Object::as_ref),
557        markup,
558        state: read_state(&entries).await,
559        open: entries.flag("Open").await,
560        icon: entries.named("Name", |n| Some(n.to_string())).await,
561        parent: dict.get("Parent").and_then(Object::as_ref),
562        file,
563        destination,
564        action,
565        additional_actions,
566    }
567}
568
569/// The Table 170 entries of a markup annotation.
570///
571/// Covers ISO 32000-1 §12.5.6.2.
572async fn read_markup<S: AsyncObjectSource>(src: &S, dict: &Dict) -> Markup {
573    let entries = Entries { src, dict };
574    let opacity = entries
575        .value("CA")
576        .await
577        .and_then(|ca| ca.as_f64())
578        .filter(|ca| ca.is_finite())
579        .map_or(1.0, |ca| ca as f32);
580    Markup {
581        title: entries.text("T").await,
582        popup: dict.get("Popup").and_then(Object::as_ref),
583        opacity,
584        rich_contents: text_or_stream(src, dict.get("RC")).await,
585        created: match entries.text("CreationDate").await {
586            Some(date) => Date::parse_pdf(&date),
587            None => None,
588        },
589        in_reply_to: dict.get("IRT").and_then(Object::as_ref),
590        subject: entries.text("Subj").await,
591        reply_type: entries
592            .named("RT", ReplyType::from_name)
593            .await
594            .unwrap_or_default(),
595        intent: entries.named("IT", |n| Some(n.to_string())).await,
596    }
597}
598
599/// The `/StateModel` and `/State` of a reply, the state defaulted per
600/// model when only the model is given.
601///
602/// Covers ISO 32000-1 §12.5.6.3.
603async fn read_state<S: AsyncObjectSource>(entries: &Entries<'_, S>) -> Option<AnnotationState> {
604    let model = entries.text("StateModel").await;
605    let state = entries.text("State").await;
606    let state = match (state, model.as_deref()) {
607        (Some(state), _) => state,
608        (None, Some("Marked")) => "Unmarked".to_string(),
609        (None, Some("Review")) => "None".to_string(),
610        (None, _) => return None,
611    };
612    Some(AnnotationState { model, state })
613}
614
615/// A text string given directly or as a text stream (`/RC`, `/JS`),
616/// decoded (§7.9.2.2 and §7.9.3).
617async fn text_or_stream<S: AsyncObjectSource>(src: &S, value: Option<&Object>) -> Option<String> {
618    match src.resolve(value?).await.ok()? {
619        Object::String(bytes) => Some(decode_text_string(&bytes)),
620        Object::Stream(stream) => src
621            .stream_data(&stream)
622            .await
623            .ok()
624            .map(|bytes| decode_text_string(&bytes)),
625        _ => None,
626    }
627}
628
629/// An array of numbers, each possibly indirect; `None` when the value is
630/// no array or holds anything else.
631async fn number_array<S: AsyncObjectSource>(src: &S, value: Option<&Object>) -> Option<Vec<f32>> {
632    let array = src.resolve(value?).await.ok()?;
633    let items = array.as_array()?;
634    let mut numbers = Vec::with_capacity(items.len());
635    for item in items {
636        let number = src.resolve(item).await.ok()?.as_f64()?;
637        if !number.is_finite() {
638            return None;
639        }
640        numbers.push(number as f32);
641    }
642    Some(numbers)
643}
644
645/// The `/Border` array: three numbers and an optional dash array.
646///
647/// Covers ISO 32000-1 §12.5.2.
648async fn read_border<S: AsyncObjectSource>(src: &S, value: Option<&Object>) -> Option<Border> {
649    let array = src.resolve(value?).await.ok()?;
650    let items = array.as_array()?;
651    if items.len() < 3 {
652        return None;
653    }
654    let mut numbers = [0.0f32; 3];
655    for (slot, item) in numbers.iter_mut().zip(items) {
656        let number = src.resolve(item).await.ok()?.as_f64()?;
657        if !number.is_finite() {
658            return None;
659        }
660        *slot = number as f32;
661    }
662    let dash = match items.get(3) {
663        Some(dash) => Some(number_array(src, Some(dash)).await?),
664        None => None,
665    };
666    Some(Border {
667        horizontal_radius: numbers[0],
668        vertical_radius: numbers[1],
669        width: numbers[2],
670        dash,
671    })
672}
673
674/// A `/D` or `/Dest` value: an explicit destination array, or a name or
675/// string. The name is kept either way; it is looked up in this document
676/// unless `remote`, when it belongs to another file.
677///
678/// Covers ISO 32000-1 §12.6.4.2 and §12.6.4.3.
679async fn destination_entry<S: AsyncObjectSource>(
680    src: &S,
681    trailer: &Dict,
682    value: &Object,
683    remote: bool,
684) -> (Option<Destination>, Option<Vec<u8>>) {
685    let Ok(resolved) = src.resolve(value).await else {
686        return (None, None);
687    };
688    let name = match &resolved {
689        Object::Array(_) => return (destination_with(src, &resolved).await, None),
690        Object::Name(name) => name.0.as_bytes().to_vec(),
691        Object::String(bytes) => bytes.clone(),
692        _ => return (None, None),
693    };
694    if remote {
695        return (None, Some(name));
696    }
697    let destination = named_destination_with(src, trailer, &name).await;
698    (destination, Some(name))
699}
700
701/// The actions an additional-actions dictionary (`/AA` of `dict`) names,
702/// in the order of `triggers`.
703///
704/// Covers ISO 32000-1 §12.6.3.
705async fn triggered_actions<S: AsyncObjectSource>(
706    src: &S,
707    trailer: &Dict,
708    dict: &Dict,
709    triggers: &[(&str, Trigger)],
710) -> Vec<TriggeredAction> {
711    let Some(aa) = dict.get("AA") else {
712        return Vec::new();
713    };
714    let Some(aa) = resolved_dict(src, aa).await else {
715        return Vec::new();
716    };
717    let mut actions = Vec::new();
718    for (key, trigger) in triggers {
719        let Some(entry) = aa.get(key) else {
720            continue;
721        };
722        if let Some(action) = action_with(src, trailer, entry).await {
723            actions.push(TriggeredAction {
724                trigger: *trigger,
725                action,
726            });
727        }
728    }
729    actions
730}
731
732/// The actions a page's `/AA` dictionary fires when the page opens and
733/// closes (Table 195), in that order.
734///
735/// Covers ISO 32000-1 §12.6.3.
736pub async fn page_additional_actions_with<S: AsyncObjectSource>(
737    src: &S,
738    trailer: &Dict,
739    page: &Page,
740) -> Vec<TriggeredAction> {
741    triggered_actions(src, trailer, page.dict(), &PAGE_TRIGGERS).await
742}
743
744/// The actions the catalog's `/AA` dictionary fires around closing,
745/// saving and printing the document (Table 197), in that order.
746///
747/// Covers ISO 32000-1 §12.6.3.
748pub async fn document_additional_actions_with<S: AsyncObjectSource>(
749    src: &S,
750    trailer: &Dict,
751) -> Vec<TriggeredAction> {
752    let Some(root) = trailer.get("Root") else {
753        return Vec::new();
754    };
755    let Some(catalog) = resolved_dict(src, root).await else {
756        return Vec::new();
757    };
758    triggered_actions(src, trailer, &catalog, &DOCUMENT_TRIGGERS).await
759}
760
761/// One action dictionary being read, before its `/Next` children are
762/// moved under it.
763struct ActionNode {
764    parent: Option<usize>,
765    action: Action,
766}
767
768/// The action `value` holds, with its whole `/Next` tree. `None` when the
769/// value is no dictionary or names no `/S`. The tree is read breadth by
770/// depth-first order with a visited set over references and a budget of
771/// [`MAX_ACTIONS`] dictionaries, so a `/Next` that points back at an
772/// earlier action is not entered twice and a runaway tree stops.
773///
774/// Covers ISO 32000-1 §12.6.2 and §12.6.4.
775pub async fn action_with<S: AsyncObjectSource>(
776    src: &S,
777    trailer: &Dict,
778    value: &Object,
779) -> Option<Action> {
780    let mut visited: FastSet<ObjRef> = FastSet::default();
781    let mut nodes: Vec<ActionNode> = Vec::new();
782    let mut pending: Vec<(Option<usize>, Object)> = vec![(None, value.clone())];
783    while let Some((parent, object)) = pending.pop() {
784        if nodes.len() >= MAX_ACTIONS {
785            break;
786        }
787        let reference = object.as_ref();
788        if let Some(r) = reference {
789            if !visited.insert(r) {
790                continue;
791            }
792        }
793        let Some(dict) = resolved_dict(src, &object).await else {
794            continue;
795        };
796        let Some(kind) = read_action_kind(src, trailer, &dict).await else {
797            continue;
798        };
799        let index = nodes.len();
800        nodes.push(ActionNode {
801            parent,
802            action: Action {
803                object: reference,
804                kind,
805                next: Vec::new(),
806            },
807        });
808        // Children are pushed last first so they pop in document order.
809        let children = match dict.get("Next") {
810            Some(next) => match src.resolve(next).await {
811                Ok(Object::Array(items)) => items,
812                Ok(Object::Dict(_)) => vec![next.clone()],
813                _ => Vec::new(),
814            },
815            None => Vec::new(),
816        };
817        for child in children.into_iter().rev() {
818            pending.push((Some(index), child));
819        }
820    }
821    // Children were created after their parents, so folding from the back
822    // moves every finished subtree into its parent; a node's children
823    // arrive in reverse order and are turned round before it moves.
824    while let Some(node) = nodes.pop() {
825        let mut action = node.action;
826        action.next.reverse();
827        match node.parent {
828            Some(parent) => nodes[parent].action.next.push(action),
829            None => return Some(action),
830        }
831    }
832    None
833}
834
835/// The entries of one action dictionary by its `/S`.
836///
837/// Covers ISO 32000-1 §12.6.4, §12.6.4.2, §12.6.4.3, §12.6.4.4, §12.6.4.5, §12.6.4.7, §12.6.4.11 and §12.6.4.16.
838async fn read_action_kind<S: AsyncObjectSource>(
839    src: &S,
840    trailer: &Dict,
841    dict: &Dict,
842) -> Option<ActionKind> {
843    let entries = Entries { src, dict };
844    let kind = entries.value("S").await?.as_name()?.0.clone();
845    let file = |key: &'static str| async move {
846        match dict.get(key) {
847            Some(spec) => file_spec_with(src, spec).await,
848            None => None,
849        }
850    };
851    let destination = |remote: bool| async move {
852        match dict.get("D") {
853            Some(d) => destination_entry(src, trailer, d, remote).await,
854            None => (None, None),
855        }
856    };
857    Some(match kind.as_str() {
858        "GoTo" => {
859            let (destination, named_destination) = destination(false).await;
860            ActionKind::GoTo {
861                destination,
862                named_destination,
863            }
864        }
865        "GoToR" => {
866            let (destination, named_destination) = destination(true).await;
867            ActionKind::GoToR {
868                file: file("F").await,
869                destination,
870                named_destination,
871                new_window: entries.flag("NewWindow").await,
872            }
873        }
874        "GoToE" => {
875            let (destination, named_destination) = destination(true).await;
876            ActionKind::GoToE {
877                file: file("F").await,
878                destination,
879                named_destination,
880                new_window: entries.flag("NewWindow").await,
881                target: read_target(src, dict.get("T")).await,
882            }
883        }
884        "Launch" => ActionKind::Launch {
885            file: file("F").await,
886            windows: read_windows_launch(src, dict.get("Win")).await,
887            new_window: entries.flag("NewWindow").await,
888        },
889        "URI" => ActionKind::Uri {
890            uri: entries.text("URI").await.unwrap_or_default(),
891            is_map: entries.flag("IsMap").await.unwrap_or(false),
892        },
893        "Named" => ActionKind::Named {
894            name: entries
895                .named("N", |n| Some(n.to_string()))
896                .await
897                .unwrap_or_default(),
898        },
899        "JavaScript" => ActionKind::JavaScript {
900            script: text_or_stream(src, dict.get("JS")).await,
901        },
902        _ => ActionKind::Other {
903            kind,
904            entries: dict.clone(),
905        },
906    })
907}
908
909/// The chain of target dictionaries from `value`, at most
910/// [`MAX_TARGET_DEPTH`] deep.
911///
912/// Covers ISO 32000-1 §12.6.4.4.
913async fn read_target<S: AsyncObjectSource>(src: &S, value: Option<&Object>) -> Option<Target> {
914    let mut steps: Vec<Target> = Vec::new();
915    let mut current = value.cloned();
916    while let Some(object) = current.take() {
917        if steps.len() >= MAX_TARGET_DEPTH {
918            break;
919        }
920        let Some(dict) = resolved_dict(src, &object).await else {
921            break;
922        };
923        let entries = Entries { src, dict: &dict };
924        let relationship = match entries.value("R").await?.as_name()?.0.as_str() {
925            "P" => Relationship::Parent,
926            "C" => Relationship::Child,
927            _ => break,
928        };
929        let page = match entries.value("P").await {
930            Some(Object::Int(n)) => u32::try_from(n).ok().map(TargetPage::Number),
931            Some(Object::String(bytes)) => Some(TargetPage::Named(bytes)),
932            _ => None,
933        };
934        let annotation = match entries.value("A").await {
935            Some(Object::Int(n)) => u32::try_from(n).ok().map(TargetAnnotation::Index),
936            Some(Object::String(bytes)) => Some(TargetAnnotation::Name(decode_text_string(&bytes))),
937            _ => None,
938        };
939        steps.push(Target {
940            relationship,
941            name: entries
942                .value("N")
943                .await
944                .and_then(|n| n.as_str_bytes().map(<[u8]>::to_vec)),
945            page,
946            annotation,
947            next: None,
948        });
949        current = dict.get("T").cloned();
950    }
951    steps.into_iter().rev().fold(None, |next, mut step| {
952        step.next = next.map(Box::new);
953        Some(step)
954    })
955}
956
957/// The Table 204 entries of a launch action's `/Win` dictionary.
958///
959/// Covers ISO 32000-1 §12.6.4.5.
960async fn read_windows_launch<S: AsyncObjectSource>(
961    src: &S,
962    value: Option<&Object>,
963) -> Option<WindowsLaunch> {
964    let dict = resolved_dict(src, value?).await?;
965    Some(WindowsLaunch {
966        file: bytes_entry(src, &dict, "F").await?,
967        directory: bytes_entry(src, &dict, "D").await,
968        operation: match bytes_entry(src, &dict, "O").await {
969            Some(operation) => String::from_utf8_lossy(&operation).into_owned(),
970            None => "open".to_string(),
971        },
972        parameters: bytes_entry(src, &dict, "P").await,
973    })
974}
975
976#[cfg(test)]
977mod tests {
978    use super::*;
979    use crate::destination::{DestinationPage, Fit};
980    use crate::document::Document;
981    use pdfboss_testkit::PdfBuilder;
982
983    /// A one-page document whose page carries `page_extra`, with a PDF 1.1
984    /// `/Dests` dictionary naming `Here`; `objects` supplies 10 and up and
985    /// `streams` any streams.
986    fn doc(page_extra: &str, objects: &[(u32, &str)], streams: &[(u32, &str, &[u8])]) -> Document {
987        let mut b = PdfBuilder::new();
988        b.object(
989            1,
990            "<< /Type /Catalog /Pages 2 0 R /Dests << /Here [3 0 R /FitH 500] >> \
991             /AA << /WC << /S /JavaScript /JS (bye) >> /DP 30 0 R >> >>",
992        );
993        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
994        b.object(
995            3,
996            &format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 100] {page_extra} >>"),
997        );
998        b.object(30, "<< /S /Named /N /NextPage >>");
999        for (num, body) in objects {
1000            b.object(*num, body);
1001        }
1002        for (num, dict, data) in streams {
1003            b.stream(*num, dict, data);
1004        }
1005        Document::load(b.build(1)).expect("load")
1006    }
1007
1008    fn annotations(page_extra: &str, objects: &[(u32, &str)]) -> Vec<Annotation> {
1009        let doc = doc(page_extra, objects, &[]);
1010        doc.annotations(&doc.page(0).unwrap())
1011    }
1012
1013    fn one(page_extra: &str, objects: &[(u32, &str)]) -> Annotation {
1014        let mut all = annotations(page_extra, objects);
1015        assert_eq!(all.len(), 1, "{all:?}");
1016        all.remove(0)
1017    }
1018
1019    fn r(num: u32) -> ObjRef {
1020        ObjRef { num, gen: 0 }
1021    }
1022
1023    /// Every Table 164 entry reads: the subtype, the rectangle normalized,
1024    /// the text and name entries decoded, the date both as written and
1025    /// parsed, the flag word, the appearance dictionary and state, the
1026    /// border with its dash, the colour, the structure parent key and the
1027    /// optional content reference.
1028    // Covers ISO 32000-1 §12.5.2 and §12.5.3.
1029    #[test]
1030    fn reads_the_entries_common_to_every_annotation() {
1031        let annotation = one(
1032            "/Annots [10 0 R]",
1033            &[(
1034                10,
1035                "<< /Type /Annot /Subtype /Square /Rect [150 90 50 10] /Contents (Draft) \
1036                 /P 3 0 R /NM (sq-1) /M (D:20240102030405Z) /F 68 \
1037                 /AP << /N 11 0 R >> /AS /On /Border [2 3 1 [3 2]] /C [1 0 0] \
1038                 /StructParent 7 /OC 12 0 R >>",
1039            )],
1040        );
1041        assert_eq!(annotation.object, Some(r(10)));
1042        assert_eq!(annotation.subtype, "Square");
1043        assert_eq!(annotation.rect, Some(Rect::new(50.0, 10.0, 150.0, 90.0)));
1044        assert_eq!(annotation.contents.as_deref(), Some("Draft"));
1045        assert_eq!(annotation.page, Some(r(3)));
1046        assert_eq!(annotation.name.as_deref(), Some("sq-1"));
1047        assert_eq!(annotation.modified.as_deref(), Some("D:20240102030405Z"));
1048        assert_eq!(
1049            annotation.modified_date.map(|d| d.to_iso8601()).as_deref(),
1050            Some("2024-01-02T03:04:05Z")
1051        );
1052        assert_eq!(annotation.flags, AnnotationFlags(68));
1053        assert!(annotation.flags.print());
1054        assert!(annotation.flags.read_only());
1055        assert!(!annotation.flags.hidden());
1056        assert!(annotation.has_appearance);
1057        assert_eq!(annotation.appearance_state.as_deref(), Some("On"));
1058        assert_eq!(
1059            annotation.border,
1060            Some(Border {
1061                horizontal_radius: 2.0,
1062                vertical_radius: 3.0,
1063                width: 1.0,
1064                dash: Some(vec![3.0, 2.0]),
1065            })
1066        );
1067        assert_eq!(annotation.color, Some(vec![1.0, 0.0, 0.0]));
1068        assert_eq!(annotation.struct_parent, Some(7));
1069        assert_eq!(annotation.optional_content, Some(r(12)));
1070        assert!(
1071            annotation.markup.is_some(),
1072            "a square is a markup annotation"
1073        );
1074        assert_eq!(annotation.state, None);
1075        assert_eq!(annotation.action, None);
1076    }
1077
1078    /// A direct dictionary in `/Annots` reads without a reference, an item
1079    /// that is no dictionary or has no `/Subtype` is skipped, a page
1080    /// without `/Annots` has none, and the optional entries read as absent
1081    /// with the flag word 0 and no appearance.
1082    // Covers ISO 32000-1 §12.5.2.
1083    #[test]
1084    fn direct_malformed_and_missing_annotations() {
1085        let all = annotations(
1086            "/Annots [<< /Subtype /Link /Rect [0 0 10 10] >> 5 << /Rect [0 0 1 1] >> 10 0 R]",
1087            &[(10, "<< /Subtype /Square /Rect [0 0 1] >>")],
1088        );
1089        assert_eq!(all.len(), 2, "{all:?}");
1090        assert_eq!(all[0].object, None);
1091        assert_eq!(all[0].subtype, "Link");
1092        assert_eq!(all[0].flags, AnnotationFlags(0));
1093        assert!(!all[0].has_appearance);
1094        assert_eq!(all[0].markup, None, "a link is not a markup annotation");
1095        assert_eq!(all[0].border, None);
1096        assert_eq!(all[1].rect, None, "a three-number rectangle is malformed");
1097        assert!(annotations("", &[]).is_empty());
1098        assert!(annotations("/Annots 4", &[]).is_empty());
1099    }
1100
1101    /// The Table 170 entries of a markup annotation: the title, pop-up
1102    /// reference, opacity (1 by default), rich contents from a string or a
1103    /// stream, creation date, reply reference, subject, reply type (a reply
1104    /// by default) and intent.
1105    // Covers ISO 32000-1 §12.5.6.2.
1106    #[test]
1107    fn reads_the_markup_entries() {
1108        let doc = doc(
1109            "/Annots [10 0 R 11 0 R]",
1110            &[
1111                (
1112                    10,
1113                    "<< /Subtype /Square /Rect [0 0 10 10] /T (Ada) /Popup 12 0 R /CA 0.5 \
1114                     /RC (<p>rich</p>) /CreationDate (D:20240102030405Z) /IRT 11 0 R \
1115                     /Subj (Shape) /RT /Group /IT /SquareCloud >>",
1116                ),
1117                (11, "<< /Subtype /Circle /Rect [0 0 10 10] /RC 13 0 R >>"),
1118            ],
1119            &[(13, "", b"from a stream")],
1120        );
1121        let all = doc.annotations(&doc.page(0).unwrap());
1122        let square = all[0].markup.as_ref().unwrap();
1123        assert_eq!(square.title.as_deref(), Some("Ada"));
1124        assert_eq!(square.popup, Some(r(12)));
1125        assert_eq!(square.opacity, 0.5);
1126        assert_eq!(square.rich_contents.as_deref(), Some("<p>rich</p>"));
1127        assert_eq!(
1128            square.created.map(|d| d.to_iso8601()).as_deref(),
1129            Some("2024-01-02T03:04:05Z")
1130        );
1131        assert_eq!(square.in_reply_to, Some(r(11)));
1132        assert_eq!(square.subject.as_deref(), Some("Shape"));
1133        assert_eq!(square.reply_type, ReplyType::Group);
1134        assert_eq!(square.intent.as_deref(), Some("SquareCloud"));
1135        let circle = all[1].markup.as_ref().unwrap();
1136        assert_eq!(circle.opacity, 1.0);
1137        assert_eq!(circle.rich_contents.as_deref(), Some("from a stream"));
1138        assert_eq!(circle.reply_type, ReplyType::Reply);
1139        assert_eq!(circle.title, None);
1140    }
1141
1142    /// A text annotation replying with a state model and state reads both;
1143    /// a model without a state takes the model's default; a state without
1144    /// a model keeps the state alone.
1145    // Covers ISO 32000-1 §12.5.6.3 and §12.5.6.4.
1146    #[test]
1147    fn reads_the_state_a_reply_sets() {
1148        let all = annotations(
1149            "/Annots [10 0 R 11 0 R 12 0 R 13 0 R]",
1150            &[
1151                (
1152                    10,
1153                    "<< /Subtype /Text /Rect [0 0 10 10] /IRT 14 0 R /T (Ada) \
1154                     /StateModel (Review) /State (Accepted) /Open true /Name /Comment >>",
1155                ),
1156                (
1157                    11,
1158                    "<< /Subtype /Text /Rect [0 0 10 10] /StateModel (Marked) >>",
1159                ),
1160                (
1161                    12,
1162                    "<< /Subtype /Text /Rect [0 0 10 10] /StateModel (Review) >>",
1163                ),
1164                (
1165                    13,
1166                    "<< /Subtype /Text /Rect [0 0 10 10] /State (Completed) >>",
1167                ),
1168            ],
1169        );
1170        assert_eq!(
1171            all[0].state,
1172            Some(AnnotationState {
1173                model: Some("Review".to_string()),
1174                state: "Accepted".to_string(),
1175            })
1176        );
1177        assert_eq!(all[0].open, Some(true));
1178        assert_eq!(all[0].icon.as_deref(), Some("Comment"));
1179        assert_eq!(all[0].markup.as_ref().unwrap().in_reply_to, Some(r(14)));
1180        assert_eq!(all[1].state.as_ref().unwrap().state, "Unmarked");
1181        assert_eq!(all[2].state.as_ref().unwrap().state, "None");
1182        assert_eq!(
1183            all[3].state,
1184            Some(AnnotationState {
1185                model: None,
1186                state: "Completed".to_string(),
1187            })
1188        );
1189        assert_eq!(all[3].open, None);
1190    }
1191
1192    /// A link's `/Dest` reads explicit or by name through the catalog's
1193    /// destinations, and its `/A` reads as an action; a pop-up's parent
1194    /// and open flag read too.
1195    // Covers ISO 32000-1 §12.5.6.5, §12.5.6.14 and §12.6.4.2.
1196    #[test]
1197    fn reads_link_destinations_and_popups() {
1198        let all = annotations(
1199            "/Annots [10 0 R 11 0 R 12 0 R 13 0 R]",
1200            &[
1201                (
1202                    10,
1203                    "<< /Subtype /Link /Rect [0 0 10 10] /Dest [3 0 R /Fit] >>",
1204                ),
1205                (11, "<< /Subtype /Link /Rect [0 0 10 10] /Dest /Here >>"),
1206                (
1207                    12,
1208                    "<< /Subtype /Link /Rect [0 0 10 10] /A << /S /GoTo /D (Here) >> >>",
1209                ),
1210                (
1211                    13,
1212                    "<< /Subtype /Popup /Rect [0 0 10 10] /Parent 10 0 R /Open false >>",
1213                ),
1214            ],
1215        );
1216        let page = DestinationPage::Object(r(3));
1217        assert_eq!(
1218            all[0].destination,
1219            Some(Destination {
1220                page,
1221                fit: Fit::Fit
1222            })
1223        );
1224        assert_eq!(
1225            all[1].destination,
1226            Some(Destination {
1227                page,
1228                fit: Fit::FitH { top: Some(500.0) }
1229            })
1230        );
1231        assert_eq!(all[2].destination, None);
1232        assert_eq!(
1233            all[2].action.as_ref().unwrap().kind,
1234            ActionKind::GoTo {
1235                destination: Some(Destination {
1236                    page,
1237                    fit: Fit::FitH { top: Some(500.0) }
1238                }),
1239                named_destination: Some(b"Here".to_vec()),
1240            }
1241        );
1242        assert_eq!(all[3].parent, Some(r(10)));
1243        assert_eq!(all[3].open, Some(false));
1244        assert_eq!(all[3].markup, None, "a pop-up is not a markup annotation");
1245    }
1246
1247    /// A file attachment annotation carries its file specification, whose
1248    /// embedded stream the document can decode, and its icon name.
1249    // Covers ISO 32000-1 §12.5.6.15 and §7.11.4.
1250    #[test]
1251    fn reads_a_file_attachment_and_its_data() {
1252        let doc = doc(
1253            "/Annots [10 0 R]",
1254            &[
1255                (
1256                    10,
1257                    "<< /Subtype /FileAttachment /Rect [0 0 10 10] /FS 11 0 R /Name /Paperclip \
1258                     /Contents (The data) >>",
1259                ),
1260                (
1261                    11,
1262                    "<< /Type /Filespec /F (data.csv) /EF << /F 12 0 R >> >>",
1263                ),
1264            ],
1265            &[(12, "/Type /EmbeddedFile", b"a,b\n1,2\n")],
1266        );
1267        let all = doc.annotations(&doc.page(0).unwrap());
1268        let file = all[0].file.as_ref().unwrap();
1269        assert_eq!(file.name(), "data.csv");
1270        assert_eq!(all[0].icon.as_deref(), Some("Paperclip"));
1271        assert_eq!(doc.file_spec_data(file).unwrap(), b"a,b\n1,2\n");
1272        let doc = self::doc(
1273            "/Annots [10 0 R]",
1274            &[(
1275                10,
1276                "<< /Subtype /Square /Rect [0 0 10 10] /FS (ignored.txt) >>",
1277            )],
1278            &[],
1279        );
1280        let all = doc.annotations(&doc.page(0).unwrap());
1281        assert_eq!(all[0].file, None, "only a file attachment carries a file");
1282    }
1283
1284    /// `/Next` chains read in order whether a dictionary or an array, each
1285    /// with its own chain; an action that chains back to an earlier one is
1286    /// read once.
1287    // Covers ISO 32000-1 §12.6.2.
1288    #[test]
1289    fn follows_next_chains_once() {
1290        let annotation = one(
1291            "/Annots [10 0 R]",
1292            &[
1293                (
1294                    10,
1295                    "<< /Subtype /Link /Rect [0 0 10 10] /A 11 0 R >>",
1296                ),
1297                (
1298                    11,
1299                    "<< /S /URI /URI (http://a.example) /Next [12 0 R << /S /Named /N /LastPage >>] >>",
1300                ),
1301                (
1302                    12,
1303                    "<< /S /Named /N /NextPage /Next 13 0 R >>",
1304                ),
1305                (13, "<< /S /Named /N /PrevPage /Next 11 0 R >>"),
1306            ],
1307        );
1308        let action = annotation.action.unwrap();
1309        assert_eq!(action.object, Some(r(11)));
1310        assert_eq!(
1311            action.kind,
1312            ActionKind::Uri {
1313                uri: "http://a.example".to_string(),
1314                is_map: false
1315            }
1316        );
1317        assert_eq!(action.next.len(), 2, "{:?}", action.next);
1318        assert_eq!(action.next[0].object, Some(r(12)));
1319        assert_eq!(
1320            action.next[0].kind,
1321            ActionKind::Named {
1322                name: "NextPage".to_string()
1323            }
1324        );
1325        assert_eq!(action.next[0].next.len(), 1);
1326        assert_eq!(action.next[0].next[0].object, Some(r(13)));
1327        assert!(
1328            action.next[0].next[0].next.is_empty(),
1329            "the chain back to 11 is not entered again"
1330        );
1331        assert_eq!(action.next[1].object, None);
1332        assert_eq!(
1333            action.next[1].kind,
1334            ActionKind::Named {
1335                name: "LastPage".to_string()
1336            }
1337        );
1338    }
1339
1340    /// A chain longer than the budget stops at it.
1341    // Covers ISO 32000-1 §12.6.2.
1342    #[test]
1343    fn a_runaway_next_chain_stops_at_the_budget() {
1344        let first = 100u32;
1345        let last = first + MAX_ACTIONS as u32 + 20;
1346        let bodies: Vec<(u32, String)> = (first..=last)
1347            .map(|num| {
1348                let next = if num < last {
1349                    format!(" /Next {} 0 R", num + 1)
1350                } else {
1351                    String::new()
1352                };
1353                (num, format!("<< /S /Named /N /NextPage{next} >>"))
1354            })
1355            .collect();
1356        let objects: Vec<(u32, &str)> = bodies
1357            .iter()
1358            .map(|(num, body)| (*num, body.as_str()))
1359            .collect();
1360        let annotation = one(
1361            "/Annots [<< /Subtype /Link /Rect [0 0 10 10] /A 100 0 R >>]",
1362            &objects,
1363        );
1364        let mut depth = 1;
1365        let mut action = annotation.action.as_ref().unwrap();
1366        while let Some(next) = action.next.first() {
1367            depth += 1;
1368            action = next;
1369        }
1370        assert_eq!(depth, MAX_ACTIONS);
1371    }
1372
1373    /// The additional-actions dictionaries of an annotation (with the
1374    /// field triggers a merged widget carries), a page and the catalog
1375    /// read in table order, each trigger with its action.
1376    // Covers ISO 32000-1 §12.6.3.
1377    #[test]
1378    fn reads_trigger_events_on_annotations_pages_and_the_catalog() {
1379        let doc = doc(
1380            "/Annots [10 0 R] /AA << /C << /S /Named /N /FirstPage >> /O 30 0 R >>",
1381            &[(
1382                10,
1383                "<< /Subtype /Widget /Rect [0 0 10 10] /FT /Tx /T (f) \
1384                 /AA << /C << /S /JavaScript /JS (calc) >> /E 30 0 R /Fo << /S /Named /N /LastPage >> \
1385                 /Zz << /S /Named /N /Ignored >> >> >>",
1386            )],
1387            &[],
1388        );
1389        let page = doc.page(0).unwrap();
1390        let all = doc.annotations(&page);
1391        let triggers: Vec<Trigger> = all[0]
1392            .additional_actions
1393            .iter()
1394            .map(|t| t.trigger)
1395            .collect();
1396        assert_eq!(
1397            triggers,
1398            [Trigger::CursorEnter, Trigger::Focus, Trigger::Calculate]
1399        );
1400        assert_eq!(
1401            all[0].additional_actions[2].action.kind,
1402            ActionKind::JavaScript {
1403                script: Some("calc".to_string())
1404            }
1405        );
1406        let page_actions = doc.page_additional_actions(&page);
1407        let triggers: Vec<Trigger> = page_actions.iter().map(|t| t.trigger).collect();
1408        assert_eq!(triggers, [Trigger::Open, Trigger::Close]);
1409        assert_eq!(page_actions[0].action.object, Some(r(30)));
1410        let document_actions = doc.additional_actions();
1411        let triggers: Vec<Trigger> = document_actions.iter().map(|t| t.trigger).collect();
1412        assert_eq!(triggers, [Trigger::WillClose, Trigger::DidPrint]);
1413        assert_eq!(
1414            document_actions[0].action.kind,
1415            ActionKind::JavaScript {
1416                script: Some("bye".to_string())
1417            }
1418        );
1419        let bare = self::doc("", &[], &[]);
1420        assert!(bare
1421            .page_additional_actions(&bare.page(0).unwrap())
1422            .is_empty());
1423    }
1424
1425    /// The action object `num` of a document whose objects 20 and up are
1426    /// `objects` and `streams`.
1427    fn action_of(
1428        objects: &[(u32, &str)],
1429        streams: &[(u32, &str, &[u8])],
1430        num: u32,
1431    ) -> Option<Action> {
1432        let doc = doc("", objects, streams);
1433        doc.action(&Object::Ref(r(num)))
1434    }
1435
1436    /// Every Table 198 action type reads: the typed ones into their
1437    /// entries, the rest with their dictionary; a dictionary without `/S`
1438    /// or a value that is no dictionary is no action.
1439    // Covers ISO 32000-1 §12.6.4, §12.6.4.7, §12.6.4.11 and §12.6.4.16.
1440    #[test]
1441    fn reads_every_action_type() {
1442        let others = [
1443            "Thread",
1444            "Sound",
1445            "Movie",
1446            "Hide",
1447            "ResetForm",
1448            "ImportData",
1449            "SetOCGState",
1450            "Rendition",
1451            "Trans",
1452            "GoTo3DView",
1453        ];
1454        let other_bodies: Vec<String> = others.iter().map(|k| format!("<< /S /{k} >>")).collect();
1455        let mut objects = vec![
1456            (
1457                20,
1458                "<< /S /SubmitForm /F (http://x.example/post) /Flags 4 >>",
1459            ),
1460            (22, "<< /S /URI /URI (http://x.example/?q) /IsMap true >>"),
1461            (23, "<< /S /Named /N /FirstPage >>"),
1462            (24, "<< /S /JavaScript /JS 21 0 R >>"),
1463            (25, "<< /Type /Action >>"),
1464            (26, "(not a dictionary)"),
1465        ];
1466        objects.extend(
1467            other_bodies
1468                .iter()
1469                .enumerate()
1470                .map(|(i, body)| (30 + i as u32, body.as_str())),
1471        );
1472        let doc = doc("", &objects, &[(21, "", b"app.alert(1)")]);
1473        let action = |num: u32| doc.action(&Object::Ref(r(num)));
1474        assert_eq!(
1475            action(22).unwrap().kind,
1476            ActionKind::Uri {
1477                uri: "http://x.example/?q".to_string(),
1478                is_map: true
1479            }
1480        );
1481        assert_eq!(
1482            action(23).unwrap().kind,
1483            ActionKind::Named {
1484                name: "FirstPage".to_string()
1485            }
1486        );
1487        assert_eq!(
1488            action(24).unwrap().kind,
1489            ActionKind::JavaScript {
1490                script: Some("app.alert(1)".to_string())
1491            }
1492        );
1493        let other = action(20).unwrap();
1494        assert_eq!(other.object, Some(r(20)));
1495        match other.kind {
1496            ActionKind::Other { kind, entries } => {
1497                assert_eq!(kind, "SubmitForm");
1498                assert_eq!(entries.get("Flags").and_then(Object::as_int), Some(4));
1499            }
1500            other => panic!("{other:?}"),
1501        }
1502        for (i, kind) in others.iter().enumerate() {
1503            let read = action(30 + i as u32).unwrap();
1504            assert!(
1505                matches!(&read.kind, ActionKind::Other { kind: k, .. } if k == kind),
1506                "{kind}: {read:?}"
1507            );
1508        }
1509        assert_eq!(action(25), None);
1510        assert_eq!(action(26), None);
1511    }
1512
1513    /// A remote go-to action carries its file, a page-numbered explicit
1514    /// destination or a name that stays a name, and the window flag.
1515    // Covers ISO 32000-1 §12.6.4.3.
1516    #[test]
1517    fn reads_remote_go_to_actions() {
1518        let objects = [
1519            (
1520                20,
1521                "<< /S /GoToR /F (other.pdf) /D [2 /XYZ 10 20 1.5] /NewWindow true >>",
1522            ),
1523            (
1524                21,
1525                "<< /S /GoToR /F << /F (o.pdf) /UF (o.pdf) >> /D (Here) >>",
1526            ),
1527        ];
1528        assert_eq!(
1529            action_of(&objects, &[], 20).unwrap().kind,
1530            ActionKind::GoToR {
1531                file: Some(FileSpec {
1532                    file: Some(b"other.pdf".to_vec()),
1533                    ..FileSpec::default()
1534                }),
1535                destination: Some(Destination {
1536                    page: DestinationPage::Number(2),
1537                    fit: Fit::Xyz {
1538                        left: Some(10.0),
1539                        top: Some(20.0),
1540                        zoom: Some(1.5)
1541                    }
1542                }),
1543                named_destination: None,
1544                new_window: Some(true),
1545            }
1546        );
1547        assert_eq!(
1548            action_of(&objects, &[], 21).unwrap().kind,
1549            ActionKind::GoToR {
1550                file: Some(FileSpec {
1551                    file: Some(b"o.pdf".to_vec()),
1552                    unicode_file: Some("o.pdf".to_string()),
1553                    ..FileSpec::default()
1554                }),
1555                destination: None,
1556                named_destination: Some(b"Here".to_vec()),
1557                new_window: None,
1558            }
1559        );
1560    }
1561
1562    /// An embedded go-to action's target chain reads each step's
1563    /// relationship, name, page and annotation, nested through `/T`, stops
1564    /// at the depth cap, and ends at a step without a valid relationship.
1565    // Covers ISO 32000-1 §12.6.4.4.
1566    #[test]
1567    fn reads_embedded_go_to_targets() {
1568        let mut deep = String::from("<< /R /C >>");
1569        for _ in 0..(MAX_TARGET_DEPTH + 5) {
1570            deep = format!("<< /R /C /T {deep} >>");
1571        }
1572        let deep = format!("<< /S /GoToE /D (x) /T {deep} >>");
1573        let objects = [
1574            (
1575                20,
1576                "<< /S /GoToE /D [0 /Fit] /T << /R /P /T << /R /C /N (child.pdf) \
1577                 /T << /R /C /P 3 /A (annotName) >> >> >> >>",
1578            ),
1579            (21, "<< /S /GoToE /D (x) /T << /R /C /P (Here) /A 2 >> >>"),
1580            (22, deep.as_str()),
1581            (23, "<< /S /GoToE /D (x) /T << /R /Sideways >> >>"),
1582        ];
1583        let target_of = |num: u32| match action_of(&objects, &[], num).unwrap().kind {
1584            ActionKind::GoToE {
1585                target,
1586                destination,
1587                file,
1588                ..
1589            } => (target, destination, file),
1590            other => panic!("{other:?}"),
1591        };
1592        let (target, destination, file) = target_of(20);
1593        assert_eq!(file, None);
1594        assert_eq!(
1595            destination.map(|d| d.page),
1596            Some(DestinationPage::Number(0))
1597        );
1598        let first = target.unwrap();
1599        assert_eq!(first.relationship, Relationship::Parent);
1600        let second = first.next.as_deref().unwrap();
1601        assert_eq!(second.relationship, Relationship::Child);
1602        assert_eq!(second.name.as_deref(), Some(b"child.pdf".as_slice()));
1603        let third = second.next.as_deref().unwrap();
1604        assert_eq!(third.page, Some(TargetPage::Number(3)));
1605        assert_eq!(
1606            third.annotation,
1607            Some(TargetAnnotation::Name("annotName".to_string()))
1608        );
1609        assert_eq!(third.next, None);
1610        let (target, _, _) = target_of(21);
1611        let target = target.unwrap();
1612        assert_eq!(target.page, Some(TargetPage::Named(b"Here".to_vec())));
1613        assert_eq!(target.annotation, Some(TargetAnnotation::Index(2)));
1614        let (target, _, _) = target_of(22);
1615        let mut depth = 1;
1616        let mut step = target.unwrap();
1617        while let Some(next) = step.next {
1618            depth += 1;
1619            step = *next;
1620        }
1621        assert_eq!(depth, MAX_TARGET_DEPTH);
1622        let (target, _, _) = target_of(23);
1623        assert_eq!(target, None);
1624    }
1625
1626    /// A launch action carries its file specification, its Windows
1627    /// parameters with `open` as the default operation, and the window
1628    /// flag.
1629    // Covers ISO 32000-1 §12.6.4.5.
1630    #[test]
1631    fn reads_launch_actions() {
1632        let objects = [
1633            (
1634                20,
1635                "<< /S /Launch /F (setup.exe) /NewWindow false \
1636                 /Win << /F (c:\\\\tools\\\\setup.exe) /D (c:\\\\tools) /O (print) /P (-q) >> >>",
1637            ),
1638            (21, "<< /S /Launch /Win << /F (notepad.exe) >> >>"),
1639        ];
1640        assert_eq!(
1641            action_of(&objects, &[], 20).unwrap().kind,
1642            ActionKind::Launch {
1643                file: Some(FileSpec {
1644                    file: Some(b"setup.exe".to_vec()),
1645                    ..FileSpec::default()
1646                }),
1647                windows: Some(WindowsLaunch {
1648                    file: b"c:\\tools\\setup.exe".to_vec(),
1649                    directory: Some(b"c:\\tools".to_vec()),
1650                    operation: "print".to_string(),
1651                    parameters: Some(b"-q".to_vec()),
1652                }),
1653                new_window: Some(false),
1654            }
1655        );
1656        assert_eq!(
1657            action_of(&objects, &[], 21).unwrap().kind,
1658            ActionKind::Launch {
1659                file: None,
1660                windows: Some(WindowsLaunch {
1661                    file: b"notepad.exe".to_vec(),
1662                    directory: None,
1663                    operation: "open".to_string(),
1664                    parameters: None,
1665                }),
1666                new_window: None,
1667            }
1668        );
1669    }
1670}