diff --git a/crates/organize/src/import.rs b/crates/organize/src/import.rs index 9f86d6455a..b52c9175b3 100644 --- a/crates/organize/src/import.rs +++ b/crates/organize/src/import.rs @@ -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()); diff --git a/crates/organize/src/prune.rs b/crates/organize/src/prune.rs index 5a2a319fb4..8683b4d810 100644 --- a/crates/organize/src/prune.rs +++ b/crates/organize/src/prune.rs @@ -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, CosError> { +pub(crate) fn content_bytes(doc: &Document, contents: &Object) -> Result<(Vec, 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 {} - 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); } diff --git a/crates/organize/src/tests.rs b/crates/organize/src/tests.rs index d5ff2116ff..40bb4210d4 100644 --- a/crates/organize/src/tests.rs +++ b/crates/organize/src/tests.rs @@ -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`.