diff --git a/crates/doc/src/links.rs b/crates/doc/src/links.rs index 08121b566..781e61e68 100644 --- a/crates/doc/src/links.rs +++ b/crates/doc/src/links.rs @@ -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. diff --git a/crates/engine/src/cmd/flatten/options.rs b/crates/engine/src/cmd/flatten/options.rs index d4f18f385..52a900289 100644 --- a/crates/engine/src/cmd/flatten/options.rs +++ b/crates/engine/src/cmd/flatten/options.rs @@ -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. diff --git a/crates/engine/src/tests/colorize.rs b/crates/engine/src/tests/colorize.rs index bcd590c2d..2dab48c9f 100644 --- a/crates/engine/src/tests/colorize.rs +++ b/crates/engine/src/tests/colorize.rs @@ -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(); diff --git a/crates/pdf/src/export/nodes.rs b/crates/pdf/src/export/nodes.rs index 80a95ad16..389552948 100644 --- a/crates/pdf/src/export/nodes.rs +++ b/crates/pdf/src/export/nodes.rs @@ -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); diff --git a/crates/pdf/src/print/plates.rs b/crates/pdf/src/print/plates.rs index 7c8499642..a46d1137a 100644 --- a/crates/pdf/src/print/plates.rs +++ b/crates/pdf/src/print/plates.rs @@ -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, keys: &mut BTreeSet) { +/// `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, images: &BTreeMap, keys: &mut BTreeSet) { 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, keys: &mut BTreeSet) { } if let Some(ch) = node.children_mut() { for c in ch.iter_mut() { - keep_colorized_pixels(c, keys); + keep_colorized_pixels(c, images, keys); } } } diff --git a/crates/pdf/src/tests/colorize.rs b/crates/pdf/src/tests/colorize.rs index 406ae08b8..5ec2e813a 100644 --- a/crates/pdf/src/tests/colorize.rs +++ b/crates/pdf/src/tests/colorize.rs @@ -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] diff --git a/crates/render/src/renderer/images.rs b/crates/render/src/renderer/images.rs index 6cabfc5bf..15997e797 100644 --- a/crates/render/src/renderer/images.rs +++ b/crates/render/src/renderer/images.rs @@ -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)) } diff --git a/crates/render/src/renderer/nodes.rs b/crates/render/src/renderer/nodes.rs index b69ecb10f..176739c31 100644 --- a/crates/render/src/renderer/nodes.rs +++ b/crates/render/src/renderer/nodes.rs @@ -218,7 +218,7 @@ impl Renderer { && let Some(region) = self.clip_of(&m.art) && shape_masked_image(a, im, ®ion.0) { - self.draw_image_in(ctx, f, im, Some(®ion), images::image_tint(a, f.ink), a.extras().pixelated); + self.draw_image_in(ctx, f, im, Some(®ion), 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, ®ion.0) { - self.draw_image_in(ctx, f, im, Some(®ion), images::image_tint(image, f.ink), image.extras().pixelated); + self.draw_image_in(ctx, f, im, Some(®ion), 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 diff --git a/crates/render/src/tests/colorize.rs b/crates/render/src/tests/colorize.rs index 15eb3c5a0..49e14e5e6 100644 --- a/crates/render/src/tests/colorize.rs +++ b/crates/render/src/tests/colorize.rs @@ -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| { + 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() {