From 34f9bea0add687ff54c65da5292665d00bbb6ada Mon Sep 17 00:00:00 2001 From: oracola91 <118899664+oracola91@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:51:40 +0200 Subject: [PATCH] Private data takes OPI links and alternate images (#6457) --- crates/redact/src/sanitize.rs | 18 +++++++++++++----- crates/redact/src/tests.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/crates/redact/src/sanitize.rs b/crates/redact/src/sanitize.rs index a4d1a26f4e..7661b83ddb 100644 --- a/crates/redact/src/sanitize.rs +++ b/crates/redact/src/sanitize.rs @@ -28,7 +28,8 @@ pub enum Hidden { Bookmarks, /// Links, actions (open action, additional actions, what bookmarks run) and document JavaScript. LinksActionsScripts, - /// Private data of other applications (`/PieceInfo`). + /// Private data of other applications (`/PieceInfo`), and prepress data on images and Forms + /// (`/OPI`, `/Alternates`). PrivateData, } @@ -253,9 +254,16 @@ fn outline_action_count(doc: &Document, cat: &Dict) -> usize { /// Entries that hold a category's data wherever they appear, not only on the catalog and pages: /// XMP on images, fonts and Forms, private data on Forms, associated files on pages and -/// annotations, additional actions on fields. -const OWNED: [(Hidden, &[u8]); 4] = - [(Hidden::Metadata, b"Metadata"), (Hidden::PrivateData, b"PieceInfo"), (Hidden::Attachments, b"AF"), (Hidden::LinksActionsScripts, b"AA")]; +/// annotations, additional actions on fields. Images and Forms also carry prepress data: an OPI +/// proxy's link to the original file, and alternate images, which can be the unedited original. +const OWNED: [(Hidden, &[u8]); 6] = [ + (Hidden::Metadata, b"Metadata"), + (Hidden::PrivateData, b"PieceInfo"), + (Hidden::PrivateData, b"OPI"), + (Hidden::PrivateData, b"Alternates"), + (Hidden::Attachments, b"AF"), + (Hidden::LinksActionsScripts, b"AA"), +]; /// Keys whose dictionary values map names a producer chooses to objects (resources, glyph /// procedures, named destinations, role and class maps, private data by application). A name @@ -542,7 +550,7 @@ pub fn scan(doc: &Document) -> Vec<(Hidden, usize)> { // A graph nested too deeply to walk counts as holding everything, so the removal runs and // refuses it. let held = owners(doc, &owned_keys).map_or_else(|_| vec![1; OWNED.len()], |o| o.0); - let owned = |h: Hidden| OWNED.iter().position(|o| o.0 == h).and_then(|i| held.get(i)).copied().unwrap_or(0); + let owned = |h: Hidden| OWNED.iter().zip(&held).filter(|(o, _)| o.0 == h).map(|(_, n)| n).sum::(); let mut doc2 = doc.clone(); HIDDEN .into_iter() diff --git a/crates/redact/src/tests.rs b/crates/redact/src/tests.rs index 69bab777ff..804e3d26f6 100644 --- a/crates/redact/src/tests.rs +++ b/crates/redact/src/tests.rs @@ -4580,3 +4580,29 @@ fn a_redacted_form_xobject_drops_the_source_text_of_added_content() { let bytes = write_full(&doc, &SaveOptions { object_streams: false, ..SaveOptions::default() }).unwrap(); assert!(!bytes.windows(4).any(|w| w == b"1234"), "redacted text left in the saved file"); } + +/// Private data takes an image's prepress data too: its OPI link to the original file and its +/// alternate images, which can be the unedited original. +#[test] +fn private_data_takes_opi_links_and_alternate_images() { + use crate::sanitize::{Hidden, remove_hidden, scan}; + let mut doc = pdf(vec![ + b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), + b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(), + b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources << /XObject << /Im 5 0 R /Fm 6 0 R >> >> >>".to_vec(), + stream("", b"q 50 0 0 50 20 20 cm /Im Do Q /Fm Do"), + stream( + "/Type /XObject /Subtype /Image /Width 11 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Alternates [<< /Image 7 0 R /DefaultForPrinting true >>]", + b"SHOWNIMAGE!", + ), + stream("/Type /XObject /Subtype /Form /BBox [0 0 300 300] /OPI << /1.3 << /Type /OPI /Version 1.3 /F (OPISERVERPATH.tif) >> >>", b""), + stream("/Type /XObject /Subtype /Image /Width 17 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8", b"ALTERNATEORIGINAL"), + ]); + assert_eq!(scan(&doc).into_iter().find(|c| c.0 == Hidden::PrivateData), Some((Hidden::PrivateData, 2))); + assert_eq!(remove_hidden(&mut doc, &[Hidden::PrivateData]).unwrap(), [(Hidden::PrivateData, 2)]); + let bytes = write_full(&doc, &SaveOptions { object_streams: false, ..SaveOptions::default() }).unwrap(); + let has = |needle: &[u8]| bytes.windows(needle.len()).any(|w| w == needle); + assert!(has(b"SHOWNIMAGE!"), "the image the page shows stays"); + assert!(!has(b"ALTERNATEORIGINAL"), "the alternate image survived the save"); + assert!(!has(b"OPISERVERPATH"), "the OPI link survived the save"); +}