1use crate::{EditDoc, Error};
6use pdfrum_common::{Diagnostics, Limits};
7use pdfrum_object::{
8 Array, ByteSpan, Dict, Name, ObjRef, Object, PdfString, Resolve, Stream, encode_text,
9};
10
11type Result<T> = core::result::Result<T, Error>;
13
14#[derive(Debug, Clone, PartialEq, Eq, Default)]
30#[non_exhaustive]
31pub struct AttachmentOptions {
32 pub description: Option<String>,
35 pub mime_type: Option<String>,
39 pub modified: Option<String>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Default)]
65#[must_use]
66pub struct AttachmentOptionsBuilder(AttachmentOptions);
67
68impl AttachmentOptionsBuilder {
69 pub fn description(mut self, description: impl Into<String>) -> Self {
77 self.0.description = Some(description.into());
78 self
79 }
80
81 pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
88 self.0.mime_type = Some(mime_type.into());
89 self
90 }
91
92 pub fn modified(mut self, modified: impl Into<String>) -> Self {
103 self.0.modified = Some(modified.into());
104 self
105 }
106
107 #[must_use]
114 pub fn build(self) -> AttachmentOptions {
115 self.0
116 }
117}
118
119impl AttachmentOptions {
120 pub fn builder() -> AttachmentOptionsBuilder {
126 AttachmentOptionsBuilder::default()
127 }
128}
129
130fn embedded_file(bytes: &[u8], mime_type: Option<&str>, modified: Option<&str>) -> Stream {
135 let len = i64::try_from(bytes.len()).unwrap_or(i64::MAX);
136 let mut params = Dict::new();
137 params.insert(Name::from("Size"), Object::Int(len));
138 params.insert(
139 Name::from("CheckSum"),
140 Object::Str(PdfString::hex(pdfrum_crypt::md5(bytes))),
141 );
142 if let Some(modified) = modified.filter(|date| !date.is_empty()) {
143 params.insert(
144 Name::from("ModDate"),
145 Object::Str(PdfString::literal(encode_text(modified))),
146 );
147 }
148 let mut dict = Dict::new();
149 dict.insert(Name::from("Type"), Object::Name(Name::from("EmbeddedFile")));
150 if let Some(mime_type) = mime_type.filter(|mime| !mime.is_empty()) {
151 dict.insert(
152 Name::from("Subtype"),
153 Object::Name(Name::from(mime_type.as_bytes())),
154 );
155 }
156 dict.insert(Name::from("DL"), Object::Int(len));
157 dict.insert(Name::from("Params"), Object::Dict(params));
158 Stream::new(dict, ByteSpan::from(bytes.to_vec()))
159}
160
161pub(crate) fn attachment_entries(
164 edit: &EditDoc<'_>,
165 limits: &Limits,
166) -> Result<Vec<(String, Object)>> {
167 let Some(root) = edit.base().trailer().reference(&Name::from("Root")) else {
168 return Err(Error::NoDestinationCatalog);
169 };
170 let Ok(catalog) = edit.fetch(root) else {
171 return Ok(Vec::new());
172 };
173 let Some(catalog) = catalog.as_dict() else {
174 return Ok(Vec::new());
175 };
176 let Some(names) = catalog.dict(&Name::from("Names"), edit) else {
177 return Ok(Vec::new());
178 };
179 let Some(files) = names.dict(&Name::from("EmbeddedFiles"), edit) else {
180 return Ok(Vec::new());
181 };
182 let tree = pdfrum_doc::NameTree { root: files };
183 let mut diags = Diagnostics::default();
184 let count = tree.count(edit, limits, &mut diags);
185 Ok((0..count)
186 .filter_map(|index| tree.lookup_by_index(index, edit, limits, &mut diags))
187 .collect())
188}
189
190pub(crate) fn attachment_spec(
194 edit: &EditDoc<'_>,
195 limits: &Limits,
196 index: usize,
197) -> Result<Option<(Option<ObjRef>, Dict)>> {
198 let entries = attachment_entries(edit, limits)?;
199 let Some(entry) = entries.get(index) else {
200 return Ok(None);
201 };
202 Ok(match &entry.1 {
203 Object::Ref(reference) => {
204 let Ok(object) = edit.fetch(*reference) else {
205 return Ok(None);
206 };
207 object
208 .as_dict()
209 .map(|dict| (Some(*reference), dict.clone()))
210 }
211 Object::Dict(dict) => Some((None, dict.clone())),
212 _ => None,
213 })
214}
215
216pub(crate) fn write_attachment_entries(
222 edit: &mut EditDoc<'_>,
223 entries: Vec<(String, Object)>,
224) -> Result<()> {
225 let k_root = Name::from("Root");
226 let k_names = Name::from("Names");
227 let k_ef = Name::from("EmbeddedFiles");
228 let k_kids = Name::from("Kids");
229 let k_limits = Name::from("Limits");
230 let Some(root) = edit.base().trailer().reference(&k_root) else {
231 return Err(Error::NoDestinationCatalog);
232 };
233 let dict_at = |edit: &EditDoc<'_>, reference: ObjRef| {
234 edit.fetch(reference)
235 .ok()
236 .as_deref()
237 .and_then(Object::as_dict)
238 .cloned()
239 };
240 let mut catalog = dict_at(edit, root).unwrap_or_default();
241 let mut names_array = Array::new();
242 for (name, value) in entries {
243 names_array.push(Object::Str(PdfString::literal(encode_text(&name))));
244 names_array.push(value);
245 }
246 let tree_of = |existing: Option<Dict>| {
247 let mut tree = existing.unwrap_or_default();
248 tree.remove(&k_kids);
249 tree.remove(&k_limits);
250 tree.insert(k_names.clone(), Object::Array(names_array.clone()));
251 tree
252 };
253 match catalog.raw(&k_names).cloned() {
254 Some(Object::Ref(names_ref)) => {
255 let mut names = dict_at(edit, names_ref).unwrap_or_default();
256 match names.raw(&k_ef).cloned() {
257 Some(Object::Ref(tree_ref)) => {
258 let existing = dict_at(edit, tree_ref);
259 edit.replace(tree_ref, Object::Dict(tree_of(existing)));
260 }
261 Some(Object::Dict(inline)) => {
262 names.insert(k_ef.clone(), Object::Dict(tree_of(Some(inline))));
263 edit.replace(names_ref, Object::Dict(names));
264 }
265 _ => {
266 let tree_ref = edit.add(Object::Dict(tree_of(None)));
267 names.insert(k_ef.clone(), Object::Ref(tree_ref));
268 edit.replace(names_ref, Object::Dict(names));
269 }
270 }
271 }
272 Some(Object::Dict(mut names)) => match names.raw(&k_ef).cloned() {
273 Some(Object::Ref(tree_ref)) => {
274 let existing = dict_at(edit, tree_ref);
275 edit.replace(tree_ref, Object::Dict(tree_of(existing)));
276 }
277 Some(Object::Dict(inline)) => {
278 names.insert(k_ef.clone(), Object::Dict(tree_of(Some(inline))));
279 catalog.insert(k_names.clone(), Object::Dict(names));
280 edit.replace(root, Object::Dict(catalog));
281 }
282 _ => {
283 let tree_ref = edit.add(Object::Dict(tree_of(None)));
284 names.insert(k_ef.clone(), Object::Ref(tree_ref));
285 catalog.insert(k_names.clone(), Object::Dict(names));
286 edit.replace(root, Object::Dict(catalog));
287 }
288 },
289 _ => {
290 let tree_ref = edit.add(Object::Dict(tree_of(None)));
291 let mut names = Dict::new();
292 names.insert(k_ef.clone(), Object::Ref(tree_ref));
293 let names_ref = edit.add(Object::Dict(names));
294 catalog.insert(k_names.clone(), Object::Ref(names_ref));
295 edit.replace(root, Object::Dict(catalog));
296 }
297 }
298 Ok(())
299}
300
301pub(crate) fn store_attachment_spec(
305 edit: &mut EditDoc<'_>,
306 limits: &Limits,
307 index: usize,
308 reference: Option<ObjRef>,
309 spec: Dict,
310) -> Result<()> {
311 if let Some(reference) = reference {
312 edit.replace(reference, Object::Dict(spec));
313 return Ok(());
314 }
315 let mut entries = attachment_entries(edit, limits)?;
316 if let Some(entry) = entries.get_mut(index) {
317 entry.1 = Object::Dict(spec);
318 }
319 write_attachment_entries(edit, entries)
320}
321
322pub fn delete_attachment(edit: &mut EditDoc<'_>, limits: &Limits, index: usize) -> Result<bool> {
329 let mut entries = attachment_entries(edit, limits)?;
330 if index >= entries.len() {
331 return Ok(false);
332 }
333 entries.remove(index);
334 write_attachment_entries(edit, entries)?;
335 Ok(true)
336}
337
338pub fn add_attachment(
374 edit: &mut EditDoc<'_>,
375 limits: &Limits,
376 name: &str,
377 bytes: &[u8],
378 options: &AttachmentOptions,
379) -> Result<usize> {
380 let mut entries = attachment_entries(edit, limits)?;
381 let stream_ref = edit.add(Object::Stream(Box::new(embedded_file(
382 bytes,
383 options.mime_type.as_deref(),
384 options.modified.as_deref(),
385 ))));
386 let mut spec = Dict::new();
387 spec.insert(Name::from("Type"), Object::Name(Name::from("Filespec")));
388 spec.insert(
389 Name::from("UF"),
390 Object::Str(PdfString::literal(encode_text(name))),
391 );
392 spec.insert(
393 Name::from("F"),
394 Object::Str(PdfString::literal(encode_text(name))),
395 );
396 if let Some(description) = options.description.as_deref().filter(|d| !d.is_empty()) {
397 spec.insert(
398 Name::from("Desc"),
399 Object::Str(PdfString::literal(encode_text(description))),
400 );
401 }
402 let mut ef = Dict::new();
403 ef.insert(Name::from("F"), Object::Ref(stream_ref));
404 spec.insert(Name::from("EF"), Object::Dict(ef));
405 let reference = edit.add(Object::Dict(spec));
406 let index = entries
407 .iter()
408 .position(|(existing, _)| existing.as_str() > name)
409 .unwrap_or(entries.len());
410 entries.insert(index, (name.to_owned(), Object::Ref(reference)));
411 write_attachment_entries(edit, entries)?;
412 Ok(index)
413}
414
415pub fn remove_attachment(edit: &mut EditDoc<'_>, limits: &Limits, name: &str) -> Result<bool> {
433 let mut entries = attachment_entries(edit, limits)?;
434 let before = entries.len();
435 entries.retain(|(existing, _)| existing != name);
436 if entries.len() == before {
437 return Ok(false);
438 }
439 write_attachment_entries(edit, entries)?;
440 Ok(true)
441}
442
443pub fn set_attachment_file(
453 edit: &mut EditDoc<'_>,
454 limits: &Limits,
455 index: usize,
456 bytes: &[u8],
457) -> Result<bool> {
458 let Some((reference, mut spec)) = attachment_spec(edit, limits, index)? else {
459 return Ok(false);
460 };
461 let stream_ref = edit.add(Object::Stream(Box::new(embedded_file(bytes, None, None))));
462 let mut ef = Dict::new();
463 ef.insert(Name::from("F"), Object::Ref(stream_ref));
464 spec.insert(Name::from("EF"), Object::Dict(ef));
465 store_attachment_spec(edit, limits, index, reference, spec)?;
466 Ok(true)
467}
468
469pub fn set_attachment_param(
478 edit: &mut EditDoc<'_>,
479 limits: &Limits,
480 index: usize,
481 key: &str,
482 text: &str,
483) -> Result<bool> {
484 let Some((_, spec)) = attachment_spec(edit, limits, index)? else {
485 return Ok(false);
486 };
487 let Some(ef) = spec.dict(&Name::from("EF"), edit) else {
488 return Ok(false);
489 };
490 let Some(stream_ref) = ef.reference(&Name::from("F")) else {
491 return Ok(false);
492 };
493 let Some(stream) = edit
494 .fetch(stream_ref)
495 .ok()
496 .and_then(|object| object.as_stream().cloned())
497 else {
498 return Ok(false);
499 };
500 let mut params = stream
501 .dict
502 .dict(&Name::from("Params"), edit)
503 .unwrap_or_default();
504 let hex = if key == "CheckSum" {
505 hex_bytes(text)
506 } else {
507 None
508 };
509 let value = hex.map_or_else(
510 || Object::Str(PdfString::literal(encode_text(text))),
511 |bytes| Object::Str(PdfString::hex(bytes)),
512 );
513 params.insert(Name::from(key), value);
514 let mut dict = stream.dict.clone();
515 dict.insert(Name::from("Params"), Object::Dict(params));
516 edit.replace(
517 stream_ref,
518 Object::Stream(Box::new(Stream::new(dict, stream.data.clone()))),
519 );
520 Ok(true)
521}
522
523pub fn set_attachment_description(
530 edit: &mut EditDoc<'_>,
531 limits: &Limits,
532 index: usize,
533 text: &str,
534) -> Result<bool> {
535 let Some((reference, mut spec)) = attachment_spec(edit, limits, index)? else {
536 return Ok(false);
537 };
538 spec.insert(
539 Name::from("Desc"),
540 Object::Str(PdfString::literal(encode_text(text))),
541 );
542 store_attachment_spec(edit, limits, index, reference, spec)?;
543 Ok(true)
544}
545
546fn hex_bytes(text: &str) -> Option<Vec<u8>> {
549 let mut digits = Vec::new();
550 for c in text.strip_prefix('<')?.strip_suffix('>')?.chars() {
551 if c.is_ascii_whitespace() {
552 continue;
553 }
554 digits.push(u8::try_from(c.to_digit(16)?).ok()?);
555 }
556 if !digits.len().is_multiple_of(2) {
557 return None;
558 }
559 Some(
560 digits
561 .as_chunks::<2>()
562 .0
563 .iter()
564 .map(|&[hi, lo]| (hi << 4) | lo)
565 .collect(),
566 )
567}