1use std::collections::{BTreeMap, BTreeSet};
58
59use pdfrum_common::kurbo::Affine;
60use pdfrum_object::{Dict, Name, ObjRef, Object, Resolve};
61use pdfrum_page::{Page, PageObject};
62
63use crate::content::emit::{DEFAULT_GRAPHICS, GraphicsKey, ResourceNames, default_graphics};
64use crate::content::marks::{emit_mark_diff, finish_marks};
65use crate::content::num::write_matrix;
66use crate::content::resource::ResourceTable;
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct Regenerated {
71 pub stream: Option<usize>,
73 pub bytes: String,
76}
77
78#[derive(Debug, Clone, PartialEq)]
80pub struct PageRewrite {
81 pub streams: Vec<Regenerated>,
84 pub resources: Dict,
87}
88
89#[must_use]
95pub fn regenerate(page: &Page, resources: &Dict, r: &impl Resolve) -> Option<PageRewrite> {
96 let dirty = page.dirty_stream_set();
97 if dirty.is_empty() {
98 return None;
99 }
100
101 let mut table = ResourceTable::load(resources, r);
102 let default_gs = table.realize_dict("ExtGState", &default_graphics());
105
106 let mut buffers: BTreeMap<Option<usize>, StreamBuffer> = dirty
107 .iter()
108 .map(|stream| {
109 let mut buffer = StreamBuffer::default();
110 open_frame(
111 &mut buffer.bytes,
112 page.ctm_at_start_of_stream(*stream),
113 &default_gs,
114 );
115 (*stream, buffer)
116 })
117 .collect();
118
119 let mut used: BTreeMap<String, BTreeSet<Name>> = BTreeMap::new();
120 used.entry("ExtGState".to_owned())
121 .or_default()
122 .insert(default_gs.clone());
123
124 for object in &page.objects {
125 if !object.is_active() {
126 continue;
127 }
128 let writing = buffers.contains_key(&object.content_stream());
139 let names = realize_for(object, &mut table, &mut used, writing);
140 let Some(buffer) = buffers.get_mut(&object.content_stream()) else {
141 continue;
142 };
143 let marks = std::mem::take(&mut buffer.marks);
144 let mut body = String::new();
145 let open = emit_mark_diff(&mut body, &marks, object.marks(), &|_| None);
146 if crate::content::emit::emit_object(&mut body, object, &names) {
147 buffer.bytes.push_str(&body);
148 buffer.open_marks = open;
149 buffer.marks = object.marks().clone();
150 buffer.wrote_something = true;
151 } else {
152 buffer.marks = marks;
155 }
156 }
157
158 let streams = buffers
159 .into_iter()
160 .map(|(stream, buffer)| close_frame(page, stream, buffer))
161 .collect();
162
163 table.sweep(&used);
164 Some(PageRewrite {
165 streams,
166 resources: table.to_dict(resources),
167 })
168}
169
170#[derive(Debug, Default)]
172struct StreamBuffer {
173 bytes: String,
174 marks: pdfrum_page::ContentMarks,
175 open_marks: usize,
176 wrote_something: bool,
177}
178
179fn open_frame(out: &mut String, inherited: Affine, default_gs: &Name) {
182 out.push_str("q\n");
183 if inherited != Affine::IDENTITY {
184 write_matrix(out, inherited.inverse());
185 out.push_str(" cm\n");
186 }
187 out.push_str(DEFAULT_GRAPHICS);
188 out.push('/');
189 out.push_str(&String::from_utf8_lossy(&pdfrum_object::name_encode(
190 default_gs.as_bytes(),
191 )));
192 out.push_str(" gs ");
193}
194
195fn close_frame(page: &Page, stream: Option<usize>, mut buffer: StreamBuffer) -> Regenerated {
197 let affects_ctm = stream_affects_ctm(page, stream);
198
199 if !buffer.wrote_something && !affects_ctm {
203 return Regenerated {
204 stream,
205 bytes: String::new(),
206 };
207 }
208
209 if buffer.wrote_something {
210 finish_marks(&mut buffer.bytes, buffer.open_marks);
211 }
212 buffer.bytes.push_str("Q\n");
213
214 if let Some(index) = stream.filter(|_| affects_ctm) {
217 let previous = previous_ctm(page, index);
218 let difference = previous.inverse() * page.ctm_at_end_of_stream(index);
219 if difference != Affine::IDENTITY {
220 write_matrix(&mut buffer.bytes, difference);
221 buffer.bytes.push_str(" cm\n");
222 }
223 }
224
225 Regenerated {
226 stream,
227 bytes: buffer.bytes,
228 }
229}
230
231fn previous_ctm(page: &Page, stream: usize) -> Affine {
233 if stream == 0 {
234 Affine::IDENTITY
235 } else {
236 page.ctm_at_end_of_stream(stream.saturating_sub(1))
237 }
238}
239
240fn stream_affects_ctm(page: &Page, stream: Option<usize>) -> bool {
245 let Some(stream) = stream else {
246 return false;
247 };
248 previous_ctm(page, stream) != page.ctm_at_end_of_stream(stream)
249}
250
251fn realize_for(
254 object: &PageObject,
255 table: &mut ResourceTable,
256 used: &mut BTreeMap<String, BTreeSet<Name>>,
257 writing: bool,
258) -> ResourceNames {
259 let mut names = ResourceNames::default();
260
261 if let Some(key) = GraphicsKey::of(object.state()) {
262 let dict = key.to_dict();
263 let name = if writing {
264 Some(table.realize_dict("ExtGState", &dict))
265 } else {
266 table.name_of_dict("ExtGState", &dict)
267 };
268 names.ext_gstate = record(used, "ExtGState", name);
269 }
270
271 match object {
272 PageObject::Text(text) => {
273 if let Some(source) = text.object.font_source {
274 let name = named(table, "Font", source, writing);
275 names.font = record(used, "Font", name);
276 }
277 }
278 PageObject::Image(image) => {
279 if let Some(source) = image.object.source {
280 let name = named(table, "XObject", source, writing);
281 names.xobject = record(used, "XObject", name);
282 }
283 }
284 PageObject::Form(form) => {
285 if let Some(source) = form.object.source {
286 let name = named(table, "XObject", source, writing);
287 names.xobject = record(used, "XObject", name);
288 }
289 }
290 PageObject::Path(_) | PageObject::Shading(_) => {}
293 }
294 names
295}
296
297fn named(table: &mut ResourceTable, category: &str, source: ObjRef, writing: bool) -> Option<Name> {
300 if writing {
301 Some(table.realize(category, source))
302 } else {
303 table.name_of(category, source)
304 }
305}
306
307fn record(
309 used: &mut BTreeMap<String, BTreeSet<Name>>,
310 category: &str,
311 name: Option<Name>,
312) -> Option<Name> {
313 let name = name?;
314 used.entry(category.to_owned())
315 .or_default()
316 .insert(name.clone());
317 Some(name)
318}
319
320#[derive(Debug, Clone, PartialEq, Eq)]
337pub enum ContentsShape {
338 Absent,
340 Single(ObjRef),
342 Array(Vec<ObjRef>),
344}
345
346impl ContentsShape {
347 #[must_use]
353 pub fn read(page_dict: &Dict, r: &impl Resolve) -> Self {
354 let Some(contents) = page_dict.raw(pdfrum_object::names::CONTENTS) else {
355 return Self::Absent;
356 };
357 let direct = match contents {
359 Object::Ref(reference) => match r.fetch(*reference) {
360 Ok(object) => (*object).clone(),
361 Err(_) => return Self::Absent,
362 },
363 other => other.clone(),
364 };
365 match direct {
366 Object::Stream(_) => match contents {
367 Object::Ref(reference) => Self::Single(*reference),
368 _ => Self::Absent,
371 },
372 Object::Array(array) => {
373 Self::Array(array.iter().filter_map(Object::as_ref_id).collect())
374 }
375 _ => Self::Absent,
376 }
377 }
378
379 #[must_use]
381 pub fn with_added(&self, added: ObjRef) -> (usize, Self) {
382 match self {
383 Self::Absent => (0, Self::Single(added)),
384 Self::Single(existing) => (1, Self::Array(vec![*existing, added])),
385 Self::Array(elements) => {
386 let mut next = elements.clone();
387 next.push(added);
388 (next.len().saturating_sub(1), Self::Array(next))
389 }
390 }
391 }
392
393 #[must_use]
405 pub fn with_removed(&self, removed: &BTreeSet<usize>) -> (Self, BTreeMap<usize, usize>) {
406 match self {
407 Self::Absent => (Self::Absent, BTreeMap::new()),
408 Self::Single(_) => {
409 let shape = if removed.contains(&0) {
410 Self::Absent
413 } else {
414 self.clone()
415 };
416 (shape, BTreeMap::new())
417 }
418 Self::Array(elements) => {
419 let mut mapping = BTreeMap::new();
420 let mut kept = Vec::new();
421 for (old, element) in elements.iter().enumerate() {
422 if removed.contains(&old) {
423 continue;
424 }
425 let new = kept.len();
426 kept.push(*element);
427 mapping.insert(old, new);
428 }
429 (Self::Array(kept), mapping)
431 }
432 }
433 }
434
435 #[must_use]
440 pub fn to_object(&self, array_ref: Option<ObjRef>) -> Option<Object> {
441 match self {
442 Self::Absent => None,
443 Self::Single(reference) => Some(Object::Ref(*reference)),
444 Self::Array(elements) => {
445 let array = pdfrum_object::Array::of(elements.iter().map(|e| Object::Ref(*e)));
446 Some(match array_ref {
447 Some(reference) => {
448 let _ = &array;
449 Object::Ref(reference)
450 }
451 None => Object::Array(array),
452 })
453 }
454 }
455 }
456
457 #[must_use]
459 pub fn elements(&self) -> Vec<ObjRef> {
460 match self {
461 Self::Absent => Vec::new(),
462 Self::Single(reference) => vec![*reference],
463 Self::Array(elements) => elements.clone(),
464 }
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 #![expect(
471 clippy::indexing_slicing,
472 reason = "test fixtures index collections whose length the fixture fixes"
473 )]
474
475 use super::{ContentsShape, Regenerated, regenerate};
476 use pdfrum_common::kurbo::{Affine, BezPath};
477 use pdfrum_object::{Dict, Name, NoResolve, ObjRef, Object};
478 use pdfrum_page::{Content, FillRule, Page, PageObject, PathObject};
479 use pdfrum_page::{ContentMarks, GraphicsState};
480 use std::collections::{BTreeMap, BTreeSet};
481
482 fn path(stream: usize, dirty: bool) -> PageObject {
483 let mut p = BezPath::new();
484 p.move_to((0.0, 0.0));
485 p.line_to((1.0, 0.0));
486 p.line_to((1.0, 1.0));
487 p.close_path();
488 PageObject::Path(Box::new(Content {
489 object: PathObject {
490 path: p,
491 matrix: Affine::IDENTITY,
492 fill_rule: FillRule::Winding,
493 stroke: false,
494 },
495 state: GraphicsState::default(),
496 marks: ContentMarks::new(),
497 content_stream: Some(stream),
498 dirty,
499 active: true,
500 }))
501 }
502
503 fn page_of(objects: Vec<PageObject>) -> Page {
504 Page {
505 objects,
506 ..Page::empty()
507 }
508 }
509
510 #[test]
513 fn an_untouched_page_regenerates_nothing() {
514 let page = page_of(vec![path(0, false), path(0, false)]);
515 assert!(regenerate(&page, &Dict::new(), &NoResolve).is_none());
516 }
517
518 #[test]
520 fn a_dirty_stream_gets_the_whole_frame() {
521 let page = page_of(vec![path(0, true)]);
522 let rewrite = regenerate(&page, &Dict::new(), &NoResolve).expect("dirty");
523 assert_eq!(rewrite.streams.len(), 1);
524 let bytes = &rewrite.streams[0].bytes;
525 assert!(
526 bytes.starts_with("q\n0 0 0 RG 0 0 0 rg 1 w 0 J 0 j\n/FXE1 gs "),
527 "got {bytes}"
528 );
529 assert!(bytes.ends_with("Q\n"), "got {bytes}");
530 let Some(Object::Dict(gs)) = rewrite.resources.raw(&Name::from("ExtGState")) else {
533 panic!("no ExtGState");
534 };
535 assert!(gs.contains_key(&Name::from("FXE1")));
536 }
537
538 #[test]
540 fn only_dirty_streams_are_written() {
541 let page = page_of(vec![path(0, false), path(1, true)]);
542 let rewrite = regenerate(&page, &Dict::new(), &NoResolve).expect("dirty");
543 assert_eq!(rewrite.streams.len(), 1);
544 assert_eq!(rewrite.streams[0].stream, Some(1));
545 }
546
547 #[test]
550 fn a_clean_object_in_a_dirty_stream_is_still_written() {
551 let mut page = page_of(vec![path(0, false), path(0, true)]);
552 page.objects[0].mark_clean();
553 let rewrite = regenerate(&page, &Dict::new(), &NoResolve).expect("dirty");
554 let bytes = &rewrite.streams[0].bytes;
555 assert_eq!(bytes.matches(" f Q\n").count(), 2, "got {bytes}");
556 }
557
558 #[test]
561 fn an_inactive_object_is_left_out_of_its_regenerated_stream() {
562 let mut page = page_of(vec![path(0, false), path(0, false)]);
563 page.objects[1].set_active(false);
564 let rewrite = regenerate(&page, &Dict::new(), &NoResolve).expect("dirty");
565 let bytes = &rewrite.streams[0].bytes;
566 assert_eq!(bytes.matches(" f Q\n").count(), 1, "got {bytes}");
567 }
568
569 #[test]
572 fn a_stream_left_with_nothing_comes_back_empty() {
573 let mut page = page_of(vec![path(0, false)]);
574 page.objects[0].set_active(false);
575 let rewrite = regenerate(&page, &Dict::new(), &NoResolve).expect("dirty");
576 assert_eq!(
577 rewrite.streams,
578 vec![Regenerated {
579 stream: Some(0),
580 bytes: String::new()
581 }]
582 );
583 }
584
585 #[test]
588 fn a_removal_regenerates_the_stream_it_emptied() {
589 let mut page = page_of(vec![path(0, false), path(1, false)]);
590 assert!(page.remove_object(1).is_some());
591 let rewrite = regenerate(&page, &Dict::new(), &NoResolve).expect("dirty");
592 assert_eq!(rewrite.streams.len(), 1);
593 assert_eq!(rewrite.streams[0].stream, Some(1));
594 assert!(rewrite.streams[0].bytes.is_empty());
595 }
596
597 #[test]
600 fn an_inherited_transform_is_undone_and_restated() {
601 let mut page = page_of(vec![path(1, true)]);
602 page.stream_ctms.insert(0, Affine::scale(2.0));
603 page.stream_ctms.insert(1, Affine::scale(2.0));
604 let rewrite = regenerate(&page, &Dict::new(), &NoResolve).expect("dirty");
605 let bytes = &rewrite.streams[0].bytes;
606 assert!(bytes.starts_with("q\n.5 0 0 .5 0 0 cm\n"), "got {bytes}");
608 assert!(bytes.ends_with("Q\n"), "got {bytes}");
610 }
611
612 #[test]
613 fn a_stream_that_moves_the_transform_restates_the_move() {
614 let mut page = page_of(vec![path(0, true)]);
615 page.stream_ctms.insert(0, Affine::scale(3.0));
616 let rewrite = regenerate(&page, &Dict::new(), &NoResolve).expect("dirty");
617 let bytes = &rewrite.streams[0].bytes;
618 assert!(
619 bytes.starts_with("q\n"),
620 "no inverse: stream 0 inherits none"
621 );
622 assert!(bytes.ends_with("Q\n3 0 0 3 0 0 cm\n"), "got {bytes}");
623 }
624
625 #[test]
628 fn an_empty_stream_that_moves_the_transform_survives() {
629 let mut page = page_of(vec![path(1, true)]);
630 page.dirty_streams.insert(Some(0));
631 page.stream_ctms.insert(0, Affine::scale(2.0));
632 let rewrite = regenerate(&page, &Dict::new(), &NoResolve).expect("dirty");
633 let zero = rewrite
634 .streams
635 .iter()
636 .find(|s| s.stream == Some(0))
637 .expect("stream 0");
638 assert!(!zero.bytes.is_empty(), "it moves the transform");
639 assert!(
640 zero.bytes.ends_with("2 0 0 2 0 0 cm\n"),
641 "got {}",
642 zero.bytes
643 );
644 }
645
646 #[test]
649 fn a_streamless_object_is_written_first() {
650 let mut page = page_of(vec![path(0, true)]);
651 page.push_object(path(0, false));
652 let rewrite = regenerate(&page, &Dict::new(), &NoResolve).expect("dirty");
653 let order: Vec<Option<usize>> = rewrite.streams.iter().map(|s| s.stream).collect();
654 assert_eq!(order, vec![None, Some(0)]);
655 }
656
657 #[test]
660 fn nothing_gaining_a_stream_becomes_a_lone_stream_at_zero() {
661 let (index, shape) = ContentsShape::Absent.with_added(ObjRef::new(5, 0));
662 assert_eq!(index, 0);
663 assert_eq!(shape, ContentsShape::Single(ObjRef::new(5, 0)));
664 }
665
666 #[test]
667 fn a_lone_stream_gaining_a_second_becomes_an_array_and_the_new_one_is_one() {
668 let shape = ContentsShape::Single(ObjRef::new(4, 0));
669 let (index, next) = shape.with_added(ObjRef::new(9, 0));
670 assert_eq!(index, 1);
671 assert_eq!(
672 next,
673 ContentsShape::Array(vec![ObjRef::new(4, 0), ObjRef::new(9, 0)])
674 );
675 }
676
677 #[test]
678 fn an_array_gaining_one_appends_at_the_end() {
679 let shape = ContentsShape::Array(vec![ObjRef::new(1, 0), ObjRef::new(2, 0)]);
680 let (index, next) = shape.with_added(ObjRef::new(3, 0));
681 assert_eq!(index, 2);
682 assert_eq!(next.elements().len(), 3);
683 }
684
685 #[test]
687 fn a_lone_stream_removed_leaves_no_contents_key_at_all() {
688 let shape = ContentsShape::Single(ObjRef::new(4, 0));
689 let (next, mapping) = shape.with_removed(&[0].into_iter().collect());
690 assert_eq!(next, ContentsShape::Absent);
691 assert!(mapping.is_empty());
692 assert_eq!(next.to_object(None), None);
693 }
694
695 #[test]
698 fn removing_an_array_element_shifts_the_ones_after_it_down() {
699 let shape = ContentsShape::Array(vec![
700 ObjRef::new(1, 0),
701 ObjRef::new(2, 0),
702 ObjRef::new(3, 0),
703 ]);
704 let (next, mapping) = shape.with_removed(&[1].into_iter().collect());
705 assert_eq!(
706 next,
707 ContentsShape::Array(vec![ObjRef::new(1, 0), ObjRef::new(3, 0)])
708 );
709 assert_eq!(mapping, BTreeMap::from([(0, 0), (2, 1)]));
710 }
711
712 #[test]
714 fn an_array_is_never_collapsed_back_to_a_bare_stream() {
715 let shape = ContentsShape::Array(vec![ObjRef::new(1, 0), ObjRef::new(2, 0)]);
716 let (next, _) = shape.with_removed(&[1].into_iter().collect());
717 assert!(matches!(next, ContentsShape::Array(ref e) if e.len() == 1));
718 let (empty, _) = next.with_removed(&[0].into_iter().collect());
719 assert_eq!(empty, ContentsShape::Array(Vec::new()));
720 }
721
722 #[test]
723 fn removing_several_elements_renumbers_the_survivors_in_one_pass() {
724 let shape = ContentsShape::Array((1..=5).map(|n| ObjRef::new(n, 0)).collect());
725 let removed: BTreeSet<usize> = [0, 3].into_iter().collect();
726 let (next, mapping) = shape.with_removed(&removed);
727 assert_eq!(next.elements().len(), 3);
728 assert_eq!(mapping, BTreeMap::from([(1, 0), (2, 1), (4, 2)]));
729 }
730
731 #[test]
733 fn a_page_with_no_contents_reads_as_absent() {
734 assert_eq!(
735 ContentsShape::read(&Dict::new(), &NoResolve),
736 ContentsShape::Absent
737 );
738 let odd = Dict::from_pairs([(pdfrum_object::names::CONTENTS.clone(), Object::Int(7))]);
741 assert_eq!(ContentsShape::read(&odd, &NoResolve), ContentsShape::Absent);
742 }
743
744 #[test]
745 fn an_inline_array_of_references_reads_as_an_array() {
746 let array = pdfrum_object::Array::of([
747 Object::Ref(ObjRef::new(2, 0)),
748 Object::Ref(ObjRef::new(3, 0)),
749 ]);
750 let dict =
751 Dict::from_pairs([(pdfrum_object::names::CONTENTS.clone(), Object::Array(array))]);
752 assert_eq!(
753 ContentsShape::read(&dict, &NoResolve),
754 ContentsShape::Array(vec![ObjRef::new(2, 0), ObjRef::new(3, 0)])
755 );
756 }
757}