Envelopes: a pixelated CMYK image with its own grid mask is written without /Interpolate

PDF export writes a CMYK image whose opacity mask is a gray image on its
own pixel grid with the mask as the image's soft mask. That path always
asked viewers to smooth the image, so a pixelated piece of an envelope
with Anti-Alias off was still written with /Interpolate when it had such
a mask. It now takes the pixelated flag as images without a mask do, and
caches the smoothed and pixelated versions apart.
This commit is contained in:
dotCooCoo
2026-10-29 11:07:58 +07:00
committed by bflatastic
parent ab69d8f6d9
commit acc5e53c91
3 changed files with 41 additions and 5 deletions
+8 -4
View File
@@ -47,8 +47,8 @@ impl Exporter<'_> {
/// on the image's own pixel grid (an image's soft mask as it opens): the alpha is written as the
/// image's soft mask, as an RGBA image's is, rather than as a luminosity soft mask over it,
/// through which CoreGraphics draws CMYK images on black boxes (#2159). `None` for other
/// images and masks; cached like [`Self::load_image`].
pub(super) fn masked_image(&mut self, im: &vectorcraft_doc::ImageObject, m: &vectorcraft_doc::OpacityMask) -> Option<Image> {
/// images and masks; cached like [`Self::load_image`], `interpolate` as there.
pub(super) fn masked_image(&mut self, im: &vectorcraft_doc::ImageObject, m: &vectorcraft_doc::OpacityMask, interpolate: bool) -> Option<Image> {
let art = &m.art;
let NodeKind::Image(mi) = &art.kind else { return None };
let plain =
@@ -57,14 +57,18 @@ impl Exporter<'_> {
return None;
}
let size = placed_size(im);
let cache = format!("{}\u{0}{}\u{0}{}\u{0}{:.3}x{:.3}", im.key, mi.key, m.clip, size.0, size.1);
let interpolate = interpolate && self.interpolate;
let mut cache = format!("{}\u{0}{}\u{0}{}\u{0}{:.3}x{:.3}", im.key, mi.key, m.clip, size.0, size.1);
if !interpolate && self.interpolate {
cache.push_str("\u{0}pixelated");
}
if let Some(i) = self.images.get(&cache) {
return i.clone();
}
let doc = self.doc;
let img = doc.image_blob(im).filter(|b| b.is_cmyk()).and_then(|blob| {
let alpha = crate::images::mask_alpha(doc.image_blob(mi)?, im.width, im.height, m.clip)?;
let r = crate::images::recode_cmyk(blob, size, self.compression, self.interpolate, self.out.cmyk_pixels(), Some(alpha));
let r = crate::images::recode_cmyk(blob, size, self.compression, interpolate, self.out.cmyk_pixels(), Some(alpha));
if let (Some(w), Some(_)) = (r.warning, &r.image) {
self.warn(w);
}
+1 -1
View File
@@ -137,7 +137,7 @@ impl Exporter<'_> {
}
let mask = n.extras().mask.as_deref().filter(|m| !m.disabled);
let alpha = match (&n.kind, mask) {
(NodeKind::Image(im), Some(m)) => self.masked_image(im, m),
(NodeKind::Image(im), Some(m)) => self.masked_image(im, m, !n.extras().pixelated),
_ => None,
};
if let Some(m) = mask
+32
View File
@@ -439,6 +439,38 @@ fn a_cmyk_image_masked_by_a_grey_image_of_its_grid_writes_it_as_its_smask() {
assert!(String::from_utf8_lossy(&bytes).contains("/S/Luminosity"), "a luminosity soft mask");
}
/// A pixelated CMYK image (an envelope's with Anti-Alias off) masked by a grey image of its grid
/// is written without /Interpolate, as one without a mask is.
#[test]
fn a_pixelated_cmyk_image_with_its_grid_mask_asks_viewers_not_to_smooth_it() {
for pixelated in [false, true] {
let mut d = Document::new_with_mode(100.0, 100.0, ColorMode::Cmyk);
d.images.insert("img".into(), ImageBlob::cmyk_tiff(&Inks::new(2, 2, flat([0u8, 102, 255, 0], 4)).unwrap()).unwrap());
d.images.insert("alpha".into(), ImageBlob::new("image/png", png(image::GrayImage::from_raw(2, 2, vec![255, 128, 0, 64]).unwrap())));
let image = |key: &str| {
let im = ImageObject {
key: key.into(),
width: 2,
height: 2,
xf: Affine::scale(20.0),
link: None,
placement: Default::default(),
pixels: None,
};
NodeKind::Image(Box::new(im))
};
let mask = vectorcraft_doc::OpacityMask::new(Node::new(d.alloc_id(), image("alpha")), true);
let mut n = Node::new(d.alloc_id(), image("img")).with_mask(Some(Box::new(mask)));
n.extras_mut().pixelated = pixelated;
let l = d.layers[0].id;
d.insert(Some(l), 0, n).unwrap();
let out = export_with_report(&d, &PdfOptions { created: Some(1_700_000_000), ..PdfOptions::uncompressed() }).unwrap().bytes;
let text = String::from_utf8_lossy(&out);
assert!(!text.contains("/S/Luminosity"), "the mask is the image's /SMask");
assert_eq!(text.contains("/Interpolate"), !pixelated, "pixelated: {pixelated}");
}
}
/// #2950: Press Quality converts to the document's profile with its numbers kept, so in a CMYK
/// document an RGB image comes out CMYK, as the press needs; with no conversion (the default
/// preset) it stays RGB, as before.