From 44d170f059d079dddbef7db6684722a651c8506c Mon Sep 17 00:00:00 2001 From: bflatastic Date: Tue, 13 Oct 2026 07:35:32 +0700 Subject: [PATCH] PDF import: pattern swatches read inside an alpha mask keep their images; a luminosity mask test --- crates/pdf/src/import.rs | 7 ++++++- crates/pdf/src/tests_import_fidelity.rs | 27 +++++++++++++++++-------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/crates/pdf/src/import.rs b/crates/pdf/src/import.rs index 1f1da133..bac31b72 100644 --- a/crates/pdf/src/import.rs +++ b/crates/pdf/src/import.rs @@ -1340,10 +1340,15 @@ impl<'p> Builder<'p> { { return Some(Paint::Pattern { pattern: name.clone(), xf }); } + // A swatch is shared with uses outside an alpha mask, and pattern paints keep their + // colours there: its images are read as they are. + let white = std::mem::take(&mut self.white_images); let art = self.nested(Rect::ZERO, |b| { // A cell whose content can't be decoded is empty. let _ = t.interpret(b, Affine::IDENTITY, stroke); - })?; + }); + self.white_images = white; + let art = art?; let sig = paints(&art); let entry = self.pattern_keys.entry(key).or_default(); entry.0 += 1; diff --git a/crates/pdf/src/tests_import_fidelity.rs b/crates/pdf/src/tests_import_fidelity.rs index 62e60f43..4a9a1acf 100644 --- a/crates/pdf/src/tests_import_fidelity.rs +++ b/crates/pdf/src/tests_import_fidelity.rs @@ -78,11 +78,9 @@ fn the_mask(d: &Document) -> (Node, OpacityMask) { (n, m) } -/// An alpha soft mask whose art is an image with its own alpha (cairo, and so Inkscape, rasterizes -/// masks so): the image is read white with that alpha, so the mask's luminance is the alpha, not -/// the image's colour. -#[test] -fn an_alpha_soft_mask_of_an_image_uses_the_images_alpha() { +/// The two pixels of the image a soft mask of kind `s` draws: black, opaque on the left and clear +/// on the right (its own `/SMask`). +fn mask_image_pixels(s: &str) -> [[u8; 4]; 2] { let g = first_extra(1); let group = stream( &format!( @@ -91,7 +89,6 @@ fn an_alpha_soft_mask_of_an_image_uses_the_images_alpha() { ), "q 100 0 0 100 0 0 cm /Im0 Do Q", ); - // Black pixels, opaque on the left and clear on the right. let image = stream( &format!( "/Type /XObject /Subtype /Image /Width 2 /Height 1 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /ASCIIHexDecode /SMask {} 0 R", @@ -103,7 +100,7 @@ fn an_alpha_soft_mask_of_an_image_uses_the_images_alpha() { stream("/Type /XObject /Subtype /Image /Width 2 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /ASCIIHexDecode", "FF00>"); let bytes = one_page( "/GS0 gs 1 0 0 rg 0 0 100 100 re f", - &format!("/ExtGState << /GS0 << /SMask << /Type /Mask /S /Alpha /G {g} 0 R >> >> >>"), + &format!("/ExtGState << /GS0 << /SMask << /Type /Mask /S /{s} /G {g} 0 R >> >> >>"), &[&group, &image, &smask], ); let d = open(&bytes); @@ -116,7 +113,21 @@ fn an_alpha_soft_mask_of_an_image_uses_the_images_alpha() { }); let [key] = keys.as_slice() else { panic!("one image in the mask art: {keys:?}") }; let px = image::load_from_memory(&d.images[key].bytes).unwrap().to_rgba8(); - assert_eq!((px.get_pixel(0, 0).0, px.get_pixel(1, 0).0), ([255, 255, 255, 255], [255, 255, 255, 0])); + [px.get_pixel(0, 0).0, px.get_pixel(1, 0).0] +} + +/// An alpha soft mask whose art is an image with its own alpha (some PDF writers rasterize masks +/// so): the image is read white with that alpha, so the mask's luminance is the alpha, not the +/// image's colour (ISO 32000-1 11.5.2: an alpha mask takes the group's alpha, not its colour). +#[test] +fn an_alpha_soft_mask_of_an_image_uses_the_images_alpha() { + assert_eq!(mask_image_pixels("Alpha"), [[255, 255, 255, 255], [255, 255, 255, 0]]); +} + +/// A luminosity soft mask takes the group's colour (ISO 32000-1 11.5.3): its image stays black. +#[test] +fn a_luminosity_soft_mask_of_an_image_keeps_the_images_colour() { + assert_eq!(mask_image_pixels("Luminosity"), [[0, 0, 0, 255], [0, 0, 0, 0]]); } #[test]