Skip to main content

pdfboss_core/
article.rs

1//! Article threads (ISO 32000-1 §12.4.3): the catalog's `/Threads` array,
2//! each thread's information dictionary and its chain of beads.
3
4use 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
11/// Beads followed per thread before the walk gives up, so a malformed chain
12/// that never returns to its first bead stays bounded even without a repeat.
13const MAX_BEADS: usize = 100_000;
14
15/// One bead of an article thread (ISO 32000-1 §12.4.3, Table 161): where
16/// one piece of the article lies.
17#[derive(Debug, Clone, PartialEq)]
18pub struct Bead {
19    /// The bead dictionary's own reference; the chain links by reference.
20    pub object: ObjRef,
21    /// `/P`: the page object the bead appears on.
22    pub page: Option<ObjRef>,
23    /// `/R`: the bead's rectangle on that page, normalized.
24    pub rect: Option<Rect>,
25}
26
27/// An article thread (ISO 32000-1 §12.4.3, Table 160): its information
28/// dictionary and its beads in chain order.
29#[derive(Debug, Clone, PartialEq)]
30pub struct ArticleThread {
31    /// The thread dictionary's reference, `None` when it was written
32    /// directly into the `/Threads` array.
33    pub object: Option<ObjRef>,
34    /// `/I`: the thread's title, author and the other document information
35    /// entries it carries, all `None` without one.
36    pub info: Metadata,
37    /// The beads from `/F` along `/N`, until the chain returns to a bead
38    /// already seen (the last bead points back to the first) or breaks.
39    pub beads: Vec<Bead>,
40}
41
42/// The article threads the catalog's `/Threads` array declares, in array
43/// order: every entry that is a dictionary with an `/F` bead reference.
44/// Other entries are skipped, and a catalog without the array, or whose
45/// entry is no array, has none.
46///
47/// Covers ISO 32000-1 §12.4.3.
48pub 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
88/// The beads on `page` in drawing order: the references its `/B` array
89/// holds (Table 30), anything else in it skipped; empty without one.
90///
91/// Covers ISO 32000-1 §12.4.3.
92pub 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
105/// Follows `/N` from `first` until a bead repeats, is missing, or the cap
106/// is reached.
107async 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
128/// A rectangle (§7.9.5): four numbers, each possibly indirect, normalized.
129pub(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    /// The clause's EXAMPLE 2: one thread of three beads on page 8, plus a
161    /// thread without `/F`; `catalog_extra` replaces the `/Threads` entry.
162    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    /// Each thread's beads come in chain order from `/F` until the chain
195    /// returns to the first bead; the information dictionary reads like the
196    /// document information dictionary; a thread without `/F` is skipped;
197    /// the page's `/B` array lists the beads on it.
198    // Covers ISO 32000-1 §12.4.3.
199    #[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    /// A chain stops at a bead that is missing or already visited without
221    /// looping; a catalog without `/Threads`, or with one that is no array,
222    /// has no articles.
223    // Covers ISO 32000-1 §12.4.3.
224    #[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}