diff --git a/crates/create/src/extract.rs b/crates/create/src/extract.rs index 36825e6ad6..cb81fa2044 100644 --- a/crates/create/src/extract.rs +++ b/crates/create/src/extract.rs @@ -100,13 +100,15 @@ fn filters(s: &Stream) -> Vec> { } } -/// The samples of image `s`, decoded: through its filters, or, for a JBIG2 image (its only -/// filter), through a JBIG2 decoder with its `/JBIG2Globals`, as rows of one-bit samples each -/// starting on a byte, with black as 0, as `JBIG2Decode` gives them (ISO 32000-2 §7.4.7). +/// The samples of image `s`, decoded: through its filters, with a `/DecodeParms` dictionary given +/// by reference, or as an array's entry, read too (a predictor given so would otherwise be lost; +/// a number given by reference inside it still is), or, for a JBIG2 image +/// (its only filter), through a JBIG2 decoder with its `/JBIG2Globals`, as rows of one-bit samples +/// each starting on a byte, with black as 0, as `JBIG2Decode` gives them (ISO 32000-2 §7.4.7). fn decoded(doc: &Document, s: &Stream) -> Result, String> { match filters(s).as_slice() { [f] if f == b"JBIG2Decode" => jbig2(doc, s), - _ => s.decoded().map_err(|e| e.to_string()), + _ => doc.with_direct_parms(s).decoded().map_err(|e| e.to_string()), } } @@ -129,7 +131,7 @@ fn decode_parms(doc: &Document, s: &Stream) -> Dict { /// decoding it without them. fn jbig2(doc: &Document, s: &Stream) -> Result, String> { let globals = match decode_parms(doc, s).get(b"JBIG2Globals").map(|g| doc.resolve(g)).as_deref() { - Some(Object::Stream(g)) => Some(g.decoded().map_err(|e| format!("the JBIG2 globals can't be read: {e}"))?), + Some(Object::Stream(g)) => Some(doc.with_direct_parms(g).decoded().map_err(|e| format!("the JBIG2 globals can't be read: {e}"))?), _ => None, }; let image = hayro_jbig2::Image::new_embedded(&s.raw, globals.as_deref()).map_err(|e| format!("the JBIG2 image can't be read: {e}"))?; @@ -498,9 +500,10 @@ fn space(doc: &Document, cs: Option<&Object>) -> Result { }; let table = match a.get(3).map(|t| doc.resolve(t)).as_deref() { Some(Object::String(s)) => s.bytes.clone(), - Some(Object::Stream(t)) => { - t.decoded_within(MAX_LOOKUP).map_err(|_| "an indexed colour space with an oversized or damaged lookup table".to_string())? - } + Some(Object::Stream(t)) => doc + .with_direct_parms(t) + .decoded_within(MAX_LOOKUP) + .map_err(|_| "an indexed colour space with an oversized or damaged lookup table".to_string())?, _ => return Err("an indexed colour space without a lookup table".into()), }; Ok(Space::Indexed(Box::new(base), hival, table)) diff --git a/crates/create/src/tests.rs b/crates/create/src/tests.rs index 2a11e194b2..b9e9cd5aa7 100644 --- a/crates/create/src/tests.rs +++ b/crates/create/src/tests.rs @@ -1319,6 +1319,75 @@ fn a_tiny_gif_on_a_huge_screen_is_refused() { assert_eq!(media(&pages(&doc)[0]), [0.0, 0.0, 40.0, 30.0]); } +/// An image whose PNG predictor is given by reference exports as its picture: its `/DecodeParms` +/// dictionary given by reference, or as an array's entry. So do an indexed image whose lookup +/// table is a stream with its `/DecodeParms` given by reference, and a JBIG2 image whose +/// `/JBIG2Globals` stream has them so. `Stream::decoded` reads only a +/// direct `/DecodeParms`: the row tags stayed in the samples, and the picture came out shifted. +/// (A number given by reference inside a direct `/DecodeParms` is read once +/// `Document::with_direct_parms` reads those too.) +#[test] +fn images_whose_predictor_is_given_by_reference_export_as_their_picture() { + // PNG predictor parameters for `columns` one-byte samples a row. + let parms = |columns: i64| { + let mut p = Dict::new(); + p.set(b"Predictor".to_vec(), Object::Int(15)); + p.set(b"Colors".to_vec(), Object::Int(1)); + p.set(b"BitsPerComponent".to_vec(), Object::Int(8)); + p.set(b"Columns".to_vec(), Object::Int(columns)); + p + }; + // Two rows of four grey samples, each behind a PNG "None" row tag. + let predicted = pdfcraft_filters::encode_flate(&[0, 10, 20, 30, 40, 0, 50, 60, 70, 80]); + let grey: Vec = [10u8, 20, 30, 40, 50, 60, 70, 80].iter().flat_map(|v| [*v, *v, *v, 255]).collect(); + for form in ["a reference", "an array of a reference"] { + let mut doc = from_images(&[("flat.png".into(), png_bytes(false))]).unwrap(); + with_image(&mut doc, |doc| { + let mut d = image_dict(4, 2, "DeviceGray", 8); + d.set(b"Filter".to_vec(), Object::name("FlateDecode")); + let given = Object::Ref(doc.add(Object::Dict(parms(4)))); + let decode_parms = if form == "a reference" { given } else { Object::Array(vec![given]) }; + d.set(b"DecodeParms".to_vec(), decode_parms); + Stream::from_raw(d, predicted.clone()) + }); + assert_eq!(exported_rgba(&doc), ("png", grey.clone()), "/DecodeParms as {form}"); + } + // An indexed image's lookup table, red then blue, as a stream behind a "None" row tag. + let mut doc = from_images(&[("flat.png".into(), png_bytes(false))]).unwrap(); + with_image(&mut doc, |doc| { + let mut t = Dict::new(); + t.set(b"Filter".to_vec(), Object::name("FlateDecode")); + t.set(b"DecodeParms".to_vec(), Object::Ref(doc.add(Object::Dict(parms(6))))); + let table = doc.add(Object::Stream(Stream::from_raw(t, pdfcraft_filters::encode_flate(&[0, 255, 0, 0, 0, 0, 255])))); + let mut d = image_dict(4, 1, "DeviceGray", 8); + d.set(b"ColorSpace".to_vec(), Object::Array(vec![Object::name("Indexed"), Object::name("DeviceRGB"), Object::Int(1), Object::Ref(table)])); + Stream::from_raw(d, vec![0, 1, 0, 1]) + }); + let (red, blue) = ([255, 0, 0, 255], [0, 0, 255, 255]); + assert_eq!(exported_rgba(&doc), ("png", [red, blue, red, blue].concat()), "the lookup table's /DecodeParms by reference"); + // A JBIG2 image whose globals hold one extension segment (ISO/IEC 14492 7.4.14) a decoder + // ignores, behind a "None" row tag. With the tag left in, the globals can't be read and the + // image is skipped. + let segment = [0, 0, 0, 0, 0x3E, 0, 0, 0, 0, 0, 4, 0, 0, 0, 1]; + let bits = [0xF0u8, 0x0F, 0xAA, 0x55]; + let mut doc = from_images(&[("flat.png".into(), png_bytes(false))]).unwrap(); + with_image(&mut doc, |doc| { + let mut g = Dict::new(); + g.set(b"Filter".to_vec(), Object::name("FlateDecode")); + g.set(b"DecodeParms".to_vec(), Object::Ref(doc.add(Object::Dict(parms(segment.len() as i64))))); + let tagged: Vec = [0].iter().chain(&segment).copied().collect(); + let globals = doc.add(Object::Stream(Stream::from_raw(g, pdfcraft_filters::encode_flate(&tagged)))); + let mut decode_parms = Dict::new(); + decode_parms.set(b"JBIG2Globals".to_vec(), Object::Ref(globals)); + let mut d = image_dict(16, 2, "DeviceGray", 1); + d.set(b"Filter".to_vec(), Object::name("JBIG2Decode")); + d.set(b"DecodeParms".to_vec(), Object::Dict(decode_parms)); + Stream::from_raw(d, pdfcraft_filters::encode_jbig2_generic(&bits, 16, 2).unwrap()) + }); + let picture: Vec = (0..32).flat_map(|i| if bits[i / 8] & (0x80 >> (i % 8)) != 0 { [255, 255, 255, 255] } else { [0, 0, 0, 255] }).collect(); + assert_eq!(exported_rgba(&doc), ("png", picture), "the JBIG2 globals' /DecodeParms by reference"); +} + /// Export ▸ Images reads JBIG2 images (ISO 32000-2 §7.4.7), JBIG2 black as 0: as the image, and as /// a stencil `/Mask` of another. Their `/JBIG2Globals` are read, from a dictionary or the one in /// an array, so globals that can't be read skip the image rather than being ignored. Rows past the