From 036811368baa469cf1bb9188dda1655e5a8774c4 Mon Sep 17 00:00:00 2001 From: Piohrun Date: Sun, 1 Nov 2026 21:05:26 +0100 Subject: [PATCH] M2: both ends of a /Decode array have to be numbers to invert an export (#5678) An end that is missing or isn't a number sorted below any number, so [1], or [1 7 0 R] with 7 leading nowhere, read as inverted on a JPEG or a stencil mask, and so did an end too long to hold, which parses as infinite. Such an array now leaves the default mapping, as it does on the lossless path. Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY Co-authored-by: Claude Opus 5.5 --- crates/create/src/extract.rs | 8 ++++++-- crates/create/src/tests.rs | 12 +++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/create/src/extract.rs b/crates/create/src/extract.rs index 2b3a0ffd73..b5500f60c3 100644 --- a/crates/create/src/extract.rs +++ b/crates/create/src/extract.rs @@ -236,12 +236,16 @@ fn image(doc: &Document, s: &Stream) -> Result<(&'static str, Vec), String> let mut alpha = if mask { None } else { soft_mask(doc, s, w, h).or_else(|| stencil_mask(doc, s, w, h)) }; let key = if mask { None } else { colour_key(doc, s) }; // Keep the existing JPEG and stencil-mask Decode handling separate from lossless colour images. - let endpoint = |value: Option<&Object>| value.map(|v| doc.resolve(v)).and_then(|v| v.as_f64()); + let endpoint = |value: Option<&Object>| value.map(|v| doc.resolve(v)).and_then(|v| v.as_f64()).filter(|v| v.is_finite()); let inverted = (jpeg || mask) && s.dict .get(b"Decode") .map(|d| doc.resolve(d)) - .and_then(|d| d.as_array().map(|a| endpoint(a.first()) > endpoint(a.get(1)))) + // Both ends have to be finite numbers, as for lossless images, or the default mapping stands. + .and_then(|d| { + let a = d.as_array()?; + Some(endpoint(a.first())? > endpoint(a.get(1))?) + }) .unwrap_or(false); // In a PDF a CMYK JPEG's samples are ink amounts unless /Decode inverts them, while image // viewers read a CMYK JPEG file as inverted (Adobe's convention): as is, it would show as diff --git a/crates/create/src/tests.rs b/crates/create/src/tests.rs index 4a1e007fd4..ff871d53ae 100644 --- a/crates/create/src/tests.rs +++ b/crates/create/src/tests.rs @@ -2237,12 +2237,14 @@ fn a_stencil_masks_decode_array_can_hold_references() { for decode in ["[0 5 0 R]", "[6 0 R 1]", "[6 0 R 5 0 R]"] { assert_eq!(export(decode), identity, "{decode}"); } - // A reference that only leads back to itself holds no number. The read ends, and the file comes - // out one way round or the other. - for decode in ["[7 0 R 0]", "[1 7 0 R]", "[8 0 R 0]", "[1 8 0 R]", "[7 0 R 8 0 R]"] { - let file = export(decode); - assert!(file == inverted || file == identity, "{decode}"); + // A reference that only leads back to itself holds no number, and an end that is missing or isn't + // a number leaves the default mapping, whichever end it is. + for decode in ["[7 0 R 0]", "[1 7 0 R]", "[8 0 R 0]", "[1 8 0 R]", "[7 0 R 8 0 R]", "[1]", "[1 /Name]", "[/Name 0]"] { + assert_eq!(export(decode), identity, "{decode}"); } + // Equal ends aren't the two ways round either, and a number too long to hold reads as infinite. + assert_eq!(export("[1 1]"), identity); + assert_eq!(export(&format!("[{} 0]", "9".repeat(400))), identity); } /// A mask of other proportions than its image is scaled to it, however far apart they are: the