mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 09:21:17 +00:00
review: refuse an image whose soft mask could not be decoded
This commit is contained in:
3 files changed
+28
-6
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in new issue
Block a user