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 <noreply@anthropic.com>
This commit is contained in:
Piohrun
2026-11-01 15:05:26 -05:00
committed by GitHub
co-authored by Claude Opus 5.5
parent eafe9e2c0b
commit 036811368b
2 changed files with 13 additions and 7 deletions
+6 -2
View File
@@ -236,12 +236,16 @@ fn image(doc: &Document, s: &Stream) -> Result<(&'static str, Vec<u8>), 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
+7 -5
View File
@@ -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