mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-01 05:58:12 -06:00
M11: redaction reads /DecodeParms given by reference before clearing pixels
Stream::decoded_within reads /DecodeParms only when it is direct, so an image whose PNG predictor was given by reference decoded with its row tags still in the data. The cleared bytes then drifted one byte further from the covered pixels each row, and the covered samples survived in the redacted copy, recoverable by applying the predictor. Parameters are now made direct first (Document::with_direct_parms). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9fca1d44f0
commit
6a99d26c7d
Generated
+1
@@ -4572,6 +4572,7 @@ dependencies = [
|
||||
"pdfcraft-content",
|
||||
"pdfcraft-cos",
|
||||
"pdfcraft-edit",
|
||||
"pdfcraft-filters",
|
||||
"pdfcraft-fonts",
|
||||
"pdfcraft-forms",
|
||||
"pdfcraft-model",
|
||||
|
||||
@@ -20,4 +20,5 @@ pdfcraft-xfa = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pdfcraft-filters = { workspace = true }
|
||||
pdfcraft-testkit = { workspace = true }
|
||||
|
||||
@@ -45,6 +45,9 @@ fn decode_and_clear(doc: &Document, s: &Stream, ctm: &Matrix, rects: &[[f64; 4]]
|
||||
if s.dict.contains(b"SMask") || s.dict.contains(b"Mask") {
|
||||
return None;
|
||||
}
|
||||
// `Stream::decoded*` reads `/DecodeParms` only when it is direct: a predictor given by
|
||||
// reference would be skipped, and the cleared bytes would then miss the covered pixels.
|
||||
let s = &*doc.with_direct_parms(s);
|
||||
let supported = |n: &[u8]| {
|
||||
matches!(
|
||||
n,
|
||||
|
||||
@@ -549,6 +549,32 @@ fn redaction_clears_pixels_intersected_without_covering_their_centres() {
|
||||
}
|
||||
}
|
||||
|
||||
/// An image whose PNG predictor is given by reference has its covered pixel cleared, and only
|
||||
/// that one: `Stream::decoded` reads only a direct `/DecodeParms`, so the row tags stayed in the
|
||||
/// data, the cleared byte drifted off the covered pixel and the covered sample survived.
|
||||
#[test]
|
||||
fn an_image_whose_predictor_is_given_by_reference_has_the_covered_pixel_cleared() {
|
||||
// Two rows of four grey samples, each behind a PNG "None" row tag.
|
||||
let predicted = pdfcraft_filters::encode_flate(&[0, 10, 20, 30, 40, 0, 50, 60, 70, 80]);
|
||||
let mut doc = one_page(
|
||||
b"q 40 0 0 20 0 0 cm /Im Do Q",
|
||||
"/XObject << /Im 7 0 R >>",
|
||||
vec![
|
||||
stream(
|
||||
"/Type /XObject /Subtype /Image /Width 4 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode /DecodeParms 8 0 R",
|
||||
predicted,
|
||||
),
|
||||
b"<< /Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns 4 >>".to_vec(),
|
||||
],
|
||||
);
|
||||
// The top row's second sample.
|
||||
mark(&mut doc, 0, &[[12.0, 12.0, 18.0, 18.0]], "");
|
||||
let report = apply(&mut doc, None).unwrap();
|
||||
assert_eq!(report.images_cleared, 1);
|
||||
let saved = Document::open(Arc::new(write_full(&doc, &SaveOptions::default()).unwrap())).unwrap();
|
||||
assert_eq!(named_image(&saved, 0, b"PCRedacted1"), Some(vec![10, 0, 30, 40, 50, 60, 70, 80]));
|
||||
}
|
||||
|
||||
/// Apply Redactions gives the page its own pruned /Resources, but the dictionary it drew through
|
||||
/// before (inherited from the /Pages node, or shared with another page) still named the removed
|
||||
/// image and form, so a full-rewrite save kept them.
|
||||
|
||||
Reference in New Issue
Block a user