Merge pull request #5817 from dotCooCoo/fix/ai-mask-placeholder-uncolored

Images: a colorized image's missing-link placeholder shows in its own colors
This commit is contained in:
BFlat
2026-11-05 23:39:41 +07:00
committed by GitHub
9 changed files with 112 additions and 17 deletions
+7
View File
@@ -180,6 +180,13 @@ impl ImageBlob {
pub fn is_proxy(&self) -> bool {
self.proxy.as_ref().is_some_and(|p| Arc::ptr_eq(p, &self.bytes))
}
/// Does a colorize paint ([`crate::NodeExtras::colorize`]) show on these bytes? Not while
/// they are a preview whose pixels aren't grayscale, such as [`missing_placeholder`]: until
/// the file is relinked the image shows the preview as it is, and keeps the paint.
pub fn takes_colorize(&self) -> bool {
!self.is_proxy() || self.gray_source().is_some()
}
}
/// The side of [`missing_placeholder`]'s image, in pixels.
+7 -2
View File
@@ -151,11 +151,16 @@ impl FlattenOptions {
/// Whether colorized image `n`, art of `d`, counts as transparency by itself. With
/// [`Self::keeps_colorize`], an image colorized with a solid paint whose pixels are grayscale
/// or 1-bit doesn't: the EPS writer draws it in that paint.
/// or 1-bit doesn't: the EPS writer draws it in that paint. An image whose pixels don't take
/// the paint ([`vectorcraft_doc::ImageBlob::takes_colorize`]) never counts: it draws uncolorized.
pub(crate) fn counts_colorize(&self, d: &Document, n: &Node) -> bool {
let NodeKind::Image(im) = &n.kind else { return true };
let blob = d.image_blob(im);
if blob.is_some_and(|b| !b.takes_colorize()) {
return false;
}
let solid = matches!(n.extras().colorize.as_deref(), Some(Paint::Solid { .. }));
!self.keeps_colorize || !solid || d.image_blob(im).is_some_and(|b| b.gray_source().is_none())
!self.keeps_colorize || !solid || blob.is_some_and(|b| b.gray_source().is_none())
}
/// `n` shows an image whose see-through pixels count as transparency.
+21
View File
@@ -411,6 +411,27 @@ fn eps_export_still_flattens_a_colorize_paint_on_a_color_image() {
assert!(out["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("transparency is flattened")), "{}", out["warnings"]);
}
/// A linked image showing the missing-link placeholder draws uncolorized, so its colorize paint
/// doesn't flatten it in EPS or PDF 1.3 export.
#[test]
fn a_colorized_missing_link_placeholder_is_not_flattened() {
let mut s = session();
let bytes = std::sync::Arc::new(vectorcraft_doc::links::missing_placeholder().unwrap());
let id = image(&mut s, "pic", ImageBlob { mime: "image/png".into(), bytes: bytes.clone(), proxy: Some(bytes) });
s.edit("Colorize", |d, _| {
d.node_mut(id).unwrap().extras_mut().colorize = Some(Box::new(Paint::solid(Color::rgb8(255, 0, 0))));
Ok(())
})
.unwrap();
let flattened =
|out: &serde_json::Value| out["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("transparency is flattened"));
let eps = s.execute("document.exportEps", &json!({"previewFormat": "none", "useArtboards": false})).unwrap();
assert!(!flattened(&eps), "{}", eps["warnings"]);
let pdf = s.execute("document.exportPdf", &json!({"compatibility": "1.3"})).unwrap();
assert!(!flattened(&pdf), "{}", pdf["warnings"]);
assert_eq!(colorize(&s, id), Some(Paint::solid(Color::rgb8(255, 0, 0))), "the image keeps its paint");
}
#[test]
fn flattened_colorized_image_shows_the_art_under_white_pixels() {
let mut s = session();
+1 -1
View File
@@ -196,7 +196,7 @@ impl Exporter<'_> {
}
NodeKind::Text(t) => self.text(s, n, t, page),
NodeKind::Image(im) => match n.extras().colorize.as_deref() {
Some(p @ Paint::Solid { .. }) => {
Some(p @ Paint::Solid { .. }) if self.doc.image_blob(im).is_none_or(vectorcraft_doc::ImageBlob::takes_colorize) => {
if writable(s, im.xf, Rect::new(0.0, 0.0, im.width.max(1).into(), im.height.max(1).into())) {
if let Some(colorized) = self.colorized_image(im, p, !n.extras().pixelated) {
self.image(s, im, Some(colorized), n.extras().pixelated);
+12 -5
View File
@@ -9,7 +9,7 @@ use vectorcraft_color::cms::{self, Cms, Model, ProfileKind};
use vectorcraft_color::swatch::REGISTRATION;
use vectorcraft_doc::inks::{Link, inks, plates, visit_node_colors};
use vectorcraft_doc::overprint::darken_overprints;
use vectorcraft_doc::{ColorMode, Document, Node, NodeKind};
use vectorcraft_doc::{ColorMode, Document, ImageBlob, Node, NodeKind};
use super::{DEFAULT_FREQUENCY, PrintInk, PrintSettings};
@@ -120,7 +120,7 @@ impl<'a> Separator<'a> {
for n in nodes {
darken_overprints(n, discard_white);
self.node(plate, Arc::make_mut(n));
keep_colorized_pixels(n, &mut colorized);
keep_colorized_pixels(n, &d.images, &mut colorized);
}
// A colorized image's pixels say where its color prints, on every plate: they are kept
// under their own key, and only the color is separated.
@@ -153,13 +153,20 @@ fn colorized_key(key: &str) -> String {
}
/// Point the colorized images in `n`'s subtree at [`colorized_key`], collecting their keys in
/// `keys`.
fn keep_colorized_pixels(n: &mut Arc<Node>, keys: &mut BTreeSet<String>) {
/// `keys`. An image whose pixels in `images` don't take its colorize paint
/// ([`ImageBlob::takes_colorize`]) loses the paint instead, and is separated as the image it shows.
fn keep_colorized_pixels(n: &mut Arc<Node>, images: &BTreeMap<String, ImageBlob>, keys: &mut BTreeSet<String>) {
let colorized = |n: &Node| matches!(n.kind, NodeKind::Image(ref im) if im.pixels.is_none()) && n.extras().colorize.is_some();
if !colorized(n) && n.children().is_none_or(|ch| ch.is_empty()) {
return;
}
let node = Arc::make_mut(n);
if colorized(node)
&& let NodeKind::Image(im) = &node.kind
&& images.get(&im.key).is_some_and(|b| !b.takes_colorize())
{
node.extras_mut().colorize = None;
}
if colorized(node)
&& let NodeKind::Image(im) = &mut node.kind
{
@@ -168,7 +175,7 @@ fn keep_colorized_pixels(n: &mut Arc<Node>, keys: &mut BTreeSet<String>) {
}
if let Some(ch) = node.children_mut() {
for c in ch.iter_mut() {
keep_colorized_pixels(c, keys);
keep_colorized_pixels(c, images, keys);
}
}
}
+28
View File
@@ -166,6 +166,34 @@ fn separations_print_the_image_on_its_ink_plate() {
}
}
/// A linked image showing the missing-link placeholder is written in the placeholder's own colors
/// though it keeps a colorize paint for its file, and printed separations separate it as an
/// uncolorized image.
#[test]
fn a_colorized_missing_link_placeholder_is_written_in_its_own_colors() {
let placeholder = || {
let mut d = doc(ColorMode::Cmyk, None);
let bytes = Arc::new(vectorcraft_doc::links::missing_placeholder().unwrap());
d.images.insert("pic".into(), ImageBlob { mime: "image/png".into(), bytes: bytes.clone(), proxy: Some(bytes) });
d
};
let plain = placeholder();
let mut d = placeholder();
let id = d.layers[0].children().unwrap()[0].id;
d.node_mut(id).unwrap().extras_mut().colorize = Some(Box::new(Paint::solid(Color::cmyk(1.0, 0.0, 0.0, 0.0))));
let (colorized, uncolorized) = (images(&pdf(&d)), images(&pdf(&plain)));
assert!(!colorized.is_empty());
assert_eq!(colorized, uncolorized, "the placeholder as it is, without a soft mask from its coverage");
let settings: PrintSettings = serde_json::from_value(serde_json::json!({"output": {"mode": "separations"}})).unwrap();
let job = plan(&d, &PrintOptions { settings, ..Default::default() }).unwrap();
assert!(!job.plates.is_empty());
for p in &job.plates {
let n = &p.layers[0].children().unwrap()[0];
let NodeKind::Image(im) = &n.kind else { panic!("the image") };
assert_eq!((im.key.as_str(), n.extras().colorize.is_none()), ("pic", true), "separated as the image it shows");
}
}
/// A pixelated colorized image (an envelope's with Anti-Alias off) is written without
/// /Interpolate, as an uncolorized one is.
#[test]
+12 -6
View File
@@ -1,14 +1,20 @@
use super::{
Affine, Arc, BezPath, Document, FillRule, Frame, HashMap, Ink, Node, Pixmap, Rect, RenderContext, Renderer, Shape, blob_key, blob_pixmap,
fill_rule, image_mip, ink, lookup, mip_level, peniko, proof, vello_cpu,
Affine, Arc, BezPath, Document, FillRule, Frame, HashMap, Ink, Node, NodeKind, Pixmap, Rect, RenderContext, Renderer, Shape, blob_key,
blob_pixmap, fill_rule, image_mip, ink, lookup, mip_level, peniko, proof, vello_cpu,
};
use crate::images;
/// The color image node `n` is colorized with ([`vectorcraft_doc::NodeExtras::colorize`]), if it
/// is, as `ink` paints it: the display color, or on an ink plane the color's own inks, as a fill
/// of that color paints them.
pub(super) fn image_tint(n: &Node, ink: Ink) -> Option<[u8; 3]> {
/// The color image node `n` of `doc` is colorized with ([`vectorcraft_doc::NodeExtras::colorize`]),
/// if it is and its pixels take it ([`vectorcraft_doc::ImageBlob::takes_colorize`]), as `ink`
/// paints it: the display color, or on an ink plane the color's own inks, as a fill of that color
/// paints them.
pub(super) fn image_tint(doc: &Document, n: &Node, ink: Ink) -> Option<[u8; 3]> {
let c = n.extras().colorize.as_deref()?.color()?;
if let NodeKind::Image(im) = &n.kind
&& doc.image_blob(im).is_some_and(|b| !b.takes_colorize())
{
return None;
}
Some(ink.rgb(&c).map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8))
}
+3 -3
View File
@@ -218,7 +218,7 @@ impl Renderer {
&& let Some(region) = self.clip_of(&m.art)
&& shape_masked_image(a, im, &region.0)
{
self.draw_image_in(ctx, f, im, Some(&region), images::image_tint(a, f.ink), a.extras().pixelated);
self.draw_image_in(ctx, f, im, Some(&region), images::image_tint(f.doc, a, f.ink), a.extras().pixelated);
self.stats.drawn += 1;
return;
}
@@ -486,7 +486,7 @@ impl Renderer {
&& let NodeKind::Image(im) = &image.kind
&& plain_image(image, im, &region.0)
{
self.draw_image_in(ctx, f, im, Some(&region), images::image_tint(image, f.ink), image.extras().pixelated);
self.draw_image_in(ctx, f, im, Some(&region), images::image_tint(f.doc, image, f.ink), image.extras().pixelated);
self.stats.drawn += 1;
return;
}
@@ -536,7 +536,7 @@ impl Renderer {
}
}
NodeKind::Text(t) => self.draw_text(ctx, f, n, t),
NodeKind::Image(im) => self.draw_image_in(ctx, f, im, None, images::image_tint(n, f.ink), n.extras().pixelated),
NodeKind::Image(im) => self.draw_image_in(ctx, f, im, None, images::image_tint(f.doc, n, f.ink), n.extras().pixelated),
NodeKind::SymbolInstance { symbol, xf } => {
if let Some(sym) = f.doc.symbols.iter().find(|s| &s.name == symbol) {
// The instance draws when the frame has room for its art's objects; a symbol
+21
View File
@@ -142,6 +142,27 @@ fn colorize_dims_with_the_grays() {
assert!(px[1].abs_diff(128) <= 3 && px[2].abs_diff(127) <= 3 && px[0] <= 2, "got {px:?}");
}
/// A linked image showing the missing-link placeholder draws it in its own colors though it keeps
/// a colorize paint for its file; a grayscale preview still draws colorized.
#[test]
fn a_colorized_missing_link_placeholder_draws_in_its_own_colors() {
let proxy = |bytes: Vec<u8>| {
let bytes = std::sync::Arc::new(bytes);
ImageBlob { mime: "image/png".into(), bytes: bytes.clone(), proxy: Some(bytes) }
};
let blue = Paint::solid(Color::rgb(0.0, 0.0, 1.0));
let sharp = RenderOptions { smooth_images: false, ..Default::default() };
let placeholder = proxy(vectorcraft_doc::links::missing_placeholder().unwrap());
// 64 pixels at 0.5 pt: (16, 5) is the light tile, (5, 5) the cross.
let d = doc_of(placeholder, Some(blue.clone()), (64, 64), 0.5);
let out = Renderer::new().render(&d, 40, 20, Affine::IDENTITY, &sharp);
assert!(near(out.pixel(16, 5), [236, 236, 236, 255], 2), "the light tile, got {:?}", out.pixel(16, 5));
assert!(near(out.pixel(5, 5), [200, 60, 60, 255], 2), "the red cross, got {:?}", out.pixel(5, 5));
let d = doc(proxy(gray_png()), Some(blue));
let px = Renderer::new().render(&d, 40, 20, Affine::IDENTITY, &sharp).pixel(10, 10);
assert!(px[2] >= 250 && px[0] <= 5 && px[1] <= 5, "a grayscale preview takes the color, got {px:?}");
}
/// Separations Preview shows a spot-colorized image on its ink's plate only.
#[test]
fn separations_preview_shows_the_image_on_its_spot_plate() {