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 raw_attachment_entries(
202 edit: &EditDoc<'_>,
203 limits: &Limits,
204) -> Result<Vec<(String, Object)>> {
205 let named = attachment_entries(edit, limits)?;
206 let mut out = Vec::with_capacity(named.len());
207 for (index, (name, resolved)) in named.into_iter().enumerate() {
208 let value = match attachment_spec_ref(edit, limits, index)? {
209 Some(reference) => Object::Ref(reference),
210 None => resolved,
211 };
212 out.push((name, value));
213 }
214 Ok(out)
215}
216
217pub(crate) fn attachment_spec_ref(
229 edit: &EditDoc<'_>,
230 limits: &Limits,
231 index: usize,
232) -> Result<Option<ObjRef>> {
233 let Some(root) = edit.base().trailer().reference(&Name::from("Root")) else {
234 return Err(Error::NoDestinationCatalog);
235 };
236 let Ok(catalog) = edit.fetch(root) else {
237 return Ok(None);
238 };
239 let Some(files) = catalog
240 .as_dict()
241 .and_then(|catalog| catalog.dict(&Name::from("Names"), edit))
242 .and_then(|names| names.dict(&Name::from("EmbeddedFiles"), edit))
243 else {
244 return Ok(None);
245 };
246 let mut cursor = 0;
247 Ok(raw_entry_at(edit, limits, &files, index, &mut cursor, 0))
248}
249
250fn raw_entry_at(
252 edit: &EditDoc<'_>,
253 limits: &Limits,
254 node: &Dict,
255 target: usize,
256 cursor: &mut usize,
257 depth: u32,
258) -> Option<ObjRef> {
259 if depth > limits.max_name_tree_depth {
260 return None;
261 }
262 if let Some(leaf) = node.array(&Name::from("Names"), edit) {
263 let count = leaf.len() / 2;
264 if target >= *cursor + count {
265 *cursor += count;
266 return None;
267 }
268 let slot = (target - *cursor) * 2;
269 return leaf.reference_at(slot + 1);
270 }
271 let kids = node.array(&Name::from("Kids"), edit)?;
272 for slot in 0..kids.len() {
273 let Some(kid) = kids.dict_at(slot, edit) else {
274 continue;
275 };
276 if let Some(found) = raw_entry_at(edit, limits, &kid, target, cursor, depth + 1) {
277 return Some(found);
278 }
279 }
280 None
281}
282
283pub(crate) fn attachment_spec(
287 edit: &EditDoc<'_>,
288 limits: &Limits,
289 index: usize,
290) -> Result<Option<(Option<ObjRef>, Dict)>> {
291 let entries = raw_attachment_entries(edit, limits)?;
292 let Some(entry) = entries.get(index) else {
293 return Ok(None);
294 };
295 Ok(match &entry.1 {
296 Object::Ref(reference) => {
297 let Ok(object) = edit.fetch(*reference) else {
298 return Ok(None);
299 };
300 object
301 .as_dict()
302 .map(|dict| (Some(*reference), dict.clone()))
303 }
304 Object::Dict(dict) => Some((None, dict.clone())),
305 _ => None,
306 })
307}
308
309pub(crate) fn write_attachment_entries(
315 edit: &mut EditDoc<'_>,
316 entries: Vec<(String, Object)>,
317) -> Result<()> {
318 let k_root = Name::from("Root");
319 let k_names = Name::from("Names");
320 let k_ef = Name::from("EmbeddedFiles");
321 let k_kids = Name::from("Kids");
322 let k_limits = Name::from("Limits");
323 let Some(root) = edit.base().trailer().reference(&k_root) else {
324 return Err(Error::NoDestinationCatalog);
325 };
326 let dict_at = |edit: &EditDoc<'_>, reference: ObjRef| {
327 edit.fetch(reference)
328 .ok()
329 .as_deref()
330 .and_then(Object::as_dict)
331 .cloned()
332 };
333 let mut catalog = dict_at(edit, root).unwrap_or_default();
334 let mut names_array = Array::new();
335 for (name, value) in entries {
336 names_array.push(Object::Str(PdfString::literal(encode_text(&name))));
337 names_array.push(value);
338 }
339 let tree_of = |existing: Option<Dict>| {
340 let mut tree = existing.unwrap_or_default();
341 tree.remove(&k_kids);
342 tree.remove(&k_limits);
343 tree.insert(k_names.clone(), Object::Array(names_array.clone()));
344 tree
345 };
346 match catalog.raw(&k_names).cloned() {
347 Some(Object::Ref(names_ref)) => {
348 let mut names = dict_at(edit, names_ref).unwrap_or_default();
349 match names.raw(&k_ef).cloned() {
350 Some(Object::Ref(tree_ref)) => {
351 let existing = dict_at(edit, tree_ref);
352 edit.replace(tree_ref, Object::Dict(tree_of(existing)));
353 }
354 Some(Object::Dict(inline)) => {
355 names.insert(k_ef.clone(), Object::Dict(tree_of(Some(inline))));
356 edit.replace(names_ref, Object::Dict(names));
357 }
358 _ => {
359 let tree_ref = edit.add(Object::Dict(tree_of(None)));
360 names.insert(k_ef.clone(), Object::Ref(tree_ref));
361 edit.replace(names_ref, Object::Dict(names));
362 }
363 }
364 }
365 Some(Object::Dict(mut names)) => match names.raw(&k_ef).cloned() {
366 Some(Object::Ref(tree_ref)) => {
367 let existing = dict_at(edit, tree_ref);
368 edit.replace(tree_ref, Object::Dict(tree_of(existing)));
369 }
370 Some(Object::Dict(inline)) => {
371 names.insert(k_ef.clone(), Object::Dict(tree_of(Some(inline))));
372 catalog.insert(k_names.clone(), Object::Dict(names));
373 edit.replace(root, Object::Dict(catalog));
374 }
375 _ => {
376 let tree_ref = edit.add(Object::Dict(tree_of(None)));
377 names.insert(k_ef.clone(), Object::Ref(tree_ref));
378 catalog.insert(k_names.clone(), Object::Dict(names));
379 edit.replace(root, Object::Dict(catalog));
380 }
381 },
382 _ => {
383 let tree_ref = edit.add(Object::Dict(tree_of(None)));
384 let mut names = Dict::new();
385 names.insert(k_ef.clone(), Object::Ref(tree_ref));
386 let names_ref = edit.add(Object::Dict(names));
387 catalog.insert(k_names.clone(), Object::Ref(names_ref));
388 edit.replace(root, Object::Dict(catalog));
389 }
390 }
391 Ok(())
392}
393
394pub(crate) fn store_attachment_spec(
398 edit: &mut EditDoc<'_>,
399 limits: &Limits,
400 index: usize,
401 reference: Option<ObjRef>,
402 spec: Dict,
403) -> Result<()> {
404 if let Some(reference) = reference {
405 edit.replace(reference, Object::Dict(spec));
406 return Ok(());
407 }
408 let mut entries = raw_attachment_entries(edit, limits)?;
409 if let Some(entry) = entries.get_mut(index) {
410 entry.1 = Object::Dict(spec);
411 }
412 write_attachment_entries(edit, entries)
413}
414
415pub fn delete_attachment(edit: &mut EditDoc<'_>, limits: &Limits, index: usize) -> Result<bool> {
422 let mut entries = raw_attachment_entries(edit, limits)?;
423 if index >= entries.len() {
424 return Ok(false);
425 }
426 entries.remove(index);
427 write_attachment_entries(edit, entries)?;
428 Ok(true)
429}
430
431pub fn add_attachment(
467 edit: &mut EditDoc<'_>,
468 limits: &Limits,
469 name: &str,
470 bytes: &[u8],
471 options: &AttachmentOptions,
472) -> Result<usize> {
473 let mut entries = raw_attachment_entries(edit, limits)?;
474 let stream_ref = edit.add(Object::Stream(Box::new(embedded_file(
475 bytes,
476 options.mime_type.as_deref(),
477 options.modified.as_deref(),
478 ))));
479 let mut spec = Dict::new();
480 spec.insert(Name::from("Type"), Object::Name(Name::from("Filespec")));
481 spec.insert(
482 Name::from("UF"),
483 Object::Str(PdfString::literal(encode_text(name))),
484 );
485 spec.insert(
486 Name::from("F"),
487 Object::Str(PdfString::literal(encode_text(name))),
488 );
489 if let Some(description) = options.description.as_deref().filter(|d| !d.is_empty()) {
490 spec.insert(
491 Name::from("Desc"),
492 Object::Str(PdfString::literal(encode_text(description))),
493 );
494 }
495 let mut ef = Dict::new();
496 ef.insert(Name::from("F"), Object::Ref(stream_ref));
497 spec.insert(Name::from("EF"), Object::Dict(ef));
498 let reference = edit.add(Object::Dict(spec));
499 let index = entries
500 .iter()
501 .position(|(existing, _)| existing.as_str() > name)
502 .unwrap_or(entries.len());
503 entries.insert(index, (name.to_owned(), Object::Ref(reference)));
504 write_attachment_entries(edit, entries)?;
505 Ok(index)
506}
507
508pub fn remove_attachment(edit: &mut EditDoc<'_>, limits: &Limits, name: &str) -> Result<bool> {
526 let mut entries = raw_attachment_entries(edit, limits)?;
527 let before = entries.len();
528 entries.retain(|(existing, _)| existing != name);
529 if entries.len() == before {
530 return Ok(false);
531 }
532 write_attachment_entries(edit, entries)?;
533 Ok(true)
534}
535
536pub fn set_attachment_file(
546 edit: &mut EditDoc<'_>,
547 limits: &Limits,
548 index: usize,
549 bytes: &[u8],
550) -> Result<bool> {
551 let Some((reference, mut spec)) = attachment_spec(edit, limits, index)? else {
552 return Ok(false);
553 };
554 let stream_ref = edit.add(Object::Stream(Box::new(embedded_file(bytes, None, None))));
555 let mut ef = Dict::new();
556 ef.insert(Name::from("F"), Object::Ref(stream_ref));
557 spec.insert(Name::from("EF"), Object::Dict(ef));
558 store_attachment_spec(edit, limits, index, reference, spec)?;
559 Ok(true)
560}
561
562pub fn set_attachment_file_with(
577 edit: &mut EditDoc<'_>,
578 limits: &Limits,
579 index: usize,
580 bytes: &[u8],
581 options: &AttachmentOptions,
582) -> Result<bool> {
583 let Some((reference, mut spec)) = attachment_spec(edit, limits, index)? else {
584 return Ok(false);
585 };
586 let stream_ref = edit.add(Object::Stream(Box::new(embedded_file(
587 bytes,
588 options.mime_type.as_deref(),
589 options.modified.as_deref(),
590 ))));
591 let mut ef = Dict::new();
592 ef.insert(Name::from("F"), Object::Ref(stream_ref));
593 spec.insert(Name::from("EF"), Object::Dict(ef));
594 if let Some(description) = options.description.as_deref() {
597 spec.insert(
598 Name::from("Desc"),
599 Object::Str(PdfString::literal(encode_text(description))),
600 );
601 }
602 store_attachment_spec(edit, limits, index, reference, spec)?;
603 Ok(true)
604}
605
606pub fn set_attachment_name(
622 edit: &mut EditDoc<'_>,
623 limits: &Limits,
624 index: usize,
625 name: &str,
626) -> Result<bool> {
627 let mut entries = raw_attachment_entries(edit, limits)?;
628 let Some((existing, _)) = entries.get(index) else {
629 return Ok(false);
630 };
631 if existing == name {
632 return Ok(true);
633 }
634 if entries
635 .iter()
636 .enumerate()
637 .any(|(other, (taken, _))| other != index && taken == name)
638 {
639 return Err(Error::DuplicateAttachmentName(name.to_owned()));
640 }
641
642 if let Some((reference, mut spec)) = attachment_spec(edit, limits, index)? {
645 let value = Object::Str(PdfString::literal(encode_text(name)));
646 spec.insert(Name::from("F"), value.clone());
647 spec.insert(Name::from("UF"), value);
648 store_attachment_spec(edit, limits, index, reference, spec)?;
649 entries = raw_attachment_entries(edit, limits)?;
650 }
651
652 if let Some(slot) = entries.get_mut(index) {
653 name.clone_into(&mut slot.0);
654 }
655 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
658 write_attachment_entries(edit, entries)?;
659 Ok(true)
660}
661
662pub fn set_attachment_param(
671 edit: &mut EditDoc<'_>,
672 limits: &Limits,
673 index: usize,
674 key: &str,
675 text: &str,
676) -> Result<bool> {
677 let Some((_, spec)) = attachment_spec(edit, limits, index)? else {
678 return Ok(false);
679 };
680 let Some(ef) = spec.dict(&Name::from("EF"), edit) else {
681 return Ok(false);
682 };
683 let Some(stream_ref) = ef.reference(&Name::from("F")) else {
684 return Ok(false);
685 };
686 let Some(stream) = edit
687 .fetch(stream_ref)
688 .ok()
689 .and_then(|object| object.as_stream().cloned())
690 else {
691 return Ok(false);
692 };
693 let mut params = stream
694 .dict
695 .dict(&Name::from("Params"), edit)
696 .unwrap_or_default();
697 let hex = if key == "CheckSum" {
698 hex_bytes(text)
699 } else {
700 None
701 };
702 let value = hex.map_or_else(
703 || Object::Str(PdfString::literal(encode_text(text))),
704 |bytes| Object::Str(PdfString::hex(bytes)),
705 );
706 params.insert(Name::from(key), value);
707 let mut dict = stream.dict.clone();
708 dict.insert(Name::from("Params"), Object::Dict(params));
709 edit.replace(
710 stream_ref,
711 Object::Stream(Box::new(Stream::new(dict, stream.data.clone()))),
712 );
713 Ok(true)
714}
715
716pub fn set_attachment_description(
723 edit: &mut EditDoc<'_>,
724 limits: &Limits,
725 index: usize,
726 text: &str,
727) -> Result<bool> {
728 let Some((reference, mut spec)) = attachment_spec(edit, limits, index)? else {
729 return Ok(false);
730 };
731 spec.insert(
732 Name::from("Desc"),
733 Object::Str(PdfString::literal(encode_text(text))),
734 );
735 store_attachment_spec(edit, limits, index, reference, spec)?;
736 Ok(true)
737}
738
739fn hex_bytes(text: &str) -> Option<Vec<u8>> {
742 let mut digits = Vec::new();
743 for c in text.strip_prefix('<')?.strip_suffix('>')?.chars() {
744 if c.is_ascii_whitespace() {
745 continue;
746 }
747 digits.push(u8::try_from(c.to_digit(16)?).ok()?);
748 }
749 if !digits.len().is_multiple_of(2) {
750 return None;
751 }
752 Some(
753 digits
754 .as_chunks::<2>()
755 .0
756 .iter()
757 .map(|&[hi, lo]| (hi << 4) | lo)
758 .collect(),
759 )
760}