mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 05:22:22 +00:00
Images: a colorized image's missing-link placeholder shows in its own colors
A linked 1-bit image mask from a .ai file keeps its fill color as a colorize paint, so the image takes the color once its file is relinked. Until then the image shows the missing-link placeholder, and the canvas and PDF export colorized that placeholder too: its light tile turned nearly transparent and its red cross took the fill color. ImageBlob::takes_colorize is false for a preview whose pixels are not grayscale. While it is false, the canvas draws the image and PDF export writes it as it is, printed separations separate it as an uncolorized image, and transparency flattening (EPS, PDF 1.3 and PDF/X-1a export, Flatten Transparency) does not count its paint as transparency. The paint stays on the image, and a grayscale preview is still colorized. SVG and EPS export already write only grayscale images colorized.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user