Skip to main content

pdfboss_core/
structure.rs

1//! Structure-tree reading order (ISO 32000-1 §14.7): where a page's
2//! marked-content sequences sit in the document's logical structure, so a
3//! tagged page can be read in the order its author declared, and which
4//! structure type the element holding each sequence declares (§14.7.3).
5
6use std::sync::Arc;
7
8use crate::document::Page;
9use crate::hash::{FastMap, FastSet};
10use crate::object::{Dict, ObjRef, Object};
11use crate::source::AsyncObjectSource;
12use crate::tree::resolved_dict;
13
14/// Maximum number of ancestors walked from an element up to the root.
15/// Deeper ancestry reads as malformed and leaves the element unranked.
16const MAX_ELEMENT_DEPTH: usize = 64;
17
18/// One marked-content sequence: its `/MCID`, and the `/StructParents` key of
19/// the content stream it appeared in: the page's, or a form XObject's own
20/// when the form declares one (§14.7.4.4).
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub struct MarkedContentId {
23    pub parents: u32,
24    pub mcid: u32,
25}
26
27/// The four groups §14.8.4 sorts the standard structure types into, one per
28/// clause that defines them; the block-level and inline-level ones are the
29/// two the basic layout model of §14.8.3 lays out.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum StandardKind {
32    /// §14.8.4.2: elements that group other elements and hold no content
33    /// of their own.
34    Grouping,
35    /// §14.8.4.3: paragraphs, headings, lists and tables, laid out as
36    /// blocks.
37    BlockLevel,
38    /// §14.8.4.4: elements within a block's text.
39    InlineLevel,
40    /// §14.8.4.5: figures, formulas and forms.
41    Illustration,
42}
43
44/// The standard structure types of ISO 32000-1 §14.8.4, each variant spelled
45/// as the standard spells the `/S` name it stands for.
46///
47/// Covers ISO 32000-1 §14.8.4.
48#[allow(clippy::upper_case_acronyms)]
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub enum StandardType {
51    // Grouping elements (§14.8.4.2).
52    Document,
53    Part,
54    Art,
55    Sect,
56    Div,
57    BlockQuote,
58    Caption,
59    TOC,
60    TOCI,
61    Index,
62    NonStruct,
63    Private,
64    // Block-level structure elements (§14.8.4.3).
65    P,
66    H,
67    H1,
68    H2,
69    H3,
70    H4,
71    H5,
72    H6,
73    L,
74    LI,
75    Lbl,
76    LBody,
77    Table,
78    TR,
79    TH,
80    TD,
81    THead,
82    TBody,
83    TFoot,
84    // Inline-level structure elements (§14.8.4.4).
85    Span,
86    Quote,
87    Note,
88    Reference,
89    BibEntry,
90    Code,
91    Link,
92    Annot,
93    Ruby,
94    RB,
95    RT,
96    RP,
97    Warichu,
98    WT,
99    WP,
100    // Illustration elements (§14.8.4.5).
101    Figure,
102    Formula,
103    Form,
104}
105
106impl StandardType {
107    /// Every standard type, grouping elements first, in the standard's order.
108    pub const ALL: [StandardType; 49] = [
109        StandardType::Document,
110        StandardType::Part,
111        StandardType::Art,
112        StandardType::Sect,
113        StandardType::Div,
114        StandardType::BlockQuote,
115        StandardType::Caption,
116        StandardType::TOC,
117        StandardType::TOCI,
118        StandardType::Index,
119        StandardType::NonStruct,
120        StandardType::Private,
121        StandardType::P,
122        StandardType::H,
123        StandardType::H1,
124        StandardType::H2,
125        StandardType::H3,
126        StandardType::H4,
127        StandardType::H5,
128        StandardType::H6,
129        StandardType::L,
130        StandardType::LI,
131        StandardType::Lbl,
132        StandardType::LBody,
133        StandardType::Table,
134        StandardType::TR,
135        StandardType::TH,
136        StandardType::TD,
137        StandardType::THead,
138        StandardType::TBody,
139        StandardType::TFoot,
140        StandardType::Span,
141        StandardType::Quote,
142        StandardType::Note,
143        StandardType::Reference,
144        StandardType::BibEntry,
145        StandardType::Code,
146        StandardType::Link,
147        StandardType::Annot,
148        StandardType::Ruby,
149        StandardType::RB,
150        StandardType::RT,
151        StandardType::RP,
152        StandardType::Warichu,
153        StandardType::WT,
154        StandardType::WP,
155        StandardType::Figure,
156        StandardType::Formula,
157        StandardType::Form,
158    ];
159
160    /// The standard type a structure type name stands for, `None` for a name
161    /// outside the standard set. Names are case-sensitive; a document's own
162    /// types reach the standard set through the role map (§14.7.3), not here.
163    pub fn from_name(name: &str) -> Option<StandardType> {
164        StandardType::ALL.into_iter().find(|t| t.name() == name)
165    }
166
167    /// The name as the standard spells it.
168    pub fn name(self) -> &'static str {
169        match self {
170            StandardType::Document => "Document",
171            StandardType::Part => "Part",
172            StandardType::Art => "Art",
173            StandardType::Sect => "Sect",
174            StandardType::Div => "Div",
175            StandardType::BlockQuote => "BlockQuote",
176            StandardType::Caption => "Caption",
177            StandardType::TOC => "TOC",
178            StandardType::TOCI => "TOCI",
179            StandardType::Index => "Index",
180            StandardType::NonStruct => "NonStruct",
181            StandardType::Private => "Private",
182            StandardType::P => "P",
183            StandardType::H => "H",
184            StandardType::H1 => "H1",
185            StandardType::H2 => "H2",
186            StandardType::H3 => "H3",
187            StandardType::H4 => "H4",
188            StandardType::H5 => "H5",
189            StandardType::H6 => "H6",
190            StandardType::L => "L",
191            StandardType::LI => "LI",
192            StandardType::Lbl => "Lbl",
193            StandardType::LBody => "LBody",
194            StandardType::Table => "Table",
195            StandardType::TR => "TR",
196            StandardType::TH => "TH",
197            StandardType::TD => "TD",
198            StandardType::THead => "THead",
199            StandardType::TBody => "TBody",
200            StandardType::TFoot => "TFoot",
201            StandardType::Span => "Span",
202            StandardType::Quote => "Quote",
203            StandardType::Note => "Note",
204            StandardType::Reference => "Reference",
205            StandardType::BibEntry => "BibEntry",
206            StandardType::Code => "Code",
207            StandardType::Link => "Link",
208            StandardType::Annot => "Annot",
209            StandardType::Ruby => "Ruby",
210            StandardType::RB => "RB",
211            StandardType::RT => "RT",
212            StandardType::RP => "RP",
213            StandardType::Warichu => "Warichu",
214            StandardType::WT => "WT",
215            StandardType::WP => "WP",
216            StandardType::Figure => "Figure",
217            StandardType::Formula => "Formula",
218            StandardType::Form => "Form",
219        }
220    }
221
222    /// The clause of §14.8.4 that defines the type.
223    pub fn kind(self) -> StandardKind {
224        match self {
225            StandardType::Document
226            | StandardType::Part
227            | StandardType::Art
228            | StandardType::Sect
229            | StandardType::Div
230            | StandardType::BlockQuote
231            | StandardType::Caption
232            | StandardType::TOC
233            | StandardType::TOCI
234            | StandardType::Index
235            | StandardType::NonStruct
236            | StandardType::Private => StandardKind::Grouping,
237            StandardType::P
238            | StandardType::H
239            | StandardType::H1
240            | StandardType::H2
241            | StandardType::H3
242            | StandardType::H4
243            | StandardType::H5
244            | StandardType::H6
245            | StandardType::L
246            | StandardType::LI
247            | StandardType::Lbl
248            | StandardType::LBody
249            | StandardType::Table
250            | StandardType::TR
251            | StandardType::TH
252            | StandardType::TD
253            | StandardType::THead
254            | StandardType::TBody
255            | StandardType::TFoot => StandardKind::BlockLevel,
256            StandardType::Span
257            | StandardType::Quote
258            | StandardType::Note
259            | StandardType::Reference
260            | StandardType::BibEntry
261            | StandardType::Code
262            | StandardType::Link
263            | StandardType::Annot
264            | StandardType::Ruby
265            | StandardType::RB
266            | StandardType::RT
267            | StandardType::RP
268            | StandardType::Warichu
269            | StandardType::WT
270            | StandardType::WP => StandardKind::InlineLevel,
271            StandardType::Figure | StandardType::Formula | StandardType::Form => {
272                StandardKind::Illustration
273            }
274        }
275    }
276}
277
278/// One structure element on a placement's path: its standard type and the
279/// object holding it, so two neighbouring elements of one type are told
280/// apart, and its revision number.
281///
282/// Covers ISO 32000-1 §14.8.4.3.
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
284pub struct StructureElement {
285    pub standard_type: StandardType,
286    pub object: ObjRef,
287    /// The element's `/R` (§14.7.5.3), 0 when it declares none.
288    pub revision: i64,
289}
290
291/// The standard attribute owners of ISO 32000-1 Table 331: the four owners
292/// the standard defines attributes for, and the seven document formats whose
293/// own attributes an attribute object may carry.
294///
295/// Covers ISO 32000-1 §14.8.5.2.
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
297pub enum StandardOwner {
298    /// `Layout`: layout attributes (§14.8.5.4).
299    Layout,
300    /// `List`: the list attribute (§14.8.5.5).
301    List,
302    /// `PrintField`: print-field attributes (§14.8.5.6).
303    PrintField,
304    /// `Table`: table attributes (§14.8.5.7).
305    Table,
306    /// `XML-1.00`: attributes of XML 1.00.
307    Xml100,
308    /// `HTML-3.2`: attributes of HTML 3.2.
309    Html32,
310    /// `HTML-4.01`: attributes of HTML 4.01.
311    Html401,
312    /// `OEB-1.0`: attributes of the Open eBook 1.0 format.
313    Oeb10,
314    /// `RTF-1.05`: attributes of RTF 1.05.
315    Rtf105,
316    /// `CSS-1.00`: attributes of CSS 1.00.
317    Css100,
318    /// `CSS-2.00`: attributes of CSS 2.00.
319    Css200,
320}
321
322impl StandardOwner {
323    /// Every standard owner, in the order of Table 331.
324    pub const ALL: [StandardOwner; 11] = [
325        StandardOwner::Layout,
326        StandardOwner::List,
327        StandardOwner::PrintField,
328        StandardOwner::Table,
329        StandardOwner::Xml100,
330        StandardOwner::Html32,
331        StandardOwner::Html401,
332        StandardOwner::Oeb10,
333        StandardOwner::Rtf105,
334        StandardOwner::Css100,
335        StandardOwner::Css200,
336    ];
337
338    /// The standard owner an `/O` name stands for, `None` for a producer's
339    /// own owner (or `UserProperties`, §14.7.5.4, which is not one of them).
340    /// Names are case-sensitive.
341    pub fn from_name(name: &str) -> Option<StandardOwner> {
342        StandardOwner::ALL
343            .into_iter()
344            .find(|owner| owner.name() == name)
345    }
346
347    /// The name as the standard spells it.
348    pub fn name(self) -> &'static str {
349        match self {
350            StandardOwner::Layout => "Layout",
351            StandardOwner::List => "List",
352            StandardOwner::PrintField => "PrintField",
353            StandardOwner::Table => "Table",
354            StandardOwner::Xml100 => "XML-1.00",
355            StandardOwner::Html32 => "HTML-3.2",
356            StandardOwner::Html401 => "HTML-4.01",
357            StandardOwner::Oeb10 => "OEB-1.0",
358            StandardOwner::Rtf105 => "RTF-1.05",
359            StandardOwner::Css100 => "CSS-1.00",
360            StandardOwner::Css200 => "CSS-2.00",
361        }
362    }
363}
364
365/// One attribute object of a structure element (§14.7.5): the element it
366/// belongs to, its owner (`/O`, empty when it names none), the revision
367/// number that follows it in an `/A` or `/C` array (0 when none does,
368/// §14.7.5.3), and its entries as written, `/O` included.
369///
370/// Covers ISO 32000-1 §14.7.5.
371#[derive(Debug, Clone, PartialEq)]
372pub struct AttributeObject {
373    pub element: ObjRef,
374    pub owner: String,
375    pub revision: i64,
376    pub entries: Dict,
377}
378
379impl AttributeObject {
380    /// The standard owner the object's `/O` names (§14.8.5.2), `None` for a
381    /// producer's own.
382    pub fn standard_owner(&self) -> Option<StandardOwner> {
383        StandardOwner::from_name(&self.owner)
384    }
385}
386
387/// Where one marked-content sequence sits in the tree: its rank in the
388/// tree's depth-first order, and the structure type of the element holding
389/// it, as written, after the root's `/RoleMap`, and as a standard type,
390/// with the standard-typed elements above it.
391///
392/// Covers ISO 32000-1 §14.7.3 and §14.8.4.
393#[derive(Debug, Clone, PartialEq)]
394pub struct Placement {
395    pub rank: u32,
396    /// The element's `/S`, as the file writes it.
397    pub structure_type: Option<String>,
398    /// `structure_type` followed through the role map until a name the map
399    /// has no entry for; the same name when the map never names it.
400    pub mapped_type: Option<String>,
401    /// The standard type `mapped_type` names, `None` when it names none.
402    pub standard_type: Option<StandardType>,
403    /// The element and its ancestors that have a standard type, the root's
404    /// child first and the element itself last; an ancestor of no standard
405    /// type is skipped, its children keeping their place.
406    pub path: Vec<StructureElement>,
407    /// The alternate description (`/Alt`, §14.9.3) of the element, or of
408    /// the nearest ancestor that has one, decoded as a text string.
409    pub alt: Option<String>,
410    /// The language (`/Lang`, §14.9.2) of the element, or of the nearest
411    /// ancestor that declares one; `None` leaves the document's own.
412    pub lang: Option<String>,
413    /// The expansion (`/E`, §14.9.5) of the abbreviation or acronym the
414    /// element's text is, or the nearest ancestor's, decoded as a text
415    /// string.
416    pub expansion: Option<String>,
417    /// Every attribute object of the element and its ancestors (§14.7.5),
418    /// the root's child's first; for one element, the objects its `/C`
419    /// classes name come before its direct `/A` objects, so a later object
420    /// overrides an earlier one (see [`Placement::attribute`]).
421    pub attributes: Vec<AttributeObject>,
422}
423
424impl Placement {
425    /// The value `key` takes for `element` among its attribute objects of
426    /// `owner`: the last object that has the key wins, so a direct `/A`
427    /// object overrides a class's and a later class an earlier one, the
428    /// order §14.8.5.3 gives the standard attributes. Inheritance from an
429    /// ancestor is the caller's, since only some attributes inherit.
430    ///
431    /// Covers ISO 32000-1 §14.7.5, §14.7.5.2, §14.8.5 and §14.8.5.3.
432    pub fn attribute(&self, element: ObjRef, owner: &str, key: &str) -> Option<&Object> {
433        self.attributes
434            .iter()
435            .rev()
436            .filter(|a| a.element == element && a.owner == owner)
437            .find_map(|a| a.entries.get(key))
438    }
439}
440
441/// The document's structure tree root (`/StructTreeRoot`), loaded once per
442/// document and asked per page where that page's marked content sits in
443/// the tree. `/MarkInfo` is never consulted: a tree with leaves counts,
444/// whatever the file says about itself.
445#[derive(Debug, Clone, PartialEq)]
446pub struct StructureTree {
447    root: Dict,
448    root_ref: Option<ObjRef>,
449    /// The root's `/RoleMap`: structure type names to the names they stand
450    /// for (§14.7.3), entries whose value is not a name dropped.
451    role_map: FastMap<String, String>,
452    /// The root's `/ClassMap`: attribute class names to their attribute
453    /// objects, as written (§14.7.5.2).
454    class_map: FastMap<String, Object>,
455}
456
457impl StructureTree {
458    /// Loads the catalog's `/StructTreeRoot`, or `None` when the document
459    /// declares none, or when the entry is missing or unreadable, which
460    /// leaves every page in content order.
461    ///
462    /// Covers ISO 32000-1 §14.7.2.
463    pub async fn load_with<S: AsyncObjectSource>(src: &S, trailer: &Dict) -> Option<StructureTree> {
464        let root = trailer.get("Root")?;
465        let catalog = src.resolve(root).await.ok()?;
466        let entry = catalog.as_dict()?.get("StructTreeRoot")?;
467        let root_ref = entry.as_ref();
468        let resolved = src.resolve(entry).await.ok()?;
469        let root = resolved.as_dict()?.clone();
470        let role_map = match root.get("RoleMap") {
471            Some(entry) => role_map_of(resolved_dict(src, entry).await),
472            None => FastMap::default(),
473        };
474        let class_map = match root.get("ClassMap") {
475            Some(entry) => class_map_of(resolved_dict(src, entry).await),
476            None => FastMap::default(),
477        };
478        Some(StructureTree {
479            root,
480            root_ref,
481            role_map,
482            class_map,
483        })
484    }
485
486    /// `name` followed through the root's `/RoleMap` until a name the map
487    /// has no entry for. The map is meant to reach a standard structure type
488    /// (§14.8.4) in one step; a chain is followed in case a mapped name is
489    /// itself mapped, and a cycle leaves the name as written.
490    ///
491    /// Covers ISO 32000-1 §14.7.3.
492    pub fn mapped_type(&self, name: &str) -> String {
493        let mut seen: FastSet<&str> = FastSet::default();
494        let mut current = name;
495        while let Some(next) = self.role_map.get(current) {
496            if !seen.insert(current) {
497                return name.to_string();
498            }
499            current = next;
500        }
501        current.to_string()
502    }
503
504    /// The ranks of [`StructureTree::place_with`] alone: one page's
505    /// marked-content sequences by their position in the tree's depth-first
506    /// order, 0 for the first the tree reaches.
507    ///
508    /// Covers ISO 32000-1 §14.8.2 and §14.8.2.3.
509    pub async fn ranks_with<S: AsyncObjectSource>(
510        &self,
511        src: &S,
512        page: &Page,
513        ids: &[MarkedContentId],
514    ) -> FastMap<MarkedContentId, u32> {
515        self.place_with(src, page, ids)
516            .await
517            .into_iter()
518            .map(|(id, placement)| (id, placement.rank))
519            .collect()
520    }
521
522    /// Places `ids`, one page's marked-content sequences, in the tree: each
523    /// gets its rank in the tree's depth-first order (0 for the first the
524    /// tree reaches, and so on) and the structure type of the element holding
525    /// it. An id the tree never reaches (untagged content, a key the parent
526    /// tree lacks, an element whose ancestry is broken) is absent, so an
527    /// empty map means the page has no leaves in the tree.
528    ///
529    /// The lookup goes through the parent tree (`/ParentTree`, keyed by
530    /// `/StructParents`) and each element's `/P` chain, so it costs the page's
531    /// own elements, never a walk of the whole tree.
532    ///
533    /// Covers ISO 32000-1 §14.7.3, §14.8.2 and §14.8.2.3.
534    pub async fn place_with<S: AsyncObjectSource>(
535        &self,
536        src: &S,
537        page: &Page,
538        ids: &[MarkedContentId],
539    ) -> FastMap<MarkedContentId, Placement> {
540        let mut placed: FastMap<MarkedContentId, Placement> = FastMap::default();
541        let Some(parent_tree) = self.root.get("ParentTree") else {
542            return placed;
543        };
544        let Some(parent_tree) = resolved_dict(src, parent_tree).await else {
545            return placed;
546        };
547        let mut walk = Walk {
548            src,
549            page_ref: page.object_ref(),
550            root_ref: self.root_ref,
551            class_map: &self.class_map,
552            dicts: FastMap::default(),
553            paths: FastMap::default(),
554            parents: FastMap::default(),
555        };
556        let mut keyed: Vec<(MarkedContentId, Vec<u32>, Ancestry)> = Vec::new();
557        let mut seen: FastSet<MarkedContentId> = FastSet::default();
558        for id in ids {
559            if !seen.insert(*id) {
560                continue;
561            }
562            let Some((key, ancestry)) = walk.key_of(&parent_tree, *id).await else {
563                continue;
564            };
565            keyed.push((*id, key, ancestry));
566        }
567        keyed.sort_by(|a, b| a.1.cmp(&b.1));
568        for (rank, (id, _, ancestry)) in keyed.into_iter().enumerate() {
569            let structure_type = ancestry.last().and_then(|a| a.structure_type.clone());
570            let mapped_type = structure_type.as_deref().map(|s| self.mapped_type(s));
571            let standard_type = mapped_type.as_deref().and_then(StandardType::from_name);
572            let path = ancestry
573                .iter()
574                .filter_map(|ancestor| {
575                    let name = ancestor.structure_type.as_deref()?;
576                    let standard_type = StandardType::from_name(&self.mapped_type(name))?;
577                    Some(StructureElement {
578                        standard_type,
579                        object: ancestor.object,
580                        revision: ancestor.revision,
581                    })
582                })
583                .collect();
584            let alt = ancestry.iter().rev().find_map(|a| a.alt.clone());
585            let lang = ancestry.iter().rev().find_map(|a| a.lang.clone());
586            let expansion = ancestry.iter().rev().find_map(|a| a.expansion.clone());
587            let attributes = ancestry
588                .into_iter()
589                .flat_map(|ancestor| ancestor.attributes)
590                .collect();
591            placed.insert(
592                id,
593                Placement {
594                    rank: rank as u32,
595                    structure_type,
596                    mapped_type,
597                    standard_type,
598                    path,
599                    alt,
600                    lang,
601                    expansion,
602                    attributes,
603                },
604            );
605        }
606        placed
607    }
608}
609
610/// One element on the way from a marked-content sequence up to the root:
611/// its object, its `/S` as written, its `/Alt` and `/Lang` decoded, its
612/// `/R` and its attribute objects.
613struct Ancestor {
614    object: ObjRef,
615    structure_type: Option<String>,
616    alt: Option<String>,
617    lang: Option<String>,
618    expansion: Option<String>,
619    revision: i64,
620    attributes: Vec<AttributeObject>,
621}
622
623/// An element and its ancestors up to the root, the root's child first and
624/// the element last.
625type Ancestry = Vec<Ancestor>;
626
627/// The `/ClassMap` dictionary as class name to attribute objects, the
628/// objects kept as written and read when an element names the class.
629///
630/// Covers ISO 32000-1 §14.7.5.2.
631fn class_map_of(dict: Option<Dict>) -> FastMap<String, Object> {
632    let Some(dict) = dict else {
633        return FastMap::default();
634    };
635    dict.iter()
636        .map(|(key, value)| (key.0.clone(), value.clone()))
637        .collect()
638}
639
640/// One attribute object as read: its `/O` owner (empty when it names none)
641/// and its entries, at revision 0 until an array says otherwise. A
642/// `/UserProperties` object arrives like any other, its `/P` array
643/// uninterpreted.
644///
645/// Covers ISO 32000-1 §14.7.5 and §14.7.5.4.
646fn attribute_object(element: ObjRef, entries: Dict) -> AttributeObject {
647    AttributeObject {
648        element,
649        owner: entries
650            .get_name("O")
651            .map(|n| n.0.clone())
652            .unwrap_or_default(),
653        revision: 0,
654        entries,
655    }
656}
657
658/// The `/RoleMap` dictionary as name-to-name pairs.
659///
660/// Covers ISO 32000-1 §14.7.3.
661fn role_map_of(dict: Option<Dict>) -> FastMap<String, String> {
662    let Some(dict) = dict else {
663        return FastMap::default();
664    };
665    dict.iter()
666        .filter_map(|(key, value)| Some((key.0.clone(), value.as_name()?.0.clone())))
667        .collect()
668}
669
670/// One page's walk through the tree: the dictionaries it has already read
671/// and the paths it has already computed, so a paragraph's ancestry is
672/// walked once for every marked-content sequence it contains.
673struct Walk<'a, S> {
674    src: &'a S,
675    page_ref: Option<ObjRef>,
676    root_ref: Option<ObjRef>,
677    /// The root's `/ClassMap`, for elements that name attribute classes.
678    class_map: &'a FastMap<String, Object>,
679    dicts: FastMap<ObjRef, Option<Arc<Dict>>>,
680    /// Each element's kid-index path from the root, or `None` once its
681    /// ancestry proved unwalkable.
682    paths: FastMap<ObjRef, Option<Arc<Vec<u32>>>>,
683    /// The parent tree's array for each `/StructParents` key seen.
684    parents: FastMap<u32, Option<Arc<Vec<Object>>>>,
685}
686
687impl<S: AsyncObjectSource> Walk<'_, S> {
688    /// The sort key of one marked-content sequence, its element's path from
689    /// the root then its own index among the element's kids, with the
690    /// element's ancestry and its structure types (`/S`, §14.7.3).
691    async fn key_of(
692        &mut self,
693        parent_tree: &Dict,
694        id: MarkedContentId,
695    ) -> Option<(Vec<u32>, Ancestry)> {
696        let elements = self.parent_array(parent_tree, id.parents).await?;
697        let element = elements.get(id.mcid as usize)?.as_ref()?;
698        let path = self.path_of(element).await?;
699        let dict = self.dict(element).await?;
700        let index = self.mcid_index(&dict, id.mcid).await?;
701        let mut key = Vec::with_capacity(path.len() + 1);
702        key.extend_from_slice(&path);
703        key.push(index);
704        Some((key, self.ancestry(element).await))
705    }
706
707    /// The element and its ancestors up to the root, the root's child first
708    /// and the element itself last, each with its `/S`, `/Alt`, `/Lang`,
709    /// `/R` and attribute objects: what a placement's structure type, path,
710    /// description, language and attributes are read from. The climb stops
711    /// at the root, at a missing `/P`, or after [`MAX_ELEMENT_DEPTH`]
712    /// elements; the dictionaries are the ones [`Walk::path_of`] already
713    /// read.
714    ///
715    /// Covers ISO 32000-1 §14.7.3, §14.7.5, §14.8.4.3, §14.9.2, §14.9.3 and
716    /// §14.9.5.
717    async fn ancestry(&mut self, element: ObjRef) -> Ancestry {
718        let mut chain: Ancestry = Vec::new();
719        let mut current = element;
720        for _ in 0..MAX_ELEMENT_DEPTH {
721            let Some(dict) = self.dict(current).await else {
722                break;
723            };
724            let alt = match dict.get("Alt") {
725                Some(alt) => self.text_string(alt).await,
726                None => None,
727            };
728            let lang = match dict.get("Lang") {
729                Some(lang) => self.text_string(lang).await,
730                None => None,
731            };
732            let expansion = match dict.get("E") {
733                Some(expansion) => self.text_string(expansion).await,
734                None => None,
735            };
736            let attributes = self.attribute_objects(current, &dict).await;
737            chain.push(Ancestor {
738                object: current,
739                structure_type: dict.get_name("S").map(|n| n.0.clone()),
740                alt,
741                lang,
742                expansion,
743                revision: dict.get_int("R").unwrap_or(0),
744                attributes,
745            });
746            let Some(parent) = dict.get("P").and_then(Object::as_ref) else {
747                break;
748            };
749            if self.is_root(parent).await {
750                break;
751            }
752            current = parent;
753        }
754        chain.reverse();
755        chain
756    }
757
758    /// The attribute objects of one element (§14.7.5): those its `/C`
759    /// classes name in the root's `/ClassMap` (§14.7.5.2) first, then its
760    /// direct `/A` objects, so a later object overrides an earlier one.
761    ///
762    /// Covers ISO 32000-1 §14.7.5 and §14.7.5.2.
763    async fn attribute_objects(&mut self, element: ObjRef, dict: &Dict) -> Vec<AttributeObject> {
764        let mut out = Vec::new();
765        if let Some(classes) = dict.get("C") {
766            self.class_attributes(element, classes, &mut out).await;
767        }
768        if let Some(direct) = dict.get("A") {
769            self.direct_attributes(element, direct, &mut out).await;
770        }
771        out
772    }
773
774    /// `value` as attribute objects: one dictionary, or an array of
775    /// dictionaries, indirect ones resolved, each optionally followed by an
776    /// integer, its revision number; anything else in the array is skipped.
777    ///
778    /// Covers ISO 32000-1 §14.7.5 and §14.7.5.3.
779    async fn direct_attributes(
780        &mut self,
781        element: ObjRef,
782        value: &Object,
783        out: &mut Vec<AttributeObject>,
784    ) {
785        let Ok(resolved) = self.src.resolve(value).await else {
786            return;
787        };
788        let items = match resolved {
789            Object::Dict(dict) => {
790                out.push(attribute_object(element, dict));
791                return;
792            }
793            Object::Array(items) => items,
794            _ => return,
795        };
796        let start = out.len();
797        for item in items {
798            if let Some(revision) = item.as_int() {
799                if out.len() > start {
800                    if let Some(last) = out.last_mut() {
801                        last.revision = revision;
802                    }
803                }
804                continue;
805            }
806            let Ok(resolved) = self.src.resolve(&item).await else {
807                continue;
808            };
809            if let Object::Dict(dict) = resolved {
810                out.push(attribute_object(element, dict));
811            }
812        }
813    }
814
815    /// `value` as attribute class names, one or an array each optionally
816    /// followed by a revision number that then applies to every object the
817    /// class brought; a name the root's `/ClassMap` lacks brings nothing.
818    ///
819    /// Covers ISO 32000-1 §14.7.5.2 and §14.7.5.3.
820    async fn class_attributes(
821        &mut self,
822        element: ObjRef,
823        value: &Object,
824        out: &mut Vec<AttributeObject>,
825    ) {
826        let Ok(resolved) = self.src.resolve(value).await else {
827            return;
828        };
829        let names = match resolved {
830            Object::Name(_) => vec![resolved],
831            Object::Array(items) => items,
832            _ => return,
833        };
834        let mut added = 0..0;
835        for item in names {
836            if let Some(revision) = item.as_int() {
837                for attribute in &mut out[added.clone()] {
838                    attribute.revision = revision;
839                }
840                continue;
841            }
842            let Some(name) = item.as_name() else {
843                continue;
844            };
845            let Some(class) = self.class_map.get(&name.0).cloned() else {
846                continue;
847            };
848            let start = out.len();
849            self.direct_attributes(element, &class, out).await;
850            added = start..out.len();
851        }
852    }
853
854    /// A text string entry (§7.9.2.2), resolved and decoded; `None` for
855    /// anything but a string.
856    async fn text_string(&mut self, value: &Object) -> Option<String> {
857        let resolved = self.src.resolve(value).await.ok()?;
858        Some(crate::object::decode_text_string(resolved.as_str_bytes()?))
859    }
860
861    /// The parent tree's entry for a `/StructParents` key: the array whose
862    /// index is a marked-content id and whose value is that id's element.
863    ///
864    /// Covers ISO 32000-1 §14.7.4.4.
865    async fn parent_array(&mut self, parent_tree: &Dict, key: u32) -> Option<Arc<Vec<Object>>> {
866        if let Some(cached) = self.parents.get(&key) {
867            return cached.clone();
868        }
869        let found = match crate::tree::lookup(self.src, parent_tree, &i64::from(key)).await {
870            Some(entry) => match self.src.resolve(&entry).await.ok()? {
871                Object::Array(items) => Some(Arc::new(items)),
872                _ => None,
873            },
874            None => None,
875        };
876        self.parents.insert(key, found.clone());
877        found
878    }
879
880    async fn dict(&mut self, r: ObjRef) -> Option<Arc<Dict>> {
881        if let Some(cached) = self.dicts.get(&r) {
882            return cached.clone();
883        }
884        let loaded = match self.src.get(r).await.ok()? {
885            Object::Dict(dict) => Some(Arc::new(dict)),
886            Object::Stream(stream) => Some(Arc::new(stream.dict)),
887            _ => None,
888        };
889        self.dicts.insert(r, loaded.clone());
890        loaded
891    }
892
893    /// An element's kid-index path from the root: the ancestry is followed
894    /// up through `/P` until it reaches the structure tree root, then each
895    /// ancestor's index among its parent's kids is read on the way back down.
896    /// Every ancestor's own path is remembered as a by-product.
897    ///
898    /// Covers ISO 32000-1 §14.7.2.
899    async fn path_of(&mut self, element: ObjRef) -> Option<Arc<Vec<u32>>> {
900        if let Some(cached) = self.paths.get(&element) {
901            return cached.clone();
902        }
903        let mut chain: Vec<ObjRef> = vec![element];
904        // The ancestor the climb stopped at, with its path: the root
905        // itself, or an ancestor whose path an earlier climb computed.
906        let mut stop: Option<(ObjRef, bool, Arc<Vec<u32>>)> = None;
907        let mut current = element;
908        for _ in 0..MAX_ELEMENT_DEPTH {
909            let Some(dict) = self.dict(current).await else {
910                break;
911            };
912            let Some(parent) = dict.get("P").and_then(Object::as_ref) else {
913                break;
914            };
915            if self.is_root(parent).await {
916                stop = Some((parent, true, Arc::new(Vec::new())));
917                break;
918            }
919            if let Some(cached) = self.paths.get(&parent) {
920                stop = cached.clone().map(|path| (parent, false, path));
921                break;
922            }
923            chain.push(parent);
924            current = parent;
925        }
926        let Some((mut parent_ref, mut parent_is_root, known)) = stop else {
927            for r in chain {
928                self.paths.insert(r, None);
929            }
930            return None;
931        };
932        // From the topmost unresolved ancestor down to the element itself.
933        let mut path: Vec<u32> = (*known).clone();
934        let mut resolved: Option<Arc<Vec<u32>>> = None;
935        for child in chain.into_iter().rev() {
936            let index = if parent_is_root {
937                self.root_kid_index(child).await
938            } else {
939                match self.dict(parent_ref).await {
940                    Some(parent) => kid_index(&parent, child),
941                    None => None,
942                }
943            };
944            let Some(index) = index else {
945                self.paths.insert(child, None);
946                return None;
947            };
948            path.push(index);
949            let shared = Arc::new(path.clone());
950            self.paths.insert(child, Some(shared.clone()));
951            resolved = Some(shared);
952            parent_ref = child;
953            parent_is_root = false;
954        }
955        resolved
956    }
957
958    /// Whether `r` is the structure tree root: the reference the catalog
959    /// named, or failing that a dictionary typed `/StructTreeRoot`.
960    async fn is_root(&mut self, r: ObjRef) -> bool {
961        if self.root_ref == Some(r) {
962            return true;
963        }
964        let Some(dict) = self.dict(r).await else {
965            return false;
966        };
967        dict.get_name("Type")
968            .is_some_and(|n| n.0 == "StructTreeRoot")
969    }
970
971    /// The index of a top-level element among the root's `/K` kids.
972    async fn root_kid_index(&mut self, child: ObjRef) -> Option<u32> {
973        let root_ref = self.root_ref?;
974        let root = self.dict(root_ref).await?;
975        kid_index(&root, child)
976    }
977
978    /// The index among `element`'s kids of the marked-content sequence
979    /// numbered `mcid` on this page: a bare integer (the element's `/Pg`
980    /// page) or a marked-content reference dictionary naming the page. A
981    /// direct match is taken first; only when there is none are the
982    /// indirect kids read, in case the reference dictionary is one of them.
983    ///
984    /// Covers ISO 32000-1 §14.7.4 and §14.7.4.2.
985    async fn mcid_index(&mut self, element: &Dict, mcid: u32) -> Option<u32> {
986        let kids = kids_of(element);
987        let page_ref = self.page_ref;
988        let element_page = element.get_ref("Pg");
989        let direct = kids.iter().position(|kid| match kid {
990            Object::Int(n) => {
991                u32::try_from(*n).is_ok_and(|n| n == mcid) && on_page(element_page, page_ref)
992            }
993            Object::Dict(d) => is_mcr(d, mcid) && on_page(d.get_ref("Pg"), page_ref),
994            _ => false,
995        });
996        if let Some(index) = direct {
997            return u32::try_from(index).ok();
998        }
999        for (index, kid) in kids.iter().enumerate() {
1000            let Some(r) = kid.as_ref() else {
1001                continue;
1002            };
1003            let Some(dict) = self.dict(r).await else {
1004                continue;
1005            };
1006            if is_mcr(&dict, mcid) && on_page(dict.get_ref("Pg"), page_ref) {
1007                return u32::try_from(index).ok();
1008            }
1009        }
1010        None
1011    }
1012}
1013
1014/// Whether a kid's `/Pg` names this page. Either side unknown reads as a
1015/// match: a page inlined into `/Kids` has no reference to compare, and a
1016/// bare integer kid under an element without `/Pg` has nothing to compare
1017/// against.
1018fn on_page(pg: Option<ObjRef>, page_ref: Option<ObjRef>) -> bool {
1019    match (pg, page_ref) {
1020        (Some(pg), Some(page)) => pg == page,
1021        _ => true,
1022    }
1023}
1024
1025/// Whether `dict` is the marked-content reference for `mcid`.
1026///
1027/// Covers ISO 32000-1 §14.7.4.2.
1028fn is_mcr(dict: &Dict, mcid: u32) -> bool {
1029    dict.get_int("MCID")
1030        .and_then(|n| u32::try_from(n).ok())
1031        .is_some_and(|n| n == mcid)
1032}
1033
1034/// An element's `/K` as a list: a single kid stands alone, an array is its
1035/// items, nothing is empty.
1036fn kids_of(element: &Dict) -> Vec<Object> {
1037    match element.get("K") {
1038        Some(Object::Array(items)) => items.clone(),
1039        Some(single) => vec![single.clone()],
1040        None => Vec::new(),
1041    }
1042}
1043
1044/// The index of `child` among `parent`'s kids, by reference identity.
1045fn kid_index(parent: &Dict, child: ObjRef) -> Option<u32> {
1046    let index = kids_of(parent)
1047        .iter()
1048        .position(|kid| kid.as_ref() == Some(child))?;
1049    u32::try_from(index).ok()
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054    use super::*;
1055    use crate::{block_on, Document, Immediate};
1056    use pdfboss_testkit::PdfBuilder;
1057
1058    fn id(parents: u32, mcid: u32) -> MarkedContentId {
1059        MarkedContentId { parents, mcid }
1060    }
1061
1062    /// A one-page document whose catalog names object 10 as the structure
1063    /// tree root; `objects` supplies the tree (10 and up) and `page_extra`
1064    /// lands in the page dictionary.
1065    fn tagged_doc(page_extra: &str, objects: &[(u32, &str)]) -> Document {
1066        let mut b = PdfBuilder::new();
1067        b.object(
1068            1,
1069            "<< /Type /Catalog /Pages 2 0 R /StructTreeRoot 10 0 R >>",
1070        );
1071        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1072        b.object(
1073            3,
1074            &format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] {page_extra} >>"),
1075        );
1076        for (num, body) in objects {
1077            b.object(*num, body);
1078        }
1079        Document::load(b.build(1)).expect("load")
1080    }
1081
1082    /// Two paragraphs on one page, the left one holding ids 0 and 2, the
1083    /// right one 1 and 3: tree order is 0, 2, 1, 3.
1084    fn two_paragraphs(parent_tree: &str) -> Document {
1085        tagged_doc(
1086            "/StructParents 0",
1087            &[
1088                (
1089                    10,
1090                    "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
1091                ),
1092                (
1093                    11,
1094                    "<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R 14 0 R] >>",
1095                ),
1096                (12, parent_tree),
1097                (
1098                    13,
1099                    "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0 2] >>",
1100                ),
1101                (
1102                    14,
1103                    "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [1 3] >>",
1104                ),
1105            ],
1106        )
1107    }
1108
1109    fn ranks(doc: &Document, ids: &[MarkedContentId]) -> FastMap<MarkedContentId, u32> {
1110        let tree = doc.structure_tree().expect("tree");
1111        let page = doc.page(0).unwrap();
1112        block_on(tree.ranks_with(&Immediate(doc), &page, ids))
1113    }
1114
1115    fn placements(doc: &Document, ids: &[MarkedContentId]) -> FastMap<MarkedContentId, Placement> {
1116        let tree = doc.structure_tree().expect("tree");
1117        let page = doc.page(0).unwrap();
1118        block_on(tree.place_with(&Immediate(doc), &page, ids))
1119    }
1120
1121    // Covers ISO 32000-1 §14.7.3.
1122    #[test]
1123    fn structure_types_are_read_and_mapped_through_the_role_map() {
1124        // Four elements typed Para, Sub, Loop1 and P. The role map, an
1125        // indirect object, maps Para to P, Sub to Head and Head to H1, and
1126        // Loop1 and Loop2 to each other.
1127        let doc = tagged_doc(
1128            "/StructParents 0",
1129            &[
1130                (
1131                    10,
1132                    "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R /RoleMap 20 0 R >>",
1133                ),
1134                (
1135                    11,
1136                    "<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R 14 0 R 15 0 R 16 0 R] >>",
1137                ),
1138                (12, "<< /Nums [0 [13 0 R 14 0 R 15 0 R 16 0 R]] >>"),
1139                (
1140                    13,
1141                    "<< /Type /StructElem /S /Para /P 11 0 R /Pg 3 0 R /K [0] >>",
1142                ),
1143                (
1144                    14,
1145                    "<< /Type /StructElem /S /Sub /P 11 0 R /Pg 3 0 R /K [1] >>",
1146                ),
1147                (
1148                    15,
1149                    "<< /Type /StructElem /S /Loop1 /P 11 0 R /Pg 3 0 R /K [2] >>",
1150                ),
1151                (
1152                    16,
1153                    "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [3] >>",
1154                ),
1155                (
1156                    20,
1157                    "<< /Para /P /Sub /Head /Head /H1 /Loop1 /Loop2 /Loop2 /Loop1 >>",
1158                ),
1159            ],
1160        );
1161        let placed = placements(&doc, &[id(0, 3), id(0, 2), id(0, 1), id(0, 0)]);
1162        let typed = |n: u32| {
1163            let p = &placed[&id(0, n)];
1164            (p.structure_type.as_deref(), p.mapped_type.as_deref())
1165        };
1166        assert_eq!(typed(0), (Some("Para"), Some("P")));
1167        assert_eq!(typed(1), (Some("Sub"), Some("H1")));
1168        assert_eq!(typed(2), (Some("Loop1"), Some("Loop1")));
1169        assert_eq!(typed(3), (Some("P"), Some("P")));
1170        assert_eq!(placed[&id(0, 3)].rank, 3);
1171        assert_eq!(placed[&id(0, 0)].standard_type, Some(StandardType::P));
1172        assert_eq!(placed[&id(0, 1)].standard_type, Some(StandardType::H1));
1173        assert_eq!(placed[&id(0, 2)].standard_type, None);
1174        let tree = doc.structure_tree().unwrap();
1175        assert_eq!(tree.mapped_type("Head"), "H1");
1176        assert_eq!(tree.mapped_type("Span"), "Span");
1177    }
1178
1179    // Covers ISO 32000-1 §14.7.3.
1180    #[test]
1181    fn an_element_without_a_type_places_with_none() {
1182        let doc = tagged_doc(
1183            "/StructParents 0",
1184            &[
1185                (
1186                    10,
1187                    "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
1188                ),
1189                (11, "<< /Type /StructElem /P 10 0 R /Pg 3 0 R /K [0] >>"),
1190                (12, "<< /Nums [0 [11 0 R]] >>"),
1191            ],
1192        );
1193        let placed = placements(&doc, &[id(0, 0)]);
1194        assert_eq!(placed[&id(0, 0)].rank, 0);
1195        assert_eq!(placed[&id(0, 0)].structure_type, None);
1196        assert_eq!(placed[&id(0, 0)].mapped_type, None);
1197        assert_eq!(placed[&id(0, 0)].standard_type, None);
1198    }
1199
1200    // Covers ISO 32000-1 §14.7.3 and §14.8.4.3.
1201    #[test]
1202    fn placements_carry_the_standard_typed_ancestry() {
1203        // Document > Sect > H holds id 0; Document > L > LI > Lbl holds 1
1204        // and > LBody > P holds 2; a Sidebar element of no standard type
1205        // holds a P with id 3.
1206        let doc = tagged_doc(
1207            "/StructParents 0",
1208            &[
1209                (
1210                    10,
1211                    "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
1212                ),
1213                (
1214                    11,
1215                    "<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R 15 0 R 20 0 R] >>",
1216                ),
1217                (12, "<< /Nums [0 [14 0 R 17 0 R 19 0 R 21 0 R]] >>"),
1218                (13, "<< /Type /StructElem /S /Sect /P 11 0 R /K [14 0 R] >>"),
1219                (
1220                    14,
1221                    "<< /Type /StructElem /S /H /P 13 0 R /Pg 3 0 R /K [0] >>",
1222                ),
1223                (15, "<< /Type /StructElem /S /L /P 11 0 R /K [16 0 R] >>"),
1224                (
1225                    16,
1226                    "<< /Type /StructElem /S /LI /P 15 0 R /K [17 0 R 18 0 R] >>",
1227                ),
1228                (
1229                    17,
1230                    "<< /Type /StructElem /S /Lbl /P 16 0 R /Pg 3 0 R /K [1] >>",
1231                ),
1232                (
1233                    18,
1234                    "<< /Type /StructElem /S /LBody /P 16 0 R /K [19 0 R] >>",
1235                ),
1236                (
1237                    19,
1238                    "<< /Type /StructElem /S /P /P 18 0 R /Pg 3 0 R /K [2] >>",
1239                ),
1240                (
1241                    20,
1242                    "<< /Type /StructElem /S /Sidebar /P 11 0 R /K [21 0 R] >>",
1243                ),
1244                (
1245                    21,
1246                    "<< /Type /StructElem /S /P /P 20 0 R /Pg 3 0 R /K [3] >>",
1247                ),
1248            ],
1249        );
1250        let placed = placements(&doc, &[id(0, 0), id(0, 1), id(0, 2), id(0, 3)]);
1251        let kinds = |n: u32| -> Vec<StandardType> {
1252            placed[&id(0, n)]
1253                .path
1254                .iter()
1255                .map(|e| e.standard_type)
1256                .collect()
1257        };
1258        let objects = |n: u32| -> Vec<u32> {
1259            placed[&id(0, n)]
1260                .path
1261                .iter()
1262                .map(|e| e.object.num)
1263                .collect()
1264        };
1265        assert_eq!(
1266            kinds(0),
1267            [StandardType::Document, StandardType::Sect, StandardType::H]
1268        );
1269        assert_eq!(objects(0), [11, 13, 14]);
1270        assert_eq!(
1271            kinds(1),
1272            [
1273                StandardType::Document,
1274                StandardType::L,
1275                StandardType::LI,
1276                StandardType::Lbl
1277            ]
1278        );
1279        assert_eq!(
1280            kinds(2),
1281            [
1282                StandardType::Document,
1283                StandardType::L,
1284                StandardType::LI,
1285                StandardType::LBody,
1286                StandardType::P
1287            ]
1288        );
1289        assert_eq!(kinds(3), [StandardType::Document, StandardType::P]);
1290        assert_eq!(objects(3), [11, 21]);
1291    }
1292
1293    // Covers ISO 32000-1 §14.9.3.
1294    #[test]
1295    fn placements_carry_the_nearest_alternate_description() {
1296        // A Figure with /Alt holding a P with id 0; a P with no /Alt anywhere
1297        // above it holding id 1; a Span with a UTF-16 /Alt holding id 2.
1298        let doc = tagged_doc(
1299            "/StructParents 0",
1300            &[
1301                (
1302                    10,
1303                    "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
1304                ),
1305                (
1306                    11,
1307                    "<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R 15 0 R 16 0 R] >>",
1308                ),
1309                (12, "<< /Nums [0 [14 0 R 15 0 R 16 0 R]] >>"),
1310                (
1311                    13,
1312                    "<< /Type /StructElem /S /Figure /P 11 0 R /Alt (A chart) /K [14 0 R] >>",
1313                ),
1314                (
1315                    14,
1316                    "<< /Type /StructElem /S /P /P 13 0 R /Pg 3 0 R /K [0] >>",
1317                ),
1318                (
1319                    15,
1320                    "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [1] >>",
1321                ),
1322                (
1323                    16,
1324                    "<< /Type /StructElem /S /Span /P 11 0 R /Pg 3 0 R /K [2] /Alt <FEFF00E9> >>",
1325                ),
1326            ],
1327        );
1328        let placed = placements(&doc, &[id(0, 0), id(0, 1), id(0, 2)]);
1329        assert_eq!(placed[&id(0, 0)].alt.as_deref(), Some("A chart"));
1330        assert_eq!(placed[&id(0, 1)].alt, None);
1331        assert_eq!(placed[&id(0, 2)].alt.as_deref(), Some("\u{e9}"));
1332    }
1333
1334    // Covers ISO 32000-1 §14.9.5.
1335    #[test]
1336    fn placements_carry_the_nearest_expansion() {
1337        // A Span with /E holding a Span with id 0; a P with no /E anywhere
1338        // above it holding id 1; a Span with a UTF-16 /E holding id 2.
1339        let doc = tagged_doc(
1340            "/StructParents 0",
1341            &[
1342                (
1343                    10,
1344                    "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
1345                ),
1346                (
1347                    11,
1348                    "<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R 15 0 R 16 0 R] >>",
1349                ),
1350                (12, "<< /Nums [0 [14 0 R 15 0 R 16 0 R]] >>"),
1351                (
1352                    13,
1353                    "<< /Type /StructElem /S /Span /P 11 0 R /E (Portable Document Format) /K [14 0 R] >>",
1354                ),
1355                (
1356                    14,
1357                    "<< /Type /StructElem /S /Span /P 13 0 R /Pg 3 0 R /K [0] >>",
1358                ),
1359                (
1360                    15,
1361                    "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [1] >>",
1362                ),
1363                (
1364                    16,
1365                    "<< /Type /StructElem /S /Span /P 11 0 R /Pg 3 0 R /K [2] /E <FEFF00C9> >>",
1366                ),
1367            ],
1368        );
1369        let placed = placements(&doc, &[id(0, 0), id(0, 1), id(0, 2)]);
1370        assert_eq!(
1371            placed[&id(0, 0)].expansion.as_deref(),
1372            Some("Portable Document Format")
1373        );
1374        assert_eq!(placed[&id(0, 1)].expansion, None);
1375        assert_eq!(placed[&id(0, 2)].expansion.as_deref(), Some("\u{c9}"));
1376    }
1377
1378    // Covers ISO 32000-1 §14.9.2 and §14.9.2.3.
1379    #[test]
1380    fn placements_carry_the_nearest_language() {
1381        // The Document element says en; the second paragraph says de for
1382        // itself; the tree of `two_paragraphs` says nothing at all.
1383        let doc = tagged_doc(
1384            "/StructParents 0",
1385            &[
1386                (
1387                    10,
1388                    "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
1389                ),
1390                (
1391                    11,
1392                    "<< /Type /StructElem /S /Document /P 10 0 R /Lang (en) /K [13 0 R 14 0 R] >>",
1393                ),
1394                (12, "<< /Nums [0 [13 0 R 14 0 R]] >>"),
1395                (
1396                    13,
1397                    "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0] >>",
1398                ),
1399                (
1400                    14,
1401                    "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /Lang (de) /K [1] >>",
1402                ),
1403            ],
1404        );
1405        let placed = placements(&doc, &[id(0, 0), id(0, 1)]);
1406        assert_eq!(placed[&id(0, 0)].lang.as_deref(), Some("en"));
1407        assert_eq!(placed[&id(0, 1)].lang.as_deref(), Some("de"));
1408        let untagged = two_paragraphs("<< /Nums [0 [13 0 R 14 0 R 13 0 R 14 0 R]] >>");
1409        assert_eq!(placements(&untagged, &[id(0, 0)])[&id(0, 0)].lang, None);
1410    }
1411
1412    // Covers ISO 32000-1 §14.7.5, §14.7.5.2 and §14.7.5.3.
1413    #[test]
1414    fn placements_carry_the_elements_attribute_objects() {
1415        // Cell 14 has one direct attribute object and revision 1; cell 15 a
1416        // class from the root's /ClassMap, numbered 4 in its /C array, and a
1417        // direct object overriding one of the class's entries; cell 16 an
1418        // /A array whose first object is followed by its revision number and
1419        // whose second is indirect; paragraph 17 has none.
1420        let doc = tagged_doc(
1421            "/StructParents 0",
1422            &[
1423                (
1424                    10,
1425                    "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R \
1426                     /ClassMap << /Wide << /O /Table /ColSpan 3 /RowSpan 2 >> >> >>",
1427                ),
1428                (
1429                    11,
1430                    "<< /Type /StructElem /S /Table /P 10 0 R /K [13 0 R 17 0 R] >>",
1431                ),
1432                (12, "<< /Nums [0 [14 0 R 15 0 R 16 0 R 17 0 R]] >>"),
1433                (
1434                    13,
1435                    "<< /Type /StructElem /S /TR /P 11 0 R /K [14 0 R 15 0 R 16 0 R] >>",
1436                ),
1437                (
1438                    14,
1439                    "<< /Type /StructElem /S /TD /P 13 0 R /Pg 3 0 R /K [0] /R 1 \
1440                     /A << /O /Table /ColSpan 2 >> >>",
1441                ),
1442                (
1443                    15,
1444                    "<< /Type /StructElem /S /TD /P 13 0 R /Pg 3 0 R /K [1] \
1445                     /C [/Wide 4] /A << /O /Table /ColSpan 1 >> >>",
1446                ),
1447                (
1448                    16,
1449                    "<< /Type /StructElem /S /TD /P 13 0 R /Pg 3 0 R /K [2] \
1450                     /A [<< /O /Layout /Placement /Block >> 2 20 0 R] >>",
1451                ),
1452                (
1453                    17,
1454                    "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [3] >>",
1455                ),
1456                (20, "<< /O /Table /RowSpan 3 >>"),
1457            ],
1458        );
1459        let placed = placements(&doc, &[id(0, 0), id(0, 1), id(0, 2), id(0, 3)]);
1460        let cell = |num: u32| ObjRef { num, gen: 0 };
1461        let int = |value: Option<&Object>| value.and_then(Object::as_int);
1462        let objects = |p: &Placement, element: ObjRef| -> Vec<(String, i64)> {
1463            p.attributes
1464                .iter()
1465                .filter(|a| a.element == element)
1466                .map(|a| (a.owner.clone(), a.revision))
1467                .collect()
1468        };
1469
1470        let p0 = &placed[&id(0, 0)];
1471        assert_eq!(int(p0.attribute(cell(14), "Table", "ColSpan")), Some(2));
1472        assert_eq!(p0.attribute(cell(14), "Table", "RowSpan"), None);
1473        assert_eq!(p0.attributes.len(), 1, "the row and table carry none");
1474        assert_eq!(p0.path.last().unwrap().revision, 1);
1475
1476        // Class objects come first, direct objects after and win.
1477        let p1 = &placed[&id(0, 1)];
1478        assert_eq!(
1479            objects(p1, cell(15)),
1480            [("Table".to_string(), 4), ("Table".to_string(), 0)]
1481        );
1482        assert_eq!(int(p1.attribute(cell(15), "Table", "ColSpan")), Some(1));
1483        assert_eq!(int(p1.attribute(cell(15), "Table", "RowSpan")), Some(2));
1484        assert_eq!(p1.path.last().unwrap().revision, 0);
1485
1486        let p2 = &placed[&id(0, 2)];
1487        assert_eq!(
1488            objects(p2, cell(16)),
1489            [("Layout".to_string(), 2), ("Table".to_string(), 0)]
1490        );
1491        assert_eq!(int(p2.attribute(cell(16), "Table", "RowSpan")), Some(3));
1492        assert_eq!(
1493            p2.attribute(cell(16), "Layout", "Placement")
1494                .and_then(Object::as_name)
1495                .map(|n| n.0.as_str()),
1496            Some("Block")
1497        );
1498        assert_eq!(p2.attribute(cell(16), "Table", "ColSpan"), None);
1499
1500        assert!(placed[&id(0, 3)].attributes.is_empty());
1501    }
1502
1503    // Covers ISO 32000-1 §14.8.5.2.
1504    #[test]
1505    fn standard_attribute_owners_are_recognized_by_name() {
1506        assert_eq!(
1507            StandardOwner::from_name("Table"),
1508            Some(StandardOwner::Table)
1509        );
1510        assert_eq!(
1511            StandardOwner::from_name("XML-1.00"),
1512            Some(StandardOwner::Xml100)
1513        );
1514        assert_eq!(StandardOwner::from_name("Acme"), None);
1515        assert_eq!(StandardOwner::from_name("table"), None);
1516        assert_eq!(StandardOwner::ALL.len(), 11);
1517        for owner in StandardOwner::ALL {
1518            assert_eq!(
1519                StandardOwner::from_name(owner.name()),
1520                Some(owner),
1521                "{}",
1522                owner.name()
1523            );
1524        }
1525        let object = AttributeObject {
1526            element: ObjRef { num: 1, gen: 0 },
1527            owner: "Layout".to_string(),
1528            revision: 0,
1529            entries: Dict::default(),
1530        };
1531        assert_eq!(object.standard_owner(), Some(StandardOwner::Layout));
1532        let own = AttributeObject {
1533            owner: "Acme".to_string(),
1534            ..object
1535        };
1536        assert_eq!(own.standard_owner(), None);
1537    }
1538
1539    // Covers ISO 32000-1 §14.8.4, §14.8.4.2, §14.8.4.3, §14.8.4.4 and
1540    // §14.8.4.5.
1541    #[test]
1542    fn standard_types_are_recognized_by_name_and_grouped_by_clause() {
1543        assert_eq!(StandardType::from_name("H1"), Some(StandardType::H1));
1544        assert_eq!(StandardType::H1.kind(), StandardKind::BlockLevel);
1545        assert_eq!(
1546            StandardType::from_name("TOCI").map(StandardType::kind),
1547            Some(StandardKind::Grouping)
1548        );
1549        assert_eq!(
1550            StandardType::from_name("LBody").map(StandardType::kind),
1551            Some(StandardKind::BlockLevel)
1552        );
1553        assert_eq!(
1554            StandardType::from_name("TFoot").map(StandardType::kind),
1555            Some(StandardKind::BlockLevel)
1556        );
1557        assert_eq!(
1558            StandardType::from_name("Ruby").map(StandardType::kind),
1559            Some(StandardKind::InlineLevel)
1560        );
1561        assert_eq!(
1562            StandardType::from_name("Formula").map(StandardType::kind),
1563            Some(StandardKind::Illustration)
1564        );
1565        // Names are case-sensitive and a document's own types are not standard.
1566        assert_eq!(StandardType::from_name("h1"), None);
1567        assert_eq!(StandardType::from_name("Para"), None);
1568        assert_eq!(StandardType::ALL.len(), 49);
1569        for t in StandardType::ALL {
1570            assert_eq!(StandardType::from_name(t.name()), Some(t), "{}", t.name());
1571        }
1572        let grouping = StandardType::ALL
1573            .iter()
1574            .filter(|t| t.kind() == StandardKind::Grouping)
1575            .count();
1576        assert_eq!(grouping, 12);
1577    }
1578
1579    // Covers ISO 32000-1 §7.7.2.
1580    #[test]
1581    fn no_struct_tree_root_means_no_tree() {
1582        let mut b = PdfBuilder::new();
1583        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1584        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1585        b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
1586        let doc = Document::load(b.build(1)).unwrap();
1587        assert!(doc.structure_tree().is_none());
1588    }
1589
1590    // Covers ISO 32000-1 §14.7.2 and §7.9.7.
1591    #[test]
1592    fn ranks_follow_the_tree_not_the_ids() {
1593        let doc = two_paragraphs("<< /Nums [0 [13 0 R 14 0 R 13 0 R 14 0 R]] >>");
1594        let ranks = ranks(&doc, &[id(0, 2), id(0, 3), id(0, 0), id(0, 1)]);
1595        assert_eq!(ranks[&id(0, 0)], 0);
1596        assert_eq!(ranks[&id(0, 2)], 1);
1597        assert_eq!(ranks[&id(0, 1)], 2);
1598        assert_eq!(ranks[&id(0, 3)], 3);
1599    }
1600
1601    // Covers ISO 32000-1 §14.7.4, §14.7.4.4 and §7.9.7.
1602    #[test]
1603    fn parent_tree_kids_and_limits_are_descended() {
1604        let doc = tagged_doc(
1605            "/StructParents 7",
1606            &[
1607                (
1608                    10,
1609                    "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
1610                ),
1611                (
1612                    11,
1613                    "<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R] >>",
1614                ),
1615                (12, "<< /Kids [15 0 R 16 0 R] >>"),
1616                (
1617                    13,
1618                    "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0] >>",
1619                ),
1620                (15, "<< /Limits [0 3] /Nums [0 [] 3 []] >>"),
1621                (16, "<< /Limits [7 9] /Nums [7 [13 0 R] 9 []] >>"),
1622            ],
1623        );
1624        let ranks = ranks(&doc, &[id(7, 0)]);
1625        assert_eq!(ranks[&id(7, 0)], 0);
1626    }
1627
1628    // Covers ISO 32000-1 §14.7.4.4.
1629    #[test]
1630    fn a_page_without_a_parent_tree_entry_has_no_ranks() {
1631        let doc = two_paragraphs("<< /Nums [5 [13 0 R]] >>");
1632        assert!(ranks(&doc, &[id(0, 0), id(0, 1)]).is_empty());
1633    }
1634
1635    #[test]
1636    fn untagged_and_out_of_range_ids_are_absent() {
1637        let doc = two_paragraphs("<< /Nums [0 [13 0 R 14 0 R 13 0 R 14 0 R]] >>");
1638        let ranks = ranks(&doc, &[id(0, 0), id(0, 9), id(4, 0)]);
1639        assert_eq!(ranks.len(), 1);
1640        assert_eq!(ranks[&id(0, 0)], 0);
1641    }
1642
1643    // Covers ISO 32000-1 §14.7.2.
1644    #[test]
1645    fn a_broken_ancestry_leaves_only_that_element_unranked() {
1646        let doc = tagged_doc(
1647            "/StructParents 0",
1648            &[
1649                (
1650                    10,
1651                    "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
1652                ),
1653                (
1654                    11,
1655                    "<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R] >>",
1656                ),
1657                (12, "<< /Nums [0 [13 0 R 14 0 R]] >>"),
1658                (
1659                    13,
1660                    "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0] >>",
1661                ),
1662                // Not among its parent's kids: its path cannot be read.
1663                (
1664                    14,
1665                    "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [1] >>",
1666                ),
1667            ],
1668        );
1669        let ranks = ranks(&doc, &[id(0, 0), id(0, 1)]);
1670        assert_eq!(ranks.len(), 1);
1671        assert_eq!(ranks[&id(0, 0)], 0);
1672    }
1673
1674    // Covers ISO 32000-1 §14.7.4 and §14.7.4.2.
1675    #[test]
1676    fn marked_content_references_name_their_page() {
1677        // One element spanning two pages: the same id number on each. The
1678        // reference naming this page wins the index, the other is skipped.
1679        let doc = tagged_doc(
1680            "/StructParents 0",
1681            &[
1682                (
1683                    10,
1684                    "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
1685                ),
1686                (
1687                    11,
1688                    "<< /Type /StructElem /S /P /P 10 0 R \
1689                     /K [<< /Type /MCR /Pg 99 0 R /MCID 0 >> 15 0 R] >>",
1690                ),
1691                (12, "<< /Nums [0 [11 0 R]] >>"),
1692                (15, "<< /Type /MCR /Pg 3 0 R /MCID 0 >>"),
1693            ],
1694        );
1695        let ranks = ranks(&doc, &[id(0, 0)]);
1696        assert_eq!(ranks[&id(0, 0)], 0);
1697        let tree = doc.structure_tree().unwrap();
1698        let page = doc.page(0).unwrap();
1699        let mut walk = Walk {
1700            src: &Immediate(&doc),
1701            page_ref: page.object_ref(),
1702            root_ref: Some(ObjRef { num: 10, gen: 0 }),
1703            class_map: &tree.class_map,
1704            dicts: FastMap::default(),
1705            paths: FastMap::default(),
1706            parents: FastMap::default(),
1707        };
1708        let element = block_on(walk.dict(ObjRef { num: 11, gen: 0 })).unwrap();
1709        assert_eq!(block_on(walk.mcid_index(&element, 0)), Some(1));
1710        drop(tree);
1711    }
1712
1713    // Covers ISO 32000-1 §14.7.4.2.
1714    #[test]
1715    fn a_bare_integer_on_another_page_is_not_this_page() {
1716        let doc = tagged_doc(
1717            "/StructParents 0",
1718            &[
1719                (
1720                    10,
1721                    "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
1722                ),
1723                (
1724                    11,
1725                    "<< /Type /StructElem /S /P /P 10 0 R /Pg 99 0 R /K [0] >>",
1726                ),
1727                (12, "<< /Nums [0 [11 0 R]] >>"),
1728            ],
1729        );
1730        assert!(ranks(&doc, &[id(0, 0)]).is_empty());
1731    }
1732
1733    // Covers ISO 32000-1 §14.7.4.4.
1734    #[test]
1735    fn a_form_key_ranks_alongside_the_page() {
1736        // The form's marked content (key 1) is a child of the second
1737        // paragraph; the page's (key 0) fills the first.
1738        let doc = tagged_doc(
1739            "/StructParents 0",
1740            &[
1741                (
1742                    10,
1743                    "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
1744                ),
1745                (
1746                    11,
1747                    "<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R 14 0 R] >>",
1748                ),
1749                (12, "<< /Nums [0 [13 0 R] 1 [14 0 R]] >>"),
1750                (
1751                    13,
1752                    "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0] >>",
1753                ),
1754                (
1755                    14,
1756                    "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0] >>",
1757                ),
1758            ],
1759        );
1760        let ranks = ranks(&doc, &[id(1, 0), id(0, 0)]);
1761        assert_eq!(ranks[&id(0, 0)], 0);
1762        assert_eq!(ranks[&id(1, 0)], 1);
1763    }
1764
1765    // Covers ISO 32000-1 §14.7.2.
1766    #[test]
1767    fn a_single_kid_needs_no_array() {
1768        let doc = tagged_doc(
1769            "/StructParents 0",
1770            &[
1771                (
1772                    10,
1773                    "<< /Type /StructTreeRoot /K 11 0 R /ParentTree 12 0 R >>",
1774                ),
1775                (11, "<< /Type /StructElem /S /P /P 10 0 R /Pg 3 0 R /K 0 >>"),
1776                (12, "<< /Nums [0 [11 0 R]] >>"),
1777            ],
1778        );
1779        assert_eq!(ranks(&doc, &[id(0, 0)])[&id(0, 0)], 0);
1780    }
1781}