1use crate::document::{metadata_with, Metadata, Page};
5use crate::geom::Rect;
6use crate::hash::FastSet;
7use crate::object::{Dict, ObjRef, Object};
8use crate::source::AsyncObjectSource;
9use crate::tree::resolved_dict;
10
11const MAX_BEADS: usize = 100_000;
14
15#[derive(Debug, Clone, PartialEq)]
18pub struct Bead {
19 pub object: ObjRef,
21 pub page: Option<ObjRef>,
23 pub rect: Option<Rect>,
25}
26
27#[derive(Debug, Clone, PartialEq)]
30pub struct ArticleThread {
31 pub object: Option<ObjRef>,
34 pub info: Metadata,
37 pub beads: Vec<Bead>,
40}
41
42pub async fn articles_with<S: AsyncObjectSource>(src: &S, trailer: &Dict) -> Vec<ArticleThread> {
49 let mut threads = Vec::new();
50 let Some(root) = trailer.get("Root") else {
51 return threads;
52 };
53 let Some(catalog) = resolved_dict(src, root).await else {
54 return threads;
55 };
56 let Some(entry) = catalog.get("Threads") else {
57 return threads;
58 };
59 let Ok(array) = src.resolve(entry).await else {
60 return threads;
61 };
62 let Some(array) = array.as_array() else {
63 return threads;
64 };
65 for value in array {
66 let Some(dict) = resolved_dict(src, value).await else {
67 continue;
68 };
69 let Some(first) = dict.get("F").and_then(Object::as_ref) else {
70 continue;
71 };
72 let info = match dict.get("I") {
73 Some(entry) => match resolved_dict(src, entry).await {
74 Some(info) => metadata_with(src, &info).await,
75 None => Metadata::default(),
76 },
77 None => Metadata::default(),
78 };
79 threads.push(ArticleThread {
80 object: value.as_ref(),
81 info,
82 beads: beads_from(src, first).await,
83 });
84 }
85 threads
86}
87
88pub async fn page_beads_with<S: AsyncObjectSource>(src: &S, page: &Page) -> Vec<ObjRef> {
93 let Some(entry) = page.dict().get("B") else {
94 return Vec::new();
95 };
96 let Ok(array) = src.resolve(entry).await else {
97 return Vec::new();
98 };
99 array
100 .as_array()
101 .map(|items| items.iter().filter_map(Object::as_ref).collect())
102 .unwrap_or_default()
103}
104
105async fn beads_from<S: AsyncObjectSource>(src: &S, first: ObjRef) -> Vec<Bead> {
108 let mut beads = Vec::new();
109 let mut visited: FastSet<ObjRef> = FastSet::default();
110 let mut next = Some(first);
111 while let Some(object) = next {
112 if beads.len() >= MAX_BEADS || !visited.insert(object) {
113 break;
114 }
115 let Some(bead) = resolved_dict(src, &Object::Ref(object)).await else {
116 break;
117 };
118 next = bead.get("N").and_then(Object::as_ref);
119 beads.push(Bead {
120 object,
121 page: bead.get("P").and_then(Object::as_ref),
122 rect: rectangle(src, bead.get("R")).await,
123 });
124 }
125 beads
126}
127
128pub(crate) async fn rectangle<S: AsyncObjectSource>(
130 src: &S,
131 value: Option<&Object>,
132) -> Option<Rect> {
133 let array = src.resolve(value?).await.ok()?;
134 let items = array.as_array()?;
135 if items.len() != 4 {
136 return None;
137 }
138 let mut coords = [0.0f32; 4];
139 for (slot, item) in coords.iter_mut().zip(items) {
140 let number = src.resolve(item).await.ok()?.as_f64()?;
141 if !number.is_finite() {
142 return None;
143 }
144 *slot = number as f32;
145 }
146 Some(Rect::new(coords[0], coords[1], coords[2], coords[3]).normalize())
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::document::Document;
153 use crate::geom::Rect;
154 use pdfboss_testkit::PdfBuilder;
155
156 fn r(num: u32) -> ObjRef {
157 ObjRef { num, gen: 0 }
158 }
159
160 fn doc_with(catalog_extra: &str, beads: impl FnOnce(&mut PdfBuilder)) -> Document {
163 let mut b = PdfBuilder::new();
164 b.object(
165 1,
166 &format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
167 );
168 b.object(2, "<< /Type /Pages /Kids [8 0 R] /Count 1 >>");
169 b.object(
170 8,
171 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /B [23 0 R 24 0 R] >>",
172 );
173 b.object(
174 22,
175 "<< /F 23 0 R /I << /Title (Man Bites Dog) /Author (Ann) >> >>",
176 );
177 b.object(30, "<< /I << /Title (No beads) >> >>");
178 beads(&mut b);
179 Document::load(b.build(1)).unwrap()
180 }
181
182 fn example_beads(b: &mut PdfBuilder) {
183 b.object(
184 23,
185 "<< /T 22 0 R /N 24 0 R /V 25 0 R /P 8 0 R /R [158 247 318 905] >>",
186 );
187 b.object(
188 24,
189 "<< /N 25 0 R /V 23 0 R /P 8 0 R /R [322 246 486 904] >>",
190 );
191 b.object(25, "<< /N 23 0 R /V 24 0 R /P 8 0 R /R [1 2 3 4] >>");
192 }
193
194 #[test]
200 fn walks_each_threads_beads_in_order_and_reads_its_information() {
201 let doc = doc_with("/Threads [22 0 R 30 0 R]", example_beads);
202 let threads = doc.articles();
203 assert_eq!(threads.len(), 1);
204 let thread = &threads[0];
205 assert_eq!(thread.object, Some(r(22)));
206 assert_eq!(thread.info.title.as_deref(), Some("Man Bites Dog"));
207 assert_eq!(thread.info.author.as_deref(), Some("Ann"));
208 let objects: Vec<ObjRef> = thread.beads.iter().map(|bead| bead.object).collect();
209 assert_eq!(objects, vec![r(23), r(24), r(25)]);
210 assert_eq!(thread.beads[0].page, Some(r(8)));
211 assert_eq!(
212 thread.beads[0].rect,
213 Some(Rect::new(158.0, 247.0, 318.0, 905.0))
214 );
215 assert_eq!(thread.beads[2].rect, Some(Rect::new(1.0, 2.0, 3.0, 4.0)));
216 let page = doc.page(0).unwrap();
217 assert_eq!(doc.page_beads(&page), vec![r(23), r(24)]);
218 }
219
220 #[test]
225 fn a_broken_chain_stops_and_a_document_without_threads_has_none() {
226 let doc = doc_with("/Threads [22 0 R 26 0 R]", |b| {
227 b.object(
228 23,
229 "<< /T 22 0 R /N 24 0 R /V 25 0 R /P 8 0 R /R [0 0 1 1] >>",
230 );
231 b.object(24, "<< /N 99 0 R /V 23 0 R /P 8 0 R >>");
232 b.object(26, "<< /F 27 0 R >>");
233 b.object(27, "<< /T 26 0 R /N 28 0 R /V 28 0 R /P 8 0 R >>");
234 b.object(28, "<< /N 27 0 R /V 27 0 R /P 8 0 R >>");
235 });
236 let threads = doc.articles();
237 assert_eq!(threads.len(), 2);
238 let first: Vec<ObjRef> = threads[0].beads.iter().map(|bead| bead.object).collect();
239 assert_eq!(first, vec![r(23), r(24)]);
240 assert_eq!(threads[0].beads[1].rect, None);
241 let second: Vec<ObjRef> = threads[1].beads.iter().map(|bead| bead.object).collect();
242 assert_eq!(second, vec![r(27), r(28)]);
243 assert_eq!(threads[1].info, Metadata::default());
244 assert_eq!(doc_with("", example_beads).articles(), vec![]);
245 assert_eq!(
246 doc_with("/Threads 22 0 R", example_beads).articles(),
247 vec![]
248 );
249 }
250}