Skip to main content

otf_pdf_doc/
edit.rs

1//! Editing operations (docs/PLAN.md Phase 2): pages, outlines, named
2//! destinations, metadata and attachments. Every edit goes into the
3//! copy-on-write overlay, so the base file is untouched and an incremental
4//! save appends exactly what changed.
5
6use std::collections::{HashMap, HashSet, VecDeque};
7use std::sync::Arc;
8
9use otf_pdf_syntax::{Dict, Error, Name, ObjId, Object, OpenOptions, PdfString, Result, Stream};
10
11use crate::document::{Document, Info, OutlineItem, name_tree};
12use crate::pages::{Rect, standalone_page_dict};
13use crate::store::Store;
14use crate::text::{decode_text_string, encode_text_string};
15use crate::xmp;
16
17/// The bytes of an empty document (a catalog and an empty page tree) that
18/// [`Document::new`] opens; offsets are computed, not hand-counted.
19fn empty_pdf() -> Vec<u8> {
20    let mut out = b"%PDF-1.7\n".to_vec();
21    let mut offsets = Vec::new();
22    for body in [
23        "<< /Type /Catalog /Pages 2 0 R >>",
24        "<< /Type /Pages /Kids [] /Count 0 >>",
25    ] {
26        offsets.push(out.len());
27        out.extend_from_slice(format!("{} 0 obj\n{body}\nendobj\n", offsets.len()).as_bytes());
28    }
29    let xref = out.len();
30    out.extend_from_slice(b"xref\n0 3\n0000000000 65535 f \n");
31    for o in offsets {
32        out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
33    }
34    out.extend_from_slice(
35        format!("trailer\n<< /Size 3 /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n").as_bytes(),
36    );
37    out
38}
39
40/// Options for [`Document::add_attachment`].
41#[derive(Debug, Clone, Default, PartialEq, Eq)]
42pub struct AttachmentOptions {
43    /// MIME type, written as the embedded file's `/Subtype`.
44    pub mime_type: Option<String>,
45    /// `/Desc` of the file specification.
46    pub description: Option<String>,
47    /// Creation date as a PDF date string (`D:...`); no clock is read.
48    pub creation_date: Option<String>,
49    /// Modification date as a PDF date string.
50    pub mod_date: Option<String>,
51}
52
53fn text(s: &str) -> Object {
54    Object::String(PdfString::literal(encode_text_string(s)))
55}
56
57impl Document {
58    /// A new, empty document (one empty page tree, no pages).
59    pub fn new() -> Result<Self> {
60        Self::open(empty_pdf(), OpenOptions::default())
61    }
62
63    /// Replaces the catalog with `catalog`, keeping its object number.
64    fn set_catalog(&mut self, catalog: Dict) -> Result<()> {
65        match self.catalog_id() {
66            Some(id) => self.store_mut().set(id, Object::Dict(catalog)),
67            None => {
68                let id = self.store_mut().add(Object::Dict(catalog))?;
69                self.store_mut().set_trailer_entry("Root", Object::Ref(id));
70            }
71        }
72        Ok(())
73    }
74
75    // ---- Pages -------------------------------------------------------------
76
77    /// Makes the page tree exactly `pages` (dictionaries already standalone,
78    /// with their ids where they have one), as one flat `/Pages` node.
79    fn write_page_tree(&mut self, pages: Vec<(Option<ObjId>, Dict)>) -> Result<()> {
80        let catalog = self.catalog();
81        let tree = match catalog.get(b"Pages") {
82            Some(Object::Ref(id)) => *id,
83            _ => self.store_mut().allocate()?,
84        };
85        let mut kids = Vec::with_capacity(pages.len());
86        let mut used = HashSet::new();
87        for (id, mut dict) in pages {
88            dict.insert("Parent", tree);
89            // A page listed twice becomes two objects.
90            let id = match id {
91                Some(id) if used.insert(id) => id,
92                _ => self.store_mut().allocate()?,
93            };
94            self.store_mut().set(id, Object::Dict(dict));
95            kids.push(Object::Ref(id));
96        }
97        let mut node = Dict::new();
98        node.insert("Type", Name::from("Pages"));
99        node.insert("Count", kids.len() as i64);
100        node.insert("Kids", kids);
101        self.store_mut().set(tree, Object::Dict(node));
102        if catalog.get_ref(b"Pages") != Some(tree) {
103            let mut catalog = catalog;
104            catalog.insert("Pages", tree);
105            self.set_catalog(catalog)?;
106        }
107        self.invalidate();
108        Ok(())
109    }
110
111    /// Keeps the pages at `indices` (0-based), in that order. Pages left out
112    /// are removed along with outline items and named destinations that
113    /// point to them; a page listed twice is duplicated.
114    pub fn select_pages(&mut self, indices: &[usize]) -> Result<()> {
115        let pages = self.pages()?;
116        let mut chosen = Vec::with_capacity(indices.len());
117        for &i in indices {
118            let page = pages
119                .get(i)
120                .ok_or(Error::syntax(0, "page index out of range"))?;
121            chosen.push((page.id, standalone_page_dict(page)));
122        }
123        let kept: HashSet<ObjId> = chosen.iter().filter_map(|(id, _)| *id).collect();
124        let removed: HashSet<ObjId> = pages
125            .iter()
126            .filter_map(|p| p.id)
127            .filter(|id| !kept.contains(id))
128            .collect();
129        self.write_page_tree(chosen)?;
130        if !removed.is_empty() {
131            for id in &removed {
132                self.store_mut().delete(*id);
133            }
134            self.remove_destinations_to(&removed)?;
135        }
136        Ok(())
137    }
138
139    /// Moves pages into `order`, a permutation of `0..page_count`.
140    pub fn reorder_pages(&mut self, order: &[usize]) -> Result<()> {
141        let count = self.page_count()?;
142        let mut sorted = order.to_vec();
143        sorted.sort_unstable();
144        if sorted != (0..count).collect::<Vec<_>>() {
145            return Err(Error::syntax(
146                0,
147                "page order must be a permutation of every page",
148            ));
149        }
150        self.select_pages(order)
151    }
152
153    /// Deletes the pages at `indices`, cleaning up outline items and named
154    /// destinations that point to them.
155    pub fn delete_pages(&mut self, indices: &[usize]) -> Result<()> {
156        let count = self.page_count()?;
157        let drop: HashSet<usize> = indices.iter().copied().collect();
158        if drop.iter().any(|&i| i >= count) {
159            return Err(Error::syntax(0, "page index out of range"));
160        }
161        let keep: Vec<usize> = (0..count).filter(|i| !drop.contains(i)).collect();
162        self.select_pages(&keep)
163    }
164
165    /// Edits one page's own dictionary through `f`.
166    fn edit_page(&mut self, index: usize, f: impl FnOnce(&mut Dict)) -> Result<()> {
167        let pages = self.pages()?;
168        let page = pages
169            .get(index)
170            .ok_or(Error::syntax(0, "page index out of range"))?;
171        let mut dict = page.dict.clone();
172        f(&mut dict);
173        match page.id {
174            Some(id) => self.store_mut().set(id, Object::Dict(dict)),
175            // A direct page dictionary: rebuild the tree to give it a number.
176            None => {
177                let mut all: Vec<_> = pages
178                    .iter()
179                    .map(|p| (p.id, standalone_page_dict(p)))
180                    .collect();
181                if let Some(slot) = all.get_mut(index) {
182                    slot.1 = dict;
183                }
184                self.write_page_tree(all)?;
185            }
186        }
187        self.invalidate();
188        Ok(())
189    }
190
191    /// Rotates a page clockwise by `degrees` (a multiple of 90) on top of
192    /// its current rotation.
193    pub fn rotate_page(&mut self, index: usize, degrees: i32) -> Result<()> {
194        if degrees % 90 != 0 {
195            return Err(Error::syntax(0, "rotation must be a multiple of 90"));
196        }
197        let current = i32::from(
198            self.pages()?
199                .get(index)
200                .ok_or(Error::syntax(0, "page index out of range"))?
201                .rotate,
202        );
203        let rotate = (current + degrees).rem_euclid(360);
204        self.edit_page(index, |d| {
205            d.insert("Rotate", i64::from(rotate));
206        })
207    }
208
209    /// Sets a page's crop box (the visible region).
210    pub fn set_crop_box(&mut self, index: usize, rect: Rect) -> Result<()> {
211        self.edit_page(index, |d| {
212            d.insert("CropBox", rect.to_object());
213        })
214    }
215
216    /// Sets a page's media box (the physical page).
217    pub fn set_media_box(&mut self, index: usize, rect: Rect) -> Result<()> {
218        self.edit_page(index, |d| {
219            d.insert("MediaBox", rect.to_object());
220        })
221    }
222
223    /// Appends pages of `other` (all of them when `indices` is `None`),
224    /// copying every object they use. Objects shared by several imported
225    /// pages are copied once.
226    pub fn append_pages_from(&mut self, other: &Document, indices: Option<&[usize]>) -> Result<()> {
227        let theirs = other.pages()?;
228        let wanted: Vec<usize> = match indices {
229            Some(i) => i.to_vec(),
230            None => (0..theirs.len()).collect(),
231        };
232        let mut map: HashMap<ObjId, ObjId> = HashMap::new();
233        // Pages first, so annotations' /P and links resolve to the copies.
234        let mut new_pages = Vec::with_capacity(wanted.len());
235        for &i in &wanted {
236            let page = theirs
237                .get(i)
238                .ok_or(Error::syntax(0, "page index out of range"))?;
239            let id = self.store_mut().allocate()?;
240            if let Some(src) = page.id {
241                map.entry(src).or_insert(id);
242            }
243            new_pages.push((id, page));
244        }
245        let mut pages: Vec<(Option<ObjId>, Dict)> = self
246            .pages()?
247            .iter()
248            .map(|p| (p.id, standalone_page_dict(p)))
249            .collect();
250        for (id, page) in new_pages {
251            let mut dict = standalone_page_dict(page);
252            // The structure tree and article threads are not copied.
253            for key in [&b"StructParents"[..], b"B"] {
254                dict.remove(key);
255            }
256            let copied = import(
257                &mut self.store,
258                other.store(),
259                &Object::Dict(dict),
260                &mut map,
261            )?;
262            let Object::Dict(dict) = copied else {
263                return Err(Error::syntax(0, "page did not copy as a dictionary"));
264            };
265            pages.push((Some(id), dict));
266        }
267        self.write_page_tree(pages)
268    }
269
270    /// A new document holding copies of the pages at `indices`.
271    pub fn extract_pages(&self, indices: &[usize]) -> Result<Document> {
272        let mut out = Document::new()?;
273        out.append_pages_from(self, Some(indices))?;
274        Ok(out)
275    }
276
277    // ---- Destinations ------------------------------------------------------
278
279    /// Removes outline items and named destinations that target `pages`.
280    fn remove_destinations_to(&mut self, pages: &HashSet<ObjId>) -> Result<()> {
281        let named = self.named_destinations()?;
282        let dead_names: HashSet<Vec<u8>> = named
283            .iter()
284            .filter(|(_, dest)| targets(dest, pages, &HashMap::new(), self.store()))
285            .map(|(k, _)| k.clone())
286            .collect();
287        let named_map: HashMap<Vec<u8>, Object> = named.into_iter().collect();
288
289        let outlines = self.outlines()?;
290        let (kept, changed) = prune_outlines(outlines, &|item: &OutlineItem| {
291            let dest = item.dest.clone().or_else(|| {
292                item.action
293                    .as_ref()
294                    .filter(|a| a.get_name(b"S") == Some(b"GoTo"))
295                    .and_then(|a| a.get(b"D").cloned())
296            });
297            dest.is_some_and(|d| targets(&d, pages, &named_map, self.store()))
298        });
299        if changed {
300            self.set_outlines(&kept)?;
301        }
302        if !dead_names.is_empty() {
303            self.remove_named_destinations(&dead_names)?;
304        }
305        Ok(())
306    }
307
308    /// Named destinations from `/Dests` (PDF 1.1) and the `/Names /Dests`
309    /// name tree, keyed by name.
310    pub fn named_destinations(&self) -> Result<Vec<(Vec<u8>, Object)>> {
311        let catalog = self.catalog();
312        let store = self.store();
313        let mut out = Vec::new();
314        if let Some(dests) = catalog.get(b"Dests").and_then(|d| store.resolve_dict(d)) {
315            for (k, v) in dests.iter() {
316                out.push((k.as_bytes().to_vec(), store.resolve(v)));
317            }
318        }
319        if let Some(tree) = catalog
320            .get(b"Names")
321            .and_then(|n| store.resolve_dict(n))
322            .and_then(|n| n.get(b"Dests").cloned())
323        {
324            for (k, v) in name_tree(store, &tree)? {
325                out.push((k, store.resolve(&v)));
326            }
327        }
328        Ok(out)
329    }
330
331    fn remove_named_destinations(&mut self, dead: &HashSet<Vec<u8>>) -> Result<()> {
332        let mut catalog = self.catalog();
333        if let Some(dests_obj) = catalog.get(b"Dests").cloned()
334            && let Some(mut dests) = self.store().resolve_dict(&dests_obj)
335        {
336            for name in dead {
337                dests.remove(name);
338            }
339            match dests_obj {
340                Object::Ref(id) => self.store_mut().set(id, Object::Dict(dests)),
341                _ => {
342                    catalog.insert("Dests", dests);
343                }
344            }
345        }
346        let names_tree = catalog
347            .get(b"Names")
348            .and_then(|n| self.store().resolve_dict(n))
349            .and_then(|n| n.get(b"Dests").cloned());
350        if let Some(tree) = names_tree {
351            let entries: Vec<_> = name_tree(self.store(), &tree)?
352                .into_iter()
353                .filter(|(k, _)| !dead.contains(k))
354                .collect();
355            catalog = self.with_name_tree(catalog, b"Dests", entries)?;
356        }
357        self.set_catalog(catalog)
358    }
359
360    /// Returns `catalog` with `/Names /<tree>` replaced by a flat tree of
361    /// `entries` (sorted by key), or removed when empty.
362    fn with_name_tree(
363        &mut self,
364        mut catalog: Dict,
365        tree: &[u8],
366        mut entries: Vec<(Vec<u8>, Object)>,
367    ) -> Result<Dict> {
368        entries.sort_by(|a, b| a.0.cmp(&b.0));
369        entries.dedup_by(|a, b| a.0 == b.0);
370        let names_obj = catalog.get(b"Names").cloned();
371        let mut names = names_obj
372            .as_ref()
373            .and_then(|n| self.store().resolve_dict(n))
374            .unwrap_or_default();
375        if entries.is_empty() {
376            names.remove(tree);
377        } else {
378            let mut flat = Vec::with_capacity(entries.len() * 2);
379            for (k, v) in entries {
380                flat.push(Object::String(PdfString::literal(k)));
381                flat.push(v);
382            }
383            let mut node = Dict::new();
384            node.insert("Names", flat);
385            let id = self.store_mut().add(Object::Dict(node))?;
386            names.insert(Name::new(tree.to_vec()), Object::Ref(id));
387        }
388        match names_obj {
389            Some(Object::Ref(id)) => self.store_mut().set(id, Object::Dict(names)),
390            _ if names.is_empty() => {
391                catalog.remove(b"Names");
392            }
393            _ => {
394                catalog.insert("Names", names);
395            }
396        }
397        Ok(catalog)
398    }
399
400    // ---- Outlines ----------------------------------------------------------
401
402    /// Replaces the outline tree with `items` (ids in the items are ignored;
403    /// fresh objects are written). An empty list removes the outline.
404    pub fn set_outlines(&mut self, items: &[OutlineItem]) -> Result<()> {
405        let mut catalog = self.catalog();
406        if items.is_empty() {
407            catalog.remove(b"Outlines");
408            return self.set_catalog(catalog);
409        }
410        let root = self.store_mut().allocate()?;
411        let (first, last, visible) = self.write_outline_level(root, items)?;
412        let mut dict = Dict::new();
413        dict.insert("Type", Name::from("Outlines"));
414        dict.insert("First", first);
415        dict.insert("Last", last);
416        dict.insert("Count", visible as i64);
417        self.store_mut().set(root, Object::Dict(dict));
418        catalog.insert("Outlines", root);
419        self.set_catalog(catalog)
420    }
421
422    /// Writes one sibling list under `parent`; returns its first and last
423    /// ids and how many items it shows when its parent is open.
424    fn write_outline_level(
425        &mut self,
426        parent: ObjId,
427        items: &[OutlineItem],
428    ) -> Result<(ObjId, ObjId, usize)> {
429        let ids: Vec<ObjId> = items
430            .iter()
431            .map(|_| self.store_mut().allocate())
432            .collect::<Result<_>>()?;
433        let mut visible = 0;
434        for (i, (item, &id)) in items.iter().zip(&ids).enumerate() {
435            let mut dict = item.extra.clone();
436            dict.insert("Title", text(&item.title));
437            dict.insert("Parent", parent);
438            if i > 0
439                && let Some(prev) = ids.get(i - 1)
440            {
441                dict.insert("Prev", *prev);
442            }
443            if let Some(next) = ids.get(i + 1) {
444                dict.insert("Next", *next);
445            }
446            if let Some(dest) = &item.dest {
447                dict.insert("Dest", dest.clone());
448            }
449            if let Some(action) = &item.action {
450                dict.insert("A", action.clone());
451            }
452            visible += 1;
453            if !item.children.is_empty() {
454                let (first, last, below) = self.write_outline_level(id, &item.children)?;
455                dict.insert("First", first);
456                dict.insert("Last", last);
457                // Positive: open, showing `below` descendants; negative:
458                // closed, hiding them (ยง12.3.3).
459                if item.open {
460                    dict.insert("Count", below as i64);
461                    visible += below;
462                } else {
463                    dict.insert("Count", -(below as i64));
464                }
465            }
466            self.store_mut().set(id, Object::Dict(dict));
467        }
468        let first = *ids.first().ok_or(Error::syntax(0, "empty outline level"))?;
469        let last = *ids.last().ok_or(Error::syntax(0, "empty outline level"))?;
470        Ok((first, last, visible))
471    }
472
473    // ---- Metadata ----------------------------------------------------------
474
475    /// Replaces the document information: fields that are `None` are
476    /// removed, other entries of the Info dictionary are kept. The XMP
477    /// packet is rewritten to match (D23).
478    pub fn set_info(&mut self, info: &Info) -> Result<()> {
479        let info_obj = self.store().trailer().get(b"Info").cloned();
480        let mut dict = info_obj
481            .as_ref()
482            .and_then(|i| self.store().resolve_dict(i))
483            .unwrap_or_default();
484        for (key, value) in [
485            ("Title", &info.title),
486            ("Author", &info.author),
487            ("Subject", &info.subject),
488            ("Keywords", &info.keywords),
489            ("Creator", &info.creator),
490            ("Producer", &info.producer),
491            ("CreationDate", &info.creation_date),
492            ("ModDate", &info.mod_date),
493        ] {
494            match value {
495                // Dates are ASCII by construction; keep them plain.
496                Some(v) => {
497                    dict.insert(key, text(v));
498                }
499                None => {
500                    dict.remove(key.as_bytes());
501                }
502            }
503        }
504        match &info.trapped {
505            Some(t) => {
506                dict.insert("Trapped", Name::from(t.as_str()));
507            }
508            None => {
509                dict.remove(b"Trapped");
510            }
511        }
512        match info_obj {
513            Some(Object::Ref(id)) => self.store_mut().set(id, Object::Dict(dict)),
514            _ => {
515                let id = self.store_mut().add(Object::Dict(dict))?;
516                self.store_mut().set_trailer_entry("Info", Object::Ref(id));
517            }
518        }
519        let previous = self.xmp_metadata()?;
520        let packet = xmp::packet(info, previous.as_deref());
521        let mut meta = Dict::new();
522        meta.insert("Type", Name::from("Metadata"));
523        meta.insert("Subtype", Name::from("XML"));
524        let stream = Object::Stream(Stream {
525            dict: meta,
526            raw: packet.into_bytes().into(),
527        });
528        let mut catalog = self.catalog();
529        match catalog.get(b"Metadata") {
530            Some(Object::Ref(id)) => {
531                let id = *id;
532                self.store_mut().set(id, stream);
533            }
534            _ => {
535                let id = self.store_mut().add(stream)?;
536                catalog.insert("Metadata", id);
537                self.set_catalog(catalog)?;
538            }
539        }
540        Ok(())
541    }
542
543    // ---- Attachments -------------------------------------------------------
544
545    /// Embeds `data` as an attachment named `name`, replacing one of the same
546    /// name.
547    pub fn add_attachment(
548        &mut self,
549        name: &str,
550        data: &[u8],
551        options: &AttachmentOptions,
552    ) -> Result<()> {
553        let mut params = Dict::new();
554        params.insert("Size", data.len() as i64);
555        if let Some(d) = &options.creation_date {
556            params.insert("CreationDate", text(d));
557        }
558        if let Some(d) = &options.mod_date {
559            params.insert("ModDate", text(d));
560        }
561        let mut ef_dict = Dict::new();
562        ef_dict.insert("Type", Name::from("EmbeddedFile"));
563        if let Some(mime) = &options.mime_type {
564            ef_dict.insert("Subtype", Name::from(mime.as_str()));
565        }
566        ef_dict.insert("Params", params);
567        let stream = self.store_mut().add(Object::Stream(Stream {
568            dict: ef_dict,
569            raw: Arc::from(data),
570        }))?;
571        let mut ef = Dict::new();
572        ef.insert("F", stream);
573        ef.insert("UF", stream);
574        let mut spec = Dict::new();
575        spec.insert("Type", Name::from("Filespec"));
576        // /F is a byte string for old readers; /UF the Unicode name.
577        let ascii: String = name
578            .chars()
579            .map(|c| if c.is_ascii() { c } else { '_' })
580            .collect();
581        spec.insert("F", PdfString::literal(ascii.into_bytes()));
582        spec.insert("UF", text(name));
583        spec.insert("EF", ef);
584        if let Some(desc) = &options.description {
585            spec.insert("Desc", text(desc));
586        }
587        let spec = self.store_mut().add(Object::Dict(spec))?;
588        let key = encode_text_string(name);
589        let mut entries = self.embedded_files()?;
590        entries.retain(|(k, _)| *k != key);
591        entries.push((key, Object::Ref(spec)));
592        let catalog = self.catalog();
593        let catalog = self.with_name_tree(catalog, b"EmbeddedFiles", entries)?;
594        self.set_catalog(catalog)
595    }
596
597    /// Removes the attachment named `name`; returns whether it existed.
598    pub fn remove_attachment(&mut self, name: &str) -> Result<bool> {
599        let mut entries = self.embedded_files()?;
600        let before = entries.len();
601        entries.retain(|(k, _)| decode_text_string(k) != name);
602        if entries.len() == before {
603            return Ok(false);
604        }
605        let catalog = self.catalog();
606        let catalog = self.with_name_tree(catalog, b"EmbeddedFiles", entries)?;
607        self.set_catalog(catalog)?;
608        Ok(true)
609    }
610
611    fn embedded_files(&self) -> Result<Vec<(Vec<u8>, Object)>> {
612        let tree = self
613            .catalog()
614            .get(b"Names")
615            .and_then(|n| self.store().resolve_dict(n))
616            .and_then(|n| n.get(b"EmbeddedFiles").cloned());
617        match tree {
618            Some(t) => name_tree(self.store(), &t),
619            None => Ok(Vec::new()),
620        }
621    }
622}
623
624/// Whether a destination (explicit array, name or string) targets one of
625/// `pages`.
626fn targets(
627    dest: &Object,
628    pages: &HashSet<ObjId>,
629    named: &HashMap<Vec<u8>, Object>,
630    store: &Store,
631) -> bool {
632    let explicit = match dest {
633        Object::Name(n) => named.get(n.as_bytes()).map(|d| store.resolve(d)),
634        Object::String(s) => named.get(&s.bytes).map(|d| store.resolve(d)),
635        other => Some(store.resolve(other)),
636    };
637    let array = match explicit {
638        // A named destination may be a dictionary with /D.
639        Some(Object::Dict(d)) => d.get(b"D").map(|x| store.resolve(x)),
640        other => other,
641    };
642    array
643        .as_ref()
644        .and_then(Object::as_array)
645        .and_then(|a| a.first())
646        .and_then(Object::as_ref)
647        .is_some_and(|page| pages.contains(&page))
648}
649
650/// Drops items for which `dead` holds, promoting their children; returns the
651/// pruned list and whether anything changed.
652fn prune_outlines(
653    items: Vec<OutlineItem>,
654    dead: &dyn Fn(&OutlineItem) -> bool,
655) -> (Vec<OutlineItem>, bool) {
656    let mut out = Vec::with_capacity(items.len());
657    let mut changed = false;
658    for mut item in items {
659        let (children, child_changed) = prune_outlines(std::mem::take(&mut item.children), dead);
660        changed |= child_changed;
661        if dead(&item) {
662            changed = true;
663            out.extend(children);
664        } else {
665            item.children = children;
666            out.push(item);
667        }
668    }
669    (out, changed)
670}
671
672/// Deep-copies `object` from `src` into `dst`, giving every referenced
673/// object a new number in `dst`. `map` carries ids already copied (and
674/// pre-assigned ones), so shared objects are copied once and cycles end.
675pub fn import(
676    dst: &mut Store,
677    src: &Store,
678    object: &Object,
679    map: &mut HashMap<ObjId, ObjId>,
680) -> Result<Object> {
681    let mut queue: VecDeque<ObjId> = VecDeque::new();
682    let top = remap_into(dst, object, map, &mut queue)?;
683    while let Some(src_id) = queue.pop_front() {
684        dst.limits().charge(1)?;
685        let Some(&dst_id) = map.get(&src_id) else {
686            continue;
687        };
688        let obj = match src.get(src_id) {
689            Ok(o) => (*o).clone(),
690            Err(_) => Object::Null,
691        };
692        let copied = remap_into(dst, &obj, map, &mut queue)?;
693        dst.set(dst_id, copied);
694    }
695    Ok(top)
696}
697
698/// Copies `object`, rewriting references through `map` and allocating (and
699/// queueing for copy) any not seen yet.
700fn remap_into(
701    dst: &mut Store,
702    object: &Object,
703    map: &mut HashMap<ObjId, ObjId>,
704    queue: &mut VecDeque<ObjId>,
705) -> Result<Object> {
706    Ok(match object {
707        Object::Ref(id) => {
708            let new = match map.get(id) {
709                Some(n) => *n,
710                None => {
711                    let n = dst.allocate()?;
712                    map.insert(*id, n);
713                    queue.push_back(*id);
714                    n
715                }
716            };
717            Object::Ref(new)
718        }
719        Object::Array(items) => Object::Array(
720            items
721                .iter()
722                .map(|i| remap_into(dst, i, map, queue))
723                .collect::<Result<_>>()?,
724        ),
725        Object::Dict(d) => Object::Dict(remap_dict_into(dst, d, map, queue)?),
726        Object::Stream(s) => Object::Stream(Stream {
727            dict: remap_dict_into(dst, &s.dict, map, queue)?,
728            raw: Arc::clone(&s.raw),
729        }),
730        other => other.clone(),
731    })
732}
733
734fn remap_dict_into(
735    dst: &mut Store,
736    dict: &Dict,
737    map: &mut HashMap<ObjId, ObjId>,
738    queue: &mut VecDeque<ObjId>,
739) -> Result<Dict> {
740    dict.iter()
741        .map(|(k, v)| Ok((k.clone(), remap_into(dst, v, map, queue)?)))
742        .collect()
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748
749    fn item(title: &str, children: Vec<OutlineItem>) -> OutlineItem {
750        OutlineItem {
751            id: None,
752            title: title.into(),
753            dest: None,
754            action: None,
755            open: true,
756            children,
757            extra: Dict::new(),
758        }
759    }
760
761    #[test]
762    fn empty_document() {
763        let doc = Document::new().unwrap();
764        assert_eq!(doc.page_count().unwrap(), 0);
765        assert!(doc.diagnostics().is_empty(), "{:?}", doc.diagnostics());
766    }
767
768    #[test]
769    fn pruning_promotes_children() {
770        let tree = vec![
771            item("a", vec![item("a1", vec![]), item("x", vec![])]),
772            item("x", vec![item("x1", vec![])]),
773        ];
774        let (kept, changed) = prune_outlines(tree, &|i| i.title == "x");
775        assert!(changed);
776        let titles: Vec<_> = kept.iter().map(|i| i.title.as_str()).collect();
777        assert_eq!(titles, ["a", "x1"]);
778        assert_eq!(kept[0].children.len(), 1);
779        let (_, unchanged) = prune_outlines(vec![item("a", vec![])], &|_| false);
780        assert!(!unchanged);
781    }
782}