review: refuse an image whose soft mask could not be decoded

This commit is contained in:
Dylan M. Taylor committed 2026-11-08 12:13:14 -05:00
1 parent aa6eb261f3
commit c80b5909f5
3 files changed
+28 -6

No files matched your search

+6
View File
@@ -172,6 +172,12 @@ impl ImageBudget {
self.pay_pixels(samples.saturating_mul(JPEG_WORK).saturating_add(reading))
}
/// Records an image left out for a reason other than its allowance, so that the caller
/// refuses it as it refuses one whose decode spent everything.
pub fn refuse(&mut self) {
self.refused += 1;
}
/// Pays for the pixels of image `s` that its `decoded` data holds, those of its `/Width` and
/// `/Height` but no more than a bit of the data each, unless it is JBIG2, whose decoder pays for
/// its page, or isn't decoded here. What can't be paid for refuses the image.
+17 -4
View File
@@ -547,7 +547,9 @@ fn soft_mask(doc: &Document, s: &Stream, w: usize, h: usize, budget: &mut ImageB
let r = s.dict.get(b"SMask")?.as_ref()?;
let obj = doc.get(r);
let Object::Stream(m) = &*obj else { return None };
gray_plane(doc, m, w, h, budget)
// A soft mask that cannot be decoded leaves the image showing what the mask hides, so the
// image is refused rather than written out opaque.
gray_plane(doc, m, w, h, budget, true)
}
/// A stencil `/Mask` (an image mask stream, 8.9.6.3) as 8-bit alpha: its 1 samples (0 with
@@ -556,12 +558,12 @@ fn stencil_mask(doc: &Document, s: &Stream, w: usize, h: usize, budget: &mut Ima
let r = s.dict.get(b"Mask")?.as_ref()?;
let obj = doc.get(r);
let Object::Stream(m) = &*obj else { return None };
Some(gray_plane(doc, m, w, h, budget)?.into_iter().map(|v| 255 - v).collect())
Some(gray_plane(doc, m, w, h, budget, false)?.into_iter().map(|v| 255 - v).collect())
}
/// A one-component mask image as 8-bit values (its `/Decode` interval applied), scaled to
/// `w` × `h`. `None` when it can't be decoded (JPEG 2000 masks).
fn gray_plane(doc: &Document, m: &Stream, w: usize, h: usize, budget: &mut ImageBudget) -> Option<Vec<u8>> {
fn gray_plane(doc: &Document, m: &Stream, w: usize, h: usize, budget: &mut ImageBudget, soft: bool) -> Option<Vec<u8>> {
let (mw, mh) = size(m.dict.int(b"Width")?, m.dict.int(b"Height")?)?;
let stencil = m.dict.get(b"ImageMask").is_some_and(|v| matches!(&*doc.resolve(v), Object::Bool(true)));
let f = filters(m);
@@ -585,7 +587,18 @@ fn gray_plane(doc: &Document, m: &Stream, w: usize, h: usize, budget: &mut Image
_ => return None,
};
let max = (1u32 << bpc.min(8)) - 1;
unpack(&decoded(doc, m, (mw, mh, 1, bpc), budget).ok()?, mw, mh, 1, bpc)?.into_iter().map(|v| (v as u32 * 255 / max) as u8).collect()
let data = match decoded(doc, m, (mw, mh, 1, bpc), budget) {
Ok(data) => data,
Err(_) => {
// A stage limit stops the mask being decoded: a soft mask leaves the image
// refusing it rather than showing what the mask hides.
if soft {
budget.refuse();
}
return None;
}
};
unpack(&data, mw, mh, 1, bpc)?.into_iter().map(|v| (v as u32 * 255 / max) as u8).collect()
}
};
if let Some([lo, hi]) = decode(doc, m, 1).and_then(|pairs| pairs.first().copied()) {
+5 -2
View File
@@ -859,7 +859,8 @@ fn image_data_far_past_its_samples_is_not_decoded() {
let (ext, rgba) = exported_rgba(&bomb(4, 2, 4 * 2 + 64));
assert_eq!((ext, rgba), ("png", [7, 7, 7, 255].repeat(8)));
// The same for a soft mask: it is not decoded, and the image exports without it.
// The same for a soft mask: it is not decoded, and the image that needs it is refused rather
// than written out without the transparency it hides.
let mut doc = from_images(&[("flat.png".into(), png_bytes(false))]).unwrap();
with_image(&mut doc, |doc| {
let m = doc.add(Object::Stream(Stream::flate(image_dict(1, 1, "DeviceGray", 8), &vec![0u8; 64 << 20])));
@@ -867,7 +868,9 @@ fn image_data_far_past_its_samples_is_not_decoded() {
d.set(b"SMask".to_vec(), Object::Ref(m));
Stream::from_raw(d, vec![10; 24])
});
assert_eq!(alphas(&exported_rgba(&doc).1), [255; 8]);
let out = extract_images(&doc, &[0], 0);
assert!(out.images.is_empty(), "an image is not written without the mask that hides its content");
assert_eq!(out.skipped.len(), 1);
}
#[test]