1use crate::error::{RedactError, Result};
14use lopdf::content::{Content, Operation};
15use lopdf::{dictionary, Document, Object, ObjectId, Stream};
16use std::io::Write;
17
18pub(crate) const MINIMAL_XMP: &[u8] = b"<?xpacket begin=\"\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>\n<x:xmpmeta xmlns:x=\"adobe:ns:meta/\">\n <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n <rdf:Description rdf:about=\"\" xmlns:pdf=\"http://ns.adobe.com/pdf/1.3/\">\n <pdf:Producer>pdfluent</pdf:Producer>\n </rdf:Description>\n </rdf:RDF>\n</x:xmpmeta>\n<?xpacket end=\"w\"?>";
19
20#[derive(Debug, Clone)]
22pub struct RedactionArea {
23 pub page: u32,
25 pub rect: [f64; 4],
27 pub fill_color: [f64; 3],
29 pub overlay_text: Option<String>,
31}
32
33impl RedactionArea {
34 pub fn new(page: u32, rect: [f64; 4]) -> Self {
36 Self {
37 page,
38 rect,
39 fill_color: [0.0, 0.0, 0.0],
40 overlay_text: None,
41 }
42 }
43
44 pub fn with_color(mut self, r: f64, g: f64, b: f64) -> Self {
46 self.fill_color = [r, g, b];
47 self
48 }
49
50 pub fn with_overlay(mut self, text: impl Into<String>) -> Self {
52 self.overlay_text = Some(text.into());
53 self
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct RedactionReport {
60 pub areas_redacted: usize,
62 pub operations_removed: usize,
64 pub pages_affected: usize,
66 pub metadata_cleaned: bool,
68}
69
70#[derive(Debug)]
72pub struct Redactor {
73 areas: Vec<RedactionArea>,
75}
76
77impl Redactor {
78 pub fn new() -> Self {
80 Self { areas: Vec::new() }
81 }
82
83 pub fn mark(&mut self, area: RedactionArea) {
85 self.areas.push(area);
86 }
87
88 pub fn mark_all(&mut self, areas: impl IntoIterator<Item = RedactionArea>) {
90 self.areas.extend(areas);
91 }
92
93 pub fn pending_count(&self) -> usize {
95 self.areas.len()
96 }
97
98 pub fn apply(&self, doc: &mut Document) -> Result<RedactionReport> {
133 if self.areas.is_empty() {
134 return Err(RedactError::NoAreas);
135 }
136
137 refuse_xfa(doc)?;
138
139 let pages = doc.get_pages();
140 let total = pages.len() as u32;
141
142 for area in &self.areas {
144 if area.page == 0 || area.page > total {
145 return Err(RedactError::PageOutOfRange(area.page, total));
146 }
147 }
148
149 let mut work = doc.clone();
152
153 let mut total_ops_removed = 0;
154 let mut affected_pages = std::collections::HashSet::new();
155 let mut orphans: Vec<ObjectId> = Vec::new();
156
157 let mut page_areas: std::collections::BTreeMap<u32, Vec<&RedactionArea>> =
159 std::collections::BTreeMap::new();
160 for area in &self.areas {
161 page_areas.entry(area.page).or_default().push(area);
162 }
163
164 for (&page_num, areas) in &page_areas {
165 let page_id = match pages.get(&page_num) {
166 Some(&id) => id,
167 None => continue,
168 };
169
170 let resources = resolve_inherited_resources_clone(&work, page_id).unwrap_or_default();
175 let (ops_removed, unused) =
176 crate::area_filter::redact_page(&mut work, page_num, page_id, resources, areas)?;
177 total_ops_removed += ops_removed;
178 orphans.extend(unused);
179
180 ensure_no_text_under(&work, page_num, areas)?;
185
186 draw_redaction_overlays(&mut work, page_id, areas)?;
188
189 strip_annotation_contents(&mut work, page_id, areas)?;
194 orphans.extend(crate::area_filter::redact_annotations(
195 &mut work, page_num, page_id, areas,
196 )?);
197
198 strip_tounicode_for_redacted_page(&mut work, page_id)?;
206
207 affected_pages.insert(page_num);
208 }
209
210 clean_metadata(&mut work);
212
213 crate::area_filter::drop_orphans(&mut work, &orphans);
216
217 crate::area_filter::drop_unreachable(&mut work);
223
224 *doc = work;
225 Ok(RedactionReport {
226 areas_redacted: self.areas.len(),
227 operations_removed: total_ops_removed,
228 pages_affected: affected_pages.len(),
229 metadata_cleaned: true,
230 })
231 }
232}
233
234impl Default for Redactor {
235 fn default() -> Self {
236 Self::new()
237 }
238}
239
240fn draw_redaction_overlays(
246 doc: &mut Document,
247 page_id: ObjectId,
248 areas: &[&RedactionArea],
249) -> Result<()> {
250 wrap_existing_content_in_save_restore(doc, page_id);
254
255 let mut ops = Vec::new();
256
257 for area in areas {
258 let [r, g, b] = area.fill_color;
259 let [x0, y0, x1, y1] = area.rect;
260
261 let (tx0, ty0, tx1, ty1) = (x0, y0, x1, y1);
270 let w = tx1 - tx0;
271 let h = ty1 - ty0;
272
273 ops.push(Operation::new("q", vec![]));
275 ops.push(Operation::new(
276 "rg",
277 vec![
278 Object::Real(r as f32),
279 Object::Real(g as f32),
280 Object::Real(b as f32),
281 ],
282 ));
283 ops.push(Operation::new(
284 "re",
285 vec![
286 Object::Real(tx0 as f32),
287 Object::Real(ty0 as f32),
288 Object::Real(w as f32),
289 Object::Real(h as f32),
290 ],
291 ));
292 ops.push(Operation::new("f", vec![]));
293
294 if let Some(ref text) = area.overlay_text {
296 let max_font_size = h.abs() * 0.7;
298 let font_size = max_font_size.clamp(4.0, 12.0) as f32;
299
300 ops.push(Operation::new("BT", vec![]));
301 ops.push(Operation::new(
303 "rg",
304 vec![Object::Real(1.0), Object::Real(1.0), Object::Real(1.0)],
305 ));
306 ops.push(Operation::new(
307 "Tf",
308 vec![Object::Name(b"Helvetica".to_vec()), Object::Real(font_size)],
309 ));
310 ops.push(Operation::new(
311 "Td",
312 vec![
313 Object::Real((tx0 + 2.0) as f32),
314 Object::Real((ty0 + 2.0) as f32),
315 ],
316 ));
317 ops.push(Operation::new(
318 "Tj",
319 vec![Object::String(
320 text.as_bytes().to_vec(),
321 lopdf::StringFormat::Literal,
322 )],
323 ));
324 ops.push(Operation::new("ET", vec![]));
325 }
326
327 ops.push(Operation::new("Q", vec![]));
328 }
329
330 let new_content = Content { operations: ops };
331 let encoded = new_content
332 .encode()
333 .map_err(|e| RedactError::Other(format!("failed to encode overlay: {e}")))?;
334
335 let overlay_stream = Stream::new(dictionary! {}, encoded);
336 let overlay_id = doc.add_object(Object::Stream(overlay_stream));
337
338 append_content_to_page(doc, page_id, overlay_id);
339
340 Ok(())
341}
342
343pub(crate) fn check_image_filter(dict: &lopdf::Dictionary) -> Result<()> {
349 let filter_obj = match dict.get(b"Filter") {
350 Ok(f) => f.clone(),
351 Err(_) => return Ok(()),
352 };
353 let names: Vec<String> = match filter_obj {
354 Object::Name(ref n) => vec![String::from_utf8_lossy(n).into_owned()],
355 Object::Array(ref arr) => arr
356 .iter()
357 .filter_map(|o| {
358 if let Object::Name(ref n) = o {
359 Some(String::from_utf8_lossy(n).into_owned())
360 } else {
361 None
362 }
363 })
364 .collect(),
365 _ => vec![],
366 };
367 for name in &names {
368 if matches!(name.as_str(), "JBIG2Decode" | "JPXDecode" | "Crypt") {
369 return Err(RedactError::UnsupportedImageFilter(name.clone()));
370 }
371 }
372 Ok(())
373}
374
375fn bbox_overlaps_any(bbox: [f64; 4], areas: &[&RedactionArea]) -> bool {
377 let [bx0, by0, bx1, by1] = bbox;
378 for area in areas {
379 let [ax0, ay0, ax1, ay1] = area.rect;
380 let (rax0, rax1) = if ax0 < ax1 { (ax0, ax1) } else { (ax1, ax0) };
381 let (ray0, ray1) = if ay0 < ay1 { (ay0, ay1) } else { (ay1, ay0) };
382 let no_overlap = bx1 < rax0 || bx0 > rax1 || by1 < ray0 || by0 > ray1;
383 if !no_overlap {
384 return true;
385 }
386 }
387 false
388}
389
390fn strip_annotation_contents(
397 doc: &mut Document,
398 page_id: ObjectId,
399 areas: &[&RedactionArea],
400) -> Result<()> {
401 let annot_ids = get_page_annotation_ids(doc, page_id);
402 for annot_id in annot_ids {
403 let rect = match get_annotation_rect(doc, annot_id) {
404 Some(r) => r,
405 None => continue,
406 };
407 if bbox_overlaps_any(rect, areas) {
408 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(annot_id) {
409 d.remove(b"Contents");
410 d.remove(b"T");
411 }
412 }
413 }
414 Ok(())
415}
416
417fn get_page_annotation_ids(doc: &Document, page_id: ObjectId) -> Vec<ObjectId> {
419 let page_dict = match doc.get_object(page_id) {
420 Ok(Object::Dictionary(ref d)) => d.clone(),
421 _ => return Vec::new(),
422 };
423 let annots_arr = match page_dict.get(b"Annots") {
424 Ok(Object::Array(ref arr)) => arr.clone(),
425 Ok(Object::Reference(id)) => match doc.get_object(*id) {
426 Ok(Object::Array(ref arr)) => arr.clone(),
427 _ => return Vec::new(),
428 },
429 _ => return Vec::new(),
430 };
431 annots_arr
432 .iter()
433 .filter_map(|o| {
434 if let Object::Reference(id) = o {
435 Some(*id)
436 } else {
437 None
438 }
439 })
440 .collect()
441}
442
443fn get_annotation_rect(doc: &Document, annot_id: ObjectId) -> Option<[f64; 4]> {
445 let dict = match doc.get_object(annot_id) {
446 Ok(Object::Dictionary(ref d)) => d.clone(),
447 _ => return None,
448 };
449 match dict.get(b"Rect") {
450 Ok(Object::Array(ref arr)) if arr.len() >= 4 => {
451 let vals: Vec<f64> = arr.iter().filter_map(as_number).collect();
452 if vals.len() >= 4 {
453 Some([vals[0], vals[1], vals[2], vals[3]])
454 } else {
455 None
456 }
457 }
458 _ => None,
459 }
460}
461
462pub(crate) fn refuse_xfa(doc: &Document) -> Result<()> {
470 if has_xfa(doc) {
471 return Err(RedactError::Other(
472 "xfa-redaction-needs-flatten: the document carries an XFA packet that holds its own \
473 copy of the form text; flatten the form before redacting"
474 .to_string(),
475 ));
476 }
477 Ok(())
478}
479
480pub fn has_xfa(doc: &Document) -> bool {
486 match doc.catalog() {
487 Ok(catalog) => {
488 let acro = match catalog.get(b"AcroForm") {
489 Ok(Object::Reference(id)) => doc.get_dictionary(*id).ok(),
490 Ok(Object::Dictionary(d)) => Some(d),
491 _ => None,
492 };
493 acro.is_some_and(|d| d.has(b"XFA")) || catalog.has(b"XFA")
494 }
495 Err(_) => false,
496 }
497}
498
499fn ensure_no_text_under(doc: &Document, page_num: u32, areas: &[&RedactionArea]) -> Result<()> {
509 let Some(&page_id) = doc.get_pages().get(&page_num) else {
510 return Ok(());
511 };
512 let resources = resolve_inherited_resources_clone(doc, page_id).unwrap_or_default();
513 let left = crate::area_filter::text_left_under(doc, page_num, page_id, resources, areas)?;
514 if left > 0 {
515 return Err(RedactError::Other(format!(
516 "text is still extractable under a redaction area on page {page_num} \
517 ({left} text operation(s) the removal rule still takes glyphs from); \
518 redaction refused instead of reporting success"
519 )));
520 }
521 Ok(())
522}
523
524fn clean_metadata(doc: &mut Document) {
526 doc.trailer.remove(b"Info");
527 replace_xmp_metadata(doc);
528 let page_ids: Vec<ObjectId> = doc.get_pages().values().copied().collect();
529 for page_id in page_ids {
530 if let Ok(Object::Dictionary(ref mut page)) = doc.get_object_mut(page_id) {
531 page.remove(b"Thumb");
532 }
533 }
534}
535
536fn replace_xmp_metadata(doc: &mut Document) {
539 let root_id = match doc.trailer.get(b"Root") {
540 Ok(Object::Reference(id)) => *id,
541 _ => return,
542 };
543
544 let meta_id: Option<ObjectId> = {
545 match doc.get_object(root_id) {
546 Ok(Object::Dictionary(ref d)) => match d.get(b"Metadata") {
547 Ok(Object::Reference(id)) => Some(*id),
548 _ => None,
549 },
550 _ => return,
551 }
552 };
553
554 let xmp = MINIMAL_XMP.to_vec();
555 let xmp_len = xmp.len() as i64;
556
557 if let Some(meta_stream_id) = meta_id {
558 if let Ok(Object::Stream(ref mut s)) = doc.get_object_mut(meta_stream_id) {
559 s.content = xmp;
560 s.dict.set("Length", Object::Integer(xmp_len));
561 s.dict.remove(b"Filter");
562 }
563 } else {
564 let xmp_stream = Stream::new(
565 dictionary! { "Type" => "Metadata", "Subtype" => "XML" },
566 xmp,
567 );
568 let new_id = doc.add_object(Object::Stream(xmp_stream));
569 if let Ok(Object::Dictionary(ref mut catalog)) = doc.get_object_mut(root_id) {
570 catalog.set("Metadata", Object::Reference(new_id));
571 }
572 }
573}
574
575fn resolve_content_streams(doc: &Document, page_id: ObjectId) -> Vec<ObjectId> {
581 let page_obj = match doc.get_object(page_id) {
582 Ok(obj) => obj,
583 Err(_) => return Vec::new(),
584 };
585 let page_dict = match page_obj {
586 Object::Dictionary(ref d) => d,
587 _ => return Vec::new(),
588 };
589 match page_dict.get(b"Contents").ok() {
590 Some(c) => flatten_content_refs(doc, c),
591 None => Vec::new(),
592 }
593}
594
595fn flatten_content_refs(doc: &Document, obj: &Object) -> Vec<ObjectId> {
596 match obj {
597 Object::Reference(id) => {
598 if let Ok(Object::Array(arr)) = doc.get_object(*id) {
599 return arr
600 .iter()
601 .flat_map(|o| flatten_content_refs(doc, o))
602 .collect();
603 }
604 vec![*id]
605 }
606 Object::Array(arr) => arr
607 .iter()
608 .flat_map(|o| flatten_content_refs(doc, o))
609 .collect(),
610 _ => Vec::new(),
611 }
612}
613
614fn append_content_to_page(doc: &mut Document, page_id: ObjectId, content_id: ObjectId) {
616 let existing = resolve_content_streams(doc, page_id);
617 let new_contents = if existing.is_empty() {
618 Object::Reference(content_id)
619 } else {
620 let mut arr: Vec<Object> = existing.into_iter().map(Object::Reference).collect();
621 arr.push(Object::Reference(content_id));
622 Object::Array(arr)
623 };
624
625 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
626 d.set("Contents", new_contents);
627 }
628}
629
630fn wrap_existing_content_in_save_restore(doc: &mut Document, page_id: ObjectId) {
635 let existing = resolve_content_streams(doc, page_id);
636 if existing.is_empty() {
637 return;
638 }
639
640 let q_stream = Stream::new(dictionary! {}, b"q\n".to_vec());
641 let q_id = doc.add_object(Object::Stream(q_stream));
642
643 let big_q_stream = Stream::new(dictionary! {}, b"\nQ\n".to_vec());
644 let big_q_id = doc.add_object(Object::Stream(big_q_stream));
645
646 let mut new_arr = Vec::with_capacity(existing.len() + 2);
648 new_arr.push(Object::Reference(q_id));
649 for id in existing {
650 new_arr.push(Object::Reference(id));
651 }
652 new_arr.push(Object::Reference(big_q_id));
653 let wrapped = Object::Array(new_arr);
654
655 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
656 d.set("Contents", wrapped);
657 }
658}
659
660#[allow(dead_code)]
662fn page_rotation_and_media_box(doc: &Document, page_id: ObjectId) -> (i64, [f64; 4]) {
663 let default_box = [0.0, 0.0, 612.0, 792.0];
664
665 let page_obj = match doc.get_object(page_id) {
666 Ok(obj) => obj,
667 Err(_) => return (0, default_box),
668 };
669 let page_dict = match page_obj {
670 Object::Dictionary(ref d) => d,
671 _ => return (0, default_box),
672 };
673
674 let rotate = page_dict
675 .get(b"Rotate")
676 .ok()
677 .and_then(|r| match r {
678 Object::Integer(i) => Some(*i),
679 _ => None,
680 })
681 .unwrap_or(0);
682
683 let media_box = page_dict
684 .get(b"MediaBox")
685 .ok()
686 .and_then(|mb| {
687 if let Object::Array(arr) = mb {
688 if arr.len() >= 4 {
689 let vals: Vec<f64> = arr.iter().filter_map(as_number).collect();
690 if vals.len() >= 4 {
691 return Some([vals[0], vals[1], vals[2], vals[3]]);
692 }
693 }
694 }
695 None
696 })
697 .unwrap_or(default_box);
698
699 (rotate, media_box)
700}
701
702#[allow(dead_code)]
707fn transform_rect_for_rotation(
708 x0: f64,
709 y0: f64,
710 x1: f64,
711 y1: f64,
712 rotate: i64,
713 media_box: &[f64; 4],
714) -> (f64, f64, f64, f64) {
715 let page_w = media_box[2] - media_box[0];
716 let page_h = media_box[3] - media_box[1];
717
718 match rotate % 360 {
723 0 => (x0, y0, x1, y1),
724 90 | -270 => (page_w - y1, x0, page_w - y0, x1),
725 180 | -180 => (page_w - x1, page_h - y1, page_w - x0, page_h - y0),
726 270 | -90 => (y0, page_h - x1, y1, page_h - x0),
727 _ => (x0, y0, x1, y1),
728 }
729}
730
731fn compress_flate(data: &[u8]) -> Vec<u8> {
733 let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
734 if encoder.write_all(data).is_ok() {
735 encoder.finish().unwrap_or_else(|_| data.to_vec())
736 } else {
737 data.to_vec()
738 }
739}
740
741fn strip_tounicode_for_redacted_page(doc: &mut Document, page_id: ObjectId) -> Result<usize> {
761 let mut form_clones: std::collections::HashMap<ObjectId, ObjectId> =
773 std::collections::HashMap::new();
774 strip_tounicode_for_container(doc, page_id, ContainerKind::Page, &mut form_clones)
775}
776
777#[derive(Copy, Clone)]
778enum ContainerKind {
779 Page,
782 FormXObject,
787}
788
789fn strip_tounicode_for_container(
809 doc: &mut Document,
810 container_id: ObjectId,
811 kind: ContainerKind,
812 form_clones: &mut std::collections::HashMap<ObjectId, ObjectId>,
813) -> Result<usize> {
814 let snapshot = match kind {
818 ContainerKind::Page => resolve_page_resources_snapshot(doc, container_id),
819 ContainerKind::FormXObject => resolve_form_xobject_resources_snapshot(doc, container_id),
820 };
821 let snapshot = match snapshot {
822 Some(s) => s,
823 None => return Ok(0),
824 };
825
826 let font_actions = collect_strip_actions(doc, &snapshot)?;
830 let form_xobject_entries = collect_form_xobject_entries(doc, &snapshot);
831
832 let needs_font_clone = !font_actions.is_empty();
833 let needs_xobject_clone = !form_xobject_entries.is_empty();
834 if !needs_font_clone && !needs_xobject_clone {
835 return Ok(0);
836 }
837
838 let private = materialize_private_path(
842 doc,
843 container_id,
844 kind,
845 &snapshot,
846 needs_font_clone,
847 needs_xobject_clone,
848 );
849
850 let mut stripped = 0;
851
852 if let Some(font_dict_id) = private.font_dict_id {
854 for FontStripPlan {
855 font_name_bytes,
856 font_dict_template,
857 codes,
858 } in font_actions
859 {
860 let stub_id = build_and_insert_stub_stream(doc, &codes);
861 let mut new_font = font_dict_template;
862 new_font.set("ToUnicode", Object::Reference(stub_id));
863 let new_font_id = doc.add_object(Object::Dictionary(new_font));
864 if let Ok(Object::Dictionary(font_dict)) = doc.get_object_mut(font_dict_id) {
865 font_dict.set(font_name_bytes.clone(), Object::Reference(new_font_id));
867 }
868 stripped += 1;
869 }
870 }
871
872 if let Some(xobject_dict_id) = private.xobject_dict_id {
878 for entry in form_xobject_entries {
879 let (cloned_form_id, recurse_into_new_clone) =
880 if let Some(&existing) = form_clones.get(&entry.original_id) {
881 (existing, false)
885 } else {
886 let new_clone = clone_form_xobject_stream(doc, entry.original_id);
887 form_clones.insert(entry.original_id, new_clone);
888 (new_clone, true)
889 };
890
891 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(xobject_dict_id) {
896 d.set(entry.name.clone(), Object::Reference(cloned_form_id));
897 }
898
899 if recurse_into_new_clone {
900 stripped += strip_tounicode_for_container(
901 doc,
902 cloned_form_id,
903 ContainerKind::FormXObject,
904 form_clones,
905 )?;
906 }
907 }
908 }
909
910 Ok(stripped)
911}
912
913struct ResourcesSnapshot {
917 #[allow(dead_code)]
919 resources_dict: lopdf::Dictionary,
920 font_dict: Option<lopdf::Dictionary>,
922 xobject_dict: Option<lopdf::Dictionary>,
925}
926
927struct FontStripPlan {
928 font_name_bytes: Vec<u8>,
935 font_dict_template: lopdf::Dictionary,
940 codes: crate::tounicode::TounicodeCodes,
941}
942
943pub(crate) fn resolve_inherited_resources_clone(
947 doc: &Document,
948 page_id: ObjectId,
949) -> Option<lopdf::Dictionary> {
950 let mut current = page_id;
951 let mut visited: std::collections::HashSet<ObjectId> = std::collections::HashSet::new();
952 loop {
953 if !visited.insert(current) {
954 return None;
956 }
957 let dict = doc.get_dictionary(current).ok()?;
958 if let Ok(res_obj) = dict.get(b"Resources") {
959 return match res_obj {
960 Object::Dictionary(d) => Some(d.clone()),
961 Object::Reference(id) => doc.get_dictionary(*id).ok().cloned(),
962 _ => None,
963 };
964 }
965 match dict.get(b"Parent") {
966 Ok(Object::Reference(id)) => current = *id,
967 _ => return None,
968 }
969 }
970}
971
972fn resolve_page_resources_snapshot(doc: &Document, page_id: ObjectId) -> Option<ResourcesSnapshot> {
973 let resources_dict = resolve_inherited_resources_clone(doc, page_id)?;
974 Some(snapshot_from_resources_dict(doc, resources_dict))
975}
976
977fn resolve_form_xobject_resources_snapshot(
982 doc: &Document,
983 form_id: ObjectId,
984) -> Option<ResourcesSnapshot> {
985 let stream_dict = match doc.get_object(form_id).ok()? {
986 Object::Stream(s) => &s.dict,
987 _ => return None,
988 };
989 let res_obj = stream_dict.get(b"Resources").ok()?;
990 let resources_dict = match res_obj {
991 Object::Dictionary(d) => d.clone(),
992 Object::Reference(id) => doc.get_dictionary(*id).ok().cloned()?,
993 _ => return None,
994 };
995 Some(snapshot_from_resources_dict(doc, resources_dict))
996}
997
998fn snapshot_from_resources_dict(
1002 doc: &Document,
1003 resources_dict: lopdf::Dictionary,
1004) -> ResourcesSnapshot {
1005 let font_dict = match resources_dict.get(b"Font") {
1006 Ok(Object::Dictionary(d)) => Some(d.clone()),
1007 Ok(Object::Reference(id)) => doc.get_dictionary(*id).ok().cloned(),
1008 _ => None,
1009 };
1010 let xobject_dict = match resources_dict.get(b"XObject") {
1011 Ok(Object::Dictionary(d)) => Some(d.clone()),
1012 Ok(Object::Reference(id)) => doc.get_dictionary(*id).ok().cloned(),
1013 _ => None,
1014 };
1015 ResourcesSnapshot {
1016 resources_dict,
1017 font_dict,
1018 xobject_dict,
1019 }
1020}
1021
1022struct FormXObjectEntry {
1026 name: Vec<u8>,
1029 original_id: ObjectId,
1031}
1032
1033fn collect_form_xobject_entries(
1034 doc: &Document,
1035 snapshot: &ResourcesSnapshot,
1036) -> Vec<FormXObjectEntry> {
1037 let Some(xobject_dict) = snapshot.xobject_dict.as_ref() else {
1038 return Vec::new();
1039 };
1040 let mut out = Vec::new();
1041 for (name_bytes, val) in xobject_dict.iter() {
1042 let id = match val {
1043 Object::Reference(id) => *id,
1044 _ => continue,
1047 };
1048 let stream = match doc.get_object(id) {
1049 Ok(Object::Stream(s)) => s,
1050 _ => continue,
1051 };
1052 let is_form = matches!(
1056 stream.dict.get(b"Subtype"),
1057 Ok(Object::Name(n)) if n == b"Form"
1058 );
1059 if !is_form {
1060 continue;
1061 }
1062 out.push(FormXObjectEntry {
1063 name: name_bytes.to_vec(),
1064 original_id: id,
1065 });
1066 }
1067 out
1068}
1069
1070fn clone_form_xobject_stream(doc: &mut Document, original_id: ObjectId) -> ObjectId {
1071 let cloned = match doc.get_object(original_id) {
1072 Ok(Object::Stream(s)) => s.clone(),
1073 _ => {
1074 return original_id;
1080 }
1081 };
1082 doc.add_object(Object::Stream(cloned))
1083}
1084
1085fn collect_strip_actions(
1086 doc: &Document,
1087 snapshot: &ResourcesSnapshot,
1088) -> Result<Vec<FontStripPlan>> {
1089 let Some(font_dict) = snapshot.font_dict.as_ref() else {
1090 return Ok(Vec::new());
1091 };
1092
1093 let mut plans = Vec::new();
1094 for (name_bytes, font_obj) in font_dict.iter() {
1095 let font_name_bytes: Vec<u8> = name_bytes.to_vec();
1102 let display_name = String::from_utf8_lossy(&font_name_bytes).into_owned();
1103
1104 let resolved_font_dict: lopdf::Dictionary = match font_obj {
1105 Object::Dictionary(d) => d.clone(),
1106 Object::Reference(id) => match doc.get_dictionary(*id).ok() {
1107 Some(d) => d.clone(),
1108 None => continue,
1109 },
1110 _ => continue,
1111 };
1112
1113 let to_unicode = match resolved_font_dict.get(b"ToUnicode") {
1114 Ok(o) => o,
1115 Err(_) => continue, };
1117
1118 if existing_tounicode_is_safe_stub(doc, to_unicode) {
1119 continue; }
1123
1124 let bytes = read_tounicode_bytes(doc, to_unicode).ok_or_else(|| {
1125 RedactError::UnsupportedToUnicodeCMap {
1126 font_resource_name: display_name.clone(),
1127 reason: "could not read /ToUnicode stream bytes".to_string(),
1128 }
1129 })?;
1130 let codes = crate::tounicode::extract_tounicode_codes(&bytes, &display_name)?;
1131 plans.push(FontStripPlan {
1132 font_name_bytes,
1133 font_dict_template: resolved_font_dict,
1134 codes,
1135 });
1136 }
1137 Ok(plans)
1138}
1139
1140struct PrivateContainerPath {
1144 font_dict_id: Option<ObjectId>,
1146 xobject_dict_id: Option<ObjectId>,
1148}
1149
1150fn materialize_private_path(
1160 doc: &mut Document,
1161 container_id: ObjectId,
1162 kind: ContainerKind,
1163 snapshot: &ResourcesSnapshot,
1164 clone_font: bool,
1165 clone_xobject: bool,
1166) -> PrivateContainerPath {
1167 let font_dict_id = if clone_font {
1168 let font_dict_clone = snapshot.font_dict.clone().unwrap_or_default();
1169 Some(doc.add_object(Object::Dictionary(font_dict_clone)))
1170 } else {
1171 None
1172 };
1173
1174 let xobject_dict_id = if clone_xobject {
1175 let xobject_dict_clone = snapshot.xobject_dict.clone().unwrap_or_default();
1176 Some(doc.add_object(Object::Dictionary(xobject_dict_clone)))
1177 } else {
1178 None
1179 };
1180
1181 let mut new_resources = snapshot.resources_dict.clone();
1184 if let Some(id) = font_dict_id {
1185 new_resources.set("Font", Object::Reference(id));
1186 }
1187 if let Some(id) = xobject_dict_id {
1188 new_resources.set("XObject", Object::Reference(id));
1189 }
1190 let new_resources_id = doc.add_object(Object::Dictionary(new_resources));
1191
1192 set_container_resources(doc, container_id, kind, Object::Reference(new_resources_id));
1196
1197 PrivateContainerPath {
1198 font_dict_id,
1199 xobject_dict_id,
1200 }
1201}
1202
1203fn set_container_resources(
1204 doc: &mut Document,
1205 container_id: ObjectId,
1206 kind: ContainerKind,
1207 value: Object,
1208) {
1209 match (kind, doc.get_object_mut(container_id)) {
1210 (ContainerKind::Page, Ok(Object::Dictionary(page))) => {
1211 page.set("Resources", value);
1212 }
1213 (ContainerKind::FormXObject, Ok(Object::Stream(s))) => {
1214 s.dict.set("Resources", value);
1215 }
1216 _ => {}
1219 }
1220}
1221
1222fn existing_tounicode_is_safe_stub(doc: &Document, to_unicode: &Object) -> bool {
1236 let stream = match to_unicode {
1237 Object::Reference(id) => match doc.get_object(*id) {
1238 Ok(Object::Stream(s)) => s.clone(),
1239 _ => return false,
1240 },
1241 Object::Stream(s) => s.clone(),
1242 _ => return false,
1243 };
1244 let marker_present = matches!(
1248 stream.dict.get(crate::tounicode::STUB_MARKER_KEY),
1249 Ok(Object::Boolean(true))
1250 );
1251 if !marker_present {
1252 return false;
1253 }
1254 let mut s = stream;
1255 let _ = s.decompress();
1256 crate::tounicode::is_safe_stub_cmap(&s.content)
1257}
1258
1259fn read_tounicode_bytes(doc: &Document, to_unicode: &Object) -> Option<Vec<u8>> {
1260 let stream = match to_unicode {
1261 Object::Reference(id) => match doc.get_object(*id) {
1262 Ok(Object::Stream(s)) => s.clone(),
1263 _ => return None,
1264 },
1265 Object::Stream(s) => s.clone(),
1266 _ => return None,
1267 };
1268 let mut s = stream;
1269 let _ = s.decompress();
1270 Some(s.content)
1271}
1272
1273fn build_and_insert_stub_stream(
1274 doc: &mut Document,
1275 codes: &crate::tounicode::TounicodeCodes,
1276) -> ObjectId {
1277 let stub_bytes = crate::tounicode::build_stub_cmap(codes);
1278 let compressed = compress_flate(&stub_bytes);
1279 let mut dict = lopdf::Dictionary::new();
1280 if compressed.len() < stub_bytes.len() {
1281 dict.set("Filter", Object::Name(b"FlateDecode".to_vec()));
1282 dict.set("Length", Object::Integer(compressed.len() as i64));
1283 dict.set(crate::tounicode::STUB_MARKER_KEY, Object::Boolean(true));
1284 let stream = lopdf::Stream::new(dict, compressed);
1285 doc.add_object(Object::Stream(stream))
1286 } else {
1287 dict.set("Length", Object::Integer(stub_bytes.len() as i64));
1288 dict.set(crate::tounicode::STUB_MARKER_KEY, Object::Boolean(true));
1289 let stream = lopdf::Stream::new(dict, stub_bytes);
1290 doc.add_object(Object::Stream(stream))
1291 }
1292}
1293
1294fn as_number(obj: &Object) -> Option<f64> {
1296 match obj {
1297 Object::Integer(i) => Some(*i as f64),
1298 Object::Real(f) => Some(*f as f64),
1299 _ => None,
1300 }
1301}
1302
1303#[cfg(test)]
1304mod tests {
1305 use super::*;
1306
1307 fn make_doc_with_text(content: &[u8]) -> Document {
1309 let mut doc = Document::with_version("1.7");
1310
1311 let content_stream = Stream::new(dictionary! {}, content.to_vec());
1312 let content_id = doc.add_object(Object::Stream(content_stream));
1313
1314 let page_dict = dictionary! {
1315 "Type" => "Page",
1316 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
1317 "Contents" => Object::Reference(content_id),
1318 };
1319 let page_id = doc.add_object(Object::Dictionary(page_dict));
1320
1321 let pages_dict = dictionary! {
1322 "Type" => "Pages",
1323 "Kids" => vec![Object::Reference(page_id)],
1324 "Count" => 1_i64,
1325 };
1326 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
1327
1328 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
1329 d.set("Parent", Object::Reference(pages_id));
1330 }
1331
1332 let info = dictionary! {
1334 "Title" => Object::String(b"Test".to_vec(), lopdf::StringFormat::Literal),
1335 "Author" => Object::String(b"Tester".to_vec(), lopdf::StringFormat::Literal),
1336 };
1337 let info_id = doc.add_object(Object::Dictionary(info));
1338 doc.trailer.set("Info", Object::Reference(info_id));
1339
1340 let catalog = dictionary! {
1341 "Type" => "Catalog",
1342 "Pages" => Object::Reference(pages_id),
1343 };
1344 let catalog_id = doc.add_object(Object::Dictionary(catalog));
1345 doc.trailer.set("Root", Object::Reference(catalog_id));
1346
1347 doc
1348 }
1349
1350 #[test]
1351 fn redact_empty_returns_error() {
1352 let mut doc = make_doc_with_text(b"BT /F1 12 Tf (Hello) Tj ET");
1353 let redactor = Redactor::new();
1354 let result = redactor.apply(&mut doc);
1355 assert!(result.is_err());
1356 }
1357
1358 #[test]
1359 fn redact_text_in_area() {
1360 let mut doc = make_doc_with_text(b"BT /F1 12 Tf 100 700 Td (Secret) Tj ET");
1361 let mut redactor = Redactor::new();
1362 redactor.mark(RedactionArea::new(1, [90.0, 690.0, 200.0, 720.0]));
1363
1364 let report = redactor.apply(&mut doc).unwrap();
1365 assert_eq!(report.areas_redacted, 1);
1366 assert!(report.operations_removed > 0);
1367 assert_eq!(report.pages_affected, 1);
1368 assert!(report.metadata_cleaned);
1369 }
1370
1371 #[test]
1372 fn redact_preserves_text_outside_area() {
1373 let mut doc =
1374 make_doc_with_text(b"BT /F1 12 Tf 100 700 Td (Keep) Tj 100 200 Td (Remove) Tj ET");
1375 let mut redactor = Redactor::new();
1376 redactor.mark(RedactionArea::new(1, [190.0, 890.0, 310.0, 920.0]));
1378
1379 let report = redactor.apply(&mut doc).unwrap();
1380 assert!(report.operations_removed > 0);
1382 }
1383
1384 #[test]
1385 fn redact_with_overlay_text() {
1386 let mut doc = make_doc_with_text(b"BT /F1 12 Tf 100 700 Td (Secret) Tj ET");
1387 let mut redactor = Redactor::new();
1388 redactor.mark(RedactionArea::new(1, [90.0, 690.0, 200.0, 720.0]).with_overlay("REDACTED"));
1389
1390 let report = redactor.apply(&mut doc).unwrap();
1391 assert_eq!(report.areas_redacted, 1);
1392 }
1393
1394 #[test]
1395 fn redact_with_custom_color() {
1396 let mut doc = make_doc_with_text(b"BT /F1 12 Tf 100 700 Td (Secret) Tj ET");
1397 let mut redactor = Redactor::new();
1398 redactor.mark(RedactionArea::new(1, [90.0, 690.0, 200.0, 720.0]).with_color(1.0, 0.0, 0.0));
1399
1400 let report = redactor.apply(&mut doc).unwrap();
1401 assert_eq!(report.areas_redacted, 1);
1402 }
1403
1404 #[test]
1405 fn redact_cleans_metadata() {
1406 let mut doc = make_doc_with_text(b"BT /F1 12 Tf 100 700 Td (Secret) Tj ET");
1407 let mut redactor = Redactor::new();
1408 redactor.mark(RedactionArea::new(1, [90.0, 690.0, 200.0, 720.0]));
1409
1410 let report = redactor.apply(&mut doc).unwrap();
1411 assert!(report.metadata_cleaned);
1412
1413 assert!(doc.trailer.get(b"Info").is_err());
1415 }
1416
1417 #[test]
1418 fn redact_page_out_of_range() {
1419 let mut doc = make_doc_with_text(b"BT /F1 12 Tf (Hello) Tj ET");
1420 let mut redactor = Redactor::new();
1421 redactor.mark(RedactionArea::new(5, [0.0, 0.0, 100.0, 100.0]));
1422
1423 let result = redactor.apply(&mut doc);
1424 assert!(result.is_err());
1425 }
1426
1427 #[test]
1428 fn redact_multiple_areas() {
1429 let mut doc =
1430 make_doc_with_text(b"BT /F1 12 Tf 100 700 Td (Secret1) Tj 100 600 Td (Secret2) Tj ET");
1431 let mut redactor = Redactor::new();
1432 redactor.mark_all(vec![
1433 RedactionArea::new(1, [90.0, 690.0, 200.0, 720.0]),
1434 RedactionArea::new(1, [190.0, 1290.0, 310.0, 1320.0]),
1435 ]);
1436
1437 assert_eq!(redactor.pending_count(), 2);
1438 let report = redactor.apply(&mut doc).unwrap();
1439 assert_eq!(report.areas_redacted, 2);
1440 }
1441
1442 const ORIGINAL_TOUNICODE: &[u8] = b"\
1447/CIDInit /ProcSet findresource begin
144812 dict begin
1449begincmap
1450/CIDSystemInfo
1451<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def
1452/CMapName /Adobe-Identity-UCS def
1453/CMapType 2 def
14541 begincodespacerange
1455<00> <FF>
1456endcodespacerange
14573 beginbfchar
1458<41> <0041>
1459<42> <0042>
1460<43> <0043>
1461endbfchar
1462endcmap
1463CMapName currentdict /CMap defineresource pop
1464end
1465end
1466";
1467
1468 fn make_doc_with_tounicode_font() -> (Document, ObjectId, ObjectId, ObjectId) {
1472 let mut doc = Document::with_version("1.7");
1473
1474 let content_stream = Stream::new(
1475 dictionary! {},
1476 b"BT /F1 12 Tf 100 700 Td <414243> Tj ET".to_vec(),
1477 );
1478 let content_id = doc.add_object(Object::Stream(content_stream));
1479
1480 let tu_stream = Stream::new(dictionary! {}, ORIGINAL_TOUNICODE.to_vec());
1482 let tu_id = doc.add_object(Object::Stream(tu_stream));
1483
1484 let font_dict = dictionary! {
1486 "Type" => "Font",
1487 "Subtype" => "Type1",
1488 "BaseFont" => "Helvetica",
1489 "ToUnicode" => Object::Reference(tu_id),
1490 };
1491 let font_id = doc.add_object(Object::Dictionary(font_dict));
1492
1493 let resources = dictionary! {
1495 "Font" => dictionary! {
1496 "F1" => Object::Reference(font_id),
1497 },
1498 };
1499
1500 let page_dict = dictionary! {
1501 "Type" => "Page",
1502 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
1503 "Contents" => Object::Reference(content_id),
1504 "Resources" => Object::Dictionary(resources),
1505 };
1506 let page_id = doc.add_object(Object::Dictionary(page_dict));
1507
1508 let pages = dictionary! {
1509 "Type" => "Pages",
1510 "Kids" => vec![Object::Reference(page_id)],
1511 "Count" => 1_i64,
1512 };
1513 let pages_id = doc.add_object(Object::Dictionary(pages));
1514
1515 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
1516 d.set("Parent", Object::Reference(pages_id));
1517 }
1518
1519 let catalog = dictionary! {
1520 "Type" => "Catalog",
1521 "Pages" => Object::Reference(pages_id),
1522 };
1523 let catalog_id = doc.add_object(Object::Dictionary(catalog));
1524 doc.trailer.set("Root", Object::Reference(catalog_id));
1525
1526 (doc, page_id, font_id, tu_id)
1527 }
1528
1529 fn current_tounicode_ref(doc: &Document, page_id: ObjectId) -> Option<ObjectId> {
1533 let page = doc.get_dictionary(page_id).ok()?;
1534 let resources = match page.get(b"Resources").ok()? {
1535 Object::Dictionary(d) => d.clone(),
1536 Object::Reference(id) => match doc.get_object(*id).ok()? {
1537 Object::Dictionary(d) => d.clone(),
1538 _ => return None,
1539 },
1540 _ => return None,
1541 };
1542 let font_dict_obj = resources.get(b"Font").ok()?;
1543 let font_dict = match font_dict_obj {
1544 Object::Dictionary(d) => d.clone(),
1545 Object::Reference(id) => match doc.get_object(*id).ok()? {
1546 Object::Dictionary(d) => d.clone(),
1547 _ => return None,
1548 },
1549 _ => return None,
1550 };
1551 let font_obj = font_dict.get(b"F1").ok()?;
1552 let font = match font_obj {
1553 Object::Dictionary(d) => d.clone(),
1554 Object::Reference(id) => match doc.get_object(*id).ok()? {
1555 Object::Dictionary(d) => d.clone(),
1556 _ => return None,
1557 },
1558 _ => return None,
1559 };
1560 match font.get(b"ToUnicode").ok()? {
1561 Object::Reference(id) => Some(*id),
1562 _ => None,
1563 }
1564 }
1565
1566 fn current_font_id(doc: &Document, page_id: ObjectId) -> Option<ObjectId> {
1567 let page = doc.get_dictionary(page_id).ok()?;
1568 let resources = match page.get(b"Resources").ok()? {
1569 Object::Dictionary(d) => d.clone(),
1570 Object::Reference(id) => match doc.get_object(*id).ok()? {
1571 Object::Dictionary(d) => d.clone(),
1572 _ => return None,
1573 },
1574 _ => return None,
1575 };
1576 let font_dict = match resources.get(b"Font").ok()? {
1577 Object::Dictionary(d) => d.clone(),
1578 Object::Reference(id) => match doc.get_object(*id).ok()? {
1579 Object::Dictionary(d) => d.clone(),
1580 _ => return None,
1581 },
1582 _ => return None,
1583 };
1584 match font_dict.get(b"F1").ok()? {
1585 Object::Reference(id) => Some(*id),
1586 _ => None,
1587 }
1588 }
1589
1590 #[test]
1591 fn tounicode_strip_replaces_cmap_on_redacted_page() {
1592 let (mut doc, _page_id, original_font_id, original_tu_id) = make_doc_with_tounicode_font();
1593
1594 let mut redactor = Redactor::new();
1595 redactor.mark(RedactionArea::new(1, [50.0, 690.0, 250.0, 720.0]));
1596 let report = redactor.apply(&mut doc).unwrap();
1597 assert!(report.pages_affected >= 1);
1598
1599 let new_font_id = current_font_id(&doc, _page_id).expect("F1 must still resolve");
1601 assert_ne!(
1602 new_font_id, original_font_id,
1603 "redacted page must point at a cloned font dict, not the shared original"
1604 );
1605
1606 let new_tu_id = current_tounicode_ref(&doc, _page_id)
1608 .expect("redacted page font must still have /ToUnicode (now a stub)");
1609 assert_ne!(
1610 new_tu_id, original_tu_id,
1611 "stub /ToUnicode must be a fresh stream, not the original"
1612 );
1613 let stub_stream = match doc.get_object(new_tu_id).unwrap() {
1614 Object::Stream(s) => s.clone(),
1615 _ => panic!("/ToUnicode must be a stream"),
1616 };
1617 assert!(
1618 matches!(
1619 stub_stream.dict.get(crate::tounicode::STUB_MARKER_KEY),
1620 Ok(Object::Boolean(true))
1621 ),
1622 "stub stream must carry the XfaRedactionStub marker"
1623 );
1624 }
1625
1626 #[test]
1627 fn tounicode_strip_preserves_original_for_non_redacted_pages_with_shared_font() {
1628 let (mut doc, page1_id, shared_font_id, original_tu_id) = make_doc_with_tounicode_font();
1631
1632 let content2 = Stream::new(
1634 dictionary! {},
1635 b"BT /F1 12 Tf 100 700 Td <414243> Tj ET".to_vec(),
1636 );
1637 let content2_id = doc.add_object(Object::Stream(content2));
1638 let resources2 = dictionary! {
1639 "Font" => dictionary! {
1640 "F1" => Object::Reference(shared_font_id),
1641 },
1642 };
1643 let page2_dict = dictionary! {
1644 "Type" => "Page",
1645 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
1646 "Contents" => Object::Reference(content2_id),
1647 "Resources" => Object::Dictionary(resources2),
1648 };
1649 let page2_id = doc.add_object(Object::Dictionary(page2_dict));
1650
1651 let pages_id = match doc
1653 .get_dictionary(page1_id)
1654 .unwrap()
1655 .get(b"Parent")
1656 .unwrap()
1657 {
1658 Object::Reference(id) => *id,
1659 _ => panic!("page must have indirect Parent"),
1660 };
1661 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(page2_id) {
1662 d.set("Parent", Object::Reference(pages_id));
1663 }
1664 if let Ok(Object::Dictionary(pages)) = doc.get_object_mut(pages_id) {
1665 pages.set(
1666 "Kids",
1667 vec![Object::Reference(page1_id), Object::Reference(page2_id)],
1668 );
1669 pages.set("Count", 2_i64);
1670 }
1671
1672 let mut redactor = Redactor::new();
1674 redactor.mark(RedactionArea::new(1, [50.0, 690.0, 250.0, 720.0]));
1675 redactor.apply(&mut doc).unwrap();
1676
1677 let page1_font_id = current_font_id(&doc, page1_id).unwrap();
1679 assert_ne!(
1680 page1_font_id, shared_font_id,
1681 "redacted page 1 must have a cloned font"
1682 );
1683
1684 let page2_font_id = current_font_id(&doc, page2_id).unwrap();
1686 assert_eq!(
1687 page2_font_id, shared_font_id,
1688 "non-redacted page 2 must keep its shared font reference"
1689 );
1690 let page2_tu_id = current_tounicode_ref(&doc, page2_id).unwrap();
1693 assert_eq!(
1694 page2_tu_id, original_tu_id,
1695 "non-redacted page 2 must keep the original /ToUnicode reference"
1696 );
1697 }
1698
1699 #[test]
1700 fn tounicode_strip_idempotent_on_repeated_redaction() {
1701 let (mut doc, page_id, _, _) = make_doc_with_tounicode_font();
1702
1703 let mut redactor = Redactor::new();
1704 redactor.mark(RedactionArea::new(1, [50.0, 690.0, 250.0, 720.0]));
1705 redactor.apply(&mut doc).unwrap();
1706
1707 let after_first = current_tounicode_ref(&doc, page_id).unwrap();
1708
1709 let mut redactor2 = Redactor::new();
1712 redactor2.mark(RedactionArea::new(1, [50.0, 690.0, 250.0, 720.0]));
1713 redactor2.apply(&mut doc).unwrap();
1714
1715 let after_second = current_tounicode_ref(&doc, page_id).unwrap();
1716 assert_eq!(
1717 after_first, after_second,
1718 "repeated redaction must not allocate a new stub /ToUnicode"
1719 );
1720 }
1721
1722 #[test]
1723 fn tounicode_strip_skips_font_without_existing_tounicode() {
1724 let mut doc = Document::with_version("1.7");
1726 let content = Stream::new(
1727 dictionary! {},
1728 b"BT /F1 12 Tf 100 700 Td (X) Tj ET".to_vec(),
1729 );
1730 let content_id = doc.add_object(Object::Stream(content));
1731 let font_dict = dictionary! {
1732 "Type" => "Font",
1733 "Subtype" => "Type1",
1734 "BaseFont" => "Helvetica",
1735 };
1736 let font_id = doc.add_object(Object::Dictionary(font_dict));
1737 let resources = dictionary! {
1738 "Font" => dictionary! { "F1" => Object::Reference(font_id) },
1739 };
1740 let page_dict = dictionary! {
1741 "Type" => "Page",
1742 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
1743 "Contents" => Object::Reference(content_id),
1744 "Resources" => Object::Dictionary(resources),
1745 };
1746 let page_id = doc.add_object(Object::Dictionary(page_dict));
1747 let pages = dictionary! {
1748 "Type" => "Pages",
1749 "Kids" => vec![Object::Reference(page_id)],
1750 "Count" => 1_i64,
1751 };
1752 let pages_id = doc.add_object(Object::Dictionary(pages));
1753 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
1754 d.set("Parent", Object::Reference(pages_id));
1755 }
1756 let catalog = dictionary! { "Type" => "Catalog", "Pages" => Object::Reference(pages_id) };
1757 let catalog_id = doc.add_object(Object::Dictionary(catalog));
1758 doc.trailer.set("Root", Object::Reference(catalog_id));
1759
1760 let original_font_id = font_id;
1761 let mut redactor = Redactor::new();
1762 redactor.mark(RedactionArea::new(1, [50.0, 690.0, 250.0, 720.0]));
1763 redactor.apply(&mut doc).unwrap();
1764
1765 let after_font_id = current_font_id(&doc, page_id).unwrap();
1768 assert_eq!(
1769 after_font_id, original_font_id,
1770 "font without /ToUnicode must not be cloned"
1771 );
1772 let font_dict = doc.get_dictionary(after_font_id).unwrap();
1773 assert!(
1774 font_dict.get(b"ToUnicode").is_err(),
1775 "no /ToUnicode key should be added by the strip"
1776 );
1777 }
1778
1779 #[test]
1780 fn tounicode_strip_returns_err_on_unsupported_cmap_shape() {
1781 let mut doc = Document::with_version("1.7");
1786 let content = Stream::new(
1787 dictionary! {},
1788 b"BT /F1 12 Tf 100 700 Td (X) Tj ET".to_vec(),
1789 );
1790 let content_id = doc.add_object(Object::Stream(content));
1791 let tu_stream = Stream::new(dictionary! {}, b"begincmap\nendcmap\n".to_vec());
1793 let tu_id = doc.add_object(Object::Stream(tu_stream));
1794 let font_dict = dictionary! {
1795 "Type" => "Font",
1796 "Subtype" => "Type1",
1797 "BaseFont" => "Helvetica",
1798 "ToUnicode" => Object::Reference(tu_id),
1799 };
1800 let font_id = doc.add_object(Object::Dictionary(font_dict));
1801 let resources = dictionary! {
1802 "Font" => dictionary! { "F1" => Object::Reference(font_id) },
1803 };
1804 let page_dict = dictionary! {
1805 "Type" => "Page",
1806 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
1807 "Contents" => Object::Reference(content_id),
1808 "Resources" => Object::Dictionary(resources),
1809 };
1810 let page_id = doc.add_object(Object::Dictionary(page_dict));
1811 let pages = dictionary! {
1812 "Type" => "Pages",
1813 "Kids" => vec![Object::Reference(page_id)],
1814 "Count" => 1_i64,
1815 };
1816 let pages_id = doc.add_object(Object::Dictionary(pages));
1817 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
1818 d.set("Parent", Object::Reference(pages_id));
1819 }
1820 let catalog = dictionary! { "Type" => "Catalog", "Pages" => Object::Reference(pages_id) };
1821 let catalog_id = doc.add_object(Object::Dictionary(catalog));
1822 doc.trailer.set("Root", Object::Reference(catalog_id));
1823
1824 let mut redactor = Redactor::new();
1825 redactor.mark(RedactionArea::new(1, [50.0, 690.0, 250.0, 720.0]));
1826 let err = redactor.apply(&mut doc).unwrap_err();
1827 assert!(
1828 matches!(err, RedactError::UnsupportedToUnicodeCMap { .. }),
1829 "expected UnsupportedToUnicodeCMap, got {err:?}"
1830 );
1831 }
1832
1833 #[test]
1834 fn tounicode_strip_resolves_resources_inherited_from_pages_parent() {
1835 let mut doc = Document::with_version("1.7");
1841
1842 let content = Stream::new(
1843 dictionary! {},
1844 b"BT /F1 12 Tf 100 700 Td <414243> Tj ET".to_vec(),
1845 );
1846 let content_id = doc.add_object(Object::Stream(content));
1847
1848 let tu_stream = Stream::new(dictionary! {}, ORIGINAL_TOUNICODE.to_vec());
1850 let tu_id = doc.add_object(Object::Stream(tu_stream));
1851 let font_dict = dictionary! {
1852 "Type" => "Font",
1853 "Subtype" => "Type1",
1854 "BaseFont" => "Helvetica",
1855 "ToUnicode" => Object::Reference(tu_id),
1856 };
1857 let font_id = doc.add_object(Object::Dictionary(font_dict));
1858
1859 let page_dict = dictionary! {
1861 "Type" => "Page",
1862 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
1863 "Contents" => Object::Reference(content_id),
1864 };
1865 let page_id = doc.add_object(Object::Dictionary(page_dict));
1866
1867 let inherited_resources = dictionary! {
1869 "Font" => dictionary! {
1870 "F1" => Object::Reference(font_id),
1871 },
1872 };
1873 let pages = dictionary! {
1874 "Type" => "Pages",
1875 "Kids" => vec![Object::Reference(page_id)],
1876 "Count" => 1_i64,
1877 "Resources" => Object::Dictionary(inherited_resources),
1878 };
1879 let pages_id = doc.add_object(Object::Dictionary(pages));
1880 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
1881 d.set("Parent", Object::Reference(pages_id));
1882 }
1883 let catalog = dictionary! { "Type" => "Catalog", "Pages" => Object::Reference(pages_id) };
1884 let catalog_id = doc.add_object(Object::Dictionary(catalog));
1885 doc.trailer.set("Root", Object::Reference(catalog_id));
1886
1887 let mut redactor = Redactor::new();
1889 redactor.mark(RedactionArea::new(1, [50.0, 690.0, 250.0, 720.0]));
1890 redactor.apply(&mut doc).unwrap();
1891
1892 let new_tu_id = current_tounicode_ref(&doc, page_id)
1897 .expect("inherited-resources page must have private /ToUnicode after redaction");
1898 assert_ne!(
1899 new_tu_id, tu_id,
1900 "stub /ToUnicode must be a fresh stream, not the original inherited one"
1901 );
1902 let stub_stream = match doc.get_object(new_tu_id).unwrap() {
1903 Object::Stream(s) => s.clone(),
1904 _ => panic!("/ToUnicode must be a stream"),
1905 };
1906 assert!(
1907 matches!(
1908 stub_stream.dict.get(crate::tounicode::STUB_MARKER_KEY),
1909 Ok(Object::Boolean(true))
1910 ),
1911 "inherited-Resources page must end up with a stub-marked /ToUnicode"
1912 );
1913 }
1914
1915 #[test]
1916 fn tounicode_strip_does_not_mutate_shared_indirect_font_subdict() {
1917 let mut doc = Document::with_version("1.7");
1923
1924 let content1 = Stream::new(
1925 dictionary! {},
1926 b"BT /F1 12 Tf 100 700 Td <41> Tj ET".to_vec(),
1927 );
1928 let content1_id = doc.add_object(Object::Stream(content1));
1929 let content2 = Stream::new(
1930 dictionary! {},
1931 b"BT /F1 12 Tf 100 700 Td <42> Tj ET".to_vec(),
1932 );
1933 let content2_id = doc.add_object(Object::Stream(content2));
1934
1935 let tu_stream = Stream::new(dictionary! {}, ORIGINAL_TOUNICODE.to_vec());
1937 let tu_id = doc.add_object(Object::Stream(tu_stream));
1938 let font_dict = dictionary! {
1939 "Type" => "Font",
1940 "Subtype" => "Type1",
1941 "BaseFont" => "Helvetica",
1942 "ToUnicode" => Object::Reference(tu_id),
1943 };
1944 let font_id = doc.add_object(Object::Dictionary(font_dict));
1945
1946 let shared_font_subdict = dictionary! {
1949 "F1" => Object::Reference(font_id),
1950 };
1951 let shared_font_subdict_id = doc.add_object(Object::Dictionary(shared_font_subdict));
1952
1953 let resources1 = dictionary! {
1954 "Font" => Object::Reference(shared_font_subdict_id),
1955 };
1956 let resources2 = dictionary! {
1957 "Font" => Object::Reference(shared_font_subdict_id),
1958 };
1959
1960 let page1_dict = dictionary! {
1961 "Type" => "Page",
1962 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
1963 "Contents" => Object::Reference(content1_id),
1964 "Resources" => Object::Dictionary(resources1),
1965 };
1966 let page1_id = doc.add_object(Object::Dictionary(page1_dict));
1967 let page2_dict = dictionary! {
1968 "Type" => "Page",
1969 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
1970 "Contents" => Object::Reference(content2_id),
1971 "Resources" => Object::Dictionary(resources2),
1972 };
1973 let page2_id = doc.add_object(Object::Dictionary(page2_dict));
1974 let pages = dictionary! {
1975 "Type" => "Pages",
1976 "Kids" => vec![Object::Reference(page1_id), Object::Reference(page2_id)],
1977 "Count" => 2_i64,
1978 };
1979 let pages_id = doc.add_object(Object::Dictionary(pages));
1980 for pid in [page1_id, page2_id] {
1981 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(pid) {
1982 d.set("Parent", Object::Reference(pages_id));
1983 }
1984 }
1985 let catalog = dictionary! { "Type" => "Catalog", "Pages" => Object::Reference(pages_id) };
1986 let catalog_id = doc.add_object(Object::Dictionary(catalog));
1987 doc.trailer.set("Root", Object::Reference(catalog_id));
1988
1989 let before_subdict_f1 = match doc
1991 .get_dictionary(shared_font_subdict_id)
1992 .unwrap()
1993 .get(b"F1")
1994 .unwrap()
1995 {
1996 Object::Reference(id) => *id,
1997 _ => panic!("F1 entry must be indirect to be shareable"),
1998 };
1999 assert_eq!(before_subdict_f1, font_id);
2000
2001 let mut redactor = Redactor::new();
2003 redactor.mark(RedactionArea::new(1, [50.0, 690.0, 250.0, 720.0]));
2004 redactor.apply(&mut doc).unwrap();
2005
2006 let shared_after = doc.get_dictionary(shared_font_subdict_id).unwrap();
2010 let after_subdict_f1 = match shared_after.get(b"F1").unwrap() {
2011 Object::Reference(id) => *id,
2012 _ => panic!("F1 must still be indirect"),
2013 };
2014 assert_eq!(
2015 after_subdict_f1, font_id,
2016 "shared /Font sub-dict was mutated in place — page 2 will inherit page 1's stub"
2017 );
2018
2019 let page2_tu = current_tounicode_ref(&doc, page2_id)
2021 .expect("non-redacted page 2 must keep its /ToUnicode");
2022 assert_eq!(
2023 page2_tu, tu_id,
2024 "non-redacted page 2 must still use the original /ToUnicode"
2025 );
2026
2027 let page1_tu = current_tounicode_ref(&doc, page1_id)
2029 .expect("redacted page 1 must have its private /ToUnicode stub");
2030 assert_ne!(
2031 page1_tu, tu_id,
2032 "redacted page 1 must use the stub /ToUnicode, not the original"
2033 );
2034 }
2035
2036 #[test]
2037 fn tounicode_strip_rejects_crafted_marker_without_safe_stub_content() {
2038 let mut doc = Document::with_version("1.7");
2050 let content = Stream::new(
2051 dictionary! {},
2052 b"BT /F1 12 Tf 100 700 Td <414243> Tj ET".to_vec(),
2053 );
2054 let content_id = doc.add_object(Object::Stream(content));
2055
2056 let mut tu_dict = lopdf::Dictionary::new();
2058 tu_dict.set(crate::tounicode::STUB_MARKER_KEY, Object::Boolean(true));
2059 let tu_stream = Stream::new(tu_dict, ORIGINAL_TOUNICODE.to_vec());
2060 let crafted_tu_id = doc.add_object(Object::Stream(tu_stream));
2061
2062 let font_dict = dictionary! {
2063 "Type" => "Font",
2064 "Subtype" => "Type1",
2065 "BaseFont" => "Helvetica",
2066 "ToUnicode" => Object::Reference(crafted_tu_id),
2067 };
2068 let font_id = doc.add_object(Object::Dictionary(font_dict));
2069
2070 let resources = dictionary! {
2071 "Font" => dictionary! {
2072 "F1" => Object::Reference(font_id),
2073 },
2074 };
2075 let page_dict = dictionary! {
2076 "Type" => "Page",
2077 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
2078 "Contents" => Object::Reference(content_id),
2079 "Resources" => Object::Dictionary(resources),
2080 };
2081 let page_id = doc.add_object(Object::Dictionary(page_dict));
2082 let pages = dictionary! {
2083 "Type" => "Pages",
2084 "Kids" => vec![Object::Reference(page_id)],
2085 "Count" => 1_i64,
2086 };
2087 let pages_id = doc.add_object(Object::Dictionary(pages));
2088 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
2089 d.set("Parent", Object::Reference(pages_id));
2090 }
2091 let catalog = dictionary! { "Type" => "Catalog", "Pages" => Object::Reference(pages_id) };
2092 let catalog_id = doc.add_object(Object::Dictionary(catalog));
2093 doc.trailer.set("Root", Object::Reference(catalog_id));
2094
2095 let mut redactor = Redactor::new();
2096 redactor.mark(RedactionArea::new(1, [50.0, 690.0, 250.0, 720.0]));
2097 redactor.apply(&mut doc).unwrap();
2098
2099 let new_tu_id = current_tounicode_ref(&doc, page_id)
2103 .expect("page must still have /ToUnicode after redaction");
2104 assert_ne!(
2105 new_tu_id, crafted_tu_id,
2106 "crafted-marker /ToUnicode must NOT be honored as already-stubbed"
2107 );
2108
2109 let new_stream = match doc.get_object(new_tu_id).unwrap() {
2111 Object::Stream(s) => s.clone(),
2112 _ => panic!("/ToUnicode must be a stream"),
2113 };
2114 let mut s = new_stream;
2115 let _ = s.decompress();
2116 assert!(
2117 crate::tounicode::is_safe_stub_cmap(&s.content),
2118 "redactor must replace crafted-marker stream with a content-validated stub"
2119 );
2120 }
2121
2122 #[test]
2123 fn tounicode_strip_preserves_non_utf8_font_resource_key() {
2124 let mut doc = Document::with_version("1.7");
2135 let content = Stream::new(
2136 dictionary! {},
2137 b"BT /F1 12 Tf 100 700 Td <414243> Tj ET".to_vec(),
2138 );
2139 let content_id = doc.add_object(Object::Stream(content));
2140
2141 let tu_stream = Stream::new(dictionary! {}, ORIGINAL_TOUNICODE.to_vec());
2143 let tu_id = doc.add_object(Object::Stream(tu_stream));
2144 let font_dict = dictionary! {
2145 "Type" => "Font",
2146 "Subtype" => "Type1",
2147 "BaseFont" => "Helvetica",
2148 "ToUnicode" => Object::Reference(tu_id),
2149 };
2150 let original_font_id = doc.add_object(Object::Dictionary(font_dict));
2151
2152 let raw_key: Vec<u8> = vec![0xFF, b'F', b'1'];
2155 let mut font_subdict = lopdf::Dictionary::new();
2156 font_subdict.set(raw_key.clone(), Object::Reference(original_font_id));
2157
2158 let resources = dictionary! {
2159 "Font" => Object::Dictionary(font_subdict),
2160 };
2161 let page_dict = dictionary! {
2162 "Type" => "Page",
2163 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
2164 "Contents" => Object::Reference(content_id),
2165 "Resources" => Object::Dictionary(resources),
2166 };
2167 let page_id = doc.add_object(Object::Dictionary(page_dict));
2168 let pages = dictionary! {
2169 "Type" => "Pages",
2170 "Kids" => vec![Object::Reference(page_id)],
2171 "Count" => 1_i64,
2172 };
2173 let pages_id = doc.add_object(Object::Dictionary(pages));
2174 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
2175 d.set("Parent", Object::Reference(pages_id));
2176 }
2177 let catalog = dictionary! { "Type" => "Catalog", "Pages" => Object::Reference(pages_id) };
2178 let catalog_id = doc.add_object(Object::Dictionary(catalog));
2179 doc.trailer.set("Root", Object::Reference(catalog_id));
2180
2181 let lossy_key_bytes = String::from_utf8_lossy(&raw_key).into_owned().into_bytes();
2183 assert_ne!(
2184 lossy_key_bytes, raw_key,
2185 "test sanity: lossy conversion must produce different bytes for 0xFF F1"
2186 );
2187
2188 let mut redactor = Redactor::new();
2190 redactor.mark(RedactionArea::new(1, [50.0, 690.0, 250.0, 720.0]));
2191 redactor.apply(&mut doc).unwrap();
2192
2193 let page_dict = doc.get_dictionary(page_id).unwrap();
2195 let resources_id = match page_dict.get(b"Resources").unwrap() {
2196 Object::Reference(id) => *id,
2197 _ => panic!("strip should have made /Resources indirect"),
2198 };
2199 let resources = doc.get_dictionary(resources_id).unwrap();
2200 let font_subdict_id = match resources.get(b"Font").unwrap() {
2201 Object::Reference(id) => *id,
2202 _ => panic!("strip should have made /Font indirect"),
2203 };
2204 let font_subdict = doc.get_dictionary(font_subdict_id).unwrap();
2205
2206 let entry = font_subdict
2211 .get(raw_key.as_slice())
2212 .expect("raw byte key must still resolve in the private /Font sub-dict");
2213 let cloned_font_id = match entry {
2214 Object::Reference(id) => *id,
2215 _ => panic!("raw key entry must be indirect"),
2216 };
2217 assert_ne!(
2218 cloned_font_id, original_font_id,
2219 "raw key must reference the cloned font dict, not the original"
2220 );
2221
2222 assert!(
2226 font_subdict.get(lossy_key_bytes.as_slice()).is_err(),
2227 "lossy-converted key must not exist in /Font — silent bypass guard"
2228 );
2229
2230 let cloned_font = doc.get_dictionary(cloned_font_id).unwrap();
2232 let new_tu_id = match cloned_font.get(b"ToUnicode").unwrap() {
2233 Object::Reference(id) => *id,
2234 _ => panic!("cloned font must have indirect /ToUnicode"),
2235 };
2236 assert_ne!(new_tu_id, tu_id, "stub must be a fresh stream");
2237 let new_stream = match doc.get_object(new_tu_id).unwrap() {
2238 Object::Stream(s) => s.clone(),
2239 _ => panic!("/ToUnicode must be a stream"),
2240 };
2241 let mut s = new_stream;
2242 let _ = s.decompress();
2243 assert!(
2244 crate::tounicode::is_safe_stub_cmap(&s.content),
2245 "raw-key path must end up at a content-validated stub stream"
2246 );
2247 }
2248
2249 #[test]
2250 fn tounicode_strip_rejects_crafted_usecmap_with_trivial_bfchar() {
2251 let mut doc = Document::with_version("1.7");
2261 let content = Stream::new(
2262 dictionary! {},
2263 b"BT /F1 12 Tf 100 700 Td <414243> Tj ET".to_vec(),
2264 );
2265 let content_id = doc.add_object(Object::Stream(content));
2266
2267 let crafted_cmap = b"\
2270/CIDInit /ProcSet findresource begin
227112 dict begin
2272begincmap
2273/CIDSystemInfo
2274<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def
2275/CMapName /Adobe-Identity-UCS def
2276/CMapType 2 def
2277/Identity-H usecmap
22781 begincodespacerange
2279<00> <FF>
2280endcodespacerange
22811 beginbfchar
2282<00> <FFFD>
2283endbfchar
2284endcmap
2285CMapName currentdict /CMap defineresource pop
2286end
2287end
2288";
2289 let mut tu_dict = lopdf::Dictionary::new();
2290 tu_dict.set(crate::tounicode::STUB_MARKER_KEY, Object::Boolean(true));
2291 let tu_stream = Stream::new(tu_dict, crafted_cmap.to_vec());
2292 let crafted_tu_id = doc.add_object(Object::Stream(tu_stream));
2293
2294 let font_dict = dictionary! {
2295 "Type" => "Font",
2296 "Subtype" => "Type1",
2297 "BaseFont" => "Helvetica",
2298 "ToUnicode" => Object::Reference(crafted_tu_id),
2299 };
2300 let font_id = doc.add_object(Object::Dictionary(font_dict));
2301 let resources = dictionary! {
2302 "Font" => dictionary! { "F1" => Object::Reference(font_id) },
2303 };
2304 let page_dict = dictionary! {
2305 "Type" => "Page",
2306 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
2307 "Contents" => Object::Reference(content_id),
2308 "Resources" => Object::Dictionary(resources),
2309 };
2310 let page_id = doc.add_object(Object::Dictionary(page_dict));
2311 let pages = dictionary! {
2312 "Type" => "Pages",
2313 "Kids" => vec![Object::Reference(page_id)],
2314 "Count" => 1_i64,
2315 };
2316 let pages_id = doc.add_object(Object::Dictionary(pages));
2317 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
2318 d.set("Parent", Object::Reference(pages_id));
2319 }
2320 let catalog = dictionary! { "Type" => "Catalog", "Pages" => Object::Reference(pages_id) };
2321 let catalog_id = doc.add_object(Object::Dictionary(catalog));
2322 doc.trailer.set("Root", Object::Reference(catalog_id));
2323
2324 let mut redactor = Redactor::new();
2325 redactor.mark(RedactionArea::new(1, [50.0, 690.0, 250.0, 720.0]));
2326 redactor.apply(&mut doc).unwrap();
2327
2328 let new_tu_id = current_tounicode_ref(&doc, page_id)
2332 .expect("page must still have /ToUnicode after redaction");
2333 assert_ne!(
2334 new_tu_id, crafted_tu_id,
2335 "usecmap-carrying /ToUnicode must NOT be honoured as already-stubbed"
2336 );
2337
2338 let new_stream = match doc.get_object(new_tu_id).unwrap() {
2339 Object::Stream(s) => s.clone(),
2340 _ => panic!("/ToUnicode must be a stream"),
2341 };
2342 let mut s = new_stream;
2343 let _ = s.decompress();
2344 assert!(
2346 !s.content.windows(b"usecmap".len()).any(|w| w == b"usecmap"),
2347 "replacement stub must not contain usecmap"
2348 );
2349 assert!(
2350 crate::tounicode::is_safe_stub_cmap(&s.content),
2351 "replacement stream must validate as safe stub"
2352 );
2353 }
2354
2355 fn add_font_with_tounicode(doc: &mut Document) -> (ObjectId, ObjectId) {
2362 let tu_stream = Stream::new(dictionary! {}, ORIGINAL_TOUNICODE.to_vec());
2363 let tu_id = doc.add_object(Object::Stream(tu_stream));
2364 let font_dict = dictionary! {
2365 "Type" => "Font",
2366 "Subtype" => "Type1",
2367 "BaseFont" => "Helvetica",
2368 "ToUnicode" => Object::Reference(tu_id),
2369 };
2370 let font_id = doc.add_object(Object::Dictionary(font_dict));
2371 (font_id, tu_id)
2372 }
2373
2374 fn add_form_xobject_with_font(
2377 doc: &mut Document,
2378 font_resource_name: &[u8],
2379 font_id: ObjectId,
2380 ) -> ObjectId {
2381 let resources = dictionary! {
2382 "Font" => dictionary! {
2383 font_resource_name.to_vec() => Object::Reference(font_id),
2384 },
2385 };
2386 let mut form_dict = dictionary! {
2387 "Type" => "XObject",
2388 "Subtype" => "Form",
2389 "BBox" => vec![0.into(), 0.into(), 100.into(), 100.into()],
2390 "Resources" => Object::Dictionary(resources),
2391 };
2392 form_dict.set("Length", Object::Integer(0));
2393 let form_stream = Stream::new(form_dict, Vec::new());
2394 doc.add_object(Object::Stream(form_stream))
2395 }
2396
2397 fn page_form_font_tounicode(
2400 doc: &Document,
2401 page_id: ObjectId,
2402 xobject_name: &[u8],
2403 font_name: &[u8],
2404 ) -> Option<(ObjectId, ObjectId)> {
2405 let page = doc.get_dictionary(page_id).ok()?;
2406 let resources = match page.get(b"Resources").ok()? {
2407 Object::Dictionary(d) => d.clone(),
2408 Object::Reference(id) => doc.get_dictionary(*id).ok().cloned()?,
2409 _ => return None,
2410 };
2411 let xobject_dict = match resources.get(b"XObject").ok()? {
2412 Object::Dictionary(d) => d.clone(),
2413 Object::Reference(id) => doc.get_dictionary(*id).ok().cloned()?,
2414 _ => return None,
2415 };
2416 let form_id = match xobject_dict.get(xobject_name).ok()? {
2417 Object::Reference(id) => *id,
2418 _ => return None,
2419 };
2420 let form_stream = match doc.get_object(form_id).ok()? {
2421 Object::Stream(s) => s,
2422 _ => return None,
2423 };
2424 let form_resources = match form_stream.dict.get(b"Resources").ok()? {
2425 Object::Dictionary(d) => d.clone(),
2426 Object::Reference(id) => doc.get_dictionary(*id).ok().cloned()?,
2427 _ => return None,
2428 };
2429 let font_sub = match form_resources.get(b"Font").ok()? {
2430 Object::Dictionary(d) => d.clone(),
2431 Object::Reference(id) => doc.get_dictionary(*id).ok().cloned()?,
2432 _ => return None,
2433 };
2434 let font_id = match font_sub.get(font_name).ok()? {
2435 Object::Reference(id) => *id,
2436 _ => return None,
2437 };
2438 let font_dict = doc.get_dictionary(font_id).ok()?;
2439 let tu_id = match font_dict.get(b"ToUnicode").ok()? {
2440 Object::Reference(id) => *id,
2441 _ => return None,
2442 };
2443 Some((form_id, tu_id))
2444 }
2445
2446 #[test]
2447 fn tounicode_strip_traverses_form_xobject_fonts() {
2448 let mut doc = Document::with_version("1.7");
2454 let content = Stream::new(
2455 dictionary! {},
2456 b"BT /Fp1 12 Tf 100 700 Td (page-level) Tj ET q /Fm1 Do Q".to_vec(),
2457 );
2458 let content_id = doc.add_object(Object::Stream(content));
2459
2460 let (page_font_id, _page_tu_id) = add_font_with_tounicode(&mut doc);
2462 let (form_font_id, original_form_tu_id) = add_font_with_tounicode(&mut doc);
2464 let form_id = add_form_xobject_with_font(&mut doc, b"F1", form_font_id);
2465
2466 let resources = dictionary! {
2467 "Font" => dictionary! { "Fp1" => Object::Reference(page_font_id) },
2468 "XObject" => dictionary! { "Fm1" => Object::Reference(form_id) },
2469 };
2470 let page_dict = dictionary! {
2471 "Type" => "Page",
2472 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
2473 "Contents" => Object::Reference(content_id),
2474 "Resources" => Object::Dictionary(resources),
2475 };
2476 let page_id = doc.add_object(Object::Dictionary(page_dict));
2477 let pages = dictionary! {
2478 "Type" => "Pages",
2479 "Kids" => vec![Object::Reference(page_id)],
2480 "Count" => 1_i64,
2481 };
2482 let pages_id = doc.add_object(Object::Dictionary(pages));
2483 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
2484 d.set("Parent", Object::Reference(pages_id));
2485 }
2486 let catalog = dictionary! { "Type" => "Catalog", "Pages" => Object::Reference(pages_id) };
2487 let catalog_id = doc.add_object(Object::Dictionary(catalog));
2488 doc.trailer.set("Root", Object::Reference(catalog_id));
2489
2490 let mut redactor = Redactor::new();
2491 redactor.mark(RedactionArea::new(1, [50.0, 690.0, 250.0, 720.0]));
2492 redactor.apply(&mut doc).unwrap();
2493
2494 let (cloned_form_id, new_form_tu_id) =
2499 page_form_font_tounicode(&doc, page_id, b"Fm1", b"F1")
2500 .expect("Form XObject + font + /ToUnicode must still be reachable");
2501 assert_ne!(
2502 cloned_form_id, form_id,
2503 "Form XObject must be cloned for the redacted page"
2504 );
2505 assert_ne!(
2506 new_form_tu_id, original_form_tu_id,
2507 "Form XObject's font must point at a fresh stub /ToUnicode"
2508 );
2509 let stub_stream = match doc.get_object(new_form_tu_id).unwrap() {
2510 Object::Stream(s) => s.clone(),
2511 _ => panic!("/ToUnicode must be a stream"),
2512 };
2513 let mut s = stub_stream;
2514 let _ = s.decompress();
2515 assert!(
2516 crate::tounicode::is_safe_stub_cmap(&s.content),
2517 "Form XObject font's new /ToUnicode must validate as safe stub"
2518 );
2519 }
2520
2521 #[test]
2522 fn tounicode_strip_clones_shared_form_xobject_for_redacted_page_only() {
2523 let mut doc = Document::with_version("1.7");
2529 let (form_font_id, original_tu_id) = add_font_with_tounicode(&mut doc);
2530 let shared_form_id = add_form_xobject_with_font(&mut doc, b"F1", form_font_id);
2531
2532 let make_page_dict = |form_id: ObjectId| {
2533 dictionary! {
2534 "Type" => "Page",
2535 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
2536 "Contents" => Object::Reference(doc_objects_dummy()),
2537 "Resources" => Object::Dictionary(dictionary! {
2538 "XObject" => dictionary! { "Fm1" => Object::Reference(form_id) },
2539 }),
2540 }
2541 };
2542 let content = Stream::new(dictionary! {}, b"q /Fm1 Do Q".to_vec());
2544 let content_id = doc.add_object(Object::Stream(content));
2545 let page1_dict = dictionary! {
2546 "Type" => "Page",
2547 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
2548 "Contents" => Object::Reference(content_id),
2549 "Resources" => Object::Dictionary(dictionary! {
2550 "XObject" => dictionary! { "Fm1" => Object::Reference(shared_form_id) },
2551 }),
2552 };
2553 let page1_id = doc.add_object(Object::Dictionary(page1_dict));
2554 let page2_dict = dictionary! {
2555 "Type" => "Page",
2556 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
2557 "Contents" => Object::Reference(content_id),
2558 "Resources" => Object::Dictionary(dictionary! {
2559 "XObject" => dictionary! { "Fm1" => Object::Reference(shared_form_id) },
2560 }),
2561 };
2562 let page2_id = doc.add_object(Object::Dictionary(page2_dict));
2563 let _ = make_page_dict;
2566
2567 let pages = dictionary! {
2568 "Type" => "Pages",
2569 "Kids" => vec![Object::Reference(page1_id), Object::Reference(page2_id)],
2570 "Count" => 2_i64,
2571 };
2572 let pages_id = doc.add_object(Object::Dictionary(pages));
2573 for pid in [page1_id, page2_id] {
2574 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(pid) {
2575 d.set("Parent", Object::Reference(pages_id));
2576 }
2577 }
2578 let catalog = dictionary! { "Type" => "Catalog", "Pages" => Object::Reference(pages_id) };
2579 let catalog_id = doc.add_object(Object::Dictionary(catalog));
2580 doc.trailer.set("Root", Object::Reference(catalog_id));
2581
2582 let mut redactor = Redactor::new();
2583 redactor.mark(RedactionArea::new(1, [0.0, 0.0, 100.0, 100.0]));
2584 redactor.apply(&mut doc).unwrap();
2585
2586 let (page1_form, page1_tu) =
2588 page_form_font_tounicode(&doc, page1_id, b"Fm1", b"F1").unwrap();
2589 assert_ne!(page1_form, shared_form_id);
2590 assert_ne!(page1_tu, original_tu_id);
2591
2592 let (page2_form, page2_tu) =
2594 page_form_font_tounicode(&doc, page2_id, b"Fm1", b"F1").unwrap();
2595 assert_eq!(
2596 page2_form, shared_form_id,
2597 "non-redacted page must still reference the original Form XObject"
2598 );
2599 assert_eq!(
2600 page2_tu, original_tu_id,
2601 "non-redacted page must still see the original /ToUnicode"
2602 );
2603
2604 let shared_form_stream = match doc.get_object(shared_form_id).unwrap() {
2607 Object::Stream(s) => s.clone(),
2608 _ => panic!("shared Form XObject must still exist as a Stream"),
2609 };
2610 let shared_resources = match shared_form_stream.dict.get(b"Resources").unwrap() {
2613 Object::Dictionary(d) => d.clone(),
2614 _ => panic!("shared Form Resources must be inline dict"),
2615 };
2616 let shared_font_dict = match shared_resources.get(b"Font").unwrap() {
2617 Object::Dictionary(d) => d.clone(),
2618 _ => panic!("shared Form /Font must be inline dict"),
2619 };
2620 let shared_font_ref = match shared_font_dict.get(b"F1").unwrap() {
2621 Object::Reference(id) => *id,
2622 _ => panic!("shared Form /F1 must be indirect"),
2623 };
2624 let shared_font_dict_obj = doc.get_dictionary(shared_font_ref).unwrap();
2625 let shared_tu_ref = match shared_font_dict_obj.get(b"ToUnicode").unwrap() {
2626 Object::Reference(id) => *id,
2627 _ => panic!("shared Form font /ToUnicode must be indirect"),
2628 };
2629 assert_eq!(
2630 shared_tu_ref, original_tu_id,
2631 "shared Form's font must still reference the ORIGINAL /ToUnicode"
2632 );
2633 }
2634
2635 #[test]
2636 fn tounicode_strip_recurses_into_nested_form_xobject() {
2637 let mut doc = Document::with_version("1.7");
2641 let (inner_font_id, inner_original_tu) = add_font_with_tounicode(&mut doc);
2642 let inner_form_id = add_form_xobject_with_font(&mut doc, b"FInner", inner_font_id);
2643
2644 let outer_resources = dictionary! {
2646 "XObject" => dictionary! { "Fm2" => Object::Reference(inner_form_id) },
2647 };
2648 let mut outer_form_dict = dictionary! {
2649 "Type" => "XObject",
2650 "Subtype" => "Form",
2651 "BBox" => vec![0.into(), 0.into(), 100.into(), 100.into()],
2652 "Resources" => Object::Dictionary(outer_resources),
2653 };
2654 outer_form_dict.set("Length", Object::Integer(0));
2655 let outer_form_stream = Stream::new(outer_form_dict, Vec::new());
2656 let outer_form_id = doc.add_object(Object::Stream(outer_form_stream));
2657
2658 let content = Stream::new(dictionary! {}, b"q /Fm1 Do Q".to_vec());
2659 let content_id = doc.add_object(Object::Stream(content));
2660 let resources = dictionary! {
2661 "XObject" => dictionary! { "Fm1" => Object::Reference(outer_form_id) },
2662 };
2663 let page_dict = dictionary! {
2664 "Type" => "Page",
2665 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
2666 "Contents" => Object::Reference(content_id),
2667 "Resources" => Object::Dictionary(resources),
2668 };
2669 let page_id = doc.add_object(Object::Dictionary(page_dict));
2670 let pages = dictionary! {
2671 "Type" => "Pages",
2672 "Kids" => vec![Object::Reference(page_id)],
2673 "Count" => 1_i64,
2674 };
2675 let pages_id = doc.add_object(Object::Dictionary(pages));
2676 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
2677 d.set("Parent", Object::Reference(pages_id));
2678 }
2679 let catalog = dictionary! { "Type" => "Catalog", "Pages" => Object::Reference(pages_id) };
2680 let catalog_id = doc.add_object(Object::Dictionary(catalog));
2681 doc.trailer.set("Root", Object::Reference(catalog_id));
2682
2683 let mut redactor = Redactor::new();
2684 redactor.mark(RedactionArea::new(1, [0.0, 0.0, 100.0, 100.0]));
2685 redactor.apply(&mut doc).unwrap();
2686
2687 let page_outer_form_id = match doc
2690 .get_dictionary(page_id)
2691 .unwrap()
2692 .get(b"Resources")
2693 .unwrap()
2694 {
2695 Object::Reference(rid) => {
2696 match doc.get_dictionary(*rid).unwrap().get(b"XObject").unwrap() {
2697 Object::Reference(xrid) => {
2698 match doc.get_dictionary(*xrid).unwrap().get(b"Fm1").unwrap() {
2699 Object::Reference(id) => *id,
2700 _ => panic!("Fm1 must be indirect"),
2701 }
2702 }
2703 Object::Dictionary(d) => match d.get(b"Fm1").unwrap() {
2704 Object::Reference(id) => *id,
2705 _ => panic!("Fm1 must be indirect"),
2706 },
2707 _ => panic!("/XObject must be dict or ref"),
2708 }
2709 }
2710 _ => panic!("page /Resources must be indirect after redaction"),
2711 };
2712 assert_ne!(
2713 page_outer_form_id, outer_form_id,
2714 "outer Form must be cloned for the redacted page"
2715 );
2716 let outer_form_stream = match doc.get_object(page_outer_form_id).unwrap() {
2717 Object::Stream(s) => s.clone(),
2718 _ => panic!("outer Form must still be a Stream"),
2719 };
2720 let outer_resources = match outer_form_stream.dict.get(b"Resources").unwrap() {
2721 Object::Reference(rid) => doc.get_dictionary(*rid).unwrap().clone(),
2722 Object::Dictionary(d) => d.clone(),
2723 _ => panic!("outer Form Resources must be present"),
2724 };
2725 let inner_xobject_dict = match outer_resources.get(b"XObject").unwrap() {
2726 Object::Reference(rid) => doc.get_dictionary(*rid).unwrap().clone(),
2727 Object::Dictionary(d) => d.clone(),
2728 _ => panic!("outer Form /XObject must be dict"),
2729 };
2730 let inner_clone_id = match inner_xobject_dict.get(b"Fm2").unwrap() {
2731 Object::Reference(id) => *id,
2732 _ => panic!("Fm2 must be indirect"),
2733 };
2734 assert_ne!(
2735 inner_clone_id, inner_form_id,
2736 "inner Form must also be cloned by recursion"
2737 );
2738 let inner_form_stream = match doc.get_object(inner_clone_id).unwrap() {
2739 Object::Stream(s) => s.clone(),
2740 _ => panic!("inner clone must still be a Stream"),
2741 };
2742 let inner_resources = match inner_form_stream.dict.get(b"Resources").unwrap() {
2743 Object::Reference(rid) => doc.get_dictionary(*rid).unwrap().clone(),
2744 Object::Dictionary(d) => d.clone(),
2745 _ => panic!("inner Resources missing"),
2746 };
2747 let inner_font_subdict = match inner_resources.get(b"Font").unwrap() {
2748 Object::Reference(rid) => doc.get_dictionary(*rid).unwrap().clone(),
2749 Object::Dictionary(d) => d.clone(),
2750 _ => panic!("inner /Font missing"),
2751 };
2752 let inner_font_ref = match inner_font_subdict.get(b"FInner").unwrap() {
2753 Object::Reference(id) => *id,
2754 _ => panic!("inner FInner must be indirect"),
2755 };
2756 let inner_font_dict = doc.get_dictionary(inner_font_ref).unwrap();
2757 let inner_new_tu_ref = match inner_font_dict.get(b"ToUnicode").unwrap() {
2758 Object::Reference(id) => *id,
2759 _ => panic!("inner font /ToUnicode must be indirect"),
2760 };
2761 assert_ne!(
2762 inner_new_tu_ref, inner_original_tu,
2763 "inner Form's font /ToUnicode must be a fresh stub"
2764 );
2765 }
2766
2767 #[test]
2768 fn tounicode_strip_handles_cyclic_form_xobject_graph_without_infinite_recursion() {
2769 let mut doc = Document::with_version("1.7");
2774 let (font_id, _original_tu) = add_font_with_tounicode(&mut doc);
2775
2776 let resources = dictionary! {
2780 "Font" => dictionary! { "F1" => Object::Reference(font_id) },
2781 };
2782 let mut form_dict = dictionary! {
2783 "Type" => "XObject",
2784 "Subtype" => "Form",
2785 "BBox" => vec![0.into(), 0.into(), 100.into(), 100.into()],
2786 "Resources" => Object::Dictionary(resources),
2787 };
2788 form_dict.set("Length", Object::Integer(0));
2789 let form_stream = Stream::new(form_dict, Vec::new());
2790 let form_id = doc.add_object(Object::Stream(form_stream));
2791
2792 if let Ok(Object::Stream(s)) = doc.get_object_mut(form_id) {
2794 if let Ok(Object::Dictionary(res)) = s.dict.get_mut(b"Resources") {
2795 res.set(
2796 "XObject",
2797 Object::Dictionary(dictionary! {
2798 "Fm1" => Object::Reference(form_id),
2799 }),
2800 );
2801 }
2802 }
2803
2804 let content = Stream::new(dictionary! {}, b"q /Fm1 Do Q".to_vec());
2805 let content_id = doc.add_object(Object::Stream(content));
2806 let page_resources = dictionary! {
2807 "XObject" => dictionary! { "Fm1" => Object::Reference(form_id) },
2808 };
2809 let page_dict = dictionary! {
2810 "Type" => "Page",
2811 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
2812 "Contents" => Object::Reference(content_id),
2813 "Resources" => Object::Dictionary(page_resources),
2814 };
2815 let page_id = doc.add_object(Object::Dictionary(page_dict));
2816 let pages = dictionary! {
2817 "Type" => "Pages",
2818 "Kids" => vec![Object::Reference(page_id)],
2819 "Count" => 1_i64,
2820 };
2821 let pages_id = doc.add_object(Object::Dictionary(pages));
2822 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
2823 d.set("Parent", Object::Reference(pages_id));
2824 }
2825 let catalog = dictionary! { "Type" => "Catalog", "Pages" => Object::Reference(pages_id) };
2826 let catalog_id = doc.add_object(Object::Dictionary(catalog));
2827 doc.trailer.set("Root", Object::Reference(catalog_id));
2828
2829 let mut redactor = Redactor::new();
2831 redactor.mark(RedactionArea::new(1, [0.0, 0.0, 100.0, 100.0]));
2832 let report = redactor
2833 .apply(&mut doc)
2834 .expect("cyclic Form XObject graph must not panic the redactor");
2835 assert!(report.pages_affected >= 1);
2836 }
2837
2838 #[test]
2839 fn tounicode_strip_skips_form_xobject_without_resources() {
2840 let mut doc = Document::with_version("1.7");
2844 let mut form_dict = dictionary! {
2845 "Type" => "XObject",
2846 "Subtype" => "Form",
2847 "BBox" => vec![0.into(), 0.into(), 100.into(), 100.into()],
2848 };
2849 form_dict.set("Length", Object::Integer(0));
2850 let form_stream = Stream::new(form_dict, Vec::new());
2851 let form_id = doc.add_object(Object::Stream(form_stream));
2852
2853 let (page_font_id, _) = add_font_with_tounicode(&mut doc);
2856 let content = Stream::new(
2857 dictionary! {},
2858 b"BT /Fp1 12 Tf 100 700 Td (X) Tj ET q /Fm1 Do Q".to_vec(),
2859 );
2860 let content_id = doc.add_object(Object::Stream(content));
2861 let resources = dictionary! {
2862 "Font" => dictionary! { "Fp1" => Object::Reference(page_font_id) },
2863 "XObject" => dictionary! { "Fm1" => Object::Reference(form_id) },
2864 };
2865 let page_dict = dictionary! {
2866 "Type" => "Page",
2867 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
2868 "Contents" => Object::Reference(content_id),
2869 "Resources" => Object::Dictionary(resources),
2870 };
2871 let page_id = doc.add_object(Object::Dictionary(page_dict));
2872 let pages = dictionary! {
2873 "Type" => "Pages",
2874 "Kids" => vec![Object::Reference(page_id)],
2875 "Count" => 1_i64,
2876 };
2877 let pages_id = doc.add_object(Object::Dictionary(pages));
2878 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
2879 d.set("Parent", Object::Reference(pages_id));
2880 }
2881 let catalog = dictionary! { "Type" => "Catalog", "Pages" => Object::Reference(pages_id) };
2882 let catalog_id = doc.add_object(Object::Dictionary(catalog));
2883 doc.trailer.set("Root", Object::Reference(catalog_id));
2884
2885 let mut redactor = Redactor::new();
2886 redactor.mark(RedactionArea::new(1, [50.0, 690.0, 250.0, 720.0]));
2887 redactor.apply(&mut doc).unwrap();
2888
2889 let page_xobject = match doc
2893 .get_dictionary(page_id)
2894 .unwrap()
2895 .get(b"Resources")
2896 .unwrap()
2897 {
2898 Object::Reference(rid) => doc.get_dictionary(*rid).unwrap().clone(),
2899 Object::Dictionary(d) => d.clone(),
2900 _ => panic!("page /Resources missing"),
2901 };
2902 let xobject_sub = match page_xobject.get(b"XObject").unwrap() {
2903 Object::Reference(rid) => doc.get_dictionary(*rid).unwrap().clone(),
2904 Object::Dictionary(d) => d.clone(),
2905 _ => panic!("page /XObject missing"),
2906 };
2907 let resolved_form = match xobject_sub.get(b"Fm1").unwrap() {
2908 Object::Reference(id) => *id,
2909 _ => panic!("Fm1 must be indirect"),
2910 };
2911 let resolved_stream = match doc.get_object(resolved_form).unwrap() {
2918 Object::Stream(s) => s.clone(),
2919 _ => panic!("Form XObject must be a Stream"),
2920 };
2921 assert!(
2922 resolved_stream.dict.get(b"Resources").is_err(),
2923 "no-Resources Form XObject should remain without /Resources after strip"
2924 );
2925 }
2926
2927 fn doc_objects_dummy() -> ObjectId {
2930 ObjectId::default()
2931 }
2932
2933 #[test]
2934 fn tounicode_strip_coalesces_aliased_form_xobject_to_one_stubbed_clone() {
2935 let mut doc = Document::with_version("1.7");
2947 let (form_font_id, original_tu_id) = add_font_with_tounicode(&mut doc);
2948 let shared_form_id = add_form_xobject_with_font(&mut doc, b"F1", form_font_id);
2949
2950 let content = Stream::new(dictionary! {}, b"q /Fm1 Do Q q /Fm2 Do Q".to_vec());
2953 let content_id = doc.add_object(Object::Stream(content));
2954 let resources = dictionary! {
2955 "XObject" => dictionary! {
2956 "Fm1" => Object::Reference(shared_form_id),
2957 "Fm2" => Object::Reference(shared_form_id),
2958 },
2959 };
2960 let page_dict = dictionary! {
2961 "Type" => "Page",
2962 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
2963 "Contents" => Object::Reference(content_id),
2964 "Resources" => Object::Dictionary(resources),
2965 };
2966 let page_id = doc.add_object(Object::Dictionary(page_dict));
2967 let pages = dictionary! {
2968 "Type" => "Pages",
2969 "Kids" => vec![Object::Reference(page_id)],
2970 "Count" => 1_i64,
2971 };
2972 let pages_id = doc.add_object(Object::Dictionary(pages));
2973 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
2974 d.set("Parent", Object::Reference(pages_id));
2975 }
2976 let catalog = dictionary! { "Type" => "Catalog", "Pages" => Object::Reference(pages_id) };
2977 let catalog_id = doc.add_object(Object::Dictionary(catalog));
2978 doc.trailer.set("Root", Object::Reference(catalog_id));
2979
2980 let mut redactor = Redactor::new();
2981 redactor.mark(RedactionArea::new(1, [0.0, 0.0, 100.0, 100.0]));
2982 redactor.apply(&mut doc).unwrap();
2983
2984 let (fm1_form, fm1_tu) =
2987 page_form_font_tounicode(&doc, page_id, b"Fm1", b"F1").expect("Fm1 path must resolve");
2988 let (fm2_form, fm2_tu) =
2989 page_form_font_tounicode(&doc, page_id, b"Fm2", b"F1").expect("Fm2 path must resolve");
2990
2991 assert_eq!(
2992 fm1_form, fm2_form,
2993 "aliased /XObject entries must share the same memoised clone"
2994 );
2995 assert_ne!(
2996 fm1_form, shared_form_id,
2997 "alias clone must be different from the shared original"
2998 );
2999
3000 assert_eq!(fm1_tu, fm2_tu);
3004 assert_ne!(fm1_tu, original_tu_id);
3005
3006 let stub_stream = match doc.get_object(fm1_tu).unwrap() {
3007 Object::Stream(s) => s.clone(),
3008 _ => panic!("/ToUnicode must be a stream"),
3009 };
3010 let mut s = stub_stream;
3011 let _ = s.decompress();
3012 assert!(
3013 crate::tounicode::is_safe_stub_cmap(&s.content),
3014 "shared alias clone's font must point at a content-validated stub /ToUnicode"
3015 );
3016 }
3017
3018 fn doc_with_text_at(x: f32, y: f32) -> Document {
3020 let mut doc = Document::with_version("1.7");
3021 let content = format!("BT /F1 12 Tf {x} {y} Td (Wq7Kx9Zp) Tj ET\n");
3022 let content_id = doc.add_object(Object::Stream(Stream::new(
3023 dictionary! {},
3024 content.into_bytes(),
3025 )));
3026 let pages_id = doc.new_object_id();
3027 let page_id = doc.add_object(Object::Dictionary(dictionary! {
3028 "Type" => "Page",
3029 "Parent" => Object::Reference(pages_id),
3030 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
3031 "Contents" => Object::Reference(content_id),
3032 "Resources" => Object::Dictionary(dictionary! {
3033 "Font" => Object::Dictionary(dictionary! {
3034 "F1" => dictionary! {
3035 "Type" => "Font", "Subtype" => "Type1", "BaseFont" => "Helvetica",
3036 },
3037 }),
3038 }),
3039 }));
3040 doc.objects.insert(
3041 pages_id,
3042 Object::Dictionary(dictionary! {
3043 "Type" => "Pages",
3044 "Kids" => vec![Object::Reference(page_id)],
3045 "Count" => 1_i64,
3046 }),
3047 );
3048 let catalog_id = doc.add_object(Object::Dictionary(dictionary! {
3049 "Type" => "Catalog", "Pages" => Object::Reference(pages_id),
3050 }));
3051 doc.trailer.set("Root", Object::Reference(catalog_id));
3052 doc
3053 }
3054
3055 #[test]
3056 fn post_condition_refuses_when_text_is_still_under_an_area() {
3057 let doc = doc_with_text_at(72.0, 700.0);
3059 let under = RedactionArea::new(1, [60.0, 690.0, 200.0, 720.0]);
3060 let err = ensure_no_text_under(&doc, 1, &[&under]).expect_err("text is under the area");
3061 assert!(matches!(err, RedactError::Other(m) if m.contains("still extractable")));
3062
3063 let elsewhere = RedactionArea::new(1, [300.0, 100.0, 400.0, 150.0]);
3064 ensure_no_text_under(&doc, 1, &[&elsewhere]).expect("no text under this area");
3065
3066 let mut done = doc.clone();
3068 let mut r = Redactor::new();
3069 r.mark(under.clone());
3070 r.apply(&mut done).expect("apply");
3071 ensure_no_text_under(&done, 1, &[&under]).expect("a redacted page passes");
3072 }
3073}