mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 02:11:07 -06:00
M8: test: a cleared copy stored as Group 4 or JBIG2 can be redacted again; the README's not-yet list
The README still listed metadata and hidden-information sanitising as not done, though sanitize does it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
689a1e34cf
commit
53874375cb
@@ -109,5 +109,5 @@ Patterns are heuristic: dates, protocol identifiers and longer number runs are r
|
||||
`CodeSet::from_id("foia")?.overlay(&["(b)(6)"])` gives the overlay text for the picked codes.
|
||||
Custom code sets aren't supported yet.
|
||||
|
||||
Not yet: pattern locales, struct-tree/alt-text cleanup, metadata and hidden-information
|
||||
sanitising (M8.4), re-encoding of DCT images (they are removed instead).
|
||||
Not yet: pattern locales, struct-tree/alt-text cleanup, re-encoding of DCT images (they are removed
|
||||
instead).
|
||||
|
||||
@@ -897,14 +897,9 @@ fn a_redacted_jbig2_scan_keeps_neither_its_code_nor_its_globals() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A cleared one-bit image of one component, a scan as a rule, is stored as CCITT Group 4 when
|
||||
/// that is smaller than Flate, whatever it was stored as (Flate here), and never as JBIG2 unless it
|
||||
/// was JBIG2; an eight-bit image stays Flate. The copy decodes to the cleared samples.
|
||||
#[test]
|
||||
fn a_cleared_black_and_white_scan_is_stored_as_group_4() {
|
||||
// 800 × 200: lines of word-sized dark blocks 14 rows tall, their edges ragged by a sample from
|
||||
// row to row, as a scan's are; Group 4 codes such rows from the row above.
|
||||
let (w, h) = (800usize, 200usize);
|
||||
/// `w` × `h` rows, `w` a multiple of 8: lines of word-sized dark blocks 14 rows tall, their edges
|
||||
/// ragged by a sample from row to row, as a scan's are; Group 4 codes such rows from the row above.
|
||||
fn ragged_scan(w: usize, h: usize) -> Vec<u8> {
|
||||
let stride = w / 8;
|
||||
let mut seed = 0x9E37_79B9_7F4A_7C15u64;
|
||||
let mut next = move || {
|
||||
@@ -927,6 +922,17 @@ fn a_cleared_black_and_white_scan_is_stored_as_group_4() {
|
||||
x += len + 6 + (next() % 6) as usize;
|
||||
}
|
||||
}
|
||||
bits
|
||||
}
|
||||
|
||||
/// A cleared one-bit image of one component, a scan as a rule, is stored as CCITT Group 4 when
|
||||
/// that is smaller than Flate, whatever it was stored as (Flate here), and never as JBIG2 unless it
|
||||
/// was JBIG2; an eight-bit image stays Flate. The copy decodes to the cleared samples.
|
||||
#[test]
|
||||
fn a_cleared_black_and_white_scan_is_stored_as_group_4() {
|
||||
let (w, h) = (800usize, 200usize);
|
||||
let stride = w / 8;
|
||||
let bits = ragged_scan(w, h);
|
||||
let g4 = pdfcraft_filters::encode_g4(&bits, w as u32, h as u32).unwrap();
|
||||
assert!(g4.len() < pdfcraft_filters::encode_flate(&bits).len(), "Group 4 is smaller than Flate here");
|
||||
let grey: Vec<u8> = (0..w * h).map(|i| (i % 251) as u8).collect();
|
||||
@@ -1013,6 +1019,44 @@ fn a_cleared_image_of_more_than_one_bit_stays_flate() {
|
||||
assert_eq!(named_image(&saved, 0, b"PCRedacted1").unwrap().get(h * w / 2..), data.get(h * w / 2..), "the rows below the mark are kept");
|
||||
}
|
||||
|
||||
/// A copy stored as Group 4 or JBIG2 is read like any image: a second redaction, across a save,
|
||||
/// clears it again rather than removing it whole, keeps what the first cleared, and stores it
|
||||
/// smaller than Flate again.
|
||||
#[test]
|
||||
fn a_cleared_copy_stored_as_group_4_or_jbig2_can_be_redacted_again() {
|
||||
let (w, h) = (800usize, 200usize);
|
||||
let stride = w / 8;
|
||||
let bits = ragged_scan(w, h);
|
||||
let image = format!("/Type /XObject /Subtype /Image /Width {w} /Height {h} /BitsPerComponent 1 /ColorSpace /DeviceGray");
|
||||
for (filter, data, wanted) in [
|
||||
("/FlateDecode", pdfcraft_filters::encode_flate(&bits), &b"CCITTFaxDecode"[..]),
|
||||
("/JBIG2Decode", pdfcraft_filters::encode_jbig2(&bits, w as u32, h as u32).unwrap(), &b"JBIG2Decode"[..]),
|
||||
] {
|
||||
let dict = format!("{image} /Filter {filter}");
|
||||
let mut doc = one_page(b"q 200 0 0 50 0 100 cm /Im Do Q", "/XObject << /Im 7 0 R >>", vec![stream(&dict, data)]);
|
||||
mark(&mut doc, 0, &[[0.0, 125.0, 50.0, 150.0]], "");
|
||||
assert_eq!(apply(&mut doc, None).unwrap().images_cleared, 1, "{filter}");
|
||||
let mut saved = Document::open(Arc::new(write_full(&doc, &SaveOptions::default()).unwrap())).unwrap();
|
||||
assert_eq!(named_stream(&saved, 0, b"PCRedacted1").unwrap().dict.name(b"Filter"), Some(wanted), "{filter}");
|
||||
mark(&mut saved, 0, &[[150.0, 100.0, 200.0, 125.0]], "");
|
||||
let report = apply(&mut saved, None).unwrap();
|
||||
assert_eq!((report.images_cleared, report.images_removed), (1, 0), "{filter}: cleared again, not removed");
|
||||
let again = Document::open(Arc::new(write_full(&saved, &SaveOptions::default()).unwrap())).unwrap();
|
||||
// Rows 0 to 99 of the left quarter, then 100 to 199 of the right.
|
||||
let mut expected = bits.clone();
|
||||
for (rows, columns) in [(0..100, 0..200), (100..200, 600..800)] {
|
||||
for y in rows {
|
||||
for x in columns.clone() {
|
||||
expected[y * stride + x / 8] &= !(0x80 >> (x % 8));
|
||||
}
|
||||
}
|
||||
}
|
||||
let names: Vec<Vec<u8>> = (1..4).map(|n| format!("PCRedacted{n}").into_bytes()).collect();
|
||||
let stored = names.iter().find_map(|n| named_image(&again, 0, n));
|
||||
assert_eq!(stored, Some(expected), "{filter}");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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