Skip to main content

pdfrum_edit/content/
apply.rs

1//! Turning a regenerated page into objects on the way out.
2//!
3//! [`super::regen::regenerate`] produces bytes and a resource dictionary;
4//! this puts them into an [`EditDoc`] — replacing the streams that survive,
5//! adding the ones that are new, reshaping `/Contents`, and repointing
6//! `/Resources`.
7//!
8//! # An element is replaced in place unless something else points at it
9//!
10//! A content stream shared between two pages cannot be edited in place:
11//! rewriting it for one page would silently rewrite the other. So a shared
12//! element is copied into a fresh object and the *reference* is moved,
13//! leaving the original where the other page still finds it. Same for a
14//! shared `/Contents` array and a shared `/Resources` dictionary.
15//!
16//! # A regenerated stream carries no filter
17//!
18//! The bytes written are the operators, uncompressed, and any `/Filter` the
19//! original element had is dropped along with the `/DecodeParms` that went
20//! with it. Keeping the key while replacing the data with plaintext would
21//! describe the stream as compressed when it is not, which is a file nothing
22//! can read.
23
24use std::collections::{BTreeMap, BTreeSet};
25
26use pdfrum_object::{ByteSpan, Dict, Name, ObjRef, Object, Resolve, Stream};
27
28use crate::content::regen::{ContentsShape, PageRewrite};
29use crate::doc::EditDoc;
30
31/// How many objects still reference each object number.
32///
33/// Two page dictionaries naming one content stream is what makes that stream
34/// shared, and a shared stream is copied rather than edited.
35pub type ShareCounts = BTreeSet<u32>;
36
37/// Apply a page's regenerated content to `edit`.
38///
39/// `page_ref` names the page dictionary; `page_dict` is its current contents;
40/// `shared` names the object numbers more than one object points at.
41///
42/// Returns the map from each object's old content-stream index to its new one,
43/// so the caller can renumber the page-object graph it still holds. An index
44/// missing from the map belongs to an element that was removed, and collapses
45/// to zero.
46pub fn apply_rewrite(
47    edit: &mut EditDoc<'_>,
48    page_ref: ObjRef,
49    page_dict: &Dict,
50    rewrite: &PageRewrite,
51    shared: &ShareCounts,
52) -> BTreeMap<usize, usize> {
53    let mut shape = ContentsShape::read(page_dict, edit);
54    let mut removed: BTreeSet<usize> = BTreeSet::new();
55    let mut contents_changed = false;
56
57    for regenerated in &rewrite.streams {
58        let index = regenerated.stream;
59        if regenerated.bytes.is_empty() {
60            // An empty buffer is the deletion signal, not an empty stream.
61            // A streamless element has nothing to delete.
62            if let Some(index) = index {
63                removed.insert(index);
64                contents_changed = true;
65            }
66            continue;
67        }
68        let stream = content_stream(regenerated.bytes.as_bytes());
69        let existing = index.and_then(|i| shape.elements().get(i).copied());
70        match existing {
71            // The element exists: rewrite it, or copy it if it is shared.
72            Some(reference) if !shared.contains(&reference.num) => {
73                edit.replace(reference, Object::Stream(Box::new(stream)));
74            }
75            Some(_) => {
76                let fresh = edit.add(Object::Stream(Box::new(stream)));
77                if let Some(index) = index {
78                    shape = replace_element(&shape, index, fresh);
79                    contents_changed = true;
80                }
81            }
82            // A brand-new element, or one past the end of the array.
83            None => {
84                let fresh = edit.add(Object::Stream(Box::new(stream)));
85                let (_, next) = shape.with_added(fresh);
86                shape = next;
87                contents_changed = true;
88            }
89        }
90    }
91
92    let (shape, mapping) = if removed.is_empty() {
93        (shape, BTreeMap::new())
94    } else {
95        shape.with_removed(&removed)
96    };
97
98    let mut dict = page_dict.clone();
99    if contents_changed {
100        dict = set_contents(edit, &dict, &shape, page_dict, shared);
101    }
102    dict = set_resources(edit, &dict, &rewrite.resources, shared);
103    edit.replace(page_ref, Object::Dict(dict));
104    mapping
105}
106
107/// A content stream holding `bytes`, with no filter.
108fn content_stream(bytes: &[u8]) -> Stream {
109    let dict = Dict::from_pairs([(
110        pdfrum_object::names::LENGTH.clone(),
111        Object::Int(i64::try_from(bytes.len()).unwrap_or(0)),
112    )]);
113    Stream::new(dict, ByteSpan::from(bytes.to_vec()))
114}
115
116/// The shape with element `index` repointed at `fresh`.
117fn replace_element(shape: &ContentsShape, index: usize, fresh: ObjRef) -> ContentsShape {
118    match shape {
119        ContentsShape::Single(_) if index == 0 => ContentsShape::Single(fresh),
120        ContentsShape::Array(elements) => {
121            let mut next = elements.clone();
122            if let Some(slot) = next.get_mut(index) {
123                *slot = fresh;
124            }
125            ContentsShape::Array(next)
126        }
127        other => other.clone(),
128    }
129}
130
131/// Write the new `/Contents` into a copy of the page dictionary.
132///
133/// An array reached through a reference is written back through that same
134/// reference — unless it is shared, in which case a fresh array is added and
135/// the page points at that instead.
136fn set_contents(
137    edit: &mut EditDoc<'_>,
138    dict: &Dict,
139    shape: &ContentsShape,
140    original: &Dict,
141    shared: &ShareCounts,
142) -> Dict {
143    let key = pdfrum_object::names::CONTENTS;
144    let value = match shape {
145        ContentsShape::Absent => None,
146        ContentsShape::Single(reference) => Some(Object::Ref(*reference)),
147        ContentsShape::Array(elements) => {
148            let array = shape
149                .to_object(None)
150                .unwrap_or(Object::Array(pdfrum_object::Array::new()));
151            // The array object the page already points at is reused, when the
152            // page had one to itself — and when it is not one of the elements.
153            // A page whose `/Contents` was a lone *stream* points at that
154            // stream, and writing the new array over it would destroy the very
155            // element the array's first entry names.
156            let reusable = match original.raw(key) {
157                Some(Object::Ref(reference)) => {
158                    !shared.contains(&reference.num) && !elements.contains(reference)
159                }
160                _ => false,
161            };
162            match original.raw(key) {
163                Some(Object::Ref(reference)) if reusable => {
164                    edit.replace(*reference, array);
165                    Some(Object::Ref(*reference))
166                }
167                _ => Some(Object::Ref(edit.add(array))),
168            }
169        }
170    };
171    with_key(dict, key, value)
172}
173
174/// Write the swept `/Resources` into a copy of the page dictionary.
175fn set_resources(
176    edit: &mut EditDoc<'_>,
177    dict: &Dict,
178    resources: &Dict,
179    shared: &ShareCounts,
180) -> Dict {
181    let key = pdfrum_object::names::RESOURCES;
182    let value = match dict.raw(key) {
183        // The page reaches its resources through a reference it does not
184        // share: rewrite that object and leave the page's key alone.
185        Some(Object::Ref(reference)) if !shared.contains(&reference.num) => {
186            edit.replace(*reference, Object::Dict(resources.clone()));
187            return dict.clone();
188        }
189        // Shared, or written inline, or absent: the page gets its own copy.
190        _ => Some(Object::Dict(resources.clone())),
191    };
192    with_key(dict, key, value)
193}
194
195/// A copy of `dict` with `key` set to `value`, or removed when `value` is
196/// `None`, keeping every other entry in its place.
197fn with_key(dict: &Dict, key: &Name, value: Option<Object>) -> Dict {
198    let mut out = Dict::new();
199    let mut written = false;
200    for (existing, held) in dict.iter() {
201        if existing == key {
202            if let Some(value) = value.clone()
203                && !written
204            {
205                out.push(existing.clone(), value);
206                written = true;
207            }
208        } else {
209            out.push(existing.clone(), held.clone());
210        }
211    }
212    if !written && let Some(value) = value {
213        out.push(key.clone(), value);
214    }
215    out
216}
217
218/// The object numbers more than one object in `doc` points at.
219///
220/// A shared object cannot be edited in place, so this is what decides between
221/// rewriting an element and copying it. The sweep walks the reachable graph
222/// once, counting references; anything reached twice is shared.
223#[must_use]
224pub fn shared_objects(doc: &EditDoc<'_>) -> ShareCounts {
225    let mut seen: BTreeMap<u32, u32> = BTreeMap::new();
226    let mut queue: Vec<Object> = Vec::new();
227    let mut visited: BTreeSet<u32> = BTreeSet::new();
228
229    if let Ok(root) = doc.base().catalog() {
230        queue.push(Object::Dict(root));
231    }
232    for (_, object) in doc.edited() {
233        queue.push((**object).clone());
234    }
235
236    while let Some(object) = queue.pop() {
237        match object {
238            Object::Ref(reference) => {
239                *seen.entry(reference.num).or_default() += 1;
240                if visited.insert(reference.num)
241                    && let Ok(target) = doc.fetch(reference)
242                {
243                    queue.push((*target).clone());
244                }
245            }
246            Object::Dict(dict) => {
247                for (_, value) in dict.iter() {
248                    queue.push(value.clone());
249                }
250            }
251            Object::Array(array) => {
252                for value in array.iter() {
253                    queue.push(value.clone());
254                }
255            }
256            Object::Stream(stream) => {
257                for (_, value) in stream.dict.iter() {
258                    queue.push(value.clone());
259                }
260            }
261            _ => {}
262        }
263    }
264
265    seen.into_iter()
266        .filter(|(_, count)| *count > 1)
267        .map(|(num, _)| num)
268        .collect()
269}
270
271#[cfg(test)]
272mod tests {
273    use super::{apply_rewrite, shared_objects};
274    use crate::content::regen::{PageRewrite, Regenerated};
275    use crate::doc::EditDoc;
276    use pdfrum_object::{Dict, Name, ObjRef, Object, Resolve};
277    use pdfrum_parser::{Document, LoadOptions, load};
278    use std::collections::BTreeSet;
279    use std::sync::Arc;
280
281    /// A one-page document whose `/Contents` is a lone stream (object 4).
282    fn single_stream() -> Document {
283        let file = b"%PDF-1.7\n\
2841 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n\
2852 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
2863 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R >>\nendobj\n\
2874 0 obj\n<< /Length 8 >>\nstream\n0 0 1 1 re f\nendstream\nendobj\n\
288trailer\n<< /Root 1 0 R /Size 5 >>\n";
289        load(Arc::from(&file[..]), &LoadOptions::default()).expect("opens")
290    }
291
292    /// A one-page document whose `/Contents` is an array of two streams.
293    fn two_streams() -> Document {
294        let file = b"%PDF-1.7\n\
2951 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n\
2962 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
2973 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents [4 0 R 5 0 R] >>\nendobj\n\
2984 0 obj\n<< /Length 4 >>\nstream\n1 w\nendstream\nendobj\n\
2995 0 obj\n<< /Length 4 >>\nstream\n2 w\nendstream\nendobj\n\
300trailer\n<< /Root 1 0 R /Size 6 >>\n";
301        load(Arc::from(&file[..]), &LoadOptions::default()).expect("opens")
302    }
303
304    fn rewrite(streams: &[(Option<usize>, &str)]) -> PageRewrite {
305        PageRewrite {
306            streams: streams
307                .iter()
308                .map(|(stream, bytes)| Regenerated {
309                    stream: *stream,
310                    bytes: (*bytes).to_owned(),
311                })
312                .collect(),
313            resources: Dict::new(),
314        }
315    }
316
317    fn contents_of(edit: &EditDoc<'_>, page: ObjRef) -> Object {
318        let dict = edit.fetch(page).expect("page");
319        dict.as_dict()
320            .and_then(|d| d.raw(pdfrum_object::names::CONTENTS).cloned())
321            .unwrap_or(Object::Null)
322    }
323
324    // A rewritten element that nothing else points at is replaced in place, so
325    // `/Contents` still names the same object.
326    #[test]
327    fn a_private_stream_is_rewritten_in_place() {
328        let base = single_stream();
329        let mut edit = EditDoc::new(&base);
330        let page = ObjRef::new(3, 0);
331        let dict = base.page(0).expect("page").dict;
332        apply_rewrite(
333            &mut edit,
334            page,
335            &dict,
336            &rewrite(&[(Some(0), "q\nQ\n")]),
337            &BTreeSet::new(),
338        );
339        assert_eq!(contents_of(&edit, page), Object::Ref(ObjRef::new(4, 0)));
340        let stream = edit.fetch(ObjRef::new(4, 0)).expect("stream");
341        assert_eq!(
342            stream.as_stream().map(|s| s.data.as_ref().to_vec()),
343            Some(b"q\nQ\n".to_vec())
344        );
345    }
346
347    // A regenerated stream carries no filter: keeping one would describe
348    // plaintext as compressed.
349    #[test]
350    fn a_regenerated_stream_has_no_filter() {
351        let base = single_stream();
352        let mut edit = EditDoc::new(&base);
353        let dict = base.page(0).expect("page").dict;
354        apply_rewrite(
355            &mut edit,
356            ObjRef::new(3, 0),
357            &dict,
358            &rewrite(&[(Some(0), "q\nQ\n")]),
359            &BTreeSet::new(),
360        );
361        let stream = edit.fetch(ObjRef::new(4, 0)).expect("stream");
362        let stream = stream.as_stream().expect("a stream");
363        assert!(!stream.dict.contains_key(pdfrum_object::names::FILTER));
364        assert_eq!(
365            stream.dict.direct_int(pdfrum_object::names::LENGTH),
366            Some(4)
367        );
368    }
369
370    // A shared stream is copied rather than edited, so the page that shares it
371    // still sees the original bytes.
372    #[test]
373    fn a_shared_stream_is_copied_and_the_reference_moved() {
374        let base = single_stream();
375        let mut edit = EditDoc::new(&base);
376        let page = ObjRef::new(3, 0);
377        let dict = base.page(0).expect("page").dict;
378        let shared: BTreeSet<u32> = [4].into_iter().collect();
379        apply_rewrite(
380            &mut edit,
381            page,
382            &dict,
383            &rewrite(&[(Some(0), "q\nQ\n")]),
384            &shared,
385        );
386
387        // The page now names a different object.
388        let Object::Ref(now) = contents_of(&edit, page) else {
389            panic!("not a reference");
390        };
391        assert_ne!(now.num, 4);
392        // And the original still holds what it always did.
393        let original = edit.fetch(ObjRef::new(4, 0)).expect("original");
394        assert_eq!(
395            original.as_stream().map(|s| s.data.as_ref().to_vec()),
396            Some(b"0 0 1 1 re f".to_vec())
397        );
398    }
399
400    // `AddStream` (cpdf_pagecontentmanager.cpp:104-117): a lone stream gaining
401    // a second becomes an array of two.
402    #[test]
403    fn a_lone_stream_gaining_one_becomes_an_array_of_two() {
404        let base = single_stream();
405        let mut edit = EditDoc::new(&base);
406        let page = ObjRef::new(3, 0);
407        let dict = base.page(0).expect("page").dict;
408        apply_rewrite(
409            &mut edit,
410            page,
411            &dict,
412            &rewrite(&[(None, "q\nQ\n")]),
413            &BTreeSet::new(),
414        );
415        let Object::Ref(array_ref) = contents_of(&edit, page) else {
416            panic!("expected a reference to an array");
417        };
418        let array = edit.fetch(array_ref).expect("array");
419        let array = array.as_array().expect("an array");
420        assert_eq!(array.len(), 2);
421        assert_eq!(array.reference_at(0), Some(ObjRef::new(4, 0)));
422    }
423
424    // `ExecuteScheduledRemovals` (:194-202): a lone stream emptied loses the
425    // `/Contents` key entirely.
426    #[test]
427    fn emptying_a_lone_stream_removes_the_contents_key() {
428        let base = single_stream();
429        let mut edit = EditDoc::new(&base);
430        let page = ObjRef::new(3, 0);
431        let dict = base.page(0).expect("page").dict;
432        apply_rewrite(
433            &mut edit,
434            page,
435            &dict,
436            &rewrite(&[(Some(0), "")]),
437            &BTreeSet::new(),
438        );
439        assert_eq!(contents_of(&edit, page), Object::Null);
440    }
441
442    // `ExecuteScheduledRemovals` (:204-238): an emptied array element is
443    // dropped, the survivors renumber, and it stays an array.
444    #[test]
445    fn emptying_one_array_element_renumbers_the_survivors() {
446        let base = two_streams();
447        let mut edit = EditDoc::new(&base);
448        let page = ObjRef::new(3, 0);
449        let dict = base.page(0).expect("page").dict;
450        let mapping = apply_rewrite(
451            &mut edit,
452            page,
453            &dict,
454            &rewrite(&[(Some(0), "")]),
455            &BTreeSet::new(),
456        );
457        assert_eq!(mapping, [(1, 0)].into_iter().collect());
458        let Object::Ref(array_ref) = contents_of(&edit, page) else {
459            panic!("expected a reference to an array");
460        };
461        let array = edit.fetch(array_ref).expect("array");
462        let array = array.as_array().expect("still an array");
463        assert_eq!(array.len(), 1);
464        assert_eq!(array.reference_at(0), Some(ObjRef::new(5, 0)));
465    }
466
467    // The resource dictionary lands where the page can see it.
468    #[test]
469    fn the_swept_resources_are_written_onto_the_page() {
470        let base = single_stream();
471        let mut edit = EditDoc::new(&base);
472        let page = ObjRef::new(3, 0);
473        let dict = base.page(0).expect("page").dict;
474        let mut rewrite = rewrite(&[(Some(0), "q\nQ\n")]);
475        rewrite.resources = Dict::from_pairs([(Name::from("ExtGState"), Object::Int(1))]);
476        apply_rewrite(&mut edit, page, &dict, &rewrite, &BTreeSet::new());
477        let page_dict = edit.fetch(page).expect("page");
478        let resources = page_dict
479            .as_dict()
480            .and_then(|d| d.dict(pdfrum_object::names::RESOURCES, &edit))
481            .expect("resources");
482        assert_eq!(
483            resources.raw(&Name::from("ExtGState")),
484            Some(&Object::Int(1))
485        );
486    }
487
488    // The sharing sweep: an object two dictionaries point at is shared, one
489    // that only one points at is not.
490    #[test]
491    fn the_sweep_finds_the_objects_two_things_point_at() {
492        let file = b"%PDF-1.7\n\
4931 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n\
4942 0 obj\n<< /Type /Pages /Count 2 /Kids [3 0 R 4 0 R] >>\nendobj\n\
4953 0 obj\n<< /Type /Page /Parent 2 0 R /Contents 5 0 R >>\nendobj\n\
4964 0 obj\n<< /Type /Page /Parent 2 0 R /Contents 5 0 R >>\nendobj\n\
4975 0 obj\n<< /Length 0 >>\nstream\n\nendstream\nendobj\n\
498trailer\n<< /Root 1 0 R /Size 6 >>\n";
499        let base = load(Arc::from(&file[..]), &LoadOptions::default()).expect("opens");
500        let edit = EditDoc::new(&base);
501        let shared = shared_objects(&edit);
502        // Both pages point at stream 5, and both point at the page tree.
503        assert!(shared.contains(&5), "the shared stream");
504        assert!(shared.contains(&2), "both pages name their parent");
505        // Nothing points at page 3 twice.
506        assert!(!shared.contains(&3));
507    }
508}