1use 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
31pub type ShareCounts = BTreeSet<u32>;
36
37pub 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 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 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 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
107fn 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
116fn 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
131fn 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 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
174fn 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 Some(Object::Ref(reference)) if !shared.contains(&reference.num) => {
186 edit.replace(*reference, Object::Dict(resources.clone()));
187 return dict.clone();
188 }
189 _ => Some(Object::Dict(resources.clone())),
191 };
192 with_key(dict, key, value)
193}
194
195fn 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#[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 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 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 #[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 #[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 #[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 let Object::Ref(now) = contents_of(&edit, page) else {
389 panic!("not a reference");
390 };
391 assert_ne!(now.num, 4);
392 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 #[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 #[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 #[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 #[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 #[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 assert!(shared.contains(&5), "the shared stream");
504 assert!(shared.contains(&2), "both pages name their parent");
505 assert!(!shared.contains(&3));
507 }
508}