From 0523ccdfdc6fcf6404b9938947b707f7b7d35b33 Mon Sep 17 00:00:00 2001 From: storybored379 Date: Sat, 3 Oct 2026 21:14:56 -0700 Subject: [PATCH] M8: Redaction drops /PCAdded from rewritten content streams so redacted added text does not stay in the file (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Text added with Edit ▸ Add content keeps its source string in the stream dictionary's /PCAdded. Applying a redaction rewrote the stream's glyphs but copied the dictionary unchanged, so the redacted text survived in the saved PDF. The redacted item now stays as plain drawn content and is no longer editable. Co-Authored-By: Claude Opus 5.5 --- crates/redact/src/lib.rs | 4 ++++ crates/redact/src/tests.rs | 20 +++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/redact/src/lib.rs b/crates/redact/src/lib.rs index f6adf1a1..3835e07d 100644 --- a/crates/redact/src/lib.rs +++ b/crates/redact/src/lib.rs @@ -296,6 +296,10 @@ pub fn apply(doc: &mut Document, pages: Option<&[usize]>) -> Result Dict::new(), }; dict.remove(b"Length"); + // Edit ▸ Add content keeps an added item's source text in `/PCAdded`; once its glyphs + // are redacted, keeping it would leave the redacted text in the file. The item stays + // as plain drawn content but is no longer editable. + dict.remove(b"PCAdded"); // A new object: the original may be shared with other pages. new_list[i] = Object::Ref(doc.add(Object::Stream(Stream::flate(dict, &bytes)))); changed = true; diff --git a/crates/redact/src/tests.rs b/crates/redact/src/tests.rs index 21c87f1a..0ca0b794 100644 --- a/crates/redact/src/tests.rs +++ b/crates/redact/src/tests.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use printcraft_annot::{Meta, NewAnnotation, Shape, Style, add_annotation, rect_quad}; -use printcraft_cos::{Document, SaveOptions, write_incremental}; +use printcraft_cos::{Document, SaveOptions, write_full, write_incremental}; use super::*; @@ -99,6 +99,24 @@ fn glyphs_under_a_mark_go_and_the_rest_stays_put() { assert!(!content(&doc, 0).contains("1234")); } +#[test] +fn added_text_parameters_do_not_keep_redacted_text() { + // An Edit ▸ Add content item: its stream dictionary keeps the source text under /PCAdded. + 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 << /Font << /F1 5 0 R >> >> >>".to_vec(), + stream("/PCMark /Added /PCAdded << /Kind /Text /Text (AB1234CD) >>", b"BT /F1 10 Tf 10 100 Td (AB1234CD) Tj ET"), + FONT.replace("95 0 R", "6 0 R").into_bytes(), + widths(), + ]); + mark(&mut doc, 0, &[[20.0, 95.0, 40.0, 110.0]], ""); + apply(&mut doc, None).unwrap(); + assert!(!content(&doc, 0).contains("1234")); + let bytes = write_full(&doc, &SaveOptions::default()).unwrap(); + assert!(!bytes.windows(4).any(|w| w == b"1234"), "redacted text left in the saved file"); +} + #[test] fn kerning_spacing_scaling_and_line_operators_are_honoured() { // TJ kerning, character and word spacing, 50% horizontal scaling, ' and ".