mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-13 14:22:25 +00:00
M8: a shared JBIG2 dictionary is found in /FDecodeParms too
/FDecodeParms are the parameters of data in another file, which a reader may draw instead of the stream's own. A dictionary that only a removed image named there stayed in the file with Ok. It is now read with /DecodeParms and /DP, and an image whose data is in another file counts as one that can't be stored without the dictionary: readers draw that file, not a copy stored here. images_restored counts what was stored again. A dictionary that names itself as its own globals (malformed) leaves with what used it but can't be stored, and was counted. The table's /Mask route gets a row in the routes test, as it was only measured. 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
14e5eec341
commit
cb1a040731
3 files changed
+58
-7
No files matched your search
@@ -26,9 +26,9 @@ clear_marks(&mut doc, None)?; // remove marks without app
|
||||
no globals. Globals hold the shapes of every image that shares them (a scan's pages, often),
|
||||
those of marks under the area included, so whatever left the document, by any route, takes its
|
||||
globals with it on the full rewrite: anything else the save would keep that names them, or a
|
||||
byte-for-byte copy of them, is stored again without them, as a copy is (Flate, Group 4 or JBIG2,
|
||||
copy of them (the same bytes once decoded), is stored again without them, as a copy is (Flate, Group 4 or JBIG2,
|
||||
whichever is smallest; `Report::images_restored`). When one can't be (it isn't JBIG2, it's past
|
||||
the size redaction clears, or it can't be decoded), or something else refers to them, `apply`
|
||||
the size redaction clears, its data is in another file, or it can't be decoded), or something else refers to them, `apply`
|
||||
refuses and changes nothing (`RedactError::SharedJbig2Globals`). `remove_hidden` takes them out
|
||||
the same way. Another dictionary holding the same shapes in other bytes isn't found, nor is a copy
|
||||
nothing names, and an image that hasn't left keeps its globals with its pixels: one another page
|
||||
|
||||
@@ -49,7 +49,8 @@ fn named(doc: &Document, s: &Stream) -> Vec<u32> {
|
||||
}
|
||||
}
|
||||
};
|
||||
for key in [b"DecodeParms".as_slice(), b"DP"] {
|
||||
// `/FDecodeParms` too: those of data in another file, which a reader may draw instead.
|
||||
for key in [b"DecodeParms".as_slice(), b"DP", b"FDecodeParms"] {
|
||||
let Some(parms) = s.dict.get(key).map(|p| followed(doc, p)) else { continue };
|
||||
match &*parms {
|
||||
Object::Array(entries) => entries.iter().for_each(&mut read),
|
||||
@@ -94,8 +95,12 @@ fn content(doc: &Document, num: u32) -> Option<u64> {
|
||||
/// referred to. `None` for a stream that isn't JBIG2, whose decoder took no dictionary and whose
|
||||
/// samples another filter may give otherwise; for one past the size of an image redaction clears,
|
||||
/// or known to have more than one component, which JBIG2 is drawn as one of whatever the dictionary
|
||||
/// says; and for one that can't be decoded.
|
||||
/// says; for one whose data is in another file (`/F`), which readers draw instead of what is here;
|
||||
/// and for one that can't be decoded.
|
||||
fn without_dictionary(doc: &Document, s: &Stream) -> Option<Stream> {
|
||||
if s.dict.contains(b"F") {
|
||||
return None;
|
||||
}
|
||||
let readable = readable(doc, s);
|
||||
let jbig2 = match readable.dict.get(b"Filter") {
|
||||
Some(Object::Array(a)) => a.iter().any(|f| f.as_name() == Some(b"JBIG2Decode".as_slice())),
|
||||
@@ -168,12 +173,15 @@ pub(crate) fn unshare(doc: &mut Document, before: &HashSet<u32>) -> Result<usize
|
||||
users.push(r);
|
||||
}
|
||||
}
|
||||
let mut stuck = 0;
|
||||
let (mut stored_again, mut stuck) = (0, 0);
|
||||
for &r in &users {
|
||||
let object = doc.get(r);
|
||||
let Object::Stream(s) = &*object else { continue };
|
||||
match without_dictionary(doc, s) {
|
||||
Some(stored) => doc.set(r, Object::Stream(stored)),
|
||||
Some(stored) => {
|
||||
doc.set(r, Object::Stream(stored));
|
||||
stored_again += 1;
|
||||
}
|
||||
None => stuck += 1,
|
||||
}
|
||||
}
|
||||
@@ -181,5 +189,7 @@ pub(crate) fn unshare(doc: &mut Document, before: &HashSet<u32>) -> Result<usize
|
||||
if dictionaries.iter().any(|n| after.contains(n)) {
|
||||
return Err(crate::RedactError::SharedJbig2Globals(stuck));
|
||||
}
|
||||
Ok(users.len())
|
||||
// What couldn't be stored has left with the dictionaries, a dictionary that names itself say:
|
||||
// it isn't counted.
|
||||
Ok(stored_again)
|
||||
}
|
||||
@@ -1401,6 +1401,23 @@ fn ways_out(scan: &SharedScan) -> Vec<(&'static str, [f64; 4], SharedPages)> {
|
||||
COVERS,
|
||||
SharedPages { first: format!("{plain} /SMask 10 0 R"), extra: vec![stream(&shared_image(SHARED), scan.page.clone())], ..base() },
|
||||
),
|
||||
(
|
||||
"a stencil mask",
|
||||
COVERS,
|
||||
SharedPages {
|
||||
first: format!("{plain} /Mask 10 0 R"),
|
||||
extra: vec![stream(
|
||||
&shared_image(SHARED).replace("/BitsPerComponent 1 /ColorSpace /DeviceGray", "/ImageMask true"),
|
||||
scan.page.clone(),
|
||||
)],
|
||||
..base()
|
||||
},
|
||||
),
|
||||
(
|
||||
"parameters for data in another file",
|
||||
COVERS,
|
||||
SharedPages { first: shared_image("/F (scan.jb2) /FFilter /JBIG2Decode /FDecodeParms << /JBIG2Globals 9 0 R >>"), ..base() },
|
||||
),
|
||||
(
|
||||
"an alternate",
|
||||
COVERS,
|
||||
@@ -1686,6 +1703,30 @@ fn a_refused_redaction_counts_what_cant_be_stored() {
|
||||
assert_eq!(text.starts_with("something else in the file refers to JBIG2 symbol data"), stuck == 0, "{other}: {text}");
|
||||
assert_eq!(text.contains("can't be stored"), stuck > 0, "{other}: {text}");
|
||||
}
|
||||
// An image whose data is in another file can't be stored here, though what is here decodes: readers
|
||||
// draw the other file, with the dictionary its own parameters name.
|
||||
let elsewhere = format!("{} /F (page2.jb2) /FFilter /JBIG2Decode /FDecodeParms << /JBIG2Globals 9 0 R >>", shared_image(SHARED));
|
||||
let mut doc = SharedPages { other: elsewhere, ..SharedPages::new(&scan) }.build(&scan);
|
||||
mark(&mut doc, 0, &[CLEARS], "");
|
||||
assert!(matches!(apply(&mut doc, None), Err(RedactError::SharedJbig2Globals(1))));
|
||||
}
|
||||
|
||||
/// A dictionary that names itself as its own globals (malformed) can't be stored without them, but
|
||||
/// leaves with what used it, so the run goes on; it isn't counted as stored.
|
||||
#[test]
|
||||
fn a_dictionary_that_names_itself_isnt_counted_as_stored() {
|
||||
let scan = shared_scan();
|
||||
let mut doc = SharedPages::new(&scan).build(&scan);
|
||||
let dictionary = pdfcraft_cos::ObjRef::new(9, 0);
|
||||
let Object::Stream(mut s) = (*doc.get(dictionary)).clone() else { panic!("object 9 is the dictionary") };
|
||||
let mut parms = pdfcraft_cos::Dict::new();
|
||||
parms.set(b"JBIG2Globals".to_vec(), Object::Ref(dictionary));
|
||||
s.dict.set(b"DecodeParms".to_vec(), Object::Dict(parms));
|
||||
doc.set(dictionary, Object::Stream(s));
|
||||
mark(&mut doc, 0, &[CLEARS], "");
|
||||
assert_eq!(apply(&mut doc, None).unwrap().images_restored, 1);
|
||||
assert_gone(&doc, b"LEAK_GLOBALS");
|
||||
assert_eq!(drawn_samples(&reopen(&doc), 1), scan.bits);
|
||||
}
|
||||
|
||||
/// An image that names the dictionary and another one is stored again without both, but the other
|
||||
|
||||
Reference in new issue
Block a user