stet_pdf_reader/
embedded_files.rs1use std::collections::HashMap;
24
25use crate::metadata::{PdfDate, pdf_string_to_rust_pub};
26use crate::name_tree::walk_name_tree;
27use crate::objects::{PdfDict, PdfObj};
28use crate::resolver::Resolver;
29
30#[derive(Debug, Clone)]
32pub struct EmbeddedFile {
33 pub name: String,
35 pub filename: Option<String>,
37 pub unicode_filename: Option<String>,
39 pub description: Option<String>,
41 pub relationship: Option<AfRelationship>,
44 pub mime_type: Option<String>,
47 pub size: Option<u64>,
49 pub creation_date: Option<PdfDate>,
51 pub mod_date: Option<PdfDate>,
53 pub checksum: Option<Vec<u8>>,
55 pub stream_obj_num: u32,
57 pub stream_gen_num: u16,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
64#[non_exhaustive]
65pub enum AfRelationship {
66 Source,
67 Data,
68 Alternative,
69 Supplement,
70 EncryptedPayload,
71 FormData,
72 Schema,
73 Unspecified,
74 Other(String),
76}
77
78impl AfRelationship {
79 fn from_name(name: &[u8]) -> Self {
80 match name {
81 b"Source" => AfRelationship::Source,
82 b"Data" => AfRelationship::Data,
83 b"Alternative" => AfRelationship::Alternative,
84 b"Supplement" => AfRelationship::Supplement,
85 b"EncryptedPayload" => AfRelationship::EncryptedPayload,
86 b"FormData" => AfRelationship::FormData,
87 b"Schema" => AfRelationship::Schema,
88 b"Unspecified" => AfRelationship::Unspecified,
89 other => AfRelationship::Other(String::from_utf8_lossy(other).into_owned()),
90 }
91 }
92}
93
94pub fn parse_embedded_files(resolver: &Resolver) -> HashMap<String, EmbeddedFile> {
99 let mut map = HashMap::new();
100
101 let Some(catalog) = catalog_dict(resolver) else {
102 return map;
103 };
104 let Some(names_obj) = catalog.get(b"Names") else {
105 return map;
106 };
107 let Ok(names) = resolver.deref(names_obj) else {
108 return map;
109 };
110 let Some(names_dict) = names.as_dict() else {
111 return map;
112 };
113 let Some(ef_root) = names_dict.get(b"EmbeddedFiles") else {
114 return map;
115 };
116
117 map = walk_name_tree(resolver, ef_root, parse_filespec);
118 for (key, value) in map.iter_mut() {
122 value.name = key.clone();
123 }
124 map
125}
126
127fn parse_filespec(resolver: &Resolver, obj: &PdfObj) -> Option<EmbeddedFile> {
128 let resolved = resolver.deref(obj).ok()?;
129 let dict = resolved.as_dict()?;
130
131 let ef_obj = dict.get(b"EF")?;
133 let ef = resolver.deref(ef_obj).ok()?;
134 let ef_dict = ef.as_dict()?;
135 let stream_ref = ef_dict.get_ref(b"UF").or_else(|| ef_dict.get_ref(b"F"))?;
137
138 let mut size = None;
140 let mut creation_date = None;
141 let mut mod_date = None;
142 let mut checksum = None;
143 let mut mime_type = None;
144
145 if let Ok(stream) = resolver.resolve(stream_ref.0, stream_ref.1)
146 && let Some(stream_dict) = stream.as_dict()
147 {
148 if let Some(subtype) = stream_dict.get_name(b"Subtype") {
149 mime_type = Some(decode_mime_name(subtype));
150 }
151 if let Some(params_obj) = stream_dict.get(b"Params")
152 && let Ok(params_resolved) = resolver.deref(params_obj)
153 && let Some(params) = params_resolved.as_dict()
154 {
155 size = params.get_int(b"Size").and_then(|n| u64::try_from(n).ok());
156 creation_date = params
157 .get(b"CreationDate")
158 .and_then(|o| o.as_str())
159 .and_then(PdfDate::parse);
160 mod_date = params
161 .get(b"ModDate")
162 .and_then(|o| o.as_str())
163 .and_then(PdfDate::parse);
164 checksum = params
165 .get(b"CheckSum")
166 .and_then(|o| o.as_str())
167 .map(<[u8]>::to_vec);
168 }
169 }
170
171 let filename = dict.get(b"F").and_then(pdf_string_to_rust_pub);
172 let unicode_filename = dict.get(b"UF").and_then(pdf_string_to_rust_pub);
173 let description = dict.get(b"Desc").and_then(pdf_string_to_rust_pub);
174 let relationship = dict
175 .get_name(b"AFRelationship")
176 .map(AfRelationship::from_name);
177
178 Some(EmbeddedFile {
179 name: String::new(), filename,
181 unicode_filename,
182 description,
183 relationship,
184 mime_type,
185 size,
186 creation_date,
187 mod_date,
188 checksum,
189 stream_obj_num: stream_ref.0,
190 stream_gen_num: stream_ref.1,
191 })
192}
193
194fn decode_mime_name(name: &[u8]) -> String {
197 let s = String::from_utf8_lossy(name);
198 let mut out = String::with_capacity(s.len());
200 let bytes = s.as_bytes();
201 let mut i = 0;
202 while i < bytes.len() {
203 if bytes[i] == b'#'
204 && i + 2 < bytes.len()
205 && let Ok(b) =
206 u8::from_str_radix(std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or(""), 16)
207 {
208 out.push(b as char);
209 i += 3;
210 continue;
211 }
212 out.push(bytes[i] as char);
213 i += 1;
214 }
215 out
216}
217
218fn catalog_dict(resolver: &Resolver) -> Option<PdfDict> {
219 if let Some((num, gen_num)) = resolver.trailer().get_ref(b"Root")
220 && let Ok(obj) = resolver.resolve(num, gen_num)
221 && let Some(dict) = obj.as_dict()
222 {
223 return Some(dict.clone());
224 }
225 crate::find_catalog(resolver).and_then(|obj| obj.as_dict().cloned())
226}
227
228pub fn decode_embedded_file_stream(
235 resolver: &Resolver,
236 obj_num: u32,
237 gen_num: u16,
238) -> Result<Vec<u8>, crate::PdfError> {
239 resolver.stream_data(obj_num, gen_num)
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn af_relationship_from_name() {
248 assert_eq!(AfRelationship::from_name(b"Source"), AfRelationship::Source);
249 assert_eq!(AfRelationship::from_name(b"Data"), AfRelationship::Data);
250 assert_eq!(
251 AfRelationship::from_name(b"Custom"),
252 AfRelationship::Other("Custom".to_string())
253 );
254 }
255
256 #[test]
257 fn decode_mime_name_with_hex_escape() {
258 assert_eq!(decode_mime_name(b"text#2Fcsv"), "text/csv");
259 assert_eq!(decode_mime_name(b"application#2Fpdf"), "application/pdf");
260 assert_eq!(decode_mime_name(b"plain"), "plain");
261 }
262}