Skip to main content

pdfboss_core/
requirement.rs

1//! Document requirements (ISO 32000-1 §12.10): the catalog's `/Requirements`
2//! array, one dictionary per feature a reader needs to show the document as
3//! intended.
4
5use crate::object::{Dict, Object};
6use crate::source::AsyncObjectSource;
7use crate::tree::{resolved_dict, Entries};
8
9/// One entry of the catalog's `/Requirements` array (ISO 32000-1 §12.10.1,
10/// Table 266): a feature the document needs a reader to support.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct Requirement {
13    /// `/S`: the requirement type. `EnableJavaScripts` is the one type ISO
14    /// 32000-1 defines; any other name is kept as written.
15    pub kind: String,
16    /// `/RH`: the handlers a reader that does not meet the requirement
17    /// runs, one dictionary or an array of them, in order (§12.10.2).
18    pub handlers: Vec<RequirementHandler>,
19}
20
21/// One requirement handler (ISO 32000-1 §12.10.2, Table 267), read as data:
22/// pdfboss runs no JavaScript, so the handler is reported, not invoked.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct RequirementHandler {
25    /// `/S`: `JS` for a document-level JavaScript, `NoOp` for doing nothing;
26    /// any other name is kept as written.
27    pub kind: String,
28    /// `/Script`: the name of the document-level JavaScript a `JS` handler
29    /// runs, as the catalog's `/Names` `/JavaScript` tree lists it.
30    pub script: Option<String>,
31}
32
33/// The requirements the catalog's `/Requirements` array lists, in order:
34/// empty when the catalog has no array. An entry that is no dictionary, or
35/// whose `/S` is no name, is skipped.
36///
37/// Covers ISO 32000-1 §12.10.1.
38pub async fn requirements_with<S: AsyncObjectSource>(src: &S, trailer: &Dict) -> Vec<Requirement> {
39    let Some(root) = trailer.get("Root") else {
40        return Vec::new();
41    };
42    let Some(catalog) = resolved_dict(src, root).await else {
43        return Vec::new();
44    };
45    let Some(entry) = catalog.get("Requirements") else {
46        return Vec::new();
47    };
48    let Ok(Object::Array(entries)) = src.resolve(entry).await else {
49        return Vec::new();
50    };
51    let mut requirements = Vec::new();
52    for entry in &entries {
53        let Some(dict) = resolved_dict(src, entry).await else {
54            continue;
55        };
56        let Some(kind) = Entries { src, dict: &dict }.value("S").await else {
57            continue;
58        };
59        let Some(kind) = kind.as_name() else {
60            continue;
61        };
62        requirements.push(Requirement {
63            kind: kind.0.clone(),
64            handlers: handlers_with(src, &dict).await,
65        });
66    }
67    requirements
68}
69
70/// The `/RH` handlers of one requirement dictionary, in order: empty when
71/// the entry is missing or neither a dictionary nor an array. A handler
72/// that is no dictionary, or whose `/S` is no name, is skipped.
73///
74/// Covers ISO 32000-1 §12.10.2.
75async fn handlers_with<S: AsyncObjectSource>(
76    src: &S,
77    requirement: &Dict,
78) -> Vec<RequirementHandler> {
79    let Some(entry) = requirement.get("RH") else {
80        return Vec::new();
81    };
82    let entries = match src.resolve(entry).await {
83        Ok(Object::Array(entries)) => entries,
84        Ok(handler @ Object::Dict(_)) => vec![handler],
85        _ => return Vec::new(),
86    };
87    let mut handlers = Vec::new();
88    for entry in &entries {
89        let Some(dict) = resolved_dict(src, entry).await else {
90            continue;
91        };
92        let entries = Entries { src, dict: &dict };
93        let Some(kind) = entries.value("S").await else {
94            continue;
95        };
96        let Some(kind) = kind.as_name() else {
97            continue;
98        };
99        handlers.push(RequirementHandler {
100            kind: kind.0.clone(),
101            script: entries.text("Script").await,
102        });
103    }
104    handlers
105}
106
107#[cfg(test)]
108mod tests {
109    use super::RequirementHandler;
110    use crate::document::Document;
111    use pdfboss_testkit::PdfBuilder;
112
113    /// A one-page document whose catalog carries `catalog_extra`; object 20
114    /// is an EnableJavaScripts requirement reachable by reference, object 21
115    /// a JavaScript requirement handler.
116    fn doc_with(catalog_extra: &str) -> Document {
117        let mut b = PdfBuilder::new();
118        b.object(
119            1,
120            &format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
121        );
122        b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
123        b.object(
124            20,
125            "<< /Type /Requirement /S /EnableJavaScripts \
126             /RH << /Type /ReqHandler /S /JS /Script (init) >> >>",
127        );
128        b.object(21, "<< /Type /ReqHandler /S /JS /Script (fallback) >>");
129        Document::load(b.build(1)).unwrap()
130    }
131
132    /// `/RH` as one handler dictionary or as an array of them reads in
133    /// order: the `/S` kind and, for `/JS`, the `/Script` name of the
134    /// document-level JavaScript that handles the requirement.
135    // Covers ISO 32000-1 §12.10.2.
136    #[test]
137    fn reads_the_requirement_handlers() {
138        let doc = doc_with(
139            "/Requirements [20 0 R << /S /Custom /RH [ << /Type /ReqHandler /S /NoOp >> 21 0 R ] >>]",
140        );
141        let requirements = doc.requirements();
142        assert_eq!(
143            requirements[0].handlers,
144            [RequirementHandler {
145                kind: "JS".into(),
146                script: Some("init".into()),
147            }]
148        );
149        assert_eq!(
150            requirements[1].handlers,
151            [
152                RequirementHandler {
153                    kind: "NoOp".into(),
154                    script: None,
155                },
156                RequirementHandler {
157                    kind: "JS".into(),
158                    script: Some("fallback".into()),
159                },
160            ]
161        );
162    }
163
164    /// A requirement without `/RH`, or whose `/RH` is neither a dictionary
165    /// nor an array, has no handlers; a handler that is no dictionary or has
166    /// no `/S` name is skipped.
167    // Covers ISO 32000-1 §12.10.2.
168    #[test]
169    fn missing_or_malformed_handlers_read_as_none() {
170        let doc = doc_with(
171            "/Requirements [ << /S /A >> << /S /B /RH 7 >> \
172             << /S /C /RH [ 7 (text) << /Type /ReqHandler >> << /S /NoOp >> ] >> ]",
173        );
174        let counts: Vec<usize> = doc
175            .requirements()
176            .iter()
177            .map(|requirement| requirement.handlers.len())
178            .collect();
179        assert_eq!(counts, [0, 0, 1]);
180    }
181
182    /// The array's dictionaries read in order, by reference or written
183    /// directly, each with its `/S` type name.
184    // Covers ISO 32000-1 §12.10.1.
185    #[test]
186    fn reads_the_requirements_in_order() {
187        let doc = doc_with("/Requirements [20 0 R << /Type /Requirement /S /Custom >>]");
188        let requirements = doc.requirements();
189        let kinds: Vec<&str> = requirements
190            .iter()
191            .map(|requirement| requirement.kind.as_str())
192            .collect();
193        assert_eq!(kinds, ["EnableJavaScripts", "Custom"]);
194    }
195
196    /// A catalog without `/Requirements`, or whose entry is no array, has
197    /// none; an entry that is no dictionary or has no `/S` name is skipped.
198    // Covers ISO 32000-1 §12.10.1.
199    #[test]
200    fn missing_or_malformed_requirements_read_as_none() {
201        assert!(doc_with("").requirements().is_empty());
202        assert!(doc_with("/Requirements 20 0 R").requirements().is_empty());
203        let odd =
204            doc_with("/Requirements [7 (text) << /Type /Requirement >> 20 0 R]").requirements();
205        assert_eq!(odd.len(), 1);
206        assert_eq!(odd[0].kind, "EnableJavaScripts");
207    }
208}