diff --git a/crates/a11y/src/alt.rs b/crates/a11y/src/alt.rs index 973dc29e33..dd4b43a641 100644 --- a/crates/a11y/src/alt.rs +++ b/crates/a11y/src/alt.rs @@ -389,7 +389,7 @@ fn decode_page_streams(doc: &Document, page: &Dict) -> Option for entry in entries { let decoded = match &*doc.resolve(&entry) { Object::Stream(s) => { - let (data, damaged) = match budget.decode_with_damage(s, usize::MAX) { + let (data, damaged) = match budget.decode_with_damage(doc, s, usize::MAX) { Ok(read) => read, // Content past the budget may hold more of the tag: editing the rest // would leave that part marked as a tag the tree no longer has. diff --git a/crates/cos/src/budget.rs b/crates/cos/src/budget.rs index 41dff04ed4..d5d67503ee 100644 --- a/crates/cos/src/budget.rs +++ b/crates/cos/src/budget.rs @@ -276,12 +276,13 @@ impl DecodeBudget { self.charge(stream, max, Stream::decoded_within_counted).map(|(data, _)| data) } - /// [`DecodeBudget::decode_within`] (`usize::MAX` for no bound of its own), also saying - /// whether the stream is damaged: `true` when a filter stopped at corrupt data, so the bytes - /// are only what decoded before it. A stream written back from them loses everything after - /// the damage, which other readers may still read. - pub fn decode_with_damage(&mut self, stream: &Stream, max: usize) -> Result<(Vec, bool), CosError> { - self.charge(stream, max, Stream::decoded_within_counted) + /// [`DecodeBudget::decode_within`] (`usize::MAX` for no bound of its own), with the stream's + /// `/DecodeParms` given by reference read in `doc`, also saying whether the stream is damaged: + /// `true` when a filter stopped at corrupt data, so the bytes are only what decoded before it. + /// A stream written back from them loses everything after the damage, which other readers may + /// still read. + pub fn decode_with_damage(&mut self, doc: &Document, stream: &Stream, max: usize) -> Result<(Vec, bool), CosError> { + self.charge(&doc.decoding_view(stream), max, Stream::decoded_within_counted) } /// `stream` decoded like [`Stream::decoded_strict`] (any corruption is an error), charged to @@ -343,23 +344,24 @@ impl DecodeBudget { self.seen.insert(r) } - /// Each stream `contents` names, decoded against the allowance, in order. `contents` is a - /// page's `/Contents`: one stream, or an array whose non-stream entries are left out. - /// A stream that fails to decode, or arrives after the allowance is spent, is an `Err` in its - /// place, so the caller chooses between skipping it and giving up. + /// Each stream `contents` names, decoded against the allowance, in order, its `/DecodeParms` + /// given by reference read in `doc`. `contents` is a page's `/Contents`: one stream, or an + /// array whose non-stream entries are left out. A stream that fails to decode, or arrives + /// after the allowance is spent, is an `Err` in its place, so the caller chooses between + /// skipping it and giving up. pub fn decode_contents(&mut self, doc: &Document, contents: &Object) -> Vec, CosError>> { self.decode_contents_with_damage(doc, contents).into_iter().map(|read| read.map(|(data, _)| data)).collect() } /// [`DecodeBudget::decode_contents`], each stream also saying whether it is damaged partway - /// ([`DecodeBudget::decode_with_damage`]). + /// ([`DecodeBudget::decode_with_damage`], which reads `/DecodeParms` given by reference). pub fn decode_contents_with_damage(&mut self, doc: &Document, contents: &Object) -> Vec, bool), CosError>> { match &*doc.resolve(contents) { - Object::Stream(s) => vec![self.decode_with_damage(s, usize::MAX)], + Object::Stream(s) => vec![self.decode_with_damage(doc, s, usize::MAX)], Object::Array(list) => list .iter() .filter_map(|entry| match &*doc.resolve(entry) { - Object::Stream(s) => Some(self.decode_with_damage(s, usize::MAX)), + Object::Stream(s) => Some(self.decode_with_damage(doc, s, usize::MAX)), _ => None, }) .collect(), @@ -823,7 +825,8 @@ mod tests { /// checksum, no filter at all. #[test] fn a_stream_damaged_partway_says_so() { - let read = |budget: &mut DecodeBudget, s: &Stream, max| budget.decode_with_damage(s, max).map(|(d, damaged)| (d.len(), damaged)).ok(); + let doc = Document::new_empty(); + let read = |budget: &mut DecodeBudget, s: &Stream, max| budget.decode_with_damage(&doc, s, max).map(|(d, damaged)| (d.len(), damaged)).ok(); let whole = Stream::flate(Dict::new(), &vec![b' '; 1 << 20]); let cut = Stream::from_raw(whole.dict.clone(), whole.raw.get(..whole.raw.len() / 2).unwrap_or_default().to_vec()); let partial = cut.decoded().map(|d| d.len()).unwrap_or(0); @@ -845,7 +848,42 @@ mod tests { let mut dict = Dict::new(); dict.set(b"Filter".to_vec(), Object::Array(vec![Object::name("FlateDecode"), Object::name("ASCIIHexDecode")])); let hex = Stream::from_raw(dict, Stream::flate(Dict::new(), b"4142zz4344>").raw.to_vec()); - assert_eq!(budget.decode_with_damage(&hex, usize::MAX).ok(), Some((b"AB".to_vec(), true))); + assert_eq!(budget.decode_with_damage(&doc, &hex, usize::MAX).ok(), Some((b"AB".to_vec(), true))); + } + + /// `/DecodeParms` given by reference, or a parameter given by reference in them, is read: a + /// predicted stream decodes as with them direct. Of the stream, decoding copies no more than + /// its filters and their parameters. + #[test] + fn parameters_given_by_reference_are_read() { + let content = b"q 10 0 0 10 0 0 cm /Im Do Q"; + // PNG Up, one column: each byte a row of its own, tagged 2. + let mut rows = Vec::new(); + let mut above = 0u8; + for &b in content { + rows.extend([2, b.wrapping_sub(above)]); + above = b; + } + let raw = Stream::flate(Dict::new(), &rows).raw; + let mut doc = Document::new_empty(); + let direct: Dict = [(b"Predictor".to_vec(), Object::Int(12))].into_iter().collect(); + let by_reference = Object::Ref(doc.add(Object::Dict(direct.clone()))); + let predictor = doc.add(Object::Int(12)); + let inner = Object::Dict([(b"Predictor".to_vec(), Object::Ref(predictor))].into_iter().collect()); + for parms in [Object::Dict(direct), by_reference, inner] { + let mut dict = Dict::new(); + dict.set(b"Filter".to_vec(), Object::name("FlateDecode")); + dict.set(b"DecodeParms".to_vec(), parms.clone()); + dict.set(b"Other".to_vec(), Object::Array(vec![Object::Int(0); 1000])); + let s = Stream::from_raw(dict, raw.to_vec()); + assert_eq!(DecodeBudget::page().decode_with_damage(&doc, &s, usize::MAX).ok(), Some((content.to_vec(), false)), "{parms:?}"); + let r = doc.add(Object::Stream(s.clone())); + let read = DecodeBudget::page().decode_contents_with_damage(&doc, &Object::Array(vec![Object::Ref(r)])); + assert_eq!(read, [Ok((content.to_vec(), false))], "{parms:?}"); + assert_eq!(DecodeBudget::page().decode_contents(&doc, &Object::Ref(r)), [Ok(content.to_vec())], "{parms:?}"); + let view = doc.decoding_view(&s); + assert!(matches!(view, std::borrow::Cow::Borrowed(_)) || view.dict.len() == 2, "{:?}", view.dict); + } } /// A page's content streams each say whether they are damaged, in their places. diff --git a/crates/cos/src/document/objects.rs b/crates/cos/src/document/objects.rs index 458840de3a..7ce9474018 100644 --- a/crates/cos/src/document/objects.rs +++ b/crates/cos/src/document/objects.rs @@ -288,7 +288,29 @@ impl Document { /// ([`pdfcraft_filters::MAX_CHAIN`]). Only small values are copied, so a file can't have a /// large object copied once for each key and filter. pub fn with_direct_parms<'s>(&self, s: &'s Stream) -> Cow<'s, Stream> { - let Some(p) = s.dict.get(b"DecodeParms") else { return Cow::Borrowed(s) }; + let Some(made) = self.direct_parms(s) else { return Cow::Borrowed(s) }; + let mut s = s.clone(); + s.dict.set(b"DecodeParms".to_vec(), made); + Cow::Owned(s) + } + + /// [`Document::with_direct_parms`] for decoding alone: where that copies `s`, this makes a + /// stream of `s`'s data, shared, and a dictionary of its `/Filter` and the parameters made + /// direct, all its filters read. A stream named again and again, as a page's `/Contents` may + /// list one, isn't copied whole each time. + pub(crate) fn decoding_view<'s>(&self, s: &'s Stream) -> Cow<'s, Stream> { + let Some(made) = self.direct_parms(s) else { return Cow::Borrowed(s) }; + let mut dict = Dict::new(); + if let Some(f) = s.dict.get(b"Filter") { + dict.set(b"Filter".to_vec(), f.clone()); + } + dict.set(b"DecodeParms".to_vec(), made); + Cow::Owned(Stream { dict, raw: s.raw.clone() }) + } + + /// The `/DecodeParms` [`Document::with_direct_parms`] gives `s`, or `None` when it keeps `s`. + fn direct_parms(&self, s: &Stream) -> Option { + let p = s.dict.get(b"DecodeParms")?; let crypt = |f: &Object| matches!(f.as_name(), Some(b"Crypt")); let filters: Vec = match s.dict.get(b"Filter") { Some(Object::Array(a)) => a.iter().map(crypt).collect(), @@ -296,7 +318,7 @@ impl Document { None => Vec::new(), }; if filters.iter().filter(|c| !**c).count() > pdfcraft_filters::MAX_CHAIN { - return Cow::Borrowed(s); + return None; } let mut changed = matches!(p, Object::Ref(_)); // Its numbers and flags kept, resolved; anything else is no filter's parameters (null). @@ -323,12 +345,7 @@ impl Document { } _ => one(p, &mut changed), }; - if !changed { - return Cow::Borrowed(s); - } - let mut s = s.clone(); - s.dict.set(b"DecodeParms".to_vec(), made); - Cow::Owned(s) + changed.then_some(made) } /// `o`, or the object it refers to, without copying either. diff --git a/crates/edit/src/objects.rs b/crates/edit/src/objects.rs index dbdbf495ae..c0a57c358a 100644 --- a/crates/edit/src/objects.rs +++ b/crates/edit/src/objects.rs @@ -316,7 +316,7 @@ fn checked_streams(doc: &Document, page: usize) -> Result, Ed return Err(EditError::Invalid("a page content stream is missing".into())); }; let (bytes, damaged) = decode - .decode_with_damage(stream, remaining.min(64 * 1024 * 1024)) + .decode_with_damage(doc, stream, remaining.min(64 * 1024 * 1024)) .map_err(|_| EditError::Invalid("the page content cannot be decoded within its size limit".into()))?; if damaged { return Err(EditError::Invalid("a page content stream is damaged partway".into())); diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index dc80cb6a29..df9bce657d 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -5218,6 +5218,38 @@ fn a_content_stream_that_does_not_decode_is_kept_and_edited_around() { assert!(editable_objects(&split_page(&large, "BT /F1 12 Tf 72 600 Td (Whole) Tj ET"), 0).is_ok()); } +/// A content stream whose `/DecodeParms`, or a parameter in them, is given by reference is read +/// through the reference: its text is listed and edited, not read with the predictor's tag bytes +/// left in it. +#[test] +fn content_with_parameters_given_by_reference_is_read_and_edited() { + use pdfcraft_cos::{Dict, ObjRef, Stream}; + let content = b"BT /F1 12 Tf 72 700 Td (Predicted) Tj ET"; + // PNG Up, one column: each byte a row of its own, tagged 2. + let mut rows = Vec::new(); + let mut above = 0u8; + for &b in content { + rows.extend([2, b.wrapping_sub(above)]); + above = b; + } + let texts = |doc: &Document| text::text_lines(doc, 0).unwrap().into_iter().map(|l| l.text).collect::>(); + for given in ["parameters", "predictor"] { + let mut doc = split_page("", "BT /F1 12 Tf 72 600 Td (Whole) Tj ET"); + let predictor = Object::Int(12); + let parms = if given == "parameters" { + Object::Ref(doc.add(Object::Dict([(b"Predictor".to_vec(), predictor)].into_iter().collect::()))) + } else { + Object::Dict([(b"Predictor".to_vec(), Object::Ref(doc.add(predictor)))].into_iter().collect()) + }; + let mut s = Stream::flate(Dict::new(), &rows); + s.dict.set(b"DecodeParms".to_vec(), parms); + doc.set(ObjRef::new(4, 0), s); + assert_eq!(texts(&doc), ["Predicted", "Whole"], "{given} by reference"); + text::replace_line(&mut doc, 0, 0, "Edited").unwrap(); + assert_eq!(texts(&reopen(&doc)), ["Edited", "Whole"], "{given} by reference"); + } +} + /// Objects are selected only from content read whole: what a damaged stream hides past the /// damage could change the graphics state of every later item. #[test] diff --git a/crates/edit/src/text/lines.rs b/crates/edit/src/text/lines.rs index a28806078b..bf66cc0f8b 100644 --- a/crates/edit/src/text/lines.rs +++ b/crates/edit/src/text/lines.rs @@ -92,7 +92,7 @@ pub(crate) fn content_streams_within(doc: &Document, page: &Dict, budget: &mut D let mut out = Vec::with_capacity(list.len()); for entry in list { let (data, damaged) = match &*doc.resolve(&entry) { - Object::Stream(s) => match budget.decode_with_damage(s, usize::MAX) { + Object::Stream(s) => match budget.decode_with_damage(doc, s, usize::MAX) { Ok(read) => read, Err(e) if budget.remaining() == 0 => return Err(EditError::Invalid(format!("the page content is too large to read: {e}"))), Err(_) => (Vec::new(), false), diff --git a/crates/optimize/src/images.rs b/crates/optimize/src/images.rs index 21f6953a8d..6ad6413696 100644 --- a/crates/optimize/src/images.rs +++ b/crates/optimize/src/images.rs @@ -215,7 +215,7 @@ impl Walk<'_> { self.complete = false; return; } - let Ok((data, false)) = self.budget.decode_with_damage(s, usize::MAX) else { + let Ok((data, false)) = self.budget.decode_with_damage(doc, s, usize::MAX) else { self.complete = false; return; }; diff --git a/crates/organize/src/prune.rs b/crates/organize/src/prune.rs index 8683b4d810..591e804b1e 100644 --- a/crates/organize/src/prune.rs +++ b/crates/organize/src/prune.rs @@ -219,7 +219,7 @@ impl Pruner<'_> { } #[cfg(test)] crate::work(1); - match DecodeBudget::new(usize::MAX, 0).decode_with_damage(s, usize::MAX) { + match DecodeBudget::new(usize::MAX, 0).decode_with_damage(self.doc, 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(_) => { diff --git a/crates/organize/src/tests.rs b/crates/organize/src/tests.rs index 40bb4210d4..8d41f474cc 100644 --- a/crates/organize/src/tests.rs +++ b/crates/organize/src/tests.rs @@ -2669,6 +2669,37 @@ fn extract_keeps_images_content_damaged_partway_might_draw() { } } +/// A form whose `/DecodeParms` is given by reference is read through it: the image it draws is +/// kept, not lost to content read with the predictor's tag bytes left in it. +#[test] +fn extract_keeps_images_a_form_with_parameters_by_reference_draws() { + use pdfcraft_cos::ObjRef; + let content = b"q 10 0 0 10 0 0 cm /ImB Do Q"; + // PNG Up, one column: each byte a row of its own, tagged 2. + let mut rows = Vec::new(); + let mut above = 0u8; + for &b in content { + rows.extend([2, b.wrapping_sub(above)]); + above = b; + } + let mut src = doc_images_with(&raw("/Fm Do"), ""); + let parms = src.add(Object::Dict([(b"Predictor".to_vec(), Object::Int(12))].into_iter().collect::())); + let mut fm = pdfcraft_cos::Stream::flate(Dict::new(), &rows); + fm.dict.set(b"DecodeParms".to_vec(), Object::Ref(parms)); + 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 = src.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(); + src.update_dict(ObjRef::new(7, 0), |d| d.set(b"XObject".to_vec(), Object::Dict(xobjects))).unwrap(); + let part = full_roundtrip(&extract_pages(&src, &[1]).unwrap()); + let names = xobject_names(&part, 0); + assert!(names.iter().any(|n| n.as_slice() == b"ImB"), "{names:?}"); + assert!(!names.iter().any(|n| n.as_slice() == b"ImA"), "what the part doesn't draw still goes: {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`.