Skip to main content

rpdfium_edit/
document.rs

1// Derived from PDFium's cpdf_indirect_object_holder.cpp / cpdf_creator.cpp
2// Original: Copyright 2014 The PDFium Authors
3// Licensed under BSD-3-Clause / Apache-2.0
4// See pdfium-upstream/LICENSE for the original license.
5
6//! EditDocument — mutable overlay on top of an immutable ObjectStore.
7//!
8//! Implements the same pattern as PDFium's `CPDF_IndirectObjectHolder`:
9//! modifications are tracked in a separate layer, leaving the base
10//! ObjectStore unchanged.
11
12use std::collections::{HashMap, HashSet};
13use std::sync::Arc;
14
15use rpdfium_core::Name;
16use rpdfium_core::error::PdfError;
17use rpdfium_parser::object::{Object, ObjectId, StreamData};
18use rpdfium_parser::store::ObjectStore;
19use rpdfium_parser::trailer::TrailerInfo;
20
21use crate::cpdf_flateencoder::create_compressed_stream;
22use crate::error::EditError;
23use crate::font_reg::{FontRegistration, FontType};
24use crate::page_object::PageObject;
25
26/// Specification for a new interactive form field.
27#[derive(Debug, Clone)]
28pub struct FormFieldSpec {
29    /// The PDF field type name (`Tx`, `Btn`, or `Ch`).
30    pub field_type: Name,
31    /// The partial field name (`/T`).
32    pub name: String,
33    /// Widget annotation rectangle `[x1, y1, x2, y2]` in page coordinates.
34    pub rect: [f32; 4],
35    /// Page index on which the widget annotation is placed (0-based).
36    pub page_index: usize,
37    /// Optional default value (`/DV`).
38    pub default_value: Option<String>,
39    /// Field flags (`/Ff`).
40    pub flags: u32,
41}
42
43/// A mutable document that layers modifications on top of an immutable
44/// `ObjectStore`.
45///
46/// Resolution order: `deleted` → `modified` → `new_objects` → `base.resolve()`.
47pub struct EditDocument {
48    /// The original immutable object store (may be empty for blank docs).
49    store: Option<ObjectStore<Arc<[u8]>>>,
50    /// Objects from the base store that have been modified.
51    modified: HashMap<ObjectId, Object>,
52    /// Newly created objects.
53    new_objects: HashMap<ObjectId, Object>,
54    /// Object IDs that have been deleted.
55    deleted: HashSet<ObjectId>,
56    /// Next object number for allocation.
57    next_object_number: u32,
58    /// Ordered list of page dictionary object IDs.
59    page_ids: Vec<ObjectId>,
60    /// The catalog object ID.
61    catalog_id: ObjectId,
62    /// Whether any modifications have been made.
63    dirty: bool,
64    /// Page objects owned by each annotation (keyed by annotation ObjectId).
65    ///
66    /// Used to build the annotation's AP stream on `update_annotation_ap()`.
67    pub(crate) ap_objects: HashMap<ObjectId, Vec<PageObject>>,
68}
69
70impl EditDocument {
71    /// Create an `EditDocument` from an existing `ObjectStore`.
72    pub fn from_store(
73        store: ObjectStore<Arc<[u8]>>,
74        page_ids: Vec<ObjectId>,
75        catalog_id: ObjectId,
76    ) -> Self {
77        let next = store.last_obj_num() + 1;
78        Self {
79            store: Some(store),
80            modified: HashMap::new(),
81            new_objects: HashMap::new(),
82            deleted: HashSet::new(),
83            next_object_number: next,
84            page_ids,
85            catalog_id,
86            dirty: false,
87            ap_objects: HashMap::new(),
88        }
89    }
90
91    /// Create a new blank document with minimal Catalog and Pages structure.
92    pub fn new_blank() -> Self {
93        let catalog_id = ObjectId::new(1, 0);
94        let pages_id = ObjectId::new(2, 0);
95
96        let mut pages_dict = HashMap::new();
97        pages_dict.insert(
98            Name::r#type(),
99            Object::Name(Name::from_bytes(b"Pages".to_vec())),
100        );
101        pages_dict.insert(Name::kids(), Object::Array(Vec::new()));
102        pages_dict.insert(Name::count(), Object::Integer(0));
103
104        let mut catalog_dict = HashMap::new();
105        catalog_dict.insert(
106            Name::r#type(),
107            Object::Name(Name::from_bytes(b"Catalog".to_vec())),
108        );
109        catalog_dict.insert(Name::pages(), Object::Reference(pages_id));
110
111        let mut new_objects = HashMap::new();
112        new_objects.insert(catalog_id, Object::Dictionary(catalog_dict));
113        new_objects.insert(pages_id, Object::Dictionary(pages_dict));
114
115        Self {
116            store: None,
117            modified: HashMap::new(),
118            new_objects,
119            deleted: HashSet::new(),
120            next_object_number: 3,
121            page_ids: Vec::new(),
122            catalog_id,
123            dirty: true,
124            ap_objects: HashMap::new(),
125        }
126    }
127
128    /// Resolve an object by ID, checking the overlay layers first.
129    pub fn resolve(&self, id: ObjectId) -> Result<&Object, EditError> {
130        if self.deleted.contains(&id) {
131            return Err(EditError::Parse(PdfError::UnknownObject(id)));
132        }
133        if let Some(obj) = self.modified.get(&id) {
134            return Ok(obj);
135        }
136        if let Some(obj) = self.new_objects.get(&id) {
137            return Ok(obj);
138        }
139        if let Some(store) = &self.store {
140            return Ok(store.resolve(id)?);
141        }
142        Err(EditError::Parse(PdfError::UnknownObject(id)))
143    }
144
145    /// Get a mutable reference to an object, cloning from base on first access.
146    pub fn get_mut(&mut self, id: ObjectId) -> Result<&mut Object, EditError> {
147        if self.deleted.contains(&id) {
148            return Err(EditError::Parse(PdfError::UnknownObject(id)));
149        }
150        self.dirty = true;
151
152        // Already in modified layer
153        if self.modified.contains_key(&id) {
154            return Ok(self.modified.get_mut(&id).unwrap());
155        }
156
157        // In new_objects layer
158        if self.new_objects.contains_key(&id) {
159            return Ok(self.new_objects.get_mut(&id).unwrap());
160        }
161
162        // Clone from base store
163        if let Some(store) = &self.store {
164            let obj = store.resolve(id)?.clone();
165            self.modified.insert(id, obj);
166            return Ok(self.modified.get_mut(&id).unwrap());
167        }
168
169        Err(EditError::Parse(PdfError::UnknownObject(id)))
170    }
171
172    /// Add a new object and return its allocated ID.
173    pub fn add_object(&mut self, obj: Object) -> ObjectId {
174        let id = self.allocate_id();
175        self.new_objects.insert(id, obj);
176        self.dirty = true;
177        id
178    }
179
180    /// Delete an object by ID.
181    pub fn delete_object(&mut self, id: ObjectId) {
182        self.modified.remove(&id);
183        self.new_objects.remove(&id);
184        self.deleted.insert(id);
185        self.dirty = true;
186    }
187
188    /// Set/replace an object at a specific ID.
189    pub fn set_object(&mut self, id: ObjectId, obj: Object) {
190        if self.store.as_ref().is_some_and(|s| s.contains(id)) {
191            self.modified.insert(id, obj);
192        } else {
193            self.new_objects.insert(id, obj);
194        }
195        self.deleted.remove(&id);
196        self.dirty = true;
197    }
198
199    /// Allocate a new unique object ID.
200    pub fn allocate_id(&mut self) -> ObjectId {
201        let id = ObjectId::new(self.next_object_number, 0);
202        self.next_object_number += 1;
203        id
204    }
205
206    /// Iterator over IDs of modified + new objects.
207    pub fn changed_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
208        self.modified.keys().chain(self.new_objects.keys()).copied()
209    }
210
211    /// Iterator over all valid (non-deleted) object IDs.
212    pub fn all_ids(&self) -> Vec<ObjectId> {
213        let mut ids: HashSet<ObjectId> = HashSet::new();
214
215        // Add base store IDs
216        if let Some(store) = &self.store {
217            for id in store.object_ids() {
218                ids.insert(*id);
219            }
220        }
221
222        // Add new/modified IDs
223        for id in self.modified.keys() {
224            ids.insert(*id);
225        }
226        for id in self.new_objects.keys() {
227            ids.insert(*id);
228        }
229
230        // Remove deleted IDs
231        for id in &self.deleted {
232            ids.remove(id);
233        }
234
235        let mut result: Vec<ObjectId> = ids.into_iter().collect();
236        result.sort_by_key(|id| (id.number, id.generation));
237        result
238    }
239
240    /// Whether any modifications have been made.
241    #[inline]
242    pub fn is_dirty(&self) -> bool {
243        self.dirty
244    }
245
246    /// Returns the security handler from the base store, if the document is encrypted.
247    pub fn security_handler(&self) -> Option<&rpdfium_parser::security::SecurityHandler> {
248        self.store.as_ref().and_then(|s| s.security_handler())
249    }
250
251    /// Returns a reference to the base object store, if present.
252    pub fn base_store(&self) -> Option<&ObjectStore<Arc<[u8]>>> {
253        self.store.as_ref()
254    }
255
256    /// Returns the trailer info from the base store.
257    pub fn trailer(&self) -> Option<&TrailerInfo> {
258        self.store.as_ref().map(|s| s.trailer())
259    }
260
261    /// Number of pages in the document.
262    pub fn page_count(&self) -> usize {
263        self.page_ids.len()
264    }
265
266    /// Get the object ID of a page by index.
267    pub fn page_id(&self, index: usize) -> Option<ObjectId> {
268        self.page_ids.get(index).copied()
269    }
270
271    /// The catalog object ID.
272    #[inline]
273    pub fn catalog_id(&self) -> ObjectId {
274        self.catalog_id
275    }
276
277    /// Mutable access to the page_ids list (for page management).
278    pub(crate) fn page_ids_mut(&mut self) -> &mut Vec<ObjectId> {
279        &mut self.page_ids
280    }
281
282    /// The page IDs list.
283    #[inline]
284    pub fn page_ids(&self) -> &[ObjectId] {
285        &self.page_ids
286    }
287
288    /// The set of deleted object IDs.
289    #[inline]
290    pub fn deleted_ids(&self) -> &HashSet<ObjectId> {
291        &self.deleted
292    }
293
294    /// The next object number that will be allocated.
295    #[inline]
296    pub fn next_object_number(&self) -> u32 {
297        self.next_object_number
298    }
299
300    /// Decode a stream from the base store.
301    pub fn decode_stream(&self, stream: &Object) -> Result<Vec<u8>, EditError> {
302        // For Decoded streams, return directly
303        if let Object::Stream {
304            data: StreamData::Decoded { data },
305            ..
306        } = stream
307        {
308            return Ok(data.clone());
309        }
310        // For Raw streams, delegate to base store
311        if let Some(store) = &self.store {
312            Ok(store.decode_stream(stream)?)
313        } else {
314            Err(EditError::Other(
315                "no base store for raw stream decoding".into(),
316            ))
317        }
318    }
319
320    /// Find the Pages dictionary ID from the catalog.
321    pub fn pages_id(&self) -> Result<ObjectId, EditError> {
322        let catalog = self.resolve(self.catalog_id)?;
323        let dict = catalog
324            .as_dict()
325            .ok_or(EditError::Other("catalog is not a dictionary".into()))?;
326        match dict.get(&Name::pages()) {
327            Some(Object::Reference(id)) => Ok(*id),
328            _ => Err(EditError::Other("catalog missing /Pages reference".into())),
329        }
330    }
331
332    /// Register a Standard-14 font in the document.
333    ///
334    /// Creates a minimal `/Font` dictionary with `/Type /Font`, `/Subtype /Type1`,
335    /// and `/BaseFont /<name>`.  The returned [`FontRegistration`] holds the
336    /// object ID of that dictionary and can be used with
337    /// [`TextObject::with_font`](crate::page_object::TextObject::with_font).
338    ///
339    /// Corresponds to `FPDFText_LoadStandardFont`.
340    pub fn load_standard_font(&mut self, name: &str) -> Result<FontRegistration, EditError> {
341        let mut dict = HashMap::new();
342        dict.insert(
343            Name::r#type(),
344            Object::Name(Name::from_bytes(b"Font".to_vec())),
345        );
346        dict.insert(
347            Name::from_bytes(b"Subtype".to_vec()),
348            Object::Name(Name::from_bytes(b"Type1".to_vec())),
349        );
350        dict.insert(
351            Name::from_bytes(b"BaseFont".to_vec()),
352            Object::Name(Name::from_bytes(name.as_bytes().to_vec())),
353        );
354
355        let id = self.add_object(Object::Dictionary(dict));
356        Ok(FontRegistration::new_standard(id, name))
357    }
358
359    /// Embed arbitrary font data in the document.
360    ///
361    /// For TrueType fonts three objects are created:
362    /// 1. A Flate-compressed `/FontFile2` stream containing the raw font bytes.
363    /// 2. A `/FontDescriptor` dictionary pointing to the stream.
364    /// 3. A `/Font` dictionary of the requested subtype.
365    ///
366    /// `is_cid=true` is not yet supported (returns [`EditError::NotSupported`]).
367    ///
368    /// Corresponds to `FPDFText_LoadFont`.
369    pub fn load_font_from_data(
370        &mut self,
371        data: &[u8],
372        font_type: FontType,
373        is_cid: bool,
374    ) -> Result<FontRegistration, EditError> {
375        if is_cid {
376            return Err(EditError::NotSupported(
377                "CID font embedding (is_cid=true) is not yet supported".into(),
378            ));
379        }
380
381        // Choose subtype and FontFile key based on font_type.
382        let (subtype_bytes, font_file_key): (&[u8], &[u8]) = match font_type {
383            FontType::TrueType => (b"TrueType", b"FontFile2"),
384            FontType::Type1 => (b"Type1", b"FontFile"),
385        };
386
387        // 1. Font file stream (Flate-compressed).
388        let mut stream_dict = HashMap::new();
389        stream_dict.insert(
390            Name::from_bytes(b"Subtype".to_vec()),
391            Object::Name(Name::from_bytes(font_file_key.to_vec())),
392        );
393        let font_stream = create_compressed_stream(data, stream_dict);
394        let stream_id = self.add_object(font_stream);
395
396        // 2. FontDescriptor dictionary.
397        let mut desc_dict = HashMap::new();
398        desc_dict.insert(
399            Name::r#type(),
400            Object::Name(Name::from_bytes(b"FontDescriptor".to_vec())),
401        );
402        desc_dict.insert(
403            Name::from_bytes(b"FontName".to_vec()),
404            Object::Name(Name::from_bytes(b"EmbeddedFont".to_vec())),
405        );
406        desc_dict.insert(
407            Name::from_bytes(b"Flags".to_vec()),
408            Object::Integer(4), // Symbolic
409        );
410        desc_dict.insert(
411            Name::from_bytes(font_file_key.to_vec()),
412            Object::Reference(stream_id),
413        );
414        let desc_id = self.add_object(Object::Dictionary(desc_dict));
415
416        // 3. Font dictionary.
417        let mut font_dict = HashMap::new();
418        font_dict.insert(
419            Name::r#type(),
420            Object::Name(Name::from_bytes(b"Font".to_vec())),
421        );
422        font_dict.insert(
423            Name::from_bytes(b"Subtype".to_vec()),
424            Object::Name(Name::from_bytes(subtype_bytes.to_vec())),
425        );
426        font_dict.insert(
427            Name::from_bytes(b"BaseFont".to_vec()),
428            Object::Name(Name::from_bytes(b"EmbeddedFont".to_vec())),
429        );
430        font_dict.insert(
431            Name::from_bytes(b"FontDescriptor".to_vec()),
432            Object::Reference(desc_id),
433        );
434        let font_id = self.add_object(Object::Dictionary(font_dict));
435
436        let mut reg =
437            FontRegistration::new_embedded(font_id, "EmbeddedFont", font_type, data.to_vec());
438        // Populate flags, weight, and italic angle from the FontDescriptor we just created.
439        reg.set_flags(4); // Symbolic
440        reg.set_weight(400); // Regular weight (default)
441        reg.set_italic_angle(0.0); // Default upright; caller may override after loading
442        Ok(reg)
443    }
444
445    /// Upstream-aligned alias for [`load_font_from_data()`](Self::load_font_from_data).
446    ///
447    /// Corresponds to `FPDFText_LoadFont`.
448    #[inline]
449    pub fn text_load_font(
450        &mut self,
451        data: &[u8],
452        font_type: FontType,
453        is_cid: bool,
454    ) -> Result<FontRegistration, EditError> {
455        self.load_font_from_data(data, font_type, is_cid)
456    }
457
458    /// Non-upstream alias — use [`text_load_font()`](Self::text_load_font).
459    #[deprecated(note = "use `text_load_font()` — matches upstream `FPDFText_LoadFont`")]
460    #[inline]
461    pub fn load_font(
462        &mut self,
463        data: &[u8],
464        font_type: FontType,
465        is_cid: bool,
466    ) -> Result<FontRegistration, EditError> {
467        self.load_font_from_data(data, font_type, is_cid)
468    }
469
470    /// Load a CID Type2 (TrueType-based CID-keyed) font for embedding.
471    ///
472    /// # Not Supported
473    ///
474    /// CID Type2 font loading requires direct font embedding with CID-keyed
475    /// glyph subsetting, which is not yet implemented in rpdfium's font layer.
476    ///
477    /// Corresponds to `FPDFText_LoadCidType2Font`.
478    pub fn load_cid_type2_font(
479        &mut self,
480        _font_data: &[u8],
481        _to_unicode_cmap: Option<&[u8]>,
482        _cid_to_gid_map: Option<&[u8]>,
483        _is_cjk: bool,
484    ) -> Result<FontRegistration, EditError> {
485        Err(EditError::NotSupported(
486            "load_cid_type2_font: CID Type2 font embedding not yet implemented".into(),
487        ))
488    }
489
490    /// Returns the rotation of the page at `page_index` in degrees (0, 90, 180, or 270).
491    ///
492    /// Reads the `/Rotate` key from the page dictionary. Defaults to 0 if
493    /// absent. Negative values are normalised via `rem_euclid(360)`.
494    ///
495    /// Corresponds to `FPDFPage_GetRotation`.
496    pub fn page_rotation(&self, page_index: usize) -> Result<u32, EditError> {
497        let page_id = self
498            .page_ids
499            .get(page_index)
500            .copied()
501            .ok_or(EditError::PageOutOfRange {
502                index: page_index,
503                count: self.page_ids.len(),
504            })?;
505        let page_obj = self.resolve(page_id)?;
506        let dict = page_obj
507            .as_dict()
508            .ok_or(EditError::Other("page not a dict".into()))?;
509        let rotate = dict
510            .get(&Name::rotate())
511            .and_then(|o| o.as_i64())
512            .unwrap_or(0);
513        Ok(rotate.rem_euclid(360) as u32)
514    }
515
516    /// Upstream-aligned alias for [`page_rotation()`](Self::page_rotation).
517    ///
518    /// Corresponds to `FPDFPage_GetRotation`.
519    #[inline]
520    pub fn page_get_rotation(&self, page_index: usize) -> Result<u32, EditError> {
521        self.page_rotation(page_index)
522    }
523
524    /// Non-upstream alias — use [`page_get_rotation()`](Self::page_get_rotation).
525    ///
526    /// There is no upstream `FPDFPage_GetPageRotation`; the exact upstream
527    /// function is `FPDFPage_GetRotation` → `page_get_rotation()`.
528    #[deprecated(note = "use `page_get_rotation()` — matches upstream `FPDFPage_GetRotation`")]
529    #[inline]
530    pub fn get_page_rotation(&self, page_index: usize) -> Result<u32, EditError> {
531        self.page_rotation(page_index)
532    }
533
534    /// Non-upstream alias — use [`page_get_rotation()`](Self::page_get_rotation).
535    #[deprecated(note = "use `page_get_rotation()` — matches upstream `FPDFPage_GetRotation`")]
536    #[inline]
537    pub fn get_rotation(&self, page_index: usize) -> Result<u32, EditError> {
538        self.page_rotation(page_index)
539    }
540
541    /// Update the /Kids array and /Count in the Pages dictionary.
542    pub(crate) fn sync_pages_tree(&mut self) -> Result<(), EditError> {
543        let pages_id = self.pages_id()?;
544        // Collect page data before the mutable borrow of self via get_mut.
545        let kids: Vec<Object> = self
546            .page_ids
547            .iter()
548            .map(|id| Object::Reference(*id))
549            .collect();
550        let count = self.page_ids.len() as i64;
551
552        let pages = self.get_mut(pages_id)?;
553        let dict = pages
554            .as_dict_mut()
555            .ok_or(EditError::Other("pages node is not a dictionary".into()))?;
556        dict.insert(Name::kids(), Object::Array(kids));
557        dict.insert(Name::count(), Object::Integer(count));
558        Ok(())
559    }
560
561    /// Add a new form field (with widget annotation) to the document.
562    ///
563    /// Creates both a field dictionary in the AcroForm `/Fields` array and a
564    /// widget annotation on the target page's `/Annots` array.  Returns the
565    /// object ID of the new field dictionary.
566    ///
567    /// If no AcroForm dictionary exists in the catalog it is created.
568    pub fn add_form_field(&mut self, spec: FormFieldSpec) -> Result<ObjectId, EditError> {
569        let count = self.page_count();
570        if spec.page_index >= count {
571            return Err(EditError::PageOutOfRange {
572                index: spec.page_index,
573                count,
574            });
575        }
576
577        // --- Ensure AcroForm exists ---
578        let acroform_id = self.ensure_acroform()?;
579
580        // --- Create widget annotation dict ---
581        let mut widget_dict = HashMap::new();
582        widget_dict.insert(
583            Name::r#type(),
584            Object::Name(Name::from_bytes(b"Annot".to_vec())),
585        );
586        widget_dict.insert(
587            Name::subtype(),
588            Object::Name(Name::from_bytes(b"Widget".to_vec())),
589        );
590        widget_dict.insert(
591            Name::rect(),
592            Object::Array(vec![
593                Object::Real(spec.rect[0] as f64),
594                Object::Real(spec.rect[1] as f64),
595                Object::Real(spec.rect[2] as f64),
596                Object::Real(spec.rect[3] as f64),
597            ]),
598        );
599        let page_id = self.page_ids()[spec.page_index];
600        widget_dict.insert(Name::p(), Object::Reference(page_id));
601        let widget_id = self.add_object(Object::Dictionary(widget_dict));
602
603        // --- Create field dict ---
604        let mut field_dict = HashMap::new();
605        field_dict.insert(Name::ft(), Object::Name(spec.field_type.clone()));
606        field_dict.insert(
607            Name::t(),
608            Object::String(rpdfium_core::PdfString::from_bytes(
609                spec.name.as_bytes().to_vec(),
610            )),
611        );
612        field_dict.insert(
613            Name::rect(),
614            Object::Array(vec![
615                Object::Real(spec.rect[0] as f64),
616                Object::Real(spec.rect[1] as f64),
617                Object::Real(spec.rect[2] as f64),
618                Object::Real(spec.rect[3] as f64),
619            ]),
620        );
621        if spec.flags != 0 {
622            field_dict.insert(Name::ff(), Object::Integer(spec.flags as i64));
623        }
624        if let Some(ref dv) = spec.default_value {
625            field_dict.insert(
626                Name::dv(),
627                Object::String(rpdfium_core::PdfString::from_bytes(dv.as_bytes().to_vec())),
628            );
629        }
630        // Widget is the sole kid
631        field_dict.insert(
632            Name::from_bytes(b"Kids".to_vec()),
633            Object::Array(vec![Object::Reference(widget_id)]),
634        );
635        let field_id = self.add_object(Object::Dictionary(field_dict));
636
637        // --- Append to AcroForm /Fields ---
638        {
639            let acroform = self.get_mut(acroform_id)?;
640            if let Object::Dictionary(dict) = acroform {
641                let fields_key = Name::from_bytes(b"Fields".to_vec());
642                let fields = dict
643                    .entry(fields_key)
644                    .or_insert_with(|| Object::Array(Vec::new()));
645                if let Object::Array(arr) = fields {
646                    arr.push(Object::Reference(field_id));
647                }
648            }
649        }
650
651        // --- Append widget to page /Annots ---
652        {
653            let page = self.get_mut(page_id)?;
654            if let Object::Dictionary(dict) = page {
655                let annots = dict
656                    .entry(Name::annots())
657                    .or_insert_with(|| Object::Array(Vec::new()));
658                if let Object::Array(arr) = annots {
659                    arr.push(Object::Reference(widget_id));
660                }
661            }
662        }
663
664        Ok(field_id)
665    }
666
667    /// Delete a form field (and its widget annotations) from the document.
668    ///
669    /// Finds the field in the AcroForm `/Fields` array by partial name (`/T`),
670    /// removes its widget annotations from their pages' `/Annots` arrays, and
671    /// deletes both the widget and field objects.
672    pub fn delete_form_field(&mut self, field_name: &str) -> Result<(), EditError> {
673        // --- Find field ID from AcroForm /Fields ---
674        let catalog_id = self.catalog_id();
675        let acroform_id = {
676            let catalog = self.resolve(catalog_id)?;
677            let dict = catalog
678                .as_dict()
679                .ok_or(EditError::Other("catalog is not a dict".into()))?;
680            match dict.get(&Name::from_bytes(b"AcroForm".to_vec())) {
681                Some(Object::Reference(id)) => *id,
682                _ => {
683                    return Err(EditError::FieldNotFound(field_name.to_string()));
684                }
685            }
686        };
687
688        let field_id = {
689            let acroform = self.resolve(acroform_id)?;
690            let adict = acroform
691                .as_dict()
692                .ok_or(EditError::Other("AcroForm is not a dict".into()))?;
693            let fields_arr = adict
694                .get(&Name::from_bytes(b"Fields".to_vec()))
695                .and_then(|o| o.as_array())
696                .ok_or(EditError::FieldNotFound(field_name.to_string()))?;
697
698            let mut found_id: Option<ObjectId> = None;
699            for item in fields_arr {
700                if let Object::Reference(fid) = item {
701                    if let Ok(fobj) = self.resolve(*fid) {
702                        if let Some(fdict) = fobj.as_dict() {
703                            let t_name = fdict
704                                .get(&Name::t())
705                                .and_then(|o| o.as_string())
706                                .map(|s| String::from_utf8_lossy(s.as_bytes()).into_owned())
707                                .unwrap_or_default();
708                            if t_name == field_name {
709                                found_id = Some(*fid);
710                                break;
711                            }
712                        }
713                    }
714                }
715            }
716            found_id.ok_or(EditError::FieldNotFound(field_name.to_string()))?
717        };
718
719        // --- Collect widget IDs from /Kids ---
720        let widget_ids: Vec<ObjectId> = {
721            let field_obj = self.resolve(field_id)?;
722            if let Some(fdict) = field_obj.as_dict() {
723                fdict
724                    .get(&Name::from_bytes(b"Kids".to_vec()))
725                    .and_then(|o| o.as_array())
726                    .map(|arr| arr.iter().filter_map(|o| o.as_reference()).collect())
727                    .unwrap_or_default()
728            } else {
729                Vec::new()
730            }
731        };
732
733        // --- Remove widgets from page /Annots ---
734        for widget_id in &widget_ids {
735            // Find which page contains this widget annotation
736            let page_ids_clone: Vec<ObjectId> = self.page_ids.clone();
737            for pid in &page_ids_clone {
738                let page = self.get_mut(*pid)?;
739                if let Object::Dictionary(dict) = page {
740                    if let Some(Object::Array(annots)) = dict.get_mut(&Name::annots()) {
741                        let wid = *widget_id;
742                        annots.retain(|obj| {
743                            if let Object::Reference(id) = obj {
744                                *id != wid
745                            } else {
746                                true
747                            }
748                        });
749                    }
750                }
751            }
752        }
753
754        // --- Delete widget objects ---
755        for widget_id in widget_ids {
756            self.delete_object(widget_id);
757        }
758
759        // --- Remove from AcroForm /Fields ---
760        {
761            let acroform = self.get_mut(acroform_id)?;
762            if let Object::Dictionary(adict) = acroform {
763                if let Some(Object::Array(fields)) =
764                    adict.get_mut(&Name::from_bytes(b"Fields".to_vec()))
765                {
766                    fields.retain(|obj| {
767                        if let Object::Reference(id) = obj {
768                            *id != field_id
769                        } else {
770                            true
771                        }
772                    });
773                }
774            }
775        }
776
777        // --- Delete field object ---
778        self.delete_object(field_id);
779        Ok(())
780    }
781
782    /// Copy the `/ViewerPreferences` dictionary from `src` into this document.
783    ///
784    /// If `src` has no `/ViewerPreferences` entry in its catalog, this is a
785    /// no-op.  Otherwise, the preferences object is cloned into this document
786    /// and linked from the catalog.
787    ///
788    /// Corresponds to `FPDF_CopyViewerPreferences`.
789    pub fn copy_viewer_preferences_from(&mut self, src: &EditDocument) -> Result<(), EditError> {
790        let vp_key = Name::from_bytes(b"ViewerPreferences".to_vec());
791
792        // Resolve ViewerPreferences from src catalog
793        let src_catalog_id = src.catalog_id();
794        let src_catalog = src.resolve(src_catalog_id)?;
795        let src_dict = src_catalog
796            .as_dict()
797            .ok_or(EditError::Other("src catalog not a dict".into()))?;
798
799        let vp_obj = match src_dict.get(&vp_key) {
800            None => return Ok(()), // nothing to copy
801            Some(Object::Reference(id)) => src.resolve(*id)?.clone(),
802            Some(obj) => obj.clone(),
803        };
804
805        // Add the cloned viewer preferences to dest
806        let vp_id = self.add_object(vp_obj);
807
808        // Link from dest catalog
809        let dest_catalog_id = self.catalog_id();
810        let dest_catalog = self.get_mut(dest_catalog_id)?;
811        if let Object::Dictionary(dict) = dest_catalog {
812            dict.insert(vp_key, Object::Reference(vp_id));
813        }
814
815        Ok(())
816    }
817
818    /// Wraps a source page as a Form XObject in this document.
819    ///
820    /// Clones the source page's `/Resources` (deeply) and content bytes into
821    /// `self`, creating a Form XObject stream.  Returns `(xobj_id, (width,
822    /// height))` where the dimensions come from the source page's `/MediaBox`.
823    ///
824    /// The returned `xobj_id` can be passed to
825    /// [`crate::page_object::FormObject::from_xobject`] to place the XObject
826    /// on a page.
827    ///
828    /// Corresponds to `FPDF_NewXObjectFromPage`.
829    pub fn new_xobject_from_page(
830        &mut self,
831        src: &EditDocument,
832        src_page_index: usize,
833    ) -> Result<(rpdfium_parser::object::ObjectId, (f64, f64)), EditError> {
834        crate::cpdf_npagetooneexporter::make_xobject_from_page(src, src_page_index, self)
835    }
836
837    // -----------------------------------------------------------------------
838    // Page box setters (FPDFPage_Set*Box)
839    // -----------------------------------------------------------------------
840
841    /// Set the `/MediaBox` of a page.
842    ///
843    /// Corresponds to `FPDFPage_SetMediaBox`.
844    pub fn set_media_box(
845        &mut self,
846        page_index: usize,
847        rect: rpdfium_core::Rect,
848    ) -> Result<(), EditError> {
849        self.set_page_box(page_index, Name::media_box(), rect)
850    }
851
852    /// Upstream alias for [`EditDocument::set_media_box`].
853    ///
854    /// Tier-2 name for `FPDFPage_SetMediaBox`.
855    #[inline]
856    pub fn page_set_media_box(
857        &mut self,
858        page_index: usize,
859        rect: rpdfium_core::Rect,
860    ) -> Result<(), EditError> {
861        self.set_media_box(page_index, rect)
862    }
863
864    /// Set the `/CropBox` of a page.
865    ///
866    /// Corresponds to `FPDFPage_SetCropBox`.
867    pub fn set_crop_box(
868        &mut self,
869        page_index: usize,
870        rect: rpdfium_core::Rect,
871    ) -> Result<(), EditError> {
872        self.set_page_box(page_index, Name::crop_box(), rect)
873    }
874
875    /// Upstream alias for [`EditDocument::set_crop_box`].
876    ///
877    /// Tier-2 name for `FPDFPage_SetCropBox`.
878    #[inline]
879    pub fn page_set_crop_box(
880        &mut self,
881        page_index: usize,
882        rect: rpdfium_core::Rect,
883    ) -> Result<(), EditError> {
884        self.set_crop_box(page_index, rect)
885    }
886
887    /// Set the `/BleedBox` of a page.
888    ///
889    /// Corresponds to `FPDFPage_SetBleedBox`.
890    pub fn set_bleed_box(
891        &mut self,
892        page_index: usize,
893        rect: rpdfium_core::Rect,
894    ) -> Result<(), EditError> {
895        self.set_page_box(page_index, Name::bleed_box(), rect)
896    }
897
898    /// Upstream alias for [`EditDocument::set_bleed_box`].
899    ///
900    /// Tier-2 name for `FPDFPage_SetBleedBox`.
901    #[inline]
902    pub fn page_set_bleed_box(
903        &mut self,
904        page_index: usize,
905        rect: rpdfium_core::Rect,
906    ) -> Result<(), EditError> {
907        self.set_bleed_box(page_index, rect)
908    }
909
910    /// Set the `/TrimBox` of a page.
911    ///
912    /// Corresponds to `FPDFPage_SetTrimBox`.
913    pub fn set_trim_box(
914        &mut self,
915        page_index: usize,
916        rect: rpdfium_core::Rect,
917    ) -> Result<(), EditError> {
918        self.set_page_box(page_index, Name::trim_box(), rect)
919    }
920
921    /// Upstream alias for [`EditDocument::set_trim_box`].
922    ///
923    /// Tier-2 name for `FPDFPage_SetTrimBox`.
924    #[inline]
925    pub fn page_set_trim_box(
926        &mut self,
927        page_index: usize,
928        rect: rpdfium_core::Rect,
929    ) -> Result<(), EditError> {
930        self.set_trim_box(page_index, rect)
931    }
932
933    /// Set the `/ArtBox` of a page.
934    ///
935    /// Corresponds to `FPDFPage_SetArtBox`.
936    pub fn set_art_box(
937        &mut self,
938        page_index: usize,
939        rect: rpdfium_core::Rect,
940    ) -> Result<(), EditError> {
941        self.set_page_box(page_index, Name::art_box(), rect)
942    }
943
944    /// Upstream alias for [`EditDocument::set_art_box`].
945    ///
946    /// Tier-2 name for `FPDFPage_SetArtBox`.
947    #[inline]
948    pub fn page_set_art_box(
949        &mut self,
950        page_index: usize,
951        rect: rpdfium_core::Rect,
952    ) -> Result<(), EditError> {
953        self.set_art_box(page_index, rect)
954    }
955
956    /// Helper: write a named box key on the page dict as a four-element array.
957    fn set_page_box(
958        &mut self,
959        page_index: usize,
960        key: Name,
961        rect: rpdfium_core::Rect,
962    ) -> Result<(), EditError> {
963        let page_id = self
964            .page_ids
965            .get(page_index)
966            .copied()
967            .ok_or(EditError::PageOutOfRange {
968                index: page_index,
969                count: self.page_ids.len(),
970            })?;
971        let page = self.get_mut(page_id)?;
972        let dict = page
973            .as_dict_mut()
974            .ok_or(EditError::Other("page is not a dict".into()))?;
975        dict.insert(
976            key,
977            Object::Array(vec![
978                Object::Real(rect.left),
979                Object::Real(rect.bottom),
980                Object::Real(rect.right),
981                Object::Real(rect.top),
982            ]),
983        );
984        Ok(())
985    }
986
987    // -----------------------------------------------------------------------
988    // Attachment mutation (FPDFDoc_AddAttachment / FPDFDoc_DeleteAttachment)
989    // -----------------------------------------------------------------------
990
991    /// Add a new empty file attachment to the document.
992    ///
993    /// Creates a `/Filespec` dictionary with the given name and an empty
994    /// embedded file stream, then inserts it into the `/Names/EmbeddedFiles`
995    /// name tree.  Returns the index of the new attachment in the name tree.
996    ///
997    /// Corresponds to `FPDFDoc_AddAttachment`.
998    pub fn add_attachment(&mut self, name: &str) -> Result<usize, EditError> {
999        // Create empty embedded file stream
1000        let mut stream_dict = HashMap::new();
1001        stream_dict.insert(
1002            Name::r#type(),
1003            Object::Name(Name::from_bytes(b"EmbeddedFile".to_vec())),
1004        );
1005        let stream = Object::Stream {
1006            dict: stream_dict,
1007            data: rpdfium_parser::object::StreamData::Decoded { data: Vec::new() },
1008        };
1009        let stream_id = self.add_object(stream);
1010
1011        // Create EF sub-dict
1012        let mut ef_dict = HashMap::new();
1013        ef_dict.insert(Name::f(), Object::Reference(stream_id));
1014
1015        // Create Filespec dict
1016        let mut fs_dict = HashMap::new();
1017        fs_dict.insert(Name::r#type(), Object::Name(Name::file_spec_type()));
1018        fs_dict.insert(
1019            Name::uf(),
1020            Object::String(rpdfium_core::PdfString::from_bytes(
1021                name.as_bytes().to_vec(),
1022            )),
1023        );
1024        fs_dict.insert(
1025            Name::f(),
1026            Object::String(rpdfium_core::PdfString::from_bytes(
1027                name.as_bytes().to_vec(),
1028            )),
1029        );
1030        fs_dict.insert(Name::ef(), Object::Dictionary(ef_dict));
1031        let fs_id = self.add_object(Object::Dictionary(fs_dict));
1032
1033        // Ensure /Names/EmbeddedFiles exists and add entry
1034        let ef_names_id = self.ensure_embedded_files()?;
1035        let ef_names = self.get_mut(ef_names_id)?;
1036        let dict = ef_names
1037            .as_dict_mut()
1038            .ok_or(EditError::Other("EmbeddedFiles is not a dict".into()))?;
1039        let names_key = Name::names();
1040        let names_arr = dict
1041            .entry(names_key)
1042            .or_insert_with(|| Object::Array(Vec::new()));
1043        if let Object::Array(arr) = names_arr {
1044            let index = arr.len() / 2;
1045            arr.push(Object::String(rpdfium_core::PdfString::from_bytes(
1046                name.as_bytes().to_vec(),
1047            )));
1048            arr.push(Object::Reference(fs_id));
1049            Ok(index)
1050        } else {
1051            Err(EditError::Other(
1052                "EmbeddedFiles/Names is not an array".into(),
1053            ))
1054        }
1055    }
1056
1057    /// Non-upstream alias — use [`add_attachment()`](Self::add_attachment).
1058    ///
1059    /// There is no upstream `FPDFDoc_AddDocAttachment`; the exact upstream
1060    /// function is `FPDFDoc_AddAttachment` → `add_attachment()`.
1061    #[deprecated(note = "use `add_attachment()` — matches upstream FPDFDoc_AddAttachment")]
1062    #[inline]
1063    pub fn add_doc_attachment(&mut self, name: &str) -> Result<usize, EditError> {
1064        self.add_attachment(name)
1065    }
1066
1067    /// Delete an attachment by index from the `/Names/EmbeddedFiles` name tree.
1068    ///
1069    /// Corresponds to `FPDFDoc_DeleteAttachment`.
1070    pub fn delete_attachment(&mut self, index: usize) -> Result<(), EditError> {
1071        let ef_names_id = self.find_embedded_files_id()?;
1072        let ef_names = self.get_mut(ef_names_id)?;
1073        let dict = ef_names
1074            .as_dict_mut()
1075            .ok_or(EditError::Other("EmbeddedFiles is not a dict".into()))?;
1076        let names_arr = dict
1077            .get_mut(&Name::names())
1078            .and_then(|o| {
1079                if let Object::Array(arr) = o {
1080                    Some(arr)
1081                } else {
1082                    None
1083                }
1084            })
1085            .ok_or(EditError::Other(
1086                "EmbeddedFiles/Names is not an array".into(),
1087            ))?;
1088
1089        // Each entry is a name-value pair: [name0, ref0, name1, ref1, ...]
1090        let pair_start = index * 2;
1091        if pair_start + 1 >= names_arr.len() {
1092            return Err(EditError::Other(format!(
1093                "attachment index {index} out of range (have {} attachments)",
1094                names_arr.len() / 2
1095            )));
1096        }
1097
1098        // Delete the referenced Filespec object
1099        if let Object::Reference(fs_id) = names_arr[pair_start + 1] {
1100            self.delete_object(fs_id);
1101        }
1102
1103        // Re-borrow to remove from the array
1104        let ef_names = self.get_mut(ef_names_id)?;
1105        let dict = ef_names.as_dict_mut().unwrap();
1106        if let Some(Object::Array(arr)) = dict.get_mut(&Name::names()) {
1107            let pair_start = index * 2;
1108            if pair_start + 1 < arr.len() {
1109                arr.remove(pair_start + 1);
1110                arr.remove(pair_start);
1111            }
1112        }
1113        Ok(())
1114    }
1115
1116    /// Non-upstream alias — use [`delete_attachment()`](Self::delete_attachment).
1117    ///
1118    /// There is no upstream `FPDFDoc_DeleteDocAttachment`; the exact upstream
1119    /// function is `FPDFDoc_DeleteAttachment` → `delete_attachment()`.
1120    #[deprecated(note = "use `delete_attachment()` — matches upstream FPDFDoc_DeleteAttachment")]
1121    #[inline]
1122    pub fn delete_doc_attachment(&mut self, index: usize) -> Result<(), EditError> {
1123        self.delete_attachment(index)
1124    }
1125
1126    /// Returns `true` if the attachment at `index` has the given key in its
1127    /// `/Filespec` dictionary.
1128    ///
1129    /// Corresponds to `FPDFAttachment_HasKey`.
1130    pub fn attachment_has_key(&self, index: usize, key: &str) -> Result<bool, EditError> {
1131        let fs_dict = self.resolve_attachment_dict(index)?;
1132        Ok(fs_dict.contains_key(&Name::from_bytes(key.as_bytes().to_vec())))
1133    }
1134
1135    /// Non-upstream alias — use [`attachment_has_key()`](Self::attachment_has_key).
1136    #[deprecated(note = "use `attachment_has_key()` — matches upstream `FPDFAttachment_HasKey`")]
1137    #[inline]
1138    pub fn has_key(&self, index: usize, key: &str) -> Result<bool, EditError> {
1139        self.attachment_has_key(index, key)
1140    }
1141
1142    /// Returns the PDF object type name for a key in the attachment's
1143    /// `/Filespec` dictionary, or `None` if the key is absent.
1144    ///
1145    /// Type names: `"boolean"`, `"number"`, `"string"`, `"name"`, `"array"`,
1146    /// `"dictionary"`, `"stream"`, `"reference"`.
1147    ///
1148    /// Corresponds to `FPDFAttachment_GetValueType`.
1149    pub fn attachment_value_type(
1150        &self,
1151        index: usize,
1152        key: &str,
1153    ) -> Result<Option<&'static str>, EditError> {
1154        let fs_dict = self.resolve_attachment_dict(index)?;
1155        let name_key = Name::from_bytes(key.as_bytes().to_vec());
1156        match fs_dict.get(&name_key) {
1157            None => Ok(None),
1158            Some(obj) => Ok(Some(object_type_name(obj))),
1159        }
1160    }
1161
1162    /// Upstream-aligned alias for [`attachment_value_type()`](Self::attachment_value_type).
1163    ///
1164    /// Corresponds to `FPDFAttachment_GetValueType`.
1165    #[inline]
1166    pub fn attachment_get_value_type(
1167        &self,
1168        index: usize,
1169        key: &str,
1170    ) -> Result<Option<&'static str>, EditError> {
1171        self.attachment_value_type(index, key)
1172    }
1173
1174    /// Non-upstream alias — use [`attachment_get_value_type()`](Self::attachment_get_value_type).
1175    #[deprecated(
1176        note = "use `attachment_get_value_type()` — matches upstream `FPDFAttachment_GetValueType`"
1177    )]
1178    #[inline]
1179    pub fn get_value_type(
1180        &self,
1181        index: usize,
1182        key: &str,
1183    ) -> Result<Option<&'static str>, EditError> {
1184        self.attachment_value_type(index, key)
1185    }
1186
1187    /// Non-upstream alias — use [`attachment_get_value_type()`](Self::attachment_get_value_type).
1188    ///
1189    /// There is no upstream `FPDFAttachment_GetAttachmentValueType`; the exact
1190    /// upstream function is `FPDFAttachment_GetValueType` → `attachment_get_value_type()`.
1191    #[deprecated(
1192        note = "use `attachment_get_value_type()` — matches upstream `FPDFAttachment_GetValueType`"
1193    )]
1194    #[inline]
1195    pub fn get_attachment_value_type(
1196        &self,
1197        index: usize,
1198        key: &str,
1199    ) -> Result<Option<&'static str>, EditError> {
1200        self.attachment_value_type(index, key)
1201    }
1202
1203    /// Set a string value on the attachment's `/Filespec` dictionary.
1204    ///
1205    /// Corresponds to `FPDFAttachment_SetStringValue`.
1206    pub fn set_attachment_string(
1207        &mut self,
1208        index: usize,
1209        key: &str,
1210        value: &str,
1211    ) -> Result<(), EditError> {
1212        let fs_id = self.resolve_attachment_object_id(index)?;
1213        let fs_obj = self.get_mut(fs_id)?;
1214        let dict = fs_obj
1215            .as_dict_mut()
1216            .ok_or(EditError::Other("filespec is not a dict".into()))?;
1217        dict.insert(
1218            Name::from_bytes(key.as_bytes().to_vec()),
1219            Object::String(rpdfium_core::PdfString::from_bytes(
1220                value.as_bytes().to_vec(),
1221            )),
1222        );
1223        Ok(())
1224    }
1225
1226    /// Upstream-aligned alias for [`set_attachment_string()`](Self::set_attachment_string).
1227    ///
1228    /// Corresponds to `FPDFAttachment_SetStringValue`.
1229    #[inline]
1230    pub fn attachment_set_string_value(
1231        &mut self,
1232        index: usize,
1233        key: &str,
1234        value: &str,
1235    ) -> Result<(), EditError> {
1236        self.set_attachment_string(index, key, value)
1237    }
1238
1239    /// Non-upstream alias — use [`attachment_set_string_value()`](Self::attachment_set_string_value).
1240    #[deprecated(
1241        note = "use `attachment_set_string_value()` — matches upstream `FPDFAttachment_SetStringValue`"
1242    )]
1243    #[inline]
1244    pub fn set_string_value(
1245        &mut self,
1246        index: usize,
1247        key: &str,
1248        value: &str,
1249    ) -> Result<(), EditError> {
1250        self.set_attachment_string(index, key, value)
1251    }
1252
1253    /// Get a string value from the attachment's `/Filespec` dictionary.
1254    ///
1255    /// Corresponds to `FPDFAttachment_GetStringValue`.
1256    pub fn attachment_string(&self, index: usize, key: &str) -> Result<Option<String>, EditError> {
1257        let fs_dict = self.resolve_attachment_dict(index)?;
1258        let name_key = Name::from_bytes(key.as_bytes().to_vec());
1259        Ok(fs_dict
1260            .get(&name_key)
1261            .and_then(|o| o.as_string())
1262            .map(|s| s.to_string_lossy()))
1263    }
1264
1265    /// Upstream-aligned alias for [`attachment_string()`](Self::attachment_string).
1266    ///
1267    /// Corresponds to `FPDFAttachment_GetStringValue`.
1268    #[inline]
1269    pub fn attachment_get_string_value(
1270        &self,
1271        index: usize,
1272        key: &str,
1273    ) -> Result<Option<String>, EditError> {
1274        self.attachment_string(index, key)
1275    }
1276
1277    /// Non-upstream alias — use [`attachment_get_string_value()`](Self::attachment_get_string_value).
1278    #[deprecated(
1279        note = "use `attachment_get_string_value()` — matches upstream `FPDFAttachment_GetStringValue`"
1280    )]
1281    #[inline]
1282    pub fn get_string_value(&self, index: usize, key: &str) -> Result<Option<String>, EditError> {
1283        self.attachment_string(index, key)
1284    }
1285
1286    /// Non-upstream alias — use [`attachment_get_string_value()`](Self::attachment_get_string_value).
1287    ///
1288    /// There is no upstream `FPDFAttachment_GetAttachmentString`; the exact
1289    /// upstream function is `FPDFAttachment_GetStringValue` → `attachment_get_string_value()`.
1290    #[deprecated(
1291        note = "use `attachment_get_string_value()` — matches upstream `FPDFAttachment_GetStringValue`"
1292    )]
1293    #[inline]
1294    pub fn get_attachment_string(
1295        &self,
1296        index: usize,
1297        key: &str,
1298    ) -> Result<Option<String>, EditError> {
1299        self.attachment_string(index, key)
1300    }
1301
1302    /// Set the content data for an attachment's embedded file stream.
1303    ///
1304    /// Writes raw bytes into the `/EF /F` stream of the attachment at `index`.
1305    ///
1306    /// Corresponds to `FPDFAttachment_SetFile`.
1307    pub fn set_attachment_file(&mut self, index: usize, data: &[u8]) -> Result<(), EditError> {
1308        // Get the EF/F stream reference from the Filespec dict
1309        let stream_id = {
1310            let fs_dict = self.resolve_attachment_dict(index)?;
1311            let ef_obj = fs_dict
1312                .get(&Name::ef())
1313                .ok_or(EditError::Other("filespec missing /EF".into()))?;
1314            let ef_dict = ef_obj
1315                .as_dict()
1316                .ok_or(EditError::Other("/EF is not a dict".into()))?;
1317            ef_dict
1318                .get(&Name::f())
1319                .and_then(|o| o.as_reference())
1320                .ok_or(EditError::Other("/EF/F is not a reference".into()))?
1321        };
1322
1323        // Replace stream content
1324        let mut stream_dict = HashMap::new();
1325        stream_dict.insert(
1326            Name::r#type(),
1327            Object::Name(Name::from_bytes(b"EmbeddedFile".to_vec())),
1328        );
1329        let stream = Object::Stream {
1330            dict: stream_dict,
1331            data: rpdfium_parser::object::StreamData::Decoded {
1332                data: data.to_vec(),
1333            },
1334        };
1335        self.set_object(stream_id, stream);
1336        Ok(())
1337    }
1338
1339    /// Upstream-aligned alias for [`set_attachment_file()`](Self::set_attachment_file).
1340    ///
1341    /// Corresponds to `FPDFAttachment_SetFile`.
1342    #[inline]
1343    pub fn attachment_set_file(&mut self, index: usize, data: &[u8]) -> Result<(), EditError> {
1344        self.set_attachment_file(index, data)
1345    }
1346
1347    /// Non-upstream alias — use [`attachment_set_file()`](Self::attachment_set_file).
1348    #[deprecated(note = "use `attachment_set_file()` — matches upstream `FPDFAttachment_SetFile`")]
1349    #[inline]
1350    pub fn set_file(&mut self, index: usize, data: &[u8]) -> Result<(), EditError> {
1351        self.set_attachment_file(index, data)
1352    }
1353
1354    /// Return the raw content bytes of the embedded file stream at `index`.
1355    ///
1356    /// Reads the `/EF /F` stream from the attachment's `/Filespec` dictionary
1357    /// and returns the decoded stream bytes.  Returns `Ok(None)` if the
1358    /// attachment exists but has no embedded file stream.
1359    ///
1360    /// Corresponds to `FPDFAttachment_GetFile`.
1361    pub fn attachment_file(&self, index: usize) -> Result<Option<Vec<u8>>, EditError> {
1362        let fs_dict = self.resolve_attachment_dict(index)?;
1363        let ef_obj = match fs_dict.get(&Name::ef()) {
1364            Some(o) => o.clone(),
1365            None => return Ok(None),
1366        };
1367        let ef_dict = match ef_obj.as_dict() {
1368            Some(d) => d.clone(),
1369            None => return Ok(None),
1370        };
1371        let stream_id = match ef_dict.get(&Name::f()).and_then(|o| o.as_reference()) {
1372            Some(id) => id,
1373            None => return Ok(None),
1374        };
1375        // Clone the stream object to avoid holding a borrow on `self` across
1376        // the `decode_stream` call below.
1377        let stream_obj = self.resolve(stream_id)?.clone();
1378        match &stream_obj {
1379            Object::Stream { data, .. } => match data {
1380                StreamData::Decoded { data } => Ok(Some(data.clone())),
1381                StreamData::Raw { .. } => {
1382                    // decode_stream takes &Object; stream_obj is a local owned clone.
1383                    self.decode_stream(&stream_obj).map(Some)
1384                }
1385            },
1386            _ => Ok(None),
1387        }
1388    }
1389
1390    /// Upstream-aligned alias for [`attachment_file()`](Self::attachment_file).
1391    ///
1392    /// Corresponds to `FPDFAttachment_GetFile`.
1393    #[inline]
1394    pub fn attachment_get_file(&self, index: usize) -> Result<Option<Vec<u8>>, EditError> {
1395        self.attachment_file(index)
1396    }
1397
1398    /// Non-upstream alias — use [`attachment_get_file()`](Self::attachment_get_file).
1399    ///
1400    /// Corresponds to `FPDFAttachment_GetFile`.
1401    #[deprecated(note = "use `attachment_get_file()` — matches upstream `FPDFAttachment_GetFile`")]
1402    #[inline]
1403    pub fn get_file(&self, index: usize) -> Result<Option<Vec<u8>>, EditError> {
1404        self.attachment_file(index)
1405    }
1406
1407    // -----------------------------------------------------------------------
1408    // Attachment helpers
1409    // -----------------------------------------------------------------------
1410
1411    /// Find the ObjectId of the `/Names/EmbeddedFiles` name tree dict.
1412    fn find_embedded_files_id(&self) -> Result<ObjectId, EditError> {
1413        let catalog_id = self.catalog_id();
1414        let catalog = self.resolve(catalog_id)?;
1415        let catalog_dict = catalog
1416            .as_dict()
1417            .ok_or(EditError::Other("catalog is not a dict".into()))?;
1418
1419        let names_ref = catalog_dict
1420            .get(&Name::names())
1421            .and_then(|o| o.as_reference())
1422            .ok_or(EditError::Other("catalog missing /Names reference".into()))?;
1423
1424        let names_obj = self.resolve(names_ref)?;
1425        let names_dict = names_obj
1426            .as_dict()
1427            .ok_or(EditError::Other("/Names is not a dict".into()))?;
1428
1429        names_dict
1430            .get(&Name::embedded_files())
1431            .and_then(|o| o.as_reference())
1432            .ok_or(EditError::Other("/Names missing /EmbeddedFiles".into()))
1433    }
1434
1435    /// Ensure `/Names/EmbeddedFiles` name tree exists in the catalog.
1436    fn ensure_embedded_files(&mut self) -> Result<ObjectId, EditError> {
1437        let catalog_id = self.catalog_id();
1438        let catalog = self.resolve(catalog_id)?;
1439        let catalog_dict = catalog
1440            .as_dict()
1441            .ok_or(EditError::Other("catalog is not a dict".into()))?;
1442
1443        // Check if /Names already exists
1444        let names_id = if let Some(Object::Reference(id)) = catalog_dict.get(&Name::names()) {
1445            *id
1446        } else {
1447            // Create /Names dict
1448            let names_dict = HashMap::new();
1449            let id = self.add_object(Object::Dictionary(names_dict));
1450            let catalog_mut = self.get_mut(catalog_id)?;
1451            if let Object::Dictionary(d) = catalog_mut {
1452                d.insert(Name::names(), Object::Reference(id));
1453            }
1454            id
1455        };
1456
1457        // Check if /EmbeddedFiles exists in /Names
1458        let names_obj = self.resolve(names_id)?;
1459        let names_dict = names_obj
1460            .as_dict()
1461            .ok_or(EditError::Other("/Names is not a dict".into()))?;
1462
1463        if let Some(Object::Reference(ef_id)) = names_dict.get(&Name::embedded_files()) {
1464            Ok(*ef_id)
1465        } else {
1466            // Create EmbeddedFiles name tree
1467            let mut ef_dict = HashMap::new();
1468            ef_dict.insert(Name::names(), Object::Array(Vec::new()));
1469            let ef_id = self.add_object(Object::Dictionary(ef_dict));
1470
1471            let names_mut = self.get_mut(names_id)?;
1472            if let Object::Dictionary(d) = names_mut {
1473                d.insert(Name::embedded_files(), Object::Reference(ef_id));
1474            }
1475            Ok(ef_id)
1476        }
1477    }
1478
1479    /// Resolve the Filespec dictionary for the attachment at `index`.
1480    fn resolve_attachment_dict(&self, index: usize) -> Result<HashMap<Name, Object>, EditError> {
1481        let fs_id = self.resolve_attachment_object_id(index)?;
1482        let fs_obj = self.resolve(fs_id)?;
1483        fs_obj
1484            .as_dict()
1485            .cloned()
1486            .ok_or(EditError::Other("filespec is not a dict".into()))
1487    }
1488
1489    /// Get the ObjectId of the Filespec at `index` in the EmbeddedFiles name tree.
1490    fn resolve_attachment_object_id(&self, index: usize) -> Result<ObjectId, EditError> {
1491        let ef_names_id = self.find_embedded_files_id()?;
1492        let ef_names = self.resolve(ef_names_id)?;
1493        let dict = ef_names
1494            .as_dict()
1495            .ok_or(EditError::Other("EmbeddedFiles is not a dict".into()))?;
1496        let names_arr =
1497            dict.get(&Name::names())
1498                .and_then(|o| o.as_array())
1499                .ok_or(EditError::Other(
1500                    "EmbeddedFiles/Names is not an array".into(),
1501                ))?;
1502
1503        let pair_start = index * 2;
1504        if pair_start + 1 >= names_arr.len() {
1505            return Err(EditError::Other(format!(
1506                "attachment index {index} out of range (have {} attachments)",
1507                names_arr.len() / 2
1508            )));
1509        }
1510
1511        names_arr[pair_start + 1]
1512            .as_reference()
1513            .ok_or(EditError::Other(
1514                "attachment value is not a reference".into(),
1515            ))
1516    }
1517
1518    /// Ensure an AcroForm dictionary exists in the catalog, creating it if
1519    /// absent.  Returns the `ObjectId` of the AcroForm dictionary.
1520    fn ensure_acroform(&mut self) -> Result<ObjectId, EditError> {
1521        let catalog_id = self.catalog_id();
1522        let catalog = self.resolve(catalog_id)?;
1523        let acroform_key = Name::from_bytes(b"AcroForm".to_vec());
1524
1525        // Check if AcroForm already present as a reference
1526        if let Some(Object::Reference(id)) = catalog.as_dict().and_then(|d| d.get(&acroform_key)) {
1527            return Ok(*id);
1528        }
1529
1530        // Create a new AcroForm dict
1531        let mut acroform_dict = HashMap::new();
1532        acroform_dict.insert(
1533            Name::from_bytes(b"Fields".to_vec()),
1534            Object::Array(Vec::new()),
1535        );
1536        let acroform_id = self.add_object(Object::Dictionary(acroform_dict));
1537
1538        // Link it from the catalog
1539        let catalog_mut = self.get_mut(catalog_id)?;
1540        if let Object::Dictionary(dict) = catalog_mut {
1541            dict.insert(acroform_key, Object::Reference(acroform_id));
1542        }
1543        Ok(acroform_id)
1544    }
1545}
1546
1547/// Returns a human-readable type name for a PDF object.
1548fn object_type_name(obj: &Object) -> &'static str {
1549    match obj {
1550        Object::Boolean(_) => "boolean",
1551        Object::Integer(_) | Object::Real(_) => "number",
1552        Object::String(_) => "string",
1553        Object::Name(_) => "name",
1554        Object::Array(_) => "array",
1555        Object::Dictionary(_) => "dictionary",
1556        Object::Stream { .. } => "stream",
1557        Object::Reference(_) => "reference",
1558        Object::Null => "null",
1559    }
1560}
1561
1562#[cfg(test)]
1563mod tests {
1564    use super::*;
1565
1566    #[test]
1567    fn test_new_blank_has_catalog_and_pages() {
1568        let doc = EditDocument::new_blank();
1569        assert_eq!(doc.catalog_id(), ObjectId::new(1, 0));
1570        assert_eq!(doc.page_count(), 0);
1571        assert!(doc.is_dirty());
1572
1573        // Can resolve catalog
1574        let catalog = doc.resolve(doc.catalog_id()).unwrap();
1575        let dict = catalog.as_dict().unwrap();
1576        assert!(dict.contains_key(&Name::r#type()));
1577
1578        // Can resolve pages
1579        let pages_ref = dict.get(&Name::pages()).unwrap();
1580        if let Object::Reference(pages_id) = pages_ref {
1581            let pages = doc.resolve(*pages_id).unwrap();
1582            let pdict = pages.as_dict().unwrap();
1583            assert_eq!(pdict.get(&Name::count()).and_then(|o| o.as_i64()), Some(0));
1584        } else {
1585            panic!("expected reference to Pages");
1586        }
1587    }
1588
1589    #[test]
1590    fn test_add_object_returns_unique_ids() {
1591        let mut doc = EditDocument::new_blank();
1592        let id1 = doc.add_object(Object::Integer(1));
1593        let id2 = doc.add_object(Object::Integer(2));
1594        assert_ne!(id1, id2);
1595
1596        let obj1 = doc.resolve(id1).unwrap();
1597        assert_eq!(obj1.as_i64(), Some(1));
1598        let obj2 = doc.resolve(id2).unwrap();
1599        assert_eq!(obj2.as_i64(), Some(2));
1600    }
1601
1602    #[test]
1603    fn test_delete_object_makes_unresolvable() {
1604        let mut doc = EditDocument::new_blank();
1605        let id = doc.add_object(Object::Integer(42));
1606        assert!(doc.resolve(id).is_ok());
1607
1608        doc.delete_object(id);
1609        assert!(doc.resolve(id).is_err());
1610    }
1611
1612    #[test]
1613    fn test_set_object_replaces_value() {
1614        let mut doc = EditDocument::new_blank();
1615        let id = doc.add_object(Object::Integer(1));
1616        doc.set_object(id, Object::Integer(2));
1617
1618        let obj = doc.resolve(id).unwrap();
1619        assert_eq!(obj.as_i64(), Some(2));
1620    }
1621
1622    #[test]
1623    fn test_get_mut_modifies_in_place() {
1624        let mut doc = EditDocument::new_blank();
1625        let id = doc.add_object(Object::Integer(1));
1626        let obj = doc.get_mut(id).unwrap();
1627        *obj = Object::Integer(99);
1628
1629        let resolved = doc.resolve(id).unwrap();
1630        assert_eq!(resolved.as_i64(), Some(99));
1631    }
1632
1633    #[test]
1634    fn test_all_ids_includes_all_non_deleted() {
1635        let mut doc = EditDocument::new_blank();
1636        let id1 = doc.add_object(Object::Integer(1));
1637        let id2 = doc.add_object(Object::Integer(2));
1638        doc.delete_object(id2);
1639
1640        let all = doc.all_ids();
1641        assert!(all.contains(&id1));
1642        assert!(!all.contains(&id2));
1643        // Should include catalog and pages
1644        assert!(all.contains(&ObjectId::new(1, 0)));
1645        assert!(all.contains(&ObjectId::new(2, 0)));
1646    }
1647
1648    #[test]
1649    fn test_changed_ids_lists_modifications() {
1650        let mut doc = EditDocument::new_blank();
1651        let id = doc.add_object(Object::Integer(42));
1652
1653        let changed: Vec<ObjectId> = doc.changed_ids().collect();
1654        // Should include catalog, pages, and the new object
1655        assert!(changed.contains(&id));
1656    }
1657
1658    #[test]
1659    fn test_pages_id_resolves_from_catalog() {
1660        let doc = EditDocument::new_blank();
1661        let pages_id = doc.pages_id().unwrap();
1662        assert_eq!(pages_id, ObjectId::new(2, 0));
1663    }
1664
1665    #[test]
1666    fn test_sync_pages_tree_updates_kids() {
1667        let mut doc = EditDocument::new_blank();
1668        let page_id = ObjectId::new(10, 0);
1669        doc.page_ids_mut().push(page_id);
1670        doc.new_objects.insert(
1671            page_id,
1672            Object::Dictionary({
1673                let mut d = HashMap::new();
1674                d.insert(
1675                    Name::r#type(),
1676                    Object::Name(Name::from_bytes(b"Page".to_vec())),
1677                );
1678                d
1679            }),
1680        );
1681        doc.sync_pages_tree().unwrap();
1682
1683        let pages_id = doc.pages_id().unwrap();
1684        let pages = doc.resolve(pages_id).unwrap();
1685        let dict = pages.as_dict().unwrap();
1686        assert_eq!(dict.get(&Name::count()).and_then(|o| o.as_i64()), Some(1));
1687        let kids = dict.get(&Name::kids()).and_then(|o| o.as_array()).unwrap();
1688        assert_eq!(kids.len(), 1);
1689    }
1690
1691    #[test]
1692    fn test_allocate_id_increments() {
1693        let mut doc = EditDocument::new_blank();
1694        let first = doc.next_object_number();
1695        let id1 = doc.allocate_id();
1696        assert_eq!(id1.number, first);
1697        let id2 = doc.allocate_id();
1698        assert_eq!(id2.number, first + 1);
1699    }
1700
1701    #[test]
1702    fn test_deleted_ids_tracked() {
1703        let mut doc = EditDocument::new_blank();
1704        let id = doc.add_object(Object::Null);
1705        assert!(doc.deleted_ids().is_empty());
1706        doc.delete_object(id);
1707        assert!(doc.deleted_ids().contains(&id));
1708    }
1709
1710    // ------------------------------------------------------------------
1711    // Font loading tests
1712    // ------------------------------------------------------------------
1713
1714    #[test]
1715    fn test_load_standard_font_creates_font_object() {
1716        let mut doc = EditDocument::new_blank();
1717        let reg = doc.load_standard_font("Helvetica").unwrap();
1718
1719        // The returned registration holds a valid object ID
1720        assert!(reg.object_id.number > 0);
1721
1722        // The object is resolvable and is a Font dictionary
1723        let obj = doc.resolve(reg.object_id).unwrap();
1724        let dict = obj.as_dict().unwrap();
1725
1726        let subtype = dict
1727            .get(&Name::from_bytes(b"Subtype".to_vec()))
1728            .and_then(|o| o.as_name())
1729            .map(|n| n.as_bytes().to_vec());
1730        assert_eq!(subtype, Some(b"Type1".to_vec()));
1731
1732        let base = dict
1733            .get(&Name::from_bytes(b"BaseFont".to_vec()))
1734            .and_then(|o| o.as_name())
1735            .map(|n| n.as_bytes().to_vec());
1736        assert_eq!(base, Some(b"Helvetica".to_vec()));
1737    }
1738
1739    #[test]
1740    fn test_load_font_from_data_creates_embedded_objects() {
1741        let mut doc = EditDocument::new_blank();
1742        let initial_count = doc.all_ids().len();
1743
1744        // Minimal fake font bytes
1745        let fake_data = b"FAKE_TRUETYPE_DATA_BYTES";
1746        let reg = doc
1747            .load_font_from_data(fake_data, crate::font_reg::FontType::TrueType, false)
1748            .unwrap();
1749
1750        // Three new objects: stream + descriptor + font
1751        let new_count = doc.all_ids().len();
1752        assert_eq!(new_count, initial_count + 3);
1753
1754        // The Font dict is resolvable
1755        let font_obj = doc.resolve(reg.object_id).unwrap();
1756        let font_dict = font_obj.as_dict().unwrap();
1757        let subtype = font_dict
1758            .get(&Name::from_bytes(b"Subtype".to_vec()))
1759            .and_then(|o| o.as_name())
1760            .map(|n| n.as_bytes().to_vec());
1761        assert_eq!(subtype, Some(b"TrueType".to_vec()));
1762    }
1763
1764    // ------------------------------------------------------------------
1765    // copy_viewer_preferences_from tests
1766    // ------------------------------------------------------------------
1767
1768    #[test]
1769    fn test_copy_viewer_preferences_from_noop_when_src_has_none() {
1770        let src = EditDocument::new_blank();
1771        let mut dest = EditDocument::new_blank();
1772        // src has no /ViewerPreferences — should be a no-op
1773        dest.copy_viewer_preferences_from(&src).unwrap();
1774        // dest catalog should not have /ViewerPreferences
1775        let catalog = dest.resolve(dest.catalog_id()).unwrap();
1776        let dict = catalog.as_dict().unwrap();
1777        assert!(!dict.contains_key(&Name::from_bytes(b"ViewerPreferences".to_vec())));
1778    }
1779
1780    #[test]
1781    fn test_copy_viewer_preferences_from_copies_dict() {
1782        let mut src = EditDocument::new_blank();
1783
1784        // Add ViewerPreferences to src catalog
1785        let vp_key = Name::from_bytes(b"ViewerPreferences".to_vec());
1786        let fit_window_key = Name::from_bytes(b"FitWindow".to_vec());
1787        let mut vp_dict = std::collections::HashMap::new();
1788        vp_dict.insert(fit_window_key.clone(), Object::Boolean(true));
1789        let vp_id = src.add_object(Object::Dictionary(vp_dict));
1790        let catalog_id = src.catalog_id();
1791        let catalog = src.get_mut(catalog_id).unwrap();
1792        if let Object::Dictionary(dict) = catalog {
1793            dict.insert(vp_key.clone(), Object::Reference(vp_id));
1794        }
1795
1796        let mut dest = EditDocument::new_blank();
1797        dest.copy_viewer_preferences_from(&src).unwrap();
1798
1799        // dest catalog should now have /ViewerPreferences
1800        let dest_catalog = dest.resolve(dest.catalog_id()).unwrap();
1801        let dest_dict = dest_catalog.as_dict().unwrap();
1802        let vp_ref = dest_dict
1803            .get(&vp_key)
1804            .expect("/ViewerPreferences not found");
1805
1806        // Resolve the copied object
1807        let new_vp_id = match vp_ref {
1808            Object::Reference(id) => *id,
1809            _ => panic!("expected reference"),
1810        };
1811        let new_vp = dest.resolve(new_vp_id).unwrap();
1812        let new_vp_dict = new_vp.as_dict().unwrap();
1813        assert_eq!(
1814            new_vp_dict.get(&fit_window_key).and_then(|o| o.as_bool()),
1815            Some(true)
1816        );
1817    }
1818
1819    #[test]
1820    fn test_load_font_from_data_cid_true_returns_error() {
1821        let mut doc = EditDocument::new_blank();
1822        let result = doc.load_font_from_data(b"data", crate::font_reg::FontType::TrueType, true);
1823        assert!(result.is_err());
1824        assert!(matches!(result, Err(EditError::NotSupported(_))));
1825    }
1826
1827    // ------------------------------------------------------------------
1828    // Fix D: Page box setters
1829    // ------------------------------------------------------------------
1830
1831    #[test]
1832    fn test_set_media_box_updates_page_dict() {
1833        use rpdfium_core::Rect;
1834        let mut doc = EditDocument::new_blank();
1835        doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1836        doc.set_media_box(0, Rect::new(10.0, 20.0, 600.0, 780.0))
1837            .unwrap();
1838        let page_id = doc.page_id(0).unwrap();
1839        let page = doc.resolve(page_id).unwrap();
1840        let dict = page.as_dict().unwrap();
1841        let arr = dict.get(&Name::media_box()).unwrap().as_array().unwrap();
1842        assert_eq!(arr[0].as_f64(), Some(10.0));
1843        assert_eq!(arr[1].as_f64(), Some(20.0));
1844        assert_eq!(arr[2].as_f64(), Some(600.0));
1845        assert_eq!(arr[3].as_f64(), Some(780.0));
1846    }
1847
1848    #[test]
1849    fn test_set_crop_box_updates_page_dict() {
1850        use rpdfium_core::Rect;
1851        let mut doc = EditDocument::new_blank();
1852        doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1853        doc.set_crop_box(0, Rect::new(5.0, 5.0, 607.0, 787.0))
1854            .unwrap();
1855        let page_id = doc.page_id(0).unwrap();
1856        let page = doc.resolve(page_id).unwrap();
1857        let dict = page.as_dict().unwrap();
1858        assert!(dict.contains_key(&Name::crop_box()));
1859    }
1860
1861    #[test]
1862    fn test_set_bleed_box_updates_page_dict() {
1863        use rpdfium_core::Rect;
1864        let mut doc = EditDocument::new_blank();
1865        doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1866        doc.set_bleed_box(0, Rect::new(-3.0, -3.0, 615.0, 795.0))
1867            .unwrap();
1868        let page_id = doc.page_id(0).unwrap();
1869        let page = doc.resolve(page_id).unwrap();
1870        let dict = page.as_dict().unwrap();
1871        assert!(dict.contains_key(&Name::bleed_box()));
1872    }
1873
1874    #[test]
1875    fn test_set_trim_box_updates_page_dict() {
1876        use rpdfium_core::Rect;
1877        let mut doc = EditDocument::new_blank();
1878        doc.add_page(Rect::new(0.0, 0.0, 595.0, 842.0));
1879        doc.set_trim_box(0, Rect::new(8.0, 8.0, 587.0, 834.0))
1880            .unwrap();
1881        let page_id = doc.page_id(0).unwrap();
1882        let page = doc.resolve(page_id).unwrap();
1883        let dict = page.as_dict().unwrap();
1884        assert!(dict.contains_key(&Name::trim_box()));
1885    }
1886
1887    #[test]
1888    fn test_set_art_box_updates_page_dict() {
1889        use rpdfium_core::Rect;
1890        let mut doc = EditDocument::new_blank();
1891        doc.add_page(Rect::new(0.0, 0.0, 595.0, 842.0));
1892        doc.set_art_box(0, Rect::new(12.0, 12.0, 583.0, 830.0))
1893            .unwrap();
1894        let page_id = doc.page_id(0).unwrap();
1895        let page = doc.resolve(page_id).unwrap();
1896        let dict = page.as_dict().unwrap();
1897        assert!(dict.contains_key(&Name::art_box()));
1898    }
1899
1900    #[test]
1901    fn test_set_page_box_out_of_range_returns_error() {
1902        use rpdfium_core::Rect;
1903        let mut doc = EditDocument::new_blank();
1904        assert!(
1905            doc.set_media_box(0, Rect::new(0.0, 0.0, 612.0, 792.0))
1906                .is_err()
1907        );
1908    }
1909
1910    #[test]
1911    fn test_get_page_rotation_default_zero() {
1912        let mut doc = EditDocument::new_blank();
1913        doc.add_page(rpdfium_core::Rect::new(0.0, 0.0, 612.0, 792.0));
1914        assert_eq!(doc.page_rotation(0).unwrap(), 0);
1915    }
1916
1917    #[test]
1918    fn test_get_page_rotation_after_set() {
1919        let mut doc = EditDocument::new_blank();
1920        doc.add_page(rpdfium_core::Rect::new(0.0, 0.0, 612.0, 792.0));
1921        doc.page_set_rotation(0, 90).unwrap();
1922        assert_eq!(doc.page_rotation(0).unwrap(), 90);
1923    }
1924
1925    #[test]
1926    fn test_get_page_rotation_out_of_range() {
1927        let doc = EditDocument::new_blank();
1928        assert!(doc.page_rotation(0).is_err());
1929    }
1930
1931    // ------------------------------------------------------------------
1932    // B2: Font registration populates flags for embedded fonts
1933    // ------------------------------------------------------------------
1934
1935    #[test]
1936    fn test_load_standard_font_returns_flags_and_weight() {
1937        let mut doc = EditDocument::new_blank();
1938        let reg = doc.load_standard_font("Helvetica").unwrap();
1939        assert_eq!(reg.flags(), Some(32));
1940        assert_eq!(reg.weight(), Some(400));
1941    }
1942
1943    #[test]
1944    fn test_load_font_from_data_returns_flags_and_weight() {
1945        let mut doc = EditDocument::new_blank();
1946        let fake_data = b"FAKE_TRUETYPE_DATA";
1947        let reg = doc
1948            .load_font_from_data(fake_data, crate::font_reg::FontType::TrueType, false)
1949            .unwrap();
1950        // The Symbolic flag (4) is set in the FontDescriptor created by load_font_from_data
1951        assert_eq!(reg.flags(), Some(4));
1952        // Default weight (400) is set
1953        assert_eq!(reg.weight(), Some(400));
1954    }
1955
1956    // -----------------------------------------------------------------------
1957    // Attachment mutation tests
1958    // -----------------------------------------------------------------------
1959
1960    #[test]
1961    fn test_add_attachment_creates_embedded_file() {
1962        let mut doc = EditDocument::new_blank();
1963        let idx = doc.add_attachment("test.pdf").unwrap();
1964        assert_eq!(idx, 0);
1965
1966        // Verify the attachment exists and has the right name
1967        let name = doc.attachment_string(0, "UF").unwrap();
1968        assert_eq!(name.as_deref(), Some("test.pdf"));
1969        let name_f = doc.attachment_string(0, "F").unwrap();
1970        assert_eq!(name_f.as_deref(), Some("test.pdf"));
1971    }
1972
1973    #[test]
1974    fn test_add_multiple_attachments() {
1975        let mut doc = EditDocument::new_blank();
1976        let idx0 = doc.add_attachment("a.pdf").unwrap();
1977        let idx1 = doc.add_attachment("b.pdf").unwrap();
1978        assert_eq!(idx0, 0);
1979        assert_eq!(idx1, 1);
1980
1981        assert_eq!(
1982            doc.attachment_string(0, "UF").unwrap().as_deref(),
1983            Some("a.pdf")
1984        );
1985        assert_eq!(
1986            doc.attachment_string(1, "UF").unwrap().as_deref(),
1987            Some("b.pdf")
1988        );
1989    }
1990
1991    #[test]
1992    fn test_delete_attachment_removes_entry() {
1993        let mut doc = EditDocument::new_blank();
1994        doc.add_attachment("a.pdf").unwrap();
1995        doc.add_attachment("b.pdf").unwrap();
1996
1997        doc.delete_attachment(0).unwrap();
1998
1999        // After deleting index 0, what was index 1 is now index 0
2000        assert_eq!(
2001            doc.attachment_string(0, "UF").unwrap().as_deref(),
2002            Some("b.pdf")
2003        );
2004        // Index 1 should be out of range
2005        assert!(doc.attachment_string(1, "UF").is_err());
2006    }
2007
2008    #[test]
2009    fn test_delete_attachment_out_of_range_returns_error() {
2010        let mut doc = EditDocument::new_blank();
2011        doc.add_attachment("a.pdf").unwrap();
2012        assert!(doc.delete_attachment(5).is_err());
2013    }
2014
2015    #[test]
2016    fn test_attachment_has_key_returns_correct_result() {
2017        let mut doc = EditDocument::new_blank();
2018        doc.add_attachment("test.pdf").unwrap();
2019
2020        assert!(doc.attachment_has_key(0, "UF").unwrap());
2021        assert!(doc.attachment_has_key(0, "F").unwrap());
2022        assert!(doc.attachment_has_key(0, "EF").unwrap());
2023        assert!(!doc.attachment_has_key(0, "NonExistent").unwrap());
2024    }
2025
2026    #[test]
2027    fn test_attachment_value_type_returns_correct_type() {
2028        let mut doc = EditDocument::new_blank();
2029        doc.add_attachment("test.pdf").unwrap();
2030
2031        assert_eq!(doc.attachment_value_type(0, "UF").unwrap(), Some("string"));
2032        assert_eq!(doc.attachment_value_type(0, "Type").unwrap(), Some("name"));
2033        assert_eq!(
2034            doc.attachment_value_type(0, "EF").unwrap(),
2035            Some("dictionary")
2036        );
2037        assert_eq!(doc.attachment_value_type(0, "NoSuchKey").unwrap(), None);
2038    }
2039
2040    #[test]
2041    fn test_set_attachment_string_writes_value() {
2042        let mut doc = EditDocument::new_blank();
2043        doc.add_attachment("test.pdf").unwrap();
2044
2045        doc.set_attachment_string(0, "Desc", "A test file").unwrap();
2046        assert_eq!(
2047            doc.attachment_string(0, "Desc").unwrap().as_deref(),
2048            Some("A test file")
2049        );
2050    }
2051
2052    #[test]
2053    fn test_set_attachment_file_writes_data() {
2054        let mut doc = EditDocument::new_blank();
2055        doc.add_attachment("test.txt").unwrap();
2056
2057        doc.set_attachment_file(0, b"Hello, World!").unwrap();
2058
2059        // Verify the stream was replaced — we can check by resolving
2060        // the /EF /F reference and checking the stream data
2061        let fs_dict = doc.resolve_attachment_dict(0).unwrap();
2062        let ef_dict = fs_dict.get(&Name::ef()).unwrap().as_dict().unwrap();
2063        let stream_id = ef_dict.get(&Name::f()).unwrap().as_reference().unwrap();
2064        let stream_obj = doc.resolve(stream_id).unwrap();
2065        if let Object::Stream {
2066            data: rpdfium_parser::object::StreamData::Decoded { data },
2067            ..
2068        } = stream_obj
2069        {
2070            assert_eq!(data, b"Hello, World!");
2071        } else {
2072            panic!("expected decoded stream");
2073        }
2074    }
2075
2076    #[test]
2077    fn test_attachment_index_out_of_range_returns_error() {
2078        let doc = EditDocument::new_blank();
2079        // No attachments exist, so index 0 should fail
2080        assert!(doc.attachment_string(0, "UF").is_err());
2081        assert!(doc.attachment_has_key(0, "UF").is_err());
2082    }
2083
2084    // ===================================================================
2085    // Upstream-ported mutation tests
2086    // ===================================================================
2087
2088    /// Upstream: PDFObjectsTest::SetString
2089    ///
2090    /// Test setting string value via edit API.
2091    #[test]
2092    fn test_set_string_value() {
2093        use rpdfium_core::PdfString;
2094        let mut doc = EditDocument::new_blank();
2095        let id = doc.add_object(Object::String(PdfString::from_bytes(b"hello".to_vec())));
2096        doc.set_object(id, Object::String(PdfString::from_bytes(b"world".to_vec())));
2097        let obj = doc.resolve(id).unwrap();
2098        assert_eq!(
2099            obj.as_string().map(|s| s.as_bytes()),
2100            Some(b"world".as_slice())
2101        );
2102    }
2103
2104    /// Upstream: PDFObjectsTest::Clone
2105    ///
2106    /// Deep-clone via Rust Clone trait.
2107    #[test]
2108    fn test_object_clone() {
2109        use rpdfium_core::PdfString;
2110        let original = Object::Dictionary({
2111            let mut d = HashMap::new();
2112            d.insert(
2113                Name::from_bytes(b"Key".to_vec()),
2114                Object::String(PdfString::from_bytes(b"Value".to_vec())),
2115            );
2116            d.insert(
2117                Name::from_bytes(b"Array".to_vec()),
2118                Object::Array(vec![Object::Integer(1), Object::Integer(2)]),
2119            );
2120            d
2121        });
2122        let cloned = original.clone();
2123        // Object doesn't implement PartialEq; verify structure manually
2124        let orig_dict = original.as_dict().unwrap();
2125        let clone_dict = cloned.as_dict().unwrap();
2126        assert_eq!(orig_dict.len(), clone_dict.len());
2127        assert!(clone_dict.contains_key(&Name::from_bytes(b"Key".to_vec())));
2128        assert!(clone_dict.contains_key(&Name::from_bytes(b"Array".to_vec())));
2129    }
2130
2131    /// Upstream: PDFArrayTest::AddNumber, AddInteger, AddStringAndName
2132    ///
2133    /// Build arrays with different value types.
2134    #[test]
2135    fn test_array_building() {
2136        use rpdfium_core::PdfString;
2137        let mut doc = EditDocument::new_blank();
2138
2139        // Add numbers
2140        let arr = Object::Array(vec![
2141            Object::Integer(1),
2142            Object::Integer(2),
2143            Object::Real(3.14),
2144        ]);
2145        let id = doc.add_object(arr);
2146        let obj = doc.resolve(id).unwrap();
2147        let a = obj.as_array().unwrap();
2148        assert_eq!(a.len(), 3);
2149        assert_eq!(a[0].as_i64(), Some(1));
2150        assert_eq!(a[1].as_i64(), Some(2));
2151        assert!((a[2].as_f64().unwrap() - 3.14).abs() < 0.01);
2152
2153        // Add string and name
2154        let arr2 = Object::Array(vec![
2155            Object::String(PdfString::from_bytes(b"test".to_vec())),
2156            Object::Name(Name::from_bytes(b"Name1".to_vec())),
2157        ]);
2158        let id2 = doc.add_object(arr2);
2159        let obj2 = doc.resolve(id2).unwrap();
2160        let a2 = obj2.as_array().unwrap();
2161        assert_eq!(
2162            a2[0].as_string().map(|s| s.as_bytes()),
2163            Some(b"test".as_slice())
2164        );
2165        assert_eq!(
2166            a2[1].as_name().map(|n| n.as_bytes()),
2167            Some(b"Name1".as_slice())
2168        );
2169    }
2170
2171    /// Upstream: PDFArrayTest::AddReferenceAndGetObjectAt
2172    ///
2173    /// Add indirect reference + retrieve.
2174    #[test]
2175    fn test_array_add_reference_and_get() {
2176        let mut doc = EditDocument::new_blank();
2177        let target_id = doc.add_object(Object::Integer(42));
2178        let arr = Object::Array(vec![Object::Reference(target_id)]);
2179        let arr_id = doc.add_object(arr);
2180
2181        let obj = doc.resolve(arr_id).unwrap();
2182        let a = obj.as_array().unwrap();
2183        assert_eq!(a.len(), 1);
2184        match &a[0] {
2185            Object::Reference(ref_id) => {
2186                let resolved = doc.resolve(*ref_id).unwrap();
2187                assert_eq!(resolved.as_i64(), Some(42));
2188            }
2189            _ => panic!("expected reference"),
2190        }
2191    }
2192
2193    /// Upstream: PDFStreamTest::SetData, SetDataAndRemoveFilter
2194    ///
2195    /// Stream data replacement.
2196    #[test]
2197    fn test_stream_set_data() {
2198        let mut doc = EditDocument::new_blank();
2199        let id = doc.add_object(Object::Stream {
2200            dict: {
2201                let mut d = HashMap::new();
2202                d.insert(Name::from_bytes(b"Length".to_vec()), Object::Integer(5));
2203                d
2204            },
2205            data: StreamData::Decoded {
2206                data: b"hello".to_vec(),
2207            },
2208        });
2209
2210        // Replace stream data
2211        doc.set_object(
2212            id,
2213            Object::Stream {
2214                dict: {
2215                    let mut d = HashMap::new();
2216                    d.insert(Name::from_bytes(b"Length".to_vec()), Object::Integer(5));
2217                    d
2218                },
2219                data: StreamData::Decoded {
2220                    data: b"world".to_vec(),
2221                },
2222            },
2223        );
2224
2225        let obj = doc.resolve(id).unwrap();
2226        if let Object::Stream {
2227            data: StreamData::Decoded { data },
2228            ..
2229        } = obj
2230        {
2231            assert_eq!(data, b"world");
2232        } else {
2233            panic!("expected decoded stream");
2234        }
2235    }
2236
2237    /// Upstream: PDFDictionaryTest::CloneDirectObject
2238    ///
2239    /// Clone a dictionary and verify independence.
2240    #[test]
2241    fn test_dictionary_clone_direct() {
2242        use rpdfium_core::PdfString;
2243        let mut doc = EditDocument::new_blank();
2244        let mut d = HashMap::new();
2245        d.insert(Name::from_bytes(b"Key1".to_vec()), Object::Integer(1));
2246        d.insert(
2247            Name::from_bytes(b"Key2".to_vec()),
2248            Object::String(PdfString::from_bytes(b"val".to_vec())),
2249        );
2250        let id = doc.add_object(Object::Dictionary(d));
2251
2252        let obj = doc.resolve(id).unwrap().clone();
2253        let dict = obj.as_dict().unwrap();
2254        assert_eq!(
2255            dict.get(&Name::from_bytes(b"Key1".to_vec()))
2256                .and_then(|o| o.as_i64()),
2257            Some(1)
2258        );
2259
2260        // Modify original, clone should be unaffected
2261        doc.set_object(id, Object::Integer(99));
2262        let original = doc.resolve(id).unwrap();
2263        assert_eq!(original.as_i64(), Some(99));
2264        // Our clone is still a dictionary
2265        assert!(dict.contains_key(&Name::from_bytes(b"Key1".to_vec())));
2266    }
2267
2268    /// Upstream: PDFArrayTest::RemoveAt, Clear, InsertAt, Clone
2269    ///
2270    /// Array mutation operations via edit API.
2271    #[test]
2272    fn test_array_mutations() {
2273        let mut doc = EditDocument::new_blank();
2274        let id = doc.add_object(Object::Array(vec![
2275            Object::Integer(10),
2276            Object::Integer(20),
2277            Object::Integer(30),
2278        ]));
2279
2280        // Remove at index 1 (value 20)
2281        let obj = doc.get_mut(id).unwrap();
2282        if let Object::Array(arr) = obj {
2283            arr.remove(1);
2284        }
2285        let obj = doc.resolve(id).unwrap();
2286        let a = obj.as_array().unwrap();
2287        assert_eq!(a.len(), 2);
2288        assert_eq!(a[0].as_i64(), Some(10));
2289        assert_eq!(a[1].as_i64(), Some(30));
2290
2291        // Insert at index 1
2292        let obj = doc.get_mut(id).unwrap();
2293        if let Object::Array(arr) = obj {
2294            arr.insert(1, Object::Integer(25));
2295        }
2296        let obj = doc.resolve(id).unwrap();
2297        let a = obj.as_array().unwrap();
2298        assert_eq!(a.len(), 3);
2299        assert_eq!(a[1].as_i64(), Some(25));
2300
2301        // Clear
2302        let obj = doc.get_mut(id).unwrap();
2303        if let Object::Array(arr) = obj {
2304            arr.clear();
2305        }
2306        let obj = doc.resolve(id).unwrap();
2307        let a = obj.as_array().unwrap();
2308        assert!(a.is_empty());
2309
2310        // Clone array
2311        let arr_obj = Object::Array(vec![Object::Integer(1), Object::Integer(2)]);
2312        let cloned = arr_obj.clone();
2313        let ca = cloned.as_array().unwrap();
2314        assert_eq!(ca.len(), 2);
2315        assert_eq!(ca[0].as_i64(), Some(1));
2316        assert_eq!(ca[1].as_i64(), Some(2));
2317    }
2318
2319    /// Upstream: PDFObjectTest::CloneCheckLoop
2320    ///
2321    /// Clone cycle detection — a reference to itself should not cause infinite recursion.
2322    #[test]
2323    fn test_clone_does_not_stack_overflow() {
2324        let mut doc = EditDocument::new_blank();
2325        // Create a self-referential structure
2326        let id = doc.allocate_id();
2327        let obj = Object::Array(vec![Object::Reference(id)]);
2328        doc.new_objects.insert(id, obj);
2329
2330        // Cloning the object should work fine since Clone is structural,
2331        // not a deep-resolve operation
2332        let resolved = doc.resolve(id).unwrap();
2333        let cloned = resolved.clone();
2334        if let Object::Array(arr) = &cloned {
2335            assert_eq!(arr.len(), 1);
2336            assert!(matches!(&arr[0], Object::Reference(r) if *r == id));
2337        } else {
2338            panic!("expected array");
2339        }
2340    }
2341
2342    /// Upstream: PDFReferenceTest::MakeReferenceToReference
2343    ///
2344    /// A reference to a reference should be resolvable.
2345    #[test]
2346    fn test_reference_to_reference() {
2347        let mut doc = EditDocument::new_blank();
2348        let inner_id = doc.add_object(Object::Integer(42));
2349        let outer_id = doc.add_object(Object::Reference(inner_id));
2350
2351        // Resolve outer reference → should get the Reference object
2352        let outer = doc.resolve(outer_id).unwrap();
2353        match outer {
2354            Object::Reference(ref_id) => {
2355                let inner = doc.resolve(*ref_id).unwrap();
2356                assert_eq!(inner.as_i64(), Some(42));
2357            }
2358            _ => panic!("expected reference object"),
2359        }
2360    }
2361
2362    /// Upstream: PDFStreamTest::SetDataAndRemoveFilter
2363    ///
2364    /// Replace stream data and strip the Filter/DecodeParms keys.
2365    #[test]
2366    fn test_stream_set_data_and_remove_filter() {
2367        let mut doc = EditDocument::new_blank();
2368        let id = doc.add_object(Object::Stream {
2369            dict: {
2370                let mut d = HashMap::new();
2371                d.insert(Name::from_bytes(b"Length".to_vec()), Object::Integer(5));
2372                d.insert(
2373                    Name::from_bytes(b"Filter".to_vec()),
2374                    Object::Name(Name::from_bytes(b"FlateDecode".to_vec())),
2375                );
2376                d.insert(
2377                    Name::from_bytes(b"DecodeParms".to_vec()),
2378                    Object::Dictionary(HashMap::new()),
2379                );
2380                d
2381            },
2382            data: StreamData::Decoded {
2383                data: b"hello".to_vec(),
2384            },
2385        });
2386
2387        // Replace data and remove filter
2388        let obj = doc.get_mut(id).unwrap();
2389        if let Object::Stream { dict, data } = obj {
2390            *data = StreamData::Decoded {
2391                data: b"new data without filter".to_vec(),
2392            };
2393            dict.remove(&Name::from_bytes(b"Filter".to_vec()));
2394            dict.remove(&Name::from_bytes(b"DecodeParms".to_vec()));
2395            dict.insert(Name::from_bytes(b"Length".to_vec()), Object::Integer(23));
2396        }
2397
2398        let obj = doc.resolve(id).unwrap();
2399        if let Object::Stream { dict, data } = obj {
2400            // Filter and DecodeParms should be removed
2401            assert!(
2402                !dict.contains_key(&Name::from_bytes(b"Filter".to_vec())),
2403                "Filter should be removed"
2404            );
2405            assert!(
2406                !dict.contains_key(&Name::from_bytes(b"DecodeParms".to_vec())),
2407                "DecodeParms should be removed"
2408            );
2409            // Length updated
2410            assert_eq!(
2411                dict.get(&Name::from_bytes(b"Length".to_vec()))
2412                    .and_then(|o| o.as_i64()),
2413                Some(23)
2414            );
2415            if let StreamData::Decoded { data: d } = data {
2416                assert_eq!(d, b"new data without filter");
2417            } else {
2418                panic!("expected decoded stream");
2419            }
2420        } else {
2421            panic!("expected stream");
2422        }
2423    }
2424
2425    /// Upstream: PDFStreamTest::LengthInDictionaryOnCreate
2426    ///
2427    /// In upstream, stream objects auto-set the Length key on creation.
2428    /// rpdfium does not auto-set Length — it must be set explicitly.
2429    /// This test documents the behavior.
2430    #[test]
2431    fn test_stream_length_in_dictionary() {
2432        let mut doc = EditDocument::new_blank();
2433        // Create stream with explicit Length
2434        let id = doc.add_object(Object::Stream {
2435            dict: {
2436                let mut d = HashMap::new();
2437                d.insert(Name::from_bytes(b"Length".to_vec()), Object::Integer(100));
2438                d
2439            },
2440            data: StreamData::Decoded {
2441                data: vec![0u8; 100],
2442            },
2443        });
2444        let obj = doc.resolve(id).unwrap();
2445        if let Object::Stream { dict, .. } = obj {
2446            assert_eq!(
2447                dict.get(&Name::from_bytes(b"Length".to_vec()))
2448                    .and_then(|o| o.as_i64()),
2449                Some(100)
2450            );
2451        } else {
2452            panic!("expected stream");
2453        }
2454    }
2455
2456    /// Upstream: PDFObjectsTest::MakeReferenceGeneric
2457    ///
2458    /// Creating a reference to an existing object.
2459    #[test]
2460    fn test_make_reference() {
2461        let mut doc = EditDocument::new_blank();
2462        let target_id = doc.add_object(Object::Integer(42));
2463        // Create a reference to the target
2464        let ref_obj = Object::Reference(target_id);
2465        let ref_id = doc.add_object(ref_obj);
2466        let resolved = doc.resolve(ref_id).unwrap();
2467        match resolved {
2468            Object::Reference(id) => {
2469                assert_eq!(*id, target_id);
2470                let inner = doc.resolve(*id).unwrap();
2471                assert_eq!(inner.as_i64(), Some(42));
2472            }
2473            _ => panic!("expected reference"),
2474        }
2475    }
2476
2477    /// Upstream: PDFDictionaryTest::ExtractObjectOnRemove
2478    ///
2479    /// Remove a key from dictionary and retrieve the removed value.
2480    #[test]
2481    fn test_extract_object_on_remove() {
2482        let mut doc = EditDocument::new_blank();
2483        let id = doc.add_object(Object::Dictionary({
2484            let mut d = HashMap::new();
2485            d.insert(Name::from_bytes(b"child".to_vec()), Object::Integer(42));
2486            d
2487        }));
2488        let obj = doc.get_mut(id).unwrap();
2489        if let Object::Dictionary(dict) = obj {
2490            let removed = dict.remove(&Name::from_bytes(b"child".to_vec()));
2491            assert!(removed.is_some());
2492            assert_eq!(removed.unwrap().as_i64(), Some(42));
2493            // Removing non-existent key returns None
2494            let missing = dict.remove(&Name::from_bytes(b"non_exists".to_vec()));
2495            assert!(missing.is_none());
2496        }
2497    }
2498
2499    /// Upstream: PDFDictionaryTest::ConvertIndirect
2500    ///
2501    /// In rpdfium, all objects in EditDocument are already indirect.
2502    /// This test verifies the equivalent: replacing a direct dict value
2503    /// with a reference to a newly created indirect object.
2504    #[test]
2505    fn test_convert_to_indirect() {
2506        let mut doc = EditDocument::new_blank();
2507        let dict_id = doc.add_object(Object::Dictionary({
2508            let mut d = HashMap::new();
2509            d.insert(Name::from_bytes(b"clams".to_vec()), Object::Integer(42));
2510            d
2511        }));
2512        // "Convert to indirect": extract value, make it an indirect object,
2513        // and replace the dict entry with a reference
2514        let value_id = doc.add_object(Object::Integer(42));
2515        let obj = doc.get_mut(dict_id).unwrap();
2516        if let Object::Dictionary(dict) = obj {
2517            dict.insert(
2518                Name::from_bytes(b"clams".to_vec()),
2519                Object::Reference(value_id),
2520            );
2521        }
2522        // Verify the dict entry is now a reference
2523        let obj = doc.resolve(dict_id).unwrap();
2524        let dict = obj.as_dict().unwrap();
2525        let entry = dict.get(&Name::from_bytes(b"clams".to_vec())).unwrap();
2526        assert!(entry.is_reference());
2527        // Resolve the reference to get the original value
2528        if let Object::Reference(ref_id) = entry {
2529            let resolved = doc.resolve(*ref_id).unwrap();
2530            assert_eq!(resolved.as_i64(), Some(42));
2531        }
2532    }
2533}