mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
M8: a dictionary named beside a shared one stays, whatever the order
A kept image that named the dictionary added every dictionary it names to what had to leave, in one pass. An image using only one of those, earlier in the file, had been passed over, so it kept that dictionary and the redaction refused, saying something else referred to the data. In the other order it was stored again. Now only the names that matched have to leave, so another dictionary stays with what uses it, in either order. The variant's doc says what is counted: something that names the data, JBIG2 or not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
1 parent
b3b47b651d
commit
14e5eec341
3 files changed
+49
-4
No files matched your search
@@ -157,11 +157,14 @@ pub(crate) fn unshare(doc: &mut Document, before: &HashSet<u32>) -> Result<usize
|
||||
if !now.contains(&r.num) {
|
||||
continue;
|
||||
}
|
||||
let names = named(doc, s);
|
||||
let copy =
|
||||
|n: &u32, read: &mut HashMap<u32, Option<u64>>| read.entry(*n).or_insert_with(|| content(doc, *n)).is_some_and(|c| contents.contains(&c));
|
||||
if names.iter().any(|n| dictionaries.contains(n) || copy(n, &mut read)) {
|
||||
dictionaries.extend(names);
|
||||
// Only what matched has to leave, a copy found here included. Another dictionary this also
|
||||
// names was shared with nothing that left: what else uses it may keep it, whichever comes
|
||||
// first in the file.
|
||||
let matched: Vec<u32> = named(doc, s).into_iter().filter(|n| dictionaries.contains(n) || copy(n, &mut read)).collect();
|
||||
if !matched.is_empty() {
|
||||
dictionaries.extend(matched);
|
||||
users.push(r);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ pub enum RedactError {
|
||||
/// could not be shown complete.
|
||||
#[error("the document's objects nest too deeply to remove hidden information completely; the operation was refused")]
|
||||
TooDeep,
|
||||
/// `/JBIG2Globals` used by something removed would stay: a JBIG2 image shares them and can't be
|
||||
/// `/JBIG2Globals` used by something removed would stay: something that names them can't be
|
||||
/// stored again without them, or something else refers to them. Either way the shapes they
|
||||
/// hold, those of marks under the area included, would stay. Counts what can't be stored without
|
||||
/// them: 0 when only something else keeps them.
|
||||
|
||||
@@ -1688,6 +1688,48 @@ fn a_refused_redaction_counts_what_cant_be_stored() {
|
||||
}
|
||||
}
|
||||
|
||||
/// An image that names the dictionary and another one is stored again without both, but the other
|
||||
/// was shared with nothing that left: an image that uses only it stays as it was, whichever of the
|
||||
/// two comes first in the file, and so does that dictionary.
|
||||
#[test]
|
||||
fn another_dictionary_named_beside_it_stays_whatever_the_order() {
|
||||
let scan = shared_scan();
|
||||
let other_dictionary = stream("", [jbig2_globals(b"OTHER_GLOBALS"), scan.symbols.clone()].concat());
|
||||
let only_other = shared_image("/DecodeParms << /JBIG2Globals 11 0 R >>");
|
||||
let chained = "/Type /XObject /Subtype /Image /Width 400 /Height 40 /BitsPerComponent 1 /ColorSpace /DeviceGray /Filter [/FlateDecode /JBIG2Decode] /DecodeParms [<< /JBIG2Globals 11 0 R >> << /JBIG2Globals 9 0 R >>]";
|
||||
let both_spellings = [
|
||||
(chained.to_string(), pdfcraft_filters::encode_flate(&scan.page)),
|
||||
(format!("{} /DP << /JBIG2Globals 11 0 R >>", shared_image(SHARED)), scan.page.clone()),
|
||||
];
|
||||
let other_stays = |doc: &Document| {
|
||||
let saved = reopen(doc);
|
||||
saved.scan_objects().any(|(_, o)| matches!(&*o, Object::Stream(s) if s.raw.windows(13).any(|w| w == b"OTHER_GLOBALS")))
|
||||
};
|
||||
for (both, data) in both_spellings {
|
||||
// Image 8 is drawn on the second page, and image 10 is its alternate.
|
||||
let only_other_first = SharedPages {
|
||||
other: format!("{only_other} /Alternates [<< /Image 10 0 R >>]"),
|
||||
extra: vec![stream(&both, data.clone()), other_dictionary.clone()],
|
||||
..SharedPages::new(&scan)
|
||||
};
|
||||
let both_first = SharedPages {
|
||||
other: format!("{both} /Alternates [<< /Image 10 0 R >>]"),
|
||||
other_data: data.clone(),
|
||||
extra: vec![stream(&only_other, scan.page.clone()), other_dictionary.clone()],
|
||||
..SharedPages::new(&scan)
|
||||
};
|
||||
for (pages, only_other_at) in [(only_other_first, 8), (both_first, 10)] {
|
||||
let mut doc = pages.build(&scan);
|
||||
let untouched = doc.get(pdfcraft_cos::ObjRef::new(only_other_at, 0));
|
||||
mark(&mut doc, 0, &[CLEARS], "");
|
||||
assert_eq!(apply(&mut doc, None).unwrap().images_restored, 1, "{both}");
|
||||
assert_gone(&doc, b"LEAK_GLOBALS");
|
||||
assert!(other_stays(&doc), "{both}");
|
||||
assert_eq!(doc.get(pdfcraft_cos::ObjRef::new(only_other_at, 0)), untouched, "{both}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove Hidden Information leaves the same way: comments go, with what their appearances drew.
|
||||
#[test]
|
||||
fn removing_hidden_information_takes_a_shared_jbig2_dictionary_too() {
|
||||
|
||||
Reference in new issue
Block a user