1use 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
14const MAX_ELEMENT_DEPTH: usize = 64;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub struct MarkedContentId {
23 pub parents: u32,
24 pub mcid: u32,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum StandardKind {
32 Grouping,
35 BlockLevel,
38 InlineLevel,
40 Illustration,
42}
43
44#[allow(clippy::upper_case_acronyms)]
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub enum StandardType {
51 Document,
53 Part,
54 Art,
55 Sect,
56 Div,
57 BlockQuote,
58 Caption,
59 TOC,
60 TOCI,
61 Index,
62 NonStruct,
63 Private,
64 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 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 Figure,
102 Formula,
103 Form,
104}
105
106impl StandardType {
107 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 pub fn from_name(name: &str) -> Option<StandardType> {
164 StandardType::ALL.into_iter().find(|t| t.name() == name)
165 }
166
167 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
284pub struct StructureElement {
285 pub standard_type: StandardType,
286 pub object: ObjRef,
287 pub revision: i64,
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
297pub enum StandardOwner {
298 Layout,
300 List,
302 PrintField,
304 Table,
306 Xml100,
308 Html32,
310 Html401,
312 Oeb10,
314 Rtf105,
316 Css100,
318 Css200,
320}
321
322impl StandardOwner {
323 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 pub fn from_name(name: &str) -> Option<StandardOwner> {
342 StandardOwner::ALL
343 .into_iter()
344 .find(|owner| owner.name() == name)
345 }
346
347 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#[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 pub fn standard_owner(&self) -> Option<StandardOwner> {
383 StandardOwner::from_name(&self.owner)
384 }
385}
386
387#[derive(Debug, Clone, PartialEq)]
394pub struct Placement {
395 pub rank: u32,
396 pub structure_type: Option<String>,
398 pub mapped_type: Option<String>,
401 pub standard_type: Option<StandardType>,
403 pub path: Vec<StructureElement>,
407 pub alt: Option<String>,
410 pub lang: Option<String>,
413 pub expansion: Option<String>,
417 pub attributes: Vec<AttributeObject>,
422}
423
424impl Placement {
425 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#[derive(Debug, Clone, PartialEq)]
446pub struct StructureTree {
447 root: Dict,
448 root_ref: Option<ObjRef>,
449 role_map: FastMap<String, String>,
452 class_map: FastMap<String, Object>,
455}
456
457impl StructureTree {
458 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 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 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 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
610struct 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
623type Ancestry = Vec<Ancestor>;
626
627fn 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
640fn 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
658fn 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
670struct Walk<'a, S> {
674 src: &'a S,
675 page_ref: Option<ObjRef>,
676 root_ref: Option<ObjRef>,
677 class_map: &'a FastMap<String, Object>,
679 dicts: FastMap<ObjRef, Option<Arc<Dict>>>,
680 paths: FastMap<ObjRef, Option<Arc<Vec<u32>>>>,
683 parents: FastMap<u32, Option<Arc<Vec<Object>>>>,
685}
686
687impl<S: AsyncObjectSource> Walk<'_, S> {
688 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 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 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 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 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 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 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 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 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 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 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 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 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
1014fn 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
1025fn 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
1034fn 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
1044fn 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 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 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 #[test]
1123 fn structure_types_are_read_and_mapped_through_the_role_map() {
1124 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 #[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 #[test]
1202 fn placements_carry_the_standard_typed_ancestry() {
1203 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 #[test]
1295 fn placements_carry_the_nearest_alternate_description() {
1296 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 #[test]
1336 fn placements_carry_the_nearest_expansion() {
1337 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 #[test]
1380 fn placements_carry_the_nearest_language() {
1381 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 #[test]
1414 fn placements_carry_the_elements_attribute_objects() {
1415 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 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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 (
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 #[test]
1676 fn marked_content_references_name_their_page() {
1677 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 #[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 #[test]
1735 fn a_form_key_ranks_alongside_the_page() {
1736 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 #[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}