pdfboss_core/
requirement.rs1use crate::object::{Dict, Object};
6use crate::source::AsyncObjectSource;
7use crate::tree::{resolved_dict, Entries};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct Requirement {
13 pub kind: String,
16 pub handlers: Vec<RequirementHandler>,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct RequirementHandler {
25 pub kind: String,
28 pub script: Option<String>,
31}
32
33pub 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
70async 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 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 #[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 #[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 #[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 #[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}