diff --git a/crates/pdf/src/image.rs b/crates/pdf/src/image.rs index 2c8adda6..4d55fef8 100644 --- a/crates/pdf/src/image.rs +++ b/crates/pdf/src/image.rs @@ -184,6 +184,11 @@ fn mask_image(file: &File, o: &Obj, soft: bool) -> Option<(Vec, usize, usize return Some((data.into_iter().step_by(jpeg_n.unwrap_or(1)).take(w * h).collect(), w, h)); } let bpc = if soft { file.get_num(d, "BitsPerComponent").unwrap_or(8.0) as usize } else { 1 }; + // Like `decode()`: only 1/2/4/8/16-bit depths are supported. Anything else (notably + // `bpc >= 32`, where `1u32 << bpc` below would overflow) means "no usable mask". + if !matches!(bpc, 1 | 2 | 4 | 8 | 16) { + return None; + } let max = ((1u32 << bpc) - 1).max(1) as f64; let dec = file.get(d, "Decode").map(|x| file.nums(x)).filter(|v| v.len() >= 2); let v = unpack(&data, w, h, 1, bpc); diff --git a/crates/pdf/src/tests_import.rs b/crates/pdf/src/tests_import.rs index 7cebcd43..4ea29801 100644 --- a/crates/pdf/src/tests_import.rs +++ b/crates/pdf/src/tests_import.rs @@ -274,11 +274,32 @@ fn inline_image_with_absurd_dimensions_falls_back_to_ei_scan() { assert!(!doc.skipped.is_empty()); // A length just under `usize::MAX` passes the row math; past byte 4096 of the content the // caller's `start + len` would then overflow. - let content = format!("{}q 10 0 0 10 0 0 cm BI /W 2305843009213693440 /H 8 /CS /G /BPC 8 ID EI Q", " ".repeat(5000)); + let content = format!("{}q 10 0 0 10 0 0 cm BI /W 2305843009213693440 /H 8 /CS /G /BPC 8 ID \0 EI Q", " ".repeat(5000)); let doc = parse(&page_pdf(&content, "", vec![])).unwrap(); let _ = render(&doc); } +#[test] +fn smask_with_absurd_bpc_is_ignored_not_fatal() { + // `/BitsPerComponent 32` in a soft mask: `1u32 << bpc` must neither panic (debug) + // nor mistranslate samples into alpha (release); the mask is treated as absent. + let bytes = page_pdf( + "q 2 0 0 2 0 0 cm /Im Do Q", + "/XObject << /Im 10 0 R >>", + vec![ + ( + 10, + "<< /Type /XObject /Subtype /Image /Width 2 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 8 /SMask 11 0 R >>".into(), + Some(vec![0x80; 4]), + ), + (11, "<< /Type /XObject /Subtype /Image /Width 2 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 32 >>".into(), Some(vec![0xFF; 16])), + ], + ); + let doc = parse(&bytes).unwrap(); + let img = render(&doc); + assert!(coverage(&img, 0, 0, 200, 100) > 0.0, "image still draws, opaque"); +} + #[test] fn truncated_and_corrupt_eps_never_panic() { let eps = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n\