mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 04:22:20 +00:00
M2.6: pruning keeps what a page or form damaged partway may draw
Extracting, splitting and combining pages drops the `/XObject` entries no page draws. A page or form damaged partway was read up to the damage as the whole of it, so an image named only after the damage was dropped, though the stream still names it and other readers may still read that far. - `content_bytes` says whether a page's stream is damaged, and pruning keeps everything such a page could reach, as it does for content that doesn't decode. A form is read the same way. - A page made a form XObject (stamps, watermarks, backgrounds from a PDF) still draws what decoded before the damage. 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
480fdb114a
commit
50d0fcc403
3 files changed
+61
-14
No files matched your search
@@ -499,9 +499,10 @@ pub fn page_as_form(dst: &mut Document, src: &Document, page: usize) -> Result<(
|
||||
let bbox = rect(b"CropBox").or_else(|| rect(b"MediaBox")).unwrap_or([0.0, 0.0, 612.0, 792.0]);
|
||||
let (w, h) = (bbox[2] - bbox[0], bbox[3] - bbox[1]);
|
||||
let rotate = d.get(b"Rotate").and_then(|r| src.resolve(r).as_int()).unwrap_or(0).rem_euclid(360);
|
||||
// Content: the page's streams, decoded and joined.
|
||||
// Content: the page's streams, decoded and joined. Of a stream damaged partway, the copy draws
|
||||
// what decoded before the damage.
|
||||
let content = match own.get(b"Contents") {
|
||||
Some(c) => crate::prune::content_bytes(src, c).map_err(|e| OrganizeError::Invalid(format!("page {} content: {e}", page + 1)))?,
|
||||
Some(c) => crate::prune::content_bytes(src, c).map_err(|e| OrganizeError::Invalid(format!("page {} content: {e}", page + 1)))?.0,
|
||||
None => Vec::new(),
|
||||
};
|
||||
let mut copier = Copier::new(src, HashMap::new(), HashMap::new());
|
||||
|
||||
@@ -219,10 +219,10 @@ impl Pruner<'_> {
|
||||
}
|
||||
#[cfg(test)]
|
||||
crate::work(1);
|
||||
match s.decoded() {
|
||||
Ok(data) => self.content(&data, &inner, inner_owner, depth + 1),
|
||||
// What the form draws is unknown: keep everything it could reach.
|
||||
Err(_) => {
|
||||
match DecodeBudget::new(usize::MAX, 0).decode_with_damage(s, usize::MAX) {
|
||||
Ok((data, false)) => self.content(&data, &inner, inner_owner, depth + 1),
|
||||
// What the form draws is unknown, past damage too: keep everything it could reach.
|
||||
Ok((_, true)) | Err(_) => {
|
||||
self.protect_type3_resources(&inner, inner_owner);
|
||||
self.distrust(&inner, inner_owner);
|
||||
}
|
||||
@@ -232,16 +232,20 @@ impl Pruner<'_> {
|
||||
}
|
||||
|
||||
/// The page's content, concatenated (`/Contents` may be one stream or an array) with a space
|
||||
/// between pieces as the renderer joins them, decoded against one page's [`DecodeBudget`]. An
|
||||
/// between pieces as the renderer joins them, decoded against one page's [`DecodeBudget`], and
|
||||
/// whether a stream is damaged partway (its part is then what decoded before the damage). An
|
||||
/// error when a stream can't be decoded or the budget runs out: the page may draw names that
|
||||
/// can't be seen.
|
||||
pub(crate) fn content_bytes(doc: &Document, contents: &Object) -> Result<Vec<u8>, CosError> {
|
||||
pub(crate) fn content_bytes(doc: &Document, contents: &Object) -> Result<(Vec<u8>, bool), CosError> {
|
||||
let mut out = Vec::new();
|
||||
for data in DecodeBudget::page().decode_contents(doc, contents) {
|
||||
out.extend(data?);
|
||||
let mut damaged = false;
|
||||
for read in DecodeBudget::page().decode_contents_with_damage(doc, contents) {
|
||||
let (data, partial) = read?;
|
||||
damaged |= partial;
|
||||
out.extend(data);
|
||||
out.push(b' ');
|
||||
}
|
||||
Ok(out)
|
||||
Ok((out, damaged))
|
||||
}
|
||||
|
||||
/// The resources of a page (as `walk` reports them) with their owner.
|
||||
@@ -324,9 +328,10 @@ pub(crate) fn prune_unused_xobjects(doc: &mut Document) -> Result<usize, Organiz
|
||||
let Some(pd) = pruner.doc.get(*page).as_dict().cloned() else { continue };
|
||||
let Some(contents) = pd.get(b"Contents").cloned() else { continue };
|
||||
match content_bytes(pruner.doc, &contents) {
|
||||
Ok(data) if data.is_empty() => {}
|
||||
Ok(data) => pruner.content(&data, &res_dict, owner, 0),
|
||||
Err(_) => {
|
||||
Ok((data, false)) if data.is_empty() => {}
|
||||
Ok((data, false)) => pruner.content(&data, &res_dict, owner, 0),
|
||||
// What the page draws past damage is unknown too.
|
||||
Ok((_, true)) | Err(_) => {
|
||||
pruner.protect_type3_resources(&res_dict, owner);
|
||||
pruner.distrust(&res_dict, owner);
|
||||
}
|
||||
|
||||
@@ -2628,6 +2628,47 @@ fn extract_keeps_images_an_undecodable_page_might_draw() {
|
||||
assert!(names.iter().any(|n| n.as_slice() == b"ImB"), "an image the unreadable page may draw must stay");
|
||||
}
|
||||
|
||||
/// zlib data that reads as `readable` and is then damaged, with more of the stream after the
|
||||
/// damage: a stored block, then a block of the reserved type (RFC 1951 §3.2.3), then `rest`.
|
||||
fn damaged_flate(readable: &[u8], rest: &[u8]) -> pdfcraft_cos::Stream {
|
||||
let len = u16::try_from(readable.len()).unwrap();
|
||||
let mut raw = vec![0x78, 0x01, 0x00];
|
||||
raw.extend_from_slice(&len.to_le_bytes());
|
||||
raw.extend_from_slice(&(!len).to_le_bytes());
|
||||
raw.extend_from_slice(readable);
|
||||
raw.push(0x07);
|
||||
raw.extend_from_slice(rest);
|
||||
let mut dict = Dict::new();
|
||||
dict.set(b"Filter".to_vec(), Object::name("FlateDecode"));
|
||||
pdfcraft_cos::Stream::from_raw(dict, raw)
|
||||
}
|
||||
|
||||
/// A page or form damaged partway may draw images past the damage, which other readers may still
|
||||
/// read: pruning keeps them, as it does for content that doesn't decode.
|
||||
#[test]
|
||||
fn extract_keeps_images_content_damaged_partway_might_draw() {
|
||||
use pdfcraft_cos::ObjRef;
|
||||
let mut page = doc_images_with(&raw("/ImA Do"), "");
|
||||
page.set(ObjRef::new(6, 0), damaged_flate(b"/ImA Do\n", b"/ImB Do"));
|
||||
|
||||
let mut form = doc_images_with(&raw("/Fm Do"), "");
|
||||
let mut fm = damaged_flate(b"/ImA Do\n", b"/ImB Do");
|
||||
fm.dict.set(b"Subtype".to_vec(), Object::name("Form"));
|
||||
fm.dict.set(b"BBox".to_vec(), Object::Array(vec![0.into(), 0.into(), 1.into(), 1.into()]));
|
||||
let fm = form.add(fm);
|
||||
let xobjects: Dict =
|
||||
[(b"ImA".to_vec(), Object::Ref(ObjRef::new(8, 0))), (b"ImB".to_vec(), Object::Ref(ObjRef::new(9, 0))), (b"Fm".to_vec(), Object::Ref(fm))]
|
||||
.into_iter()
|
||||
.collect();
|
||||
form.update_dict(ObjRef::new(7, 0), |d| d.set(b"XObject".to_vec(), Object::Dict(xobjects))).unwrap();
|
||||
|
||||
for (src, what) in [(page, "page"), (form, "form")] {
|
||||
let part = full_roundtrip(&extract_pages(&src, &[0, 1]).unwrap());
|
||||
let names = xobject_names(&part, 0);
|
||||
assert!(names.iter().any(|n| n.as_slice() == b"ImB"), "{what}: an image drawn past the damage must stay: {names:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_keeps_images_type3_glyphs_may_draw() {
|
||||
// A Type 3 font without its own /Resources draws through the page's: its glyphs could `Do`.
|
||||
|
||||
Reference in new issue
Block a user