mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 08:56:08 +00:00
Merge pull request #6881 from dotCooCoo/feat/image-color-mask
Image Tools: Color Mask hides chosen colors of an image without changing its pixels
This commit is contained in:
@@ -672,7 +672,7 @@ impl<'a> Scene<'a> {
|
||||
self.shape(n, &bp, *rule);
|
||||
}
|
||||
NodeKind::Text(t) => self.text(n, t),
|
||||
NodeKind::Image(im) => self.image(im),
|
||||
NodeKind::Image(im) => self.image(&self.doc.shown_image(n, im)),
|
||||
NodeKind::SymbolInstance { symbol, xf } => {
|
||||
let doc = self.doc;
|
||||
if self.symbol_nodes >= vectorcraft_doc::MAX_SYMBOL_NODES {
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
//! Image Tools › Color Mask: colors of an image object that are hidden while its pixels stay as
|
||||
//! they are.
|
||||
//!
|
||||
//! A masked image is an ordinary image ([`NodeKind::Image`]) with a [`ColorMask`] in its extras
|
||||
//! ([`crate::node::NodeExtras::color_mask`]). The record holds the mask's settings and the key of
|
||||
//! the image's pixels with the hidden colors made clear ([`ColorMask::key`]), made by the engine
|
||||
//! from the pixels named by [`ColorMask::from`]. The image's own key keeps naming its own pixels,
|
||||
//! so removing the mask gives them back. While the image's key is [`ColorMask::from`], it is drawn
|
||||
//! and output with the masked pixels ([`Document::shown_image`]); when another key replaced it
|
||||
//! (a link updated, the image cropped), it shows its own pixels until the engine makes the masked
|
||||
//! pixels again.
|
||||
//!
|
||||
//! The concept is that of PDF's color key masking (ISO 32000-1, 8.9.6.4): samples in the chosen
|
||||
//! colors are not painted and what is behind the image shows through. Which colors match, and how
|
||||
//! near ones fade out, are VectorCraft's own rules.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Document, ImageObject, Node, NodeKind};
|
||||
|
||||
/// Where a color mask's colors come from.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum MaskBackground {
|
||||
/// The most common color along the image's edges.
|
||||
#[default]
|
||||
Edges,
|
||||
/// The average of the image's corner pixels that are not clear.
|
||||
Corners,
|
||||
/// The colors in [`ColorMask::colors`].
|
||||
Colors,
|
||||
}
|
||||
|
||||
/// How near a color must be to a masked one.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum MaskMatch {
|
||||
/// The RGB distance, as a percentage of the largest one.
|
||||
#[default]
|
||||
Distance,
|
||||
/// The largest difference on one of red, green and blue, as a percentage of a channel's range.
|
||||
Channels,
|
||||
}
|
||||
|
||||
/// The colors an image object hides (see the module docs).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ColorMask {
|
||||
/// Key into [`Document::images`]: the pixels of [`Self::from`] with the hidden colors made
|
||||
/// clear. Named `key` in the file, so the file keeps those pixels with the document.
|
||||
pub key: String,
|
||||
/// The image's own key when the masked pixels were made.
|
||||
pub from: String,
|
||||
#[serde(default)]
|
||||
pub background: MaskBackground,
|
||||
/// The hidden colors (8-bit RGB) with [`MaskBackground::Colors`].
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub colors: Vec<[u8; 3]>,
|
||||
/// Only the matching pixels that reach in from an edge are hidden. A record without it reads
|
||||
/// as `true`, the command's default.
|
||||
#[serde(default = "default_contiguous")]
|
||||
pub contiguous: bool,
|
||||
#[serde(default, rename = "match")]
|
||||
pub measure: MaskMatch,
|
||||
/// How near a color must be to be hidden, 0 to 100 (see [`MaskMatch`]). A record without it
|
||||
/// reads as 10, the command's default.
|
||||
#[serde(default = "default_percent")]
|
||||
pub tolerance: f64,
|
||||
/// The band past the tolerance over which pixels fade from hidden to shown, 0 to 100. A record
|
||||
/// without it reads as 10, the command's default.
|
||||
#[serde(default = "default_percent")]
|
||||
pub softness: f64,
|
||||
}
|
||||
|
||||
fn default_contiguous() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_percent() -> f64 {
|
||||
10.0
|
||||
}
|
||||
|
||||
impl ColorMask {
|
||||
/// Whether the masked pixels were made from the pixels of `im`.
|
||||
pub fn made_for(&self, im: &ImageObject) -> bool {
|
||||
im.pixels.is_none() && self.from == im.key
|
||||
}
|
||||
}
|
||||
|
||||
impl Document {
|
||||
/// Image `im` of node `n` as it is drawn and output. With a color mask whose masked pixels were
|
||||
/// made from the image's own pixels and are in this document ([`ColorMask::made_for`]): a copy
|
||||
/// that shows those pixels and links to no file, so an output that refers to a linked file
|
||||
/// writes the masked pixels instead. Otherwise, and for a mask that hides nothing (its masked
|
||||
/// pixels are the image's own), `im` itself.
|
||||
pub fn shown_image<'a>(&self, n: &Node, im: &'a ImageObject) -> Cow<'a, ImageObject> {
|
||||
match n.extras().color_mask.as_deref() {
|
||||
Some(m) if m.key != im.key && m.made_for(im) && self.images.contains_key(&m.key) => {
|
||||
Cow::Owned(ImageObject { key: m.key.clone(), link: None, ..im.clone() })
|
||||
}
|
||||
_ => Cow::Borrowed(im),
|
||||
}
|
||||
}
|
||||
|
||||
/// The color mask of image node `n`, if it has one.
|
||||
pub fn color_mask_of(n: &Node) -> Option<&ColorMask> {
|
||||
matches!(n.kind, NodeKind::Image(_)).then(|| n.extras().color_mask.as_deref()).flatten()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_geom::Affine;
|
||||
|
||||
use super::*;
|
||||
use crate::{ImageBlob, NodeId};
|
||||
|
||||
fn image(key: &str) -> ImageObject {
|
||||
ImageObject { key: key.into(), width: 2, height: 2, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None }
|
||||
}
|
||||
|
||||
fn mask(key: &str, from: &str) -> ColorMask {
|
||||
ColorMask {
|
||||
key: key.into(),
|
||||
from: from.into(),
|
||||
background: MaskBackground::Colors,
|
||||
colors: vec![[255, 255, 255]],
|
||||
contiguous: false,
|
||||
measure: MaskMatch::Distance,
|
||||
tolerance: 10.0,
|
||||
softness: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_masked_image_shows_its_masked_pixels_while_they_were_made_from_its_own() {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
d.images.insert("own".into(), ImageBlob::png(vec![1]));
|
||||
d.images.insert("masked".into(), ImageBlob::png(vec![2]));
|
||||
let mut n = Node::new(NodeId(5), NodeKind::Image(Box::new(image("own"))));
|
||||
let NodeKind::Image(im) = &n.kind else { unreachable_kind() };
|
||||
assert_eq!(d.shown_image(&n, im).key, "own", "no mask: its own pixels");
|
||||
n.extras_mut().color_mask = Some(Box::new(mask("masked", "own")));
|
||||
let NodeKind::Image(im) = &n.kind else { unreachable_kind() };
|
||||
assert_eq!(d.shown_image(&n, im).key, "masked");
|
||||
// Linked: the shown copy links to nothing, so outputs write the masked pixels.
|
||||
let mut linked = (**im).clone();
|
||||
linked.link = Some(crate::LinkInfo::new("/a.png"));
|
||||
assert_eq!(d.shown_image(&n, &linked).link, None);
|
||||
// Made from other pixels: its own until they are made again.
|
||||
assert_eq!(d.shown_image(&n, &image("other")).key, "other");
|
||||
// Masked pixels the document doesn't have: its own.
|
||||
n.extras_mut().color_mask = Some(Box::new(mask("gone", "own")));
|
||||
let NodeKind::Image(im) = &n.kind else { unreachable_kind() };
|
||||
assert_eq!(d.shown_image(&n, im).key, "own");
|
||||
// Pixels an image holds itself are drawn as they are.
|
||||
let own = ImageObject { pixels: Some(Arc::new(ImageBlob::png(vec![3]))), ..image("own") };
|
||||
n.extras_mut().color_mask = Some(Box::new(mask("masked", "own")));
|
||||
assert_eq!(d.shown_image(&n, &own).key, "own");
|
||||
// A mask that hides nothing (its masked pixels are the image's own): the image itself, so
|
||||
// a linked image stays linked in outputs.
|
||||
n.extras_mut().color_mask = Some(Box::new(mask("own", "own")));
|
||||
assert_eq!(d.shown_image(&n, &linked).link, linked.link);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mask_saves_and_reads_back_with_its_node() {
|
||||
let mut n = Node::new(NodeId(5), NodeKind::Image(Box::new(image("own"))));
|
||||
n.extras_mut().color_mask = Some(Box::new(mask("masked", "own")));
|
||||
let json = serde_json::to_value(&n).unwrap();
|
||||
assert_eq!(json["color_mask"]["key"], "masked", "the masked pixels' key is a `key` member: {json}");
|
||||
assert_eq!(json["color_mask"]["match"], "distance");
|
||||
let back: Node = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(back, n);
|
||||
// A node without one writes none.
|
||||
let plain = Node::new(NodeId(6), NodeKind::Image(Box::new(image("own"))));
|
||||
assert!(serde_json::to_value(&plain).unwrap().get("color_mask").is_none());
|
||||
}
|
||||
|
||||
/// A record that leaves out settings reads them as the command's defaults: edge color,
|
||||
/// contiguous, distance, tolerance and softness 10.
|
||||
#[test]
|
||||
fn a_record_without_settings_reads_the_defaults() {
|
||||
let m: ColorMask = serde_json::from_value(serde_json::json!({"key": "masked", "from": "own"})).unwrap();
|
||||
assert_eq!(
|
||||
(m.background, m.colors.len(), m.contiguous, m.measure, m.tolerance, m.softness),
|
||||
(MaskBackground::Edges, 0, true, MaskMatch::Distance, 10.0, 10.0)
|
||||
);
|
||||
}
|
||||
|
||||
fn unreachable_kind() -> ! {
|
||||
panic!("an image node")
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ pub mod blend;
|
||||
pub mod cancel;
|
||||
pub mod clipnest;
|
||||
pub mod cmyk;
|
||||
pub mod color_mask;
|
||||
pub mod compound_shape;
|
||||
pub mod corners;
|
||||
pub mod depth;
|
||||
|
||||
@@ -206,12 +206,14 @@ pub fn mesh_warp_cost(cells: usize) -> usize {
|
||||
/// that takes the triangle where `f` takes its corners (with `masked`, the image masked by the
|
||||
/// triangle instead; with `pixelated`, drawn without smoothing). Each piece reaches a little past
|
||||
/// its edges (at most half a point) so neighbors overlap and no seams show. Each piece keeps the
|
||||
/// color `n` colorizes a grayscale image with. The result is a group with `n`'s id, name,
|
||||
/// color `n` colorizes a grayscale image with, and `n`'s color mask. The result is a group with `n`'s id, name,
|
||||
/// transparency, opacity mask and appearance.
|
||||
pub fn mesh_warp_image(n: &Node, im: &ImageObject, f: &dyn Fn(Point) -> Point, cells: usize, pixelated: bool, masked: bool) -> Node {
|
||||
let k = cells.max(1);
|
||||
// A grayscale image's fill color colorizes each piece; the group of pieces takes none.
|
||||
let colorize = n.extras().colorize.clone();
|
||||
// Each piece shows what the image shows: its color mask's masked pixels.
|
||||
let color_mask = n.extras().color_mask.clone();
|
||||
let (pw, ph) = (im.width as f64, im.height as f64);
|
||||
let at = |i: usize, j: usize| Point::new(pw * i as f64 / k as f64, ph * j as f64 / k as f64);
|
||||
let mut pieces = Vec::with_capacity(2 * k * k);
|
||||
@@ -236,6 +238,7 @@ pub fn mesh_warp_image(n: &Node, im: &ImageObject, f: &dyn Fn(Point) -> Point, c
|
||||
let mut image = Node::new(n.id, NodeKind::Image(Box::new(ImageObject { xf, ..im.clone() })));
|
||||
image.extras_mut().pixelated = pixelated;
|
||||
image.extras_mut().colorize = colorize.clone();
|
||||
image.extras_mut().color_mask = color_mask.clone();
|
||||
if masked {
|
||||
// Preserve Shape Using Transparency: the triangle is the piece's alpha (a
|
||||
// white opacity mask) instead of its clipping path.
|
||||
|
||||
@@ -261,6 +261,10 @@ fn node_bytes(n: &Node, other: Option<&Node>, counted: &mut HashSet<usize>) -> u
|
||||
b += ALLOC + size_of::<NodeExtras>();
|
||||
b += boxed(&e.mask) + boxed(&e.graph) + boxed(&e.shaper) + boxed(&e.attrs) + boxed(&e.slice) + boxed(&e.perspective);
|
||||
b += boxed(&e.dimension) + boxed(&e.live_paint_gaps) + boxed(&e.colorize);
|
||||
b += e
|
||||
.color_mask
|
||||
.as_deref()
|
||||
.map_or(0, |m| ALLOC + size_of::<crate::color_mask::ColorMask>() + m.key.capacity() + m.from.capacity() + m.colors.capacity() * 3);
|
||||
b += e.trace.as_ref().map_or(0, |t| ALLOC + size_of::<Value>() + json_bytes(t));
|
||||
b += e.intertwine.as_deref().map_or(0, |a| ALLOC + size_of::<PathData>() + path_bytes(a));
|
||||
if e.intertwine_tiers.capacity() > 0 {
|
||||
|
||||
@@ -72,6 +72,9 @@ pub struct NodeExtras {
|
||||
/// Transform panel › Align to Pixel Grid: the path's or compound path's horizontal and vertical
|
||||
/// segments stay on the pixel grid when it is transformed.
|
||||
pub pixel_aligned: bool,
|
||||
/// Image Tools › Color Mask: the colors an image hides while its pixels stay as they are
|
||||
/// ([`crate::color_mask`]).
|
||||
pub color_mask: Option<Box<crate::color_mask::ColorMask>>,
|
||||
}
|
||||
|
||||
/// No extras: what [`Node::extras`] gives for a node without any.
|
||||
@@ -99,6 +102,7 @@ pub(super) static NO_EXTRAS: NodeExtras = NodeExtras {
|
||||
symbol_overrides: Vec::new(),
|
||||
envelope_edit_margin: None,
|
||||
pixel_aligned: false,
|
||||
color_mask: None,
|
||||
};
|
||||
|
||||
/// A node's [`NodeExtras`], if it has any ([`Node::extras`]). Extras that are all unset equal
|
||||
|
||||
@@ -55,6 +55,7 @@ impl Serialize for Node {
|
||||
e.symbol_overrides.is_empty(),
|
||||
e.envelope_edit_margin.is_none(),
|
||||
is_default(&e.pixel_aligned),
|
||||
e.color_mask.is_none(),
|
||||
];
|
||||
let mut st = s.serialize_struct("Node", skips.iter().filter(|s| !**s).count())?;
|
||||
let mut skip = skips.into_iter();
|
||||
@@ -98,6 +99,7 @@ impl Serialize for Node {
|
||||
field!("symbol_overrides", &e.symbol_overrides);
|
||||
field!("envelope_edit_margin", &e.envelope_edit_margin);
|
||||
field!("pixel_aligned", &e.pixel_aligned);
|
||||
field!("color_mask", &e.color_mask);
|
||||
st.end()
|
||||
}
|
||||
}
|
||||
@@ -241,6 +243,8 @@ pub(super) struct NodeWire {
|
||||
envelope_edit_margin: Option<[f32; 4]>,
|
||||
#[serde(default)]
|
||||
pixel_aligned: bool,
|
||||
#[serde(default)]
|
||||
color_mask: Option<Box<crate::color_mask::ColorMask>>,
|
||||
}
|
||||
|
||||
impl From<NodeWire> for Node {
|
||||
@@ -269,6 +273,7 @@ impl From<NodeWire> for Node {
|
||||
symbol_overrides: w.symbol_overrides,
|
||||
envelope_edit_margin: w.envelope_edit_margin,
|
||||
pixel_aligned: w.pixel_aligned,
|
||||
color_mask: w.color_mask,
|
||||
};
|
||||
Node {
|
||||
id: w.id,
|
||||
|
||||
@@ -517,7 +517,12 @@ fn rename(n: &mut Node, prefix: &str) {
|
||||
paint(&mut r.style.stroke);
|
||||
}
|
||||
}
|
||||
NodeKind::Image(im) => im.key = format!("{prefix}{}", im.key),
|
||||
NodeKind::Image(im) => {
|
||||
im.key = format!("{prefix}{}", im.key);
|
||||
if let Some(m) = n.extras.0.as_deref_mut().and_then(|e| e.color_mask.as_deref_mut()) {
|
||||
(m.key, m.from) = (format!("{prefix}{}", m.key), format!("{prefix}{}", m.from));
|
||||
}
|
||||
}
|
||||
NodeKind::PlacedDocument(p) => p.key = format!("{prefix}{}", p.key),
|
||||
NodeKind::SymbolInstance { symbol, .. } => *symbol = format!("{prefix}{symbol}"),
|
||||
_ => {}
|
||||
@@ -681,6 +686,30 @@ mod tests {
|
||||
Some((d, Node::new(NodeId(0), NodeKind::Group { children, clip: false }), Rect::new(0.0, 0.0, 40.0, 20.0)))
|
||||
}
|
||||
|
||||
/// A color-masked image in a placed document's art keeps its masked pixels under the prefixed
|
||||
/// names its resources get.
|
||||
#[test]
|
||||
fn a_color_mask_takes_the_prefixed_image_names() {
|
||||
let im =
|
||||
ImageObject { key: "own".into(), width: 2, height: 2, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
|
||||
let mut n = Node::new(NodeId(7), NodeKind::Image(Box::new(im)));
|
||||
n.extras_mut().color_mask = Some(Box::new(crate::color_mask::ColorMask {
|
||||
key: "masked".into(),
|
||||
from: "own".into(),
|
||||
background: Default::default(),
|
||||
colors: vec![],
|
||||
contiguous: true,
|
||||
measure: Default::default(),
|
||||
tolerance: 10.0,
|
||||
softness: 10.0,
|
||||
}));
|
||||
rename(&mut n, "p:");
|
||||
let NodeKind::Image(im) = &n.kind else { panic!("an image") };
|
||||
let m = Document::color_mask_of(&n).unwrap();
|
||||
assert_eq!((im.key.as_str(), m.key.as_str(), m.from.as_str()), ("p:own", "p:masked", "p:own"));
|
||||
assert!(m.made_for(im));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trips_through_json() {
|
||||
let n = Node::new(NodeId(3), NodeKind::PlacedDocument(Box::new(placed("doc1-1"))));
|
||||
|
||||
@@ -435,11 +435,19 @@ impl ColorVisitor for Live<'_, '_> {
|
||||
/// adjustments of an object `n` is the art of) on everything it paints. The adjustment effects
|
||||
/// are gone from the result; embedded images take the keys `images` gives. `swatches` tells the
|
||||
/// document's global and spot swatches apart ([`crate::addons::duotone::swatch_kind`]).
|
||||
pub fn adjust(n: &Node, outer: Option<&ColorMap>, swatches: &SwatchKinds, images: &mut ImageHook<'_>) -> Option<Node> {
|
||||
/// `has_image` tells whether the document holds an image: an image whose color mask's pixels are
|
||||
/// missing is recoloured from its own pixels, as it is drawn without the adjustment.
|
||||
pub fn adjust(
|
||||
n: &Node,
|
||||
outer: Option<&ColorMap>,
|
||||
swatches: &SwatchKinds,
|
||||
images: &mut ImageHook<'_>,
|
||||
has_image: &dyn Fn(&str) -> bool,
|
||||
) -> Option<Node> {
|
||||
let mut m: Option<Node> = None;
|
||||
if let Some(ch) = n.children() {
|
||||
for (i, c) in ch.iter().enumerate() {
|
||||
if let Some(new) = adjust(c, None, swatches, images)
|
||||
if let Some(new) = adjust(c, None, swatches, images, has_image)
|
||||
&& let Some(slot) = m.get_or_insert_with(|| n.clone()).children_mut().and_then(|ch| ch.get_mut(i))
|
||||
{
|
||||
*slot = Arc::new(new);
|
||||
@@ -462,11 +470,57 @@ pub fn adjust(n: &Node, outer: Option<&ColorMap>, swatches: &SwatchKinds, images
|
||||
if let Some(map) = map {
|
||||
let mm = m.get_or_insert_with(|| n.clone());
|
||||
mm.appearance.effects.retain(|e| !is_adjustment(&e.id));
|
||||
show_color_masks(mm, has_image);
|
||||
recolor_node(mm, Reach::ALL, &mut Live { map: &map, swatches, images });
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
/// Whether `n` is an image that [`recolor_node`] recolours (embedded, not colorized, without
|
||||
/// pixels of its own) with a color mask made from its pixels.
|
||||
fn recoloured_with_mask(n: &Node) -> bool {
|
||||
match (&n.kind, Document::color_mask_of(n)) {
|
||||
(NodeKind::Image(im), Some(m)) => im.link.is_none() && n.extras().colorize.is_none() && m.made_for(im),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `n` is or holds an image [`recoloured_with_mask`] counts.
|
||||
fn holds_recoloured_with_mask(n: &Node) -> bool {
|
||||
recoloured_with_mask(n) || n.children().is_some_and(|ch| ch.iter().any(|c| holds_recoloured_with_mask(c)))
|
||||
}
|
||||
|
||||
/// Give each image in `n` that [`recoloured_with_mask`] counts its masked pixels as its own,
|
||||
/// without the mask, so [`recolor_node`] recolours the pixels it shows and the colors the mask
|
||||
/// hides stay hidden. An image whose masked pixels the document does not hold (`has_image`) is
|
||||
/// drawn from its own pixels ([`Document::shown_image`]): it keeps its key and loses the mask, so
|
||||
/// the recoloured copy is drawn the same way.
|
||||
fn show_color_masks(n: &mut Node, has_image: &dyn Fn(&str) -> bool) {
|
||||
if recoloured_with_mask(n)
|
||||
&& let Some(masked) = Document::color_mask_of(n).map(|m| m.key.clone())
|
||||
{
|
||||
if let NodeKind::Image(im) = &mut n.kind
|
||||
&& has_image(&masked)
|
||||
{
|
||||
im.key = masked;
|
||||
}
|
||||
n.extras_mut().color_mask = None;
|
||||
}
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
if holds_recoloured_with_mask(c) {
|
||||
show_color_masks(Arc::make_mut(c), has_image);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The keys of the masked pixels of the color masks in `n` and its members.
|
||||
fn color_mask_keys<'a>(n: &'a Node, out: &mut Vec<&'a str>) {
|
||||
out.extend(Document::color_mask_of(n).map(|m| m.key.as_str()));
|
||||
for c in n.children().into_iter().flatten() {
|
||||
color_mask_keys(c, out);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`adjust`] for export and Expand Appearance: recoloured embedded images become new images in
|
||||
/// `d` (named by their content), and a symbol instance stands for its symbol's art: `symbol_art`
|
||||
/// when given, otherwise the symbol's art with the instance's own appearance changes.
|
||||
@@ -482,13 +536,16 @@ pub fn adjust_in_document(d: &mut Document, n: &Node, symbol_art: Option<Node>)
|
||||
_ => n,
|
||||
};
|
||||
let kinds = crate::addons::duotone::swatch_kinds(d);
|
||||
let mut masked = vec![];
|
||||
color_mask_keys(src, &mut masked);
|
||||
masked.retain(|k| d.images.contains_key(*k));
|
||||
let mut hook = |key: &str, map: &ColorMap| -> Option<String> {
|
||||
let blob = d.images.get(key)?.map_rgb(|rgb| map.apply_rgb8(rgb))?;
|
||||
let k = blob.content_key();
|
||||
d.images.entry(k.clone()).or_insert(blob);
|
||||
Some(k)
|
||||
};
|
||||
adjust(src, None, &|name| kinds.get(name).copied(), &mut hook)
|
||||
adjust(src, None, &|name| kinds.get(name).copied(), &mut hook, &|key| masked.contains(&key))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -551,17 +608,17 @@ mod tests {
|
||||
let mut g = Node::group(NodeId(1), vec![leaf(2), leaf(3)]);
|
||||
g.appearance.effects.push(Effect { id: "adjust.hueSaturation".into(), params: json!({"hue": 120}), visible: true });
|
||||
assert!(has_adjustment(&g));
|
||||
let out = adjust(&g, None, &|_| None, &mut |_, _| None).unwrap();
|
||||
let out = adjust(&g, None, &|_| None, &mut |_, _| None, &|_| true).unwrap();
|
||||
assert!(out.appearance.effects.is_empty());
|
||||
for c in out.children().unwrap() {
|
||||
assert_eq!(c.appearance.fill_paint().color().unwrap().to_hex(), "#00ff00");
|
||||
}
|
||||
// A hidden effect does nothing; a stroke's own effect recolours just that stroke.
|
||||
g.appearance.effects[0].visible = false;
|
||||
assert!(!has_adjustment(&g) && adjust(&g, None, &|_| None, &mut |_, _| None).is_none());
|
||||
assert!(!has_adjustment(&g) && adjust(&g, None, &|_| None, &mut |_, _| None, &|_| true).is_none());
|
||||
let mut p = Node::path(NodeId(5), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art());
|
||||
p.appearance.items[1].effects_mut().push(Effect { id: "adjust.levels".into(), params: json!({"outputBlack": 255}), visible: true });
|
||||
let out = adjust(&p, None, &|_| None, &mut |_, _| None).unwrap();
|
||||
let out = adjust(&p, None, &|_| None, &mut |_, _| None, &|_| true).unwrap();
|
||||
assert_eq!(out.appearance.items[1].paint().color().unwrap().to_hex(), "#ffffff");
|
||||
assert_eq!(out.appearance.items[0].paint(), p.appearance.items[0].paint());
|
||||
assert!(out.appearance.items[1].effects().is_empty());
|
||||
@@ -576,7 +633,7 @@ mod tests {
|
||||
let fill = Appearance::basic(Paint::Gradient(Box::new(gp)), Paint::None, 0.0);
|
||||
let mut p = Node::path(NodeId(1), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), fill);
|
||||
p.appearance.effects.push(Effect { id: "adjust.hueSaturation".into(), params: json!({"hue": 120}), visible: true });
|
||||
let out = adjust(&p, None, &|_| None, &mut |_, _| None).unwrap();
|
||||
let out = adjust(&p, None, &|_| None, &mut |_, _| None, &|_| true).unwrap();
|
||||
let Paint::Gradient(g) = out.appearance.fill_paint() else { panic!("a gradient fill") };
|
||||
// The points are what a freeform gradient draws from, not its stops.
|
||||
let hexes: Vec<String> = g.freeform.as_ref().unwrap().points.iter().map(|p| p.color.to_hex()).collect();
|
||||
|
||||
@@ -682,12 +682,13 @@ fn as_art_opts(
|
||||
NodeKind::Image(im) => {
|
||||
let frame = shapes::rectangle(Rect::new(0.0, 0.0, im.width as f64, im.height as f64)).transformed(im.xf);
|
||||
let mut image = Node::new(n.id, n.kind.clone());
|
||||
// Its pixels draw as the image's do: colorized by a grayscale image's fill color, and
|
||||
// smoothed or not.
|
||||
// Its pixels draw as the image's do: colorized by a grayscale image's fill color,
|
||||
// smoothed or not, and with its color mask's masked pixels.
|
||||
{
|
||||
let mut e = image.extras_mut();
|
||||
e.colorize = n.extras().colorize.clone();
|
||||
e.pixelated = n.extras().pixelated;
|
||||
e.color_mask = n.extras().color_mask.clone();
|
||||
}
|
||||
let clipped = || {
|
||||
let mut clip = Node::path(n.id, frame.clone(), Appearance::default());
|
||||
|
||||
@@ -777,7 +777,7 @@ pub(crate) fn unique_name_from(base: &str, next: &mut std::collections::HashMap<
|
||||
/// The most presets one presets file imports: each is looked up among the saved ones.
|
||||
pub(crate) const MAX_IMPORTED_PRESETS: usize = 1000;
|
||||
|
||||
pub(crate) use addons::DecodedImages;
|
||||
pub(crate) use addons::{DecodedImages, refresh_color_masks};
|
||||
pub use docmenu::guide_indexes;
|
||||
pub(crate) use edit::{settle_interaction, typing_in_progress};
|
||||
pub(crate) use object::bounds_rewrite;
|
||||
|
||||
@@ -14,7 +14,7 @@ mod select;
|
||||
mod shapes;
|
||||
mod typetools;
|
||||
|
||||
pub(crate) use image::DecodedImages;
|
||||
pub(crate) use image::{DecodedImages, refresh_color_masks};
|
||||
|
||||
use super::CommandSpec;
|
||||
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
//! selected images transparent in their own pixels, as one undo step. The background is the most
|
||||
//! common color along an image's edges, the average of its four corners, or colors the caller
|
||||
//! picks. It is cleared where it reaches in from an edge, or wherever it is. The work is done here,
|
||||
//! on the pixels; nothing is sent anywhere. It is a VectorCraft addition. Its submenu (Object ›
|
||||
//! Plug-ins › Image Tools) and dialog are the UI's.
|
||||
//! on the pixels; nothing is sent anywhere. Color Mask ([`mask`]) hides colors matched the same
|
||||
//! way while the image keeps its pixels. They are VectorCraft additions. Its submenu (Object ›
|
||||
//! Plug-ins › Image Tools) and dialogs are the UI's.
|
||||
|
||||
mod mask;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub(crate) use mask::refresh as refresh_color_masks;
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::Cursor;
|
||||
use std::sync::{Arc, Weak};
|
||||
@@ -15,12 +19,12 @@ use std::sync::{Arc, Weak};
|
||||
use image::{DynamicImage, ImageBuffer, LumaA, Rgba};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::Color;
|
||||
use vectorcraft_doc::{ImageBlob, NodeId, NodeKind};
|
||||
use vectorcraft_doc::{ImageBlob, ImageObject, NodeId, NodeKind};
|
||||
use vectorcraft_geom::Affine;
|
||||
|
||||
use super::super::fileio::{MAX_RASTER_ALLOC, ppi};
|
||||
use super::super::menucmds::has_image;
|
||||
use super::super::paint::{PROCESS_ON_TRANSPARENCY, gray_colorized_with_process};
|
||||
use super::super::paint::{PROCESS_ON_TRANSPARENCY, colorized_with_process};
|
||||
use super::super::{CommandSpec, Result, Session, bad, checked_ids_param, cmd, hex_with_alpha, number_or, switch_or, text_param};
|
||||
|
||||
/// Remove Background's command.
|
||||
@@ -35,7 +39,7 @@ const MAX_COLORS: usize = 16;
|
||||
const WORK_BYTES: u64 = 10;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![cmd!(
|
||||
let mut v = vec![cmd!(
|
||||
"addon.image.removeBackground",
|
||||
"Remove Background…",
|
||||
["Object", "Plug-ins", "Image Tools"],
|
||||
@@ -43,7 +47,9 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"{background?: \"edges\" (default: the most common color along the image's edges; nothing is cleared when the edges hold at least as many clear pixels as pixels of that color) | \"corners\" (the average of the image's four corner pixels that are not clear; nothing is cleared when all four are clear) | \"colors\" (the colors in `colors`), colors?: [color…] or one string of them split by commas or spaces (1 to 16 colors, each a hex color \"#rrggbb\" or \"#rgb\" with or without the # or one of black, white, red, green and blue; needed with background: colors, and checked but not used with edges or corners), contiguous?: bool (true: only background pixels that reach in from an edge are cleared; false: they are cleared wherever they are; true), match?: \"distance\" (default: a color's RGB distance from a background color) | \"channels\" (its largest difference on one of red, green and blue), tolerance?: 0..100 (with distance, % of the largest RGB distance; with channels, % of 255 steps on one channel; a pixel this near a background color is cleared; 10), softness?: 0..100 (% past the tolerance over which pixels fade from cleared to kept; 10), ids?: [id…] (default: the selected images)} make the background of each image transparent in its own pixels, as one undo step: the image stays an image, embedded as a PNG with an alpha channel at the depth of its pixels (16-bit images keep 16 bits and grayscale images stay gray) with the source's color profile and resolution, and a linked image is embedded; a CMYK image, a linked image that shows its preview (embed or relink it first), an image too large to change, a result with no pixel left, and a grayscale image colorized with a process color (which can't colorize gray pixels with transparency) are refused, and an image whose pixels do not change is left as it is (no undo step when none changes) → {changed: the images changed, ids, cleared: the pixels made fully clear in them}",
|
||||
has_image,
|
||||
remove_background
|
||||
)]
|
||||
)];
|
||||
v.extend(mask::specs());
|
||||
v
|
||||
}
|
||||
|
||||
/// Where the background's colors come from.
|
||||
@@ -79,50 +85,50 @@ pub(crate) struct Settings {
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
/// The settings `p` gives, each checked.
|
||||
fn read(p: &Value) -> Result<Self> {
|
||||
/// The settings `p` gives to command `cmd`, each checked.
|
||||
fn read(p: &Value, cmd: &str) -> Result<Self> {
|
||||
let percent = |key: &str| -> Result<f64> {
|
||||
let v = number_or(p, key, 10.0, C)?;
|
||||
if v.is_finite() && (0.0..=100.0).contains(&v) { Ok(v) } else { Err(bad(C, format!("`{key}` must be from 0 to 100"))) }
|
||||
let v = number_or(p, key, 10.0, cmd)?;
|
||||
if v.is_finite() && (0.0..=100.0).contains(&v) { Ok(v) } else { Err(bad(cmd, format!("`{key}` must be from 0 to 100"))) }
|
||||
};
|
||||
let (tolerance, softness) = (percent("tolerance")?, percent("softness")?);
|
||||
let colors = match p.get("colors") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(v) => Some(colors(v)?),
|
||||
Some(v) => Some(colors(v, cmd)?),
|
||||
};
|
||||
let background = match text_param(p, "background", C)? {
|
||||
let background = match text_param(p, "background", cmd)? {
|
||||
None | Some("edges") => Background::Edges,
|
||||
Some("corners") => Background::Corners,
|
||||
Some("colors") => Background::Colors(colors.ok_or_else(colors_error)?),
|
||||
Some(_) => return Err(bad(C, "`background` must be edges, corners or colors")),
|
||||
Some("colors") => Background::Colors(colors.ok_or_else(|| colors_error(cmd))?),
|
||||
Some(_) => return Err(bad(cmd, "`background` must be edges, corners or colors")),
|
||||
};
|
||||
let measure = match text_param(p, "match", C)? {
|
||||
let measure = match text_param(p, "match", cmd)? {
|
||||
None | Some("distance") => Match::Distance,
|
||||
Some("channels") => Match::Channels,
|
||||
Some(_) => return Err(bad(C, "`match` must be distance or channels")),
|
||||
Some(_) => return Err(bad(cmd, "`match` must be distance or channels")),
|
||||
};
|
||||
let contiguous = switch_or(p, "contiguous", true, C)?;
|
||||
let contiguous = switch_or(p, "contiguous", true, cmd)?;
|
||||
Ok(Self { background, contiguous, measure, tolerance, softness })
|
||||
}
|
||||
}
|
||||
|
||||
fn colors_error() -> crate::EngineError {
|
||||
bad(C, "`colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue")
|
||||
fn colors_error(cmd: &str) -> crate::EngineError {
|
||||
bad(cmd, "`colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue")
|
||||
}
|
||||
|
||||
/// The colors `v` lists: an array of strings, or one string of them split by commas or spaces.
|
||||
/// Each is read as [`Color::from_hex`] reads it; one with an alpha part is refused.
|
||||
fn colors(v: &Value) -> Result<Vec<[u8; 3]>> {
|
||||
/// The colors `v` lists to command `cmd`: an array of strings, or one string of them split by
|
||||
/// commas or spaces. Each is read as [`Color::from_hex`] reads it; one with an alpha part is refused.
|
||||
fn colors(v: &Value, cmd: &str) -> Result<Vec<[u8; 3]>> {
|
||||
// One past the most is enough to tell a list that is too long.
|
||||
let texts: Vec<&str> = match v {
|
||||
Value::String(s) => s.split([',', ' ', '\t', '\n']).filter(|t| !t.is_empty()).take(MAX_COLORS + 1).collect(),
|
||||
Value::Array(a) => a.iter().take(MAX_COLORS + 1).map(Value::as_str).collect::<Option<_>>().ok_or_else(colors_error)?,
|
||||
_ => return Err(colors_error()),
|
||||
Value::Array(a) => a.iter().take(MAX_COLORS + 1).map(Value::as_str).collect::<Option<_>>().ok_or_else(|| colors_error(cmd))?,
|
||||
_ => return Err(colors_error(cmd)),
|
||||
};
|
||||
if texts.is_empty() || texts.len() > MAX_COLORS {
|
||||
return Err(colors_error());
|
||||
return Err(colors_error(cmd));
|
||||
}
|
||||
texts.into_iter().map(|t| rgb8(t).ok_or_else(colors_error)).collect()
|
||||
texts.into_iter().map(|t| rgb8(t).ok_or_else(|| colors_error(cmd))).collect()
|
||||
}
|
||||
|
||||
/// The 8-bit RGB of the color `t` writes.
|
||||
@@ -135,7 +141,7 @@ fn rgb8(t: &str) -> Option<[u8; 3]> {
|
||||
}
|
||||
|
||||
fn remove_background(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let settings = Settings::read(p)?;
|
||||
let settings = Settings::read(p, C)?;
|
||||
// A preview step keeps the pixels it decodes for the next step; other runs keep none.
|
||||
let mut kept = std::mem::take(&mut s.decoded_images);
|
||||
let previewing = s.previewing;
|
||||
@@ -165,40 +171,15 @@ fn remove_background(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// cleared as `settings` say and the transform that keeps its place and size. Decoded pixels are
|
||||
/// taken from and kept in `kept` when it is given.
|
||||
fn changes(s: &Session, p: &Value, settings: &Settings, mut kept: Option<&mut DecodedImages>) -> Result<Vec<(NodeId, Cleared, Affine)>> {
|
||||
let images = images(s, p, C)?;
|
||||
// The result has an alpha channel, which a grayscale image colorized with a process color
|
||||
// can't take. The pixels that are cleared are checked, the image's own, not the ones a color
|
||||
// mask shows.
|
||||
let st = s.doc()?;
|
||||
let ids: Vec<NodeId> = if p.get("ids").is_some_and(|v| !v.is_null()) {
|
||||
// Each image once, however often `ids` names it.
|
||||
let mut given = HashSet::new();
|
||||
let ids: Vec<NodeId> = checked_ids_param(s, p, "ids", C)?.into_iter().filter(|id| given.insert(*id)).collect();
|
||||
if let Some(id) = ids.iter().find(|id| !matches!(st.doc.node(**id).map(|n| &n.kind), Some(NodeKind::Image(_)))) {
|
||||
return Err(bad(C, format!("object {} is not an image", id.0)));
|
||||
}
|
||||
ids
|
||||
} else {
|
||||
st.selection.objects.iter().copied().filter(|id| matches!(st.doc.node(*id).map(|n| &n.kind), Some(NodeKind::Image(_)))).collect()
|
||||
};
|
||||
if ids.is_empty() {
|
||||
return Err(bad(C, "select an image"));
|
||||
}
|
||||
// Every image is checked before any is decoded.
|
||||
let mut images = vec![];
|
||||
for id in ids {
|
||||
let Some(n) = st.doc.node(id) else { continue };
|
||||
let NodeKind::Image(im) = &n.kind else { continue };
|
||||
let blob = st.doc.images.get(&im.key).ok_or_else(|| bad(C, "the image's pixels are missing"))?;
|
||||
// The result has an alpha channel, which a grayscale image colorized with a process color
|
||||
// can't take.
|
||||
if gray_colorized_with_process(&st.doc, n).is_some() {
|
||||
for (id, _, blob) in &images {
|
||||
if blob.gray_source().is_some() && st.doc.node(*id).is_some_and(|n| colorized_with_process(&st.doc, n)) {
|
||||
return Err(bad(C, PROCESS_ON_TRANSPARENCY));
|
||||
}
|
||||
// The preview stands in for a file that hasn't been read, or that is kept at its preview.
|
||||
if im.link.is_some() && blob.is_proxy() {
|
||||
return Err(bad(C, "the linked image shows its preview: embed or relink it first"));
|
||||
}
|
||||
if blob.is_cmyk() {
|
||||
return Err(bad(C, "a CMYK image has no transparency to set"));
|
||||
}
|
||||
images.push((id, im, blob));
|
||||
}
|
||||
// Each image's pixels once, however many objects show them.
|
||||
let mut cleared: HashMap<&str, Option<Cleared>> = HashMap::new();
|
||||
@@ -207,7 +188,7 @@ fn changes(s: &Session, p: &Value, settings: &Settings, mut kept: Option<&mut De
|
||||
let out = match cleared.get(im.key.as_str()) {
|
||||
Some(out) => out.clone(),
|
||||
None => {
|
||||
let out = clear(blob, settings, kept.as_deref_mut())?;
|
||||
let out = clear(blob, settings, kept.as_deref_mut(), C)?;
|
||||
cleared.insert(&im.key, out.clone());
|
||||
out
|
||||
}
|
||||
@@ -225,6 +206,58 @@ fn changes(s: &Session, p: &Value, settings: &Settings, mut kept: Option<&mut De
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
/// The image objects command `cmd` acts on (`ids`, each once, or the selected images), each with
|
||||
/// its pixels, all checked before any is decoded: an object that is not an image, missing pixels,
|
||||
/// a linked image that shows its preview and a CMYK image are errors, as is no image at all.
|
||||
fn images<'a>(s: &'a Session, p: &Value, cmd: &str) -> Result<Vec<(NodeId, &'a ImageObject, &'a ImageBlob)>> {
|
||||
let st = s.doc()?;
|
||||
let mut images = vec![];
|
||||
for id in targets(s, p, cmd)? {
|
||||
let Some(NodeKind::Image(im)) = st.doc.node(id).map(|n| &n.kind) else { continue };
|
||||
let blob = st.doc.images.get(&im.key).ok_or_else(|| bad(cmd, "the image's pixels are missing"))?;
|
||||
// The preview stands in for a file that hasn't been read, or that is kept at its preview.
|
||||
if im.link.is_some() && blob.is_proxy() {
|
||||
return Err(bad(cmd, "the linked image shows its preview: embed or relink it first"));
|
||||
}
|
||||
if blob.is_cmyk() {
|
||||
return Err(cmyk_refusal(cmd));
|
||||
}
|
||||
images.push((id, &**im, blob));
|
||||
}
|
||||
Ok(images)
|
||||
}
|
||||
|
||||
/// The image objects command `cmd` acts on: `ids`, each once, or the selected images. An object in
|
||||
/// `ids` that is not an image is an error, as is no image at all.
|
||||
fn targets(s: &Session, p: &Value, cmd: &str) -> Result<Vec<NodeId>> {
|
||||
let st = s.doc()?;
|
||||
let ids: Vec<NodeId> = if p.get("ids").is_some_and(|v| !v.is_null()) {
|
||||
// Each image once, however often `ids` names it.
|
||||
let mut given = HashSet::new();
|
||||
let ids: Vec<NodeId> = checked_ids_param(s, p, "ids", cmd)?.into_iter().filter(|id| given.insert(*id)).collect();
|
||||
if let Some(id) = ids.iter().find(|id| !matches!(st.doc.node(**id).map(|n| &n.kind), Some(NodeKind::Image(_)))) {
|
||||
return Err(bad(cmd, format!("object {} is not an image", id.0)));
|
||||
}
|
||||
ids
|
||||
} else {
|
||||
st.selection.objects.iter().copied().filter(|id| matches!(st.doc.node(*id).map(|n| &n.kind), Some(NodeKind::Image(_)))).collect()
|
||||
};
|
||||
if ids.is_empty() {
|
||||
return Err(bad(cmd, "select an image"));
|
||||
}
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
/// Whether `cmd` is one of Color Mask's commands, which hide pixels instead of clearing them.
|
||||
fn masks(cmd: &str) -> bool {
|
||||
matches!(cmd, mask::COLOR_MASK | mask::REMOVE_COLOR_MASK | mask::APPLY_COLOR_MASK)
|
||||
}
|
||||
|
||||
/// Command `cmd`'s refusal of a CMYK image.
|
||||
fn cmyk_refusal(cmd: &str) -> crate::EngineError {
|
||||
if masks(cmd) { bad(cmd, "a CMYK image can't take a color mask") } else { bad(cmd, "a CMYK image has no transparency to set") }
|
||||
}
|
||||
|
||||
/// An image with its background cleared: its PNG, its size in pixels, and how many of its pixels
|
||||
/// were made fully clear.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -352,16 +385,16 @@ impl Pixels {
|
||||
|
||||
/// `blob` with its background cleared as `settings` say, or `None` when no pixel changes. A CMYK
|
||||
/// image, one that can't be decoded or is too large to change, and a result with no pixel left are
|
||||
/// errors. The PNG keeps the source's ICC profile and resolution. The decoded pixels are taken
|
||||
/// from and kept in `kept` when it is given.
|
||||
pub(crate) fn clear(blob: &ImageBlob, settings: &Settings, mut kept: Option<&mut DecodedImages>) -> Result<Option<Cleared>> {
|
||||
/// errors of command `cmd`. The PNG keeps the source's ICC profile and resolution. The decoded
|
||||
/// pixels are taken from and kept in `kept` when it is given.
|
||||
pub(crate) fn clear(blob: &ImageBlob, settings: &Settings, mut kept: Option<&mut DecodedImages>, cmd: &str) -> Result<Option<Cleared>> {
|
||||
if blob.is_cmyk() {
|
||||
return Err(bad(C, "a CMYK image has no transparency to set"));
|
||||
return Err(cmyk_refusal(cmd));
|
||||
}
|
||||
let found = kept.as_deref_mut().and_then(|k| k.get(&blob.bytes));
|
||||
let (source, gray, icc) = match found {
|
||||
Some(found) => found,
|
||||
None => decode(blob, kept)?,
|
||||
None => decode(blob, kept, cmd)?,
|
||||
};
|
||||
// The pixels kept for the next run stay as they are; ones not kept are worked on in place.
|
||||
let mut px = Arc::try_unwrap(source).unwrap_or_else(|kept| Pixels::clone(&kept));
|
||||
@@ -373,7 +406,11 @@ pub(crate) fn clear(blob: &ImageBlob, settings: &Settings, mut kept: Option<&mut
|
||||
Pixels::Sixteen(img) => img.pixels().any(|p| p.0[3] > 0),
|
||||
};
|
||||
if !left {
|
||||
return Err(bad(C, "every pixel matches the background: no image would be left"));
|
||||
return Err(if masks(cmd) {
|
||||
bad(cmd, "the mask would hide every pixel")
|
||||
} else {
|
||||
bad(cmd, "every pixel matches the background: no image would be left")
|
||||
});
|
||||
}
|
||||
let out = match (px, gray) {
|
||||
(Pixels::Eight(img), false) => DynamicImage::ImageRgba8(img),
|
||||
@@ -389,9 +426,9 @@ pub(crate) fn clear(blob: &ImageBlob, settings: &Settings, mut kept: Option<&mut
|
||||
let mut encoder = image::codecs::png::PngEncoder::new(&mut png);
|
||||
if let Some(icc) = icc {
|
||||
use image::ImageEncoder as _;
|
||||
encoder.set_icc_profile(icc).map_err(|e| bad(C, e.to_string()))?;
|
||||
encoder.set_icc_profile(icc).map_err(|e| bad(cmd, e.to_string()))?;
|
||||
}
|
||||
out.write_with_encoder(encoder).map_err(|e| bad(C, e.to_string()))?;
|
||||
out.write_with_encoder(encoder).map_err(|e| bad(cmd, e.to_string()))?;
|
||||
// The resolution the file declares, which sizes the image at 100% when it is replaced or relinked.
|
||||
if let Some(r) = ppi::resolution(&blob.bytes) {
|
||||
png = ppi::with_png_resolution(&png, r);
|
||||
@@ -401,11 +438,12 @@ pub(crate) fn clear(blob: &ImageBlob, settings: &Settings, mut kept: Option<&mut
|
||||
|
||||
/// `blob`'s pixels as straight RGBA, whether they were gray, and its ICC profile. An image that
|
||||
/// can't be decoded or is too large to change is an error. The pixels are kept in `kept`, when it
|
||||
/// is given, if a copy of them fits the decode allowance beside the work.
|
||||
fn decode(blob: &ImageBlob, kept: Option<&mut DecodedImages>) -> Result<(Arc<Pixels>, bool, Option<Vec<u8>>)> {
|
||||
let reader =
|
||||
|| image::ImageReader::new(Cursor::new(blob.bytes.as_slice())).with_guessed_format().map_err(|_| bad(C, "the image's pixels can't be read"));
|
||||
let mut decoder = reader()?.into_decoder().map_err(|e| bad(C, format!("the image's pixels can't be read: {e}")))?;
|
||||
/// is given, if a copy of them fits the decode allowance beside the work. Errors are command `cmd`'s.
|
||||
fn decode(blob: &ImageBlob, kept: Option<&mut DecodedImages>, cmd: &str) -> Result<(Arc<Pixels>, bool, Option<Vec<u8>>)> {
|
||||
let reader = || {
|
||||
image::ImageReader::new(Cursor::new(blob.bytes.as_slice())).with_guessed_format().map_err(|_| bad(cmd, "the image's pixels can't be read"))
|
||||
};
|
||||
let mut decoder = reader()?.into_decoder().map_err(|e| bad(cmd, format!("the image's pixels can't be read: {e}")))?;
|
||||
// The ICC profile the colors are in goes with them into the PNG.
|
||||
let (w, h, kind, icc) = {
|
||||
use image::ImageDecoder as _;
|
||||
@@ -418,13 +456,13 @@ fn decode(blob: &ImageBlob, kept: Option<&mut DecodedImages>) -> Result<(Arc<Pix
|
||||
let deep = kind.bytes_per_pixel() > kind.channel_count();
|
||||
let per_pixel = u64::from(kind.bytes_per_pixel()) + 2 * if deep { 8 } else { 4 } + WORK_BYTES;
|
||||
if u64::from(w).checked_mul(u64::from(h)).and_then(|n| n.checked_mul(per_pixel)).is_none_or(|n| n > MAX_RASTER_ALLOC) {
|
||||
return Err(bad(C, format!("an image of {w} × {h} pixels is too large to change")));
|
||||
return Err(bad(cmd, format!("an image of {w} × {h} pixels is too large to change")));
|
||||
}
|
||||
let mut reader = reader()?;
|
||||
let mut limits = image::Limits::default();
|
||||
limits.max_alloc = Some(MAX_RASTER_ALLOC);
|
||||
reader.limits(limits);
|
||||
let img = reader.decode().map_err(|e| bad(C, format!("the image's pixels can't be read: {e}")))?;
|
||||
let img = reader.decode().map_err(|e| bad(cmd, format!("the image's pixels can't be read: {e}")))?;
|
||||
#[cfg(test)]
|
||||
DECODES.with(|c| c.set(c.get() + 1));
|
||||
let gray = matches!(img.color(), image::ColorType::L8 | image::ColorType::La8 | image::ColorType::L16 | image::ColorType::La16);
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
//! Image Tools › Color Mask (`addon.image.colorMask`, `addon.image.removeColorMask`,
|
||||
//! `addon.image.applyColorMask`): colors of the selected images are hidden while their pixels
|
||||
//! stay as they are. The colors are matched as Remove Background matches them ([`super::clear`]);
|
||||
//! the image keeps its own pixels, and the mask's settings and the masked pixels are kept with it
|
||||
//! ([`vectorcraft_doc::color_mask`]), so the mask can be changed, removed, or applied to the
|
||||
//! pixels later. When an image's own pixels are replaced (a link updated or relinked, the image
|
||||
//! cropped or recolored), its mask is applied to the new pixels ([`refresh`]).
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::color_mask::{ColorMask, MaskBackground, MaskMatch};
|
||||
use vectorcraft_doc::{Document, ImageBlob, Node, NodeId, NodeKind};
|
||||
use vectorcraft_geom::Affine;
|
||||
|
||||
use super::super::super::paint::{PROCESS_ON_TRANSPARENCY, colorized_with_process};
|
||||
use super::super::super::{CommandSpec, Result, Session, bad, cmd};
|
||||
use super::{Background, DecodedImages, MAX_COLORS, Match, Settings, clear, images, targets};
|
||||
|
||||
/// Color Mask's command.
|
||||
pub const COLOR_MASK: &str = "addon.image.colorMask";
|
||||
/// Remove Color Mask's command.
|
||||
pub const REMOVE_COLOR_MASK: &str = "addon.image.removeColorMask";
|
||||
/// Apply Color Mask's command.
|
||||
pub const APPLY_COLOR_MASK: &str = "addon.image.applyColorMask";
|
||||
|
||||
/// The params that set a mask; with none of them, Color Mask only reports.
|
||||
const SETTINGS: [&str; 6] = ["background", "colors", "contiguous", "match", "tolerance", "softness"];
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
"addon.image.colorMask",
|
||||
"Color Mask…",
|
||||
["Object", "Plug-ins", "Image Tools"],
|
||||
None,
|
||||
"{background?: \"edges\" (default: the most common color along the image's edges) | \"corners\" (the average of the image's four corner pixels that are not clear) | \"colors\" (the colors in `colors`), colors?: [color…] or one string of them split by commas or spaces (1 to 16 hex colors or black, white, red, green and blue, as addon.image.removeBackground takes them; needed with background: colors), contiguous?: bool (true: only matching pixels that reach in from an edge are hidden; false: wherever they are; true), match?: \"distance\" (default: a color's RGB distance from a hidden color) | \"channels\" (its largest difference on one of red, green and blue), tolerance?: 0..100 (% as addon.image.removeBackground measures it; a pixel this near a hidden color is hidden; 10), softness?: 0..100 (% past the tolerance over which pixels fade from hidden to shown; 10), ids?: [id…] (default: the selected images)} hide colors of each image without changing its pixels, as one undo step: the image keeps its own pixels, and the mask (these settings, and the image's pixels with the hidden colors made clear, made as Remove Background makes them) is kept with the image and saved with the document; the canvas, printing and separations, PDF, SVG, EPS, raster formats, DXF, EMF, WMF, the legacy .ai and the .ai editing data, copy and paste, and placing a document show the masked pixels, and a click on any part of the image's frame selects it (the Eyedropper, Object Mosaic, Create Gradient Mesh, Crop Image's trim and Image Trace read the image's own pixels); setting the mask again replaces it, and an image whose mask does not change is left as it is (no undo step when none changes); when the image's own pixels are replaced later (Update Link, Relink, Crop Image, a recolor, a linked file read again on open), the mask is made again for the new pixels; an object in `ids` that is not an image, a CMYK image, a linked image that shows its preview (embed or relink it first), an image too large to change, a mask that would hide every pixel, and a mask that would hide pixels of a grayscale image colorized with a process color (masked gray pixels have transparency, which only a spot color, black or white colorizes; each image is checked on the pixels its new mask shows) are refused. With none of background, colors, contiguous, match, tolerance and softness given, nothing changes and the mask settings of the first image (or the defaults when it has no mask) are returned → {changed: the images changed, ids, hidden: the pixels made fully clear in them} or {background, colors, contiguous, match, tolerance, softness, masked}",
|
||||
super::has_image,
|
||||
color_mask
|
||||
),
|
||||
cmd!(
|
||||
"addon.image.removeColorMask",
|
||||
"Remove Color Mask",
|
||||
["Object", "Plug-ins", "Image Tools"],
|
||||
None,
|
||||
"{ids?: [id…] (default: the selected images)} take the color mask off each image, as one undo step: the image shows its own pixels again; images without a mask are left as they are (no undo step when none has one), and an object in `ids` that is not an image is refused → {changed, ids}",
|
||||
has_color_mask,
|
||||
remove_color_mask
|
||||
),
|
||||
cmd!(
|
||||
"addon.image.applyColorMask",
|
||||
"Apply Color Mask",
|
||||
["Object", "Plug-ins", "Image Tools"],
|
||||
None,
|
||||
"{ids?: [id…] (default: the selected images)} make each image's color mask part of its pixels, as one undo step: the masked pixels become the image's own, embedded as a PNG with an alpha channel (a linked image is embedded), and the mask is taken off; a mask that hides nothing is only taken off (the image keeps its pixels and its link); images without a mask are left as they are (no undo step when none has one), and an object in `ids` that is not an image, and a mask made again for new pixels that would give a grayscale image colorized with a process color an alpha channel, are refused → {changed, ids}",
|
||||
has_color_mask,
|
||||
apply_color_mask
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// Enabled when a selected image has a color mask.
|
||||
fn has_color_mask(s: &Session) -> std::result::Result<(), String> {
|
||||
let st = s.active().ok_or("no document open")?;
|
||||
if st.selection.objects.iter().any(|id| st.doc.node(*id).and_then(Document::color_mask_of).is_some()) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("select an image with a color mask".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
/// The settings a mask was made with, `None` when a file gave ones out of range.
|
||||
fn of(m: &ColorMask) -> Option<Self> {
|
||||
let percent = |v: f64| v.is_finite() && (0.0..=100.0).contains(&v);
|
||||
if !percent(m.tolerance) || !percent(m.softness) {
|
||||
return None;
|
||||
}
|
||||
let background = match m.background {
|
||||
MaskBackground::Edges => Background::Edges,
|
||||
MaskBackground::Corners => Background::Corners,
|
||||
MaskBackground::Colors if (1..=MAX_COLORS).contains(&m.colors.len()) => Background::Colors(m.colors.clone()),
|
||||
MaskBackground::Colors => return None,
|
||||
};
|
||||
let measure = match m.measure {
|
||||
MaskMatch::Distance => Match::Distance,
|
||||
MaskMatch::Channels => Match::Channels,
|
||||
};
|
||||
Some(Self { background, contiguous: m.contiguous, measure, tolerance: m.tolerance, softness: m.softness })
|
||||
}
|
||||
|
||||
/// A mask with these settings whose masked pixels are `key`, made from the pixels `from`.
|
||||
fn mask(&self, key: String, from: String) -> ColorMask {
|
||||
let (background, colors) = match &self.background {
|
||||
Background::Edges => (MaskBackground::Edges, vec![]),
|
||||
Background::Corners => (MaskBackground::Corners, vec![]),
|
||||
Background::Colors(c) => (MaskBackground::Colors, c.clone()),
|
||||
};
|
||||
let measure = match self.measure {
|
||||
Match::Distance => MaskMatch::Distance,
|
||||
Match::Channels => MaskMatch::Channels,
|
||||
};
|
||||
ColorMask { key, from, background, colors, contiguous: self.contiguous, measure, tolerance: self.tolerance, softness: self.softness }
|
||||
}
|
||||
}
|
||||
|
||||
/// The masked pixels of `blob` (`None`: no pixel is hidden, the image's own pixels show) as
|
||||
/// `settings` say, with errors of command `cmd`.
|
||||
fn masked(blob: &ImageBlob, settings: &Settings, kept: Option<&mut DecodedImages>, cmd: &str) -> Result<Option<(String, ImageBlob, u64)>> {
|
||||
Ok(clear(blob, settings, kept, cmd)?.map(|out| (out.blob.content_key(), out.blob, out.cleared)))
|
||||
}
|
||||
|
||||
fn color_mask(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if !SETTINGS.iter().any(|k| p.get(*k).is_some_and(|v| !v.is_null())) {
|
||||
return report(s, p);
|
||||
}
|
||||
let settings = Settings::read(p, COLOR_MASK)?;
|
||||
// A preview step keeps the pixels it decodes for the next step; other runs keep none.
|
||||
let mut kept = std::mem::take(&mut s.decoded_images);
|
||||
let previewing = s.previewing;
|
||||
let found = mask_changes(s, p, &settings, previewing.then_some(&mut kept));
|
||||
s.decoded_images = kept;
|
||||
let changes = found?;
|
||||
let ids: Vec<u64> = changes.iter().map(|c| c.0.0).collect();
|
||||
let hidden: u64 = changes.iter().map(|c| c.3).sum();
|
||||
if !changes.is_empty() {
|
||||
s.edit("Color Mask", |d, _| {
|
||||
for (id, mask, blob, _) in changes {
|
||||
if let Some(blob) = blob {
|
||||
d.images.entry(mask.key.clone()).or_insert(blob);
|
||||
}
|
||||
if let Some(n) = d.node_mut(id) {
|
||||
n.extras_mut().color_mask = Some(Box::new(mask));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
Ok(json!({ "changed": ids.len(), "ids": ids, "hidden": hidden }))
|
||||
}
|
||||
|
||||
/// The images `p` names whose mask changes, each with its new mask, the masked pixels to add (when
|
||||
/// the document doesn't have them) and how many pixels they hide.
|
||||
type MaskChange = (NodeId, ColorMask, Option<ImageBlob>, u64);
|
||||
|
||||
fn mask_changes(s: &Session, p: &Value, settings: &Settings, mut kept: Option<&mut DecodedImages>) -> Result<Vec<MaskChange>> {
|
||||
let st = s.doc()?;
|
||||
// Each image's pixels once, however many objects show them.
|
||||
let mut made: HashMap<&str, (String, Option<ImageBlob>, u64)> = HashMap::new();
|
||||
let mut out = vec![];
|
||||
for (id, im, blob) in images(s, p, COLOR_MASK)? {
|
||||
let (key, pixels, hidden) = match made.get(im.key.as_str()) {
|
||||
Some(m) => m.clone(),
|
||||
None => {
|
||||
let m = match masked(blob, settings, kept.as_deref_mut(), COLOR_MASK)? {
|
||||
Some((key, blob, hidden)) => (key, Some(blob), hidden),
|
||||
None => (im.key.clone(), None, 0),
|
||||
};
|
||||
made.insert(&im.key, m.clone());
|
||||
m
|
||||
}
|
||||
};
|
||||
let mask = settings.mask(key, im.key.clone());
|
||||
if st.doc.node(id).and_then(Document::color_mask_of) == Some(&mask) {
|
||||
continue;
|
||||
}
|
||||
// Masked pixels that hide something have an alpha channel, which a grayscale image
|
||||
// colorized with a process color can't take. The pixels the new mask shows are checked,
|
||||
// made from the image's own pixels, not the ones an earlier mask shows.
|
||||
let shown = pixels.as_ref().or_else(|| st.doc.images.get(&mask.key));
|
||||
if st.doc.node(id).is_some_and(|n| refused_colorize(&st.doc, n, shown)) {
|
||||
return Err(bad(COLOR_MASK, PROCESS_ON_TRANSPARENCY));
|
||||
}
|
||||
let pixels = pixels.filter(|_| !st.doc.images.contains_key(&mask.key));
|
||||
out.push((id, mask, pixels, hidden));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Whether masked pixels `shown`, made for image node `n`, have transparency that `n`'s colorize
|
||||
/// color can't take: gray pixels with an alpha channel and a process color
|
||||
/// ([`colorized_with_process`]).
|
||||
fn refused_colorize(doc: &Document, n: &Node, shown: Option<&ImageBlob>) -> bool {
|
||||
shown.and_then(ImageBlob::gray_source).is_some_and(|g| g.alpha) && colorized_with_process(doc, n)
|
||||
}
|
||||
|
||||
/// Color Mask without settings: the first image's mask settings, or the defaults. It reads them
|
||||
/// for images a change would refuse too (CMYK, a linked image at its preview), so the dialog opens.
|
||||
fn report(s: &Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
let first = targets(s, p, COLOR_MASK)?.first().and_then(|id| st.doc.node(*id)).and_then(Document::color_mask_of).cloned();
|
||||
let m = first.as_ref();
|
||||
let hex = |c: &[u8; 3]| format!("#{:02x}{:02x}{:02x}", c[0], c[1], c[2]);
|
||||
let colors =
|
||||
m.filter(|m| !m.colors.is_empty()).map_or_else(|| "#ffffff".to_string(), |m| m.colors.iter().map(hex).collect::<Vec<_>>().join(", "));
|
||||
let name = |v: Value| v.as_str().map(str::to_string).unwrap_or_default();
|
||||
Ok(json!({
|
||||
"background": name(serde_json::to_value(m.map_or(MaskBackground::Edges, |m| m.background)).unwrap_or_default()),
|
||||
"colors": colors,
|
||||
"contiguous": m.is_none_or(|m| m.contiguous),
|
||||
"match": name(serde_json::to_value(m.map_or(MaskMatch::Distance, |m| m.measure)).unwrap_or_default()),
|
||||
"tolerance": m.map_or(10.0, |m| m.tolerance),
|
||||
"softness": m.map_or(10.0, |m| m.softness),
|
||||
"masked": m.is_some(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// The image objects `p` names ([`targets`]: `ids`, or the selected images) that have a color mask.
|
||||
fn masked_images(s: &Session, p: &Value, cmd: &str) -> Result<Vec<NodeId>> {
|
||||
let st = s.doc()?;
|
||||
Ok(targets(s, p, cmd)?.into_iter().filter(|id| st.doc.node(*id).and_then(Document::color_mask_of).is_some()).collect())
|
||||
}
|
||||
|
||||
fn remove_color_mask(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ids = masked_images(s, p, REMOVE_COLOR_MASK)?;
|
||||
if !ids.is_empty() {
|
||||
s.edit("Remove Color Mask", |d, _| {
|
||||
for id in &ids {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
n.extras_mut().color_mask = None;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
Ok(json!({ "changed": ids.len(), "ids": ids.iter().map(|id| id.0).collect::<Vec<_>>() }))
|
||||
}
|
||||
|
||||
fn apply_color_mask(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ids = masked_images(s, p, APPLY_COLOR_MASK)?;
|
||||
// Each image's masked pixels, made again first when its own pixels changed since. A mask that
|
||||
// hides nothing (its masked pixels are the image's own) is only taken off: the image keeps its
|
||||
// pixels and its link.
|
||||
let mut made = vec![];
|
||||
{
|
||||
let st = s.doc()?;
|
||||
for id in &ids {
|
||||
let Some(n) = st.doc.node(*id) else { continue };
|
||||
let (NodeKind::Image(im), Some(m)) = (&n.kind, Document::color_mask_of(n)) else { continue };
|
||||
let mask = match fresh(&st.doc, n, APPLY_COLOR_MASK) {
|
||||
Some(Ok(fresh)) => fresh,
|
||||
Some(Err(e)) => return Err(e),
|
||||
None => (m.clone(), None),
|
||||
};
|
||||
if mask.0.key == im.key {
|
||||
made.push((*id, None));
|
||||
continue;
|
||||
}
|
||||
let shown = st.doc.images.get(&mask.0.key).or(mask.1.as_ref());
|
||||
if refused_colorize(&st.doc, n, shown) {
|
||||
return Err(bad(APPLY_COLOR_MASK, PROCESS_ON_TRANSPARENCY));
|
||||
}
|
||||
let size = mask_size(shown, im.width, im.height);
|
||||
made.push((*id, Some((mask, size))));
|
||||
}
|
||||
}
|
||||
if !made.is_empty() {
|
||||
s.edit("Apply Color Mask", |d, _| {
|
||||
for (id, applied) in made {
|
||||
let Some(n) = d.node_mut(id) else { continue };
|
||||
n.extras_mut().color_mask = None;
|
||||
let Some(((mask, blob), (w, h))) = applied else { continue };
|
||||
if let Some(blob) = blob {
|
||||
d.images.entry(mask.key.clone()).or_insert(blob);
|
||||
}
|
||||
let Some(n) = d.node_mut(id) else { continue };
|
||||
if let NodeKind::Image(im) = &mut n.kind {
|
||||
// Masked pixels of another size than the object's keep its place and size.
|
||||
if (w, h) != (im.width, im.height) {
|
||||
im.xf *= Affine::scale_non_uniform(f64::from(im.width) / f64::from(w), f64::from(im.height) / f64::from(h));
|
||||
(im.width, im.height) = (w, h);
|
||||
}
|
||||
im.key = mask.key;
|
||||
im.link = None;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
Ok(json!({ "changed": ids.len(), "ids": ids.iter().map(|id| id.0).collect::<Vec<_>>() }))
|
||||
}
|
||||
|
||||
/// The size in pixels of masked pixels `blob` (from its header), or the object's when it can't be
|
||||
/// read.
|
||||
fn mask_size(blob: Option<&ImageBlob>, width: u32, height: u32) -> (u32, u32) {
|
||||
blob.and_then(|b| image::ImageReader::new(std::io::Cursor::new(b.bytes.as_slice())).with_guessed_format().ok()?.into_dimensions().ok())
|
||||
.filter(|(w, h)| *w > 0 && *h > 0)
|
||||
.unwrap_or((width, height))
|
||||
}
|
||||
|
||||
/// The mask of image node `n` made again for the image's own pixels, with the masked pixels when
|
||||
/// `doc` doesn't have them: `None` when the mask is up to date (or `n` has none), an error when the
|
||||
/// pixels can't be masked (missing, CMYK, a linked preview, too large, every pixel hidden, or
|
||||
/// settings a file gave out of range).
|
||||
fn fresh(doc: &Document, n: &Node, cmd: &str) -> Option<Result<(ColorMask, Option<ImageBlob>)>> {
|
||||
let (NodeKind::Image(im), Some(m)) = (&n.kind, Document::color_mask_of(n)) else { return None };
|
||||
if !stale_mask(doc, n) {
|
||||
return None;
|
||||
}
|
||||
let made = || -> Result<(ColorMask, Option<ImageBlob>)> {
|
||||
let settings = Settings::of(m).ok_or_else(|| bad(cmd, "the color mask's settings are out of range"))?;
|
||||
let blob = doc.images.get(&im.key).ok_or_else(|| bad(cmd, "the image's pixels are missing"))?;
|
||||
if im.link.is_some() && blob.is_proxy() {
|
||||
return Err(bad(cmd, "the linked image shows its preview: embed or relink it first"));
|
||||
}
|
||||
Ok(match masked(blob, &settings, None, cmd)? {
|
||||
Some((key, blob, _)) => {
|
||||
let blob = (!doc.images.contains_key(&key)).then_some(blob);
|
||||
(settings.mask(key, im.key.clone()), blob)
|
||||
}
|
||||
None => (settings.mask(im.key.clone(), im.key.clone()), None),
|
||||
})
|
||||
};
|
||||
Some(made())
|
||||
}
|
||||
|
||||
/// After an edit from `before` (`None`: everything is new, as after a document's links are read):
|
||||
/// the color masks of images in `doc`'s art ([`Document::art_roots`]: layers, symbols, graph
|
||||
/// designs, patterns and the original of the pattern being edited, opacity mask art included)
|
||||
/// whose own pixels were replaced are made again for the new pixels ([`Refresh::made`]). Only the
|
||||
/// art that changed since `before` is looked at.
|
||||
pub(crate) fn refresh(before: Option<&Document>, doc: &mut Document) {
|
||||
let old: Vec<&Arc<Node>> = before.map(|b| b.art_roots().collect()).unwrap_or_default();
|
||||
let mut pass = Refresh { doc: &*doc, made: HashMap::new(), blobs: vec![] };
|
||||
let mut fixed = vec![];
|
||||
for (i, root) in doc.art_roots().enumerate() {
|
||||
let counterpart = old.get(i).copied().filter(|o| o.id == root.id);
|
||||
if stale_in(doc, root, counterpart) {
|
||||
let mut root = root.clone();
|
||||
pass.fix(Arc::make_mut(&mut root));
|
||||
fixed.push((i, root));
|
||||
}
|
||||
}
|
||||
let blobs = pass.blobs;
|
||||
if fixed.is_empty() {
|
||||
return;
|
||||
}
|
||||
for (key, blob) in blobs {
|
||||
doc.images.entry(key).or_insert(blob);
|
||||
}
|
||||
let mut fixed = fixed.into_iter().peekable();
|
||||
for (i, root) in doc.art_roots_mut().enumerate() {
|
||||
if fixed.peek().is_some_and(|(at, _)| *at == i)
|
||||
&& let Some((_, new)) = fixed.next()
|
||||
{
|
||||
*root = new;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `n`, or anything in it (opacity-mask art included), is an image whose mask is out of
|
||||
/// date ([`stale_mask`]). What is the same as in `old`, `n`'s counterpart before an edit, is not
|
||||
/// looked at.
|
||||
fn stale_in(doc: &Document, n: &Arc<Node>, old: Option<&Arc<Node>>) -> bool {
|
||||
if old.is_some_and(|o| Arc::ptr_eq(o, n)) {
|
||||
return false;
|
||||
}
|
||||
if stale_mask(doc, n) {
|
||||
return true;
|
||||
}
|
||||
let old_mask = old.and_then(|o| o.extras().mask.as_ref()).map(|m| &m.art);
|
||||
if n.extras().mask.as_ref().is_some_and(|m| stale_in(doc, &m.art, old_mask)) {
|
||||
return true;
|
||||
}
|
||||
let Some(children) = n.children() else { return false };
|
||||
let before = old.and_then(|o| o.children()).map_or(&[][..], Vec::as_slice);
|
||||
let mut by_id: Option<HashMap<NodeId, &Arc<Node>>> = None;
|
||||
children.iter().enumerate().any(|(i, c)| {
|
||||
let b = match before.get(i) {
|
||||
Some(b) if b.id == c.id => Some(b),
|
||||
_ => by_id.get_or_insert_with(|| before.iter().map(|b| (b.id, b)).collect()).get(&c.id).copied(),
|
||||
};
|
||||
stale_in(doc, c, b)
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether `n` is an image whose color mask was not made from its own pixels (or whose masked
|
||||
/// pixels `doc` doesn't have). A mask that hides nothing (its masked pixels are the image's own)
|
||||
/// is up to date while the image's key stays the same, even when its pixels are missing.
|
||||
fn stale_mask(doc: &Document, n: &Node) -> bool {
|
||||
matches!((&n.kind, Document::color_mask_of(n)), (NodeKind::Image(im), Some(m))
|
||||
if im.pixels.is_none() && !(m.made_for(im) && (m.key == im.key || doc.images.contains_key(&m.key))))
|
||||
}
|
||||
|
||||
/// One [`refresh`] pass: the masks made so far, by the image's pixels, the mask's settings and
|
||||
/// whether a process color colorizes the image (an image shown by many objects is decoded once),
|
||||
/// and the masked pixels to add to the document.
|
||||
struct Refresh<'a> {
|
||||
doc: &'a Document,
|
||||
made: HashMap<(String, String, bool), ColorMask>,
|
||||
blobs: Vec<(String, ImageBlob)>,
|
||||
}
|
||||
|
||||
impl Refresh<'_> {
|
||||
/// Make the masks out of date in `n` (a copy of the document's art) again.
|
||||
fn fix(&mut self, n: &mut Node) {
|
||||
if let Some(mask) = self.made(n) {
|
||||
n.extras_mut().color_mask = Some(Box::new(mask));
|
||||
}
|
||||
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut())
|
||||
&& stale_in(self.doc, &m.art, None)
|
||||
{
|
||||
self.fix(Arc::make_mut(&mut m.art));
|
||||
}
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
if stale_in(self.doc, c, None) {
|
||||
self.fix(Arc::make_mut(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The mask of image node `n` made again for the image's own pixels ([`fresh`]), `None` when
|
||||
/// it is up to date. A mask that can't be made for them (every pixel hidden, CMYK pixels, a
|
||||
/// linked preview, too large, gray pixels with transparency under a process color) keeps its
|
||||
/// settings and hides nothing: the image shows its own pixels, and setting the mask again
|
||||
/// reports why.
|
||||
fn made(&mut self, n: &Node) -> Option<ColorMask> {
|
||||
let (NodeKind::Image(im), Some(m)) = (&n.kind, Document::color_mask_of(n)) else { return None };
|
||||
if !stale_mask(self.doc, n) {
|
||||
return None;
|
||||
}
|
||||
let settings = ColorMask { key: String::new(), from: String::new(), ..m.clone() };
|
||||
let at = (im.key.clone(), format!("{settings:?}"), colorized_with_process(self.doc, n));
|
||||
if let Some(made) = self.made.get(&at) {
|
||||
return Some(made.clone());
|
||||
}
|
||||
let hides_nothing = || ColorMask { key: im.key.clone(), from: im.key.clone(), ..m.clone() };
|
||||
let mask = match fresh(self.doc, n, COLOR_MASK)? {
|
||||
Ok((mask, blob)) if refused_colorize(self.doc, n, blob.as_ref().or_else(|| self.doc.images.get(&mask.key))) => hides_nothing(),
|
||||
Ok((mask, blob)) => {
|
||||
self.blobs.extend(blob.map(|b| (mask.key.clone(), b)));
|
||||
mask
|
||||
}
|
||||
Err(_) => hides_nothing(),
|
||||
};
|
||||
self.made.insert(at, mask.clone());
|
||||
Some(mask)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -392,7 +392,7 @@ fn the_color_profile_is_kept() {
|
||||
encoder.set_icc_profile(icc.clone()).unwrap();
|
||||
DynamicImage::ImageRgba8(framed()).write_with_encoder(encoder).unwrap();
|
||||
let blob = ImageBlob::png(bytes);
|
||||
let out = super::clear(&blob, &settings(Background::Edges), None).unwrap().unwrap();
|
||||
let out = super::clear(&blob, &settings(Background::Edges), None, super::C).unwrap().unwrap();
|
||||
let mut decoder = image::codecs::png::PngDecoder::new(Cursor::new(out.blob.bytes.as_slice())).unwrap();
|
||||
assert_eq!(decoder.icc_profile().unwrap(), Some(icc));
|
||||
}
|
||||
|
||||
@@ -123,6 +123,10 @@ fn node_refs(n: &Node, f: Visit) {
|
||||
if let Some(mask) = &m.extras().mask {
|
||||
node_refs(&mask.art, f);
|
||||
}
|
||||
// A color-masked image's masked pixels go with it.
|
||||
if let Some(cm) = Document::color_mask_of(m) {
|
||||
f(Ref::Name(Res::Image, &cm.key));
|
||||
}
|
||||
match &m.kind {
|
||||
NodeKind::Text(t) => {
|
||||
for r in &t.runs {
|
||||
@@ -663,6 +667,10 @@ impl Relink {
|
||||
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) {
|
||||
self.node(Arc::make_mut(&mut m.art));
|
||||
}
|
||||
if let Some(cm) = n.extras_if_any().and_then(|e| e.color_mask.as_deref_mut()) {
|
||||
self.name(Res::Image, &mut cm.key);
|
||||
self.name(Res::Image, &mut cm.from);
|
||||
}
|
||||
match &mut n.kind {
|
||||
NodeKind::Text(t) => {
|
||||
for r in &mut t.runs {
|
||||
|
||||
@@ -884,8 +884,8 @@ fn blended(n: &Node) -> bool {
|
||||
n.opacity < 1.0 || n.blend != BlendMode::Normal
|
||||
}
|
||||
|
||||
/// Whether `n` (art of `d`) is or holds an image with see-through pixels (key in `keys`), a
|
||||
/// colorize paint that `colorize` counts, opacity or a blending mode of its own or of a group in
|
||||
/// Whether `n` (art of `d`) is or holds an image with see-through pixels (key in `keys`, a
|
||||
/// color-masked image's masked pixels counted as its own), a colorize paint that `colorize` counts, opacity or a blending mode of its own or of a group in
|
||||
/// `n`, or anywhere when `on`, also in the tile art of the pattern fills of the paths in `n`, down
|
||||
/// to `levels` patterns. Hidden art doesn't count.
|
||||
fn transparent_image(d: &Document, n: &Node, keys: &BTreeSet<String>, colorize: &dyn Fn(&Document, &Node) -> bool, on: bool, levels: u32) -> bool {
|
||||
@@ -894,7 +894,9 @@ fn transparent_image(d: &Document, n: &Node, keys: &BTreeSet<String>, colorize:
|
||||
}
|
||||
let on = on || blended(n);
|
||||
match &n.kind {
|
||||
NodeKind::Image(im) => on || keys.contains(&im.key) || (n.extras().colorize.is_some() && colorize(d, n)),
|
||||
NodeKind::Image(im) => {
|
||||
on || keys.contains(&im.key) || keys.contains(&d.shown_image(n, im).key) || (n.extras().colorize.is_some() && colorize(d, n))
|
||||
}
|
||||
NodeKind::Path { .. } | NodeKind::Compound { .. } => {
|
||||
n.appearance.items.iter().any(|i| pattern_fill_holds_transparent_image(d, i, keys, colorize, on, levels))
|
||||
}
|
||||
|
||||
@@ -185,7 +185,8 @@ fn expand_image_patterns(d: &mut Document) -> Result<()> {
|
||||
}
|
||||
|
||||
/// The images of `d` a legacy `.ai` can't hold, in paint order with their places in it (as
|
||||
/// [`Document::walk`] counts). These have see-through pixels or a colorize paint (except the
|
||||
/// [`Document::walk`] counts). These have see-through pixels (a color-masked image's masked pixels
|
||||
/// counted as its own) or a colorize paint (except the
|
||||
/// colorized images the writer writes as image masks, clear pixels included,
|
||||
/// [`vectorcraft_eps::writes_image_mask`]), or
|
||||
/// have opacity or a blending mode of their own or, with `flatten`, of a group they are in.
|
||||
@@ -197,7 +198,8 @@ fn see_through_images(d: &Document, mode: SeeThrough) -> Vec<(usize, NodeId)> {
|
||||
let mut at = 0;
|
||||
d.walk(|n| {
|
||||
if let NodeKind::Image(im) = &n.kind
|
||||
&& (((keys.contains(&im.key) || n.extras().colorize.is_some()) && !vectorcraft_eps::writes_image_mask(d, n))
|
||||
&& (((keys.contains(&im.key) || keys.contains(&d.shown_image(n, im).key) || n.extras().colorize.is_some())
|
||||
&& !vectorcraft_eps::writes_image_mask(d, n))
|
||||
|| n.opacity < 1.0
|
||||
|| n.blend != BlendMode::Normal
|
||||
|| (mode == SeeThrough::Flatten && transparent(d, n.id)))
|
||||
|
||||
@@ -76,12 +76,13 @@ pub(crate) fn see_through_images(doc: &Document) -> BTreeSet<String> {
|
||||
doc.images.iter().filter(|(_, b)| see_through(b)).map(|(k, _)| k.clone()).collect()
|
||||
}
|
||||
|
||||
/// `n` shows an image of `keys` (itself or inside).
|
||||
/// `n` shows an image of `keys` (itself or inside), a color-masked image's masked pixels counted as
|
||||
/// what it shows.
|
||||
pub(super) fn draws_image(n: &Node, keys: &BTreeSet<String>) -> bool {
|
||||
!keys.is_empty()
|
||||
&& n.visible
|
||||
&& match &n.kind {
|
||||
NodeKind::Image(im) => keys.contains(&im.key),
|
||||
NodeKind::Image(im) => keys.contains(&im.key) || Document::color_mask_of(n).is_some_and(|m| m.made_for(im) && keys.contains(&m.key)),
|
||||
_ => n.children().is_some_and(|ch| ch.iter().any(|c| draws_image(c, keys))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,8 @@ impl FlattenOptions {
|
||||
/// 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);
|
||||
let shown = d.shown_image(n, im);
|
||||
let blob = d.image_blob(&shown);
|
||||
if blob.is_some_and(|b| !b.takes_colorize()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1060,6 +1060,8 @@ pub fn resolve(d: &mut Document, base: LinkBase, update: bool, low_res_proxy_eps
|
||||
};
|
||||
Nested { pick: &pick, make: &mut make, next: 0 }.run(d);
|
||||
}
|
||||
// Color masks of images whose files were read again are made for the new pixels.
|
||||
super::refresh_color_masks(None, d);
|
||||
out
|
||||
}
|
||||
|
||||
|
||||
@@ -1053,6 +1053,11 @@ fn crop_image(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
bare.blend = Default::default();
|
||||
bare.appearance = Appearance::default().into();
|
||||
bare.visible = true;
|
||||
// The image's own pixels are cut, not the masked ones a color mask shows: the mask is made
|
||||
// again for the cut pixels after the edit.
|
||||
if let Some(e) = bare.extras_if_any() {
|
||||
e.color_mask = None;
|
||||
}
|
||||
let tmp = isolated_doc(&st.doc, vec![bare]);
|
||||
let region = Rect::new(r.x0, r.y0, r.x0 + (r.width() * scale).round().max(1.0) / scale, r.y0 + (r.height() * scale).round().max(1.0) / scale);
|
||||
let (png, w, h, warning) =
|
||||
|
||||
@@ -701,7 +701,9 @@ fn colorized_images(d: &Document, ids: &[NodeId], paint: &Paint) -> std::result:
|
||||
out.push(*id);
|
||||
continue;
|
||||
}
|
||||
let Some(gray) = d.image_blob(im).and_then(|b| b.gray_source()) else { continue };
|
||||
// A color-masked image is colorized as it shows: its masked pixels, which have an alpha
|
||||
// channel.
|
||||
let Some(gray) = d.image_blob(&d.shown_image(n, im)).and_then(|b| b.gray_source()) else { continue };
|
||||
match paint {
|
||||
// Without a color, `none` goes to the image's fill item.
|
||||
Paint::None => continue,
|
||||
@@ -758,11 +760,17 @@ pub(super) fn check_colorized_kept(was_ok: bool, d: &Document) -> std::result::R
|
||||
if was_ok { check_colorized(d) } else { Ok(()) }
|
||||
}
|
||||
|
||||
/// How the pixels of `n` are gray, when `n` is a grayscale or 1-bit image colorized with a color
|
||||
/// that [`colorizes_transparency`] refuses. Such an image can't have an alpha channel.
|
||||
/// Is `n` an image colorized with a color that [`colorizes_transparency`] refuses? When its pixels
|
||||
/// are grayscale or 1-bit, they can't have an alpha channel.
|
||||
pub(super) fn colorized_with_process(d: &Document, n: &Node) -> bool {
|
||||
matches!(n.kind, NodeKind::Image(_)) && n.extras().colorize.as_deref().is_some_and(|paint| !colorizes_transparency(d, paint))
|
||||
}
|
||||
|
||||
/// How the pixels `n` shows are gray, when `n` is a grayscale or 1-bit image colorized with a
|
||||
/// color that [`colorizes_transparency`] refuses ([`colorized_with_process`]).
|
||||
pub(super) fn gray_colorized_with_process(d: &Document, n: &Node) -> Option<GraySource> {
|
||||
let (NodeKind::Image(im), Some(paint)) = (&n.kind, n.extras().colorize.as_deref()) else { return None };
|
||||
if colorizes_transparency(d, paint) { None } else { d.image_blob(im).and_then(|b| b.gray_source()) }
|
||||
let NodeKind::Image(im) = &n.kind else { return None };
|
||||
if colorized_with_process(d, n) { d.image_blob(&d.shown_image(n, im)).and_then(|b| b.gray_source()) } else { None }
|
||||
}
|
||||
|
||||
/// Is `e` the refusal of [`check_colorized`]?
|
||||
|
||||
@@ -78,6 +78,12 @@ fn refs(n: &mut Node, f: &mut impl FnMut(Ref)) {
|
||||
NodeKind::SymbolInstance { symbol, .. } => f(Ref::Symbol(symbol)),
|
||||
_ => {}
|
||||
}
|
||||
// A color-masked image's masked pixels come along, and the key they were made from
|
||||
// follows the image's.
|
||||
if let Some(cm) = n.extras_if_any().and_then(|e| e.color_mask.as_deref_mut()) {
|
||||
f(Ref::Image(&mut cm.key));
|
||||
f(Ref::Image(&mut cm.from));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,10 @@ pub(super) const ALLOWED: &[&str] = &[
|
||||
"addon.contour.set",
|
||||
"addon.cutting.bridges",
|
||||
"addon.cutting.weed",
|
||||
"addon.image.applyColorMask",
|
||||
"addon.image.colorMask",
|
||||
"addon.image.removeBackground",
|
||||
"addon.image.removeColorMask",
|
||||
"addon.pathtools.addPoints",
|
||||
"addon.pathtools.close",
|
||||
"addon.pathtools.corner",
|
||||
|
||||
@@ -31,6 +31,8 @@ impl Session {
|
||||
}
|
||||
// Pixel-aligned objects the edit changed go back on the pixel grid.
|
||||
cmd::refresh_pixel_aligned(&before, doc);
|
||||
// Color masks of images whose own pixels the edit replaced are made for the new ones.
|
||||
cmd::refresh_color_masks(Some(&*before), doc);
|
||||
let placed = cmd::objects_on_path::refresh(&before, doc);
|
||||
// Pixel-aligned members that placing moved go back on the pixel grid.
|
||||
cmd::keep_aligned_in(doc, &placed);
|
||||
|
||||
@@ -644,7 +644,7 @@ impl<'a> Writer<'a> {
|
||||
}
|
||||
NodeKind::Compound { rule, .. } => self.shape(n, &compound_path(n), *rule, xy),
|
||||
NodeKind::Text(_) => self.text(n, xy),
|
||||
NodeKind::Image(im) => self.image(im, xy),
|
||||
NodeKind::Image(im) => self.image(&self.doc.shown_image(n, im), xy),
|
||||
NodeKind::SymbolInstance { symbol, xf } if self.live_instance_in_room(n, symbol, *xf) => self.symbol_instance(symbol, *xf, xy),
|
||||
NodeKind::SymbolInstance { symbol, xf } => {
|
||||
if !self.open_group("u") {
|
||||
|
||||
@@ -179,7 +179,7 @@ pub fn writes_image_mask(doc: &Document, n: &Node) -> bool {
|
||||
matches!(n.extras().colorize.as_deref(), Some(Paint::Solid { .. }))
|
||||
&& !n.extras().mask.as_ref().is_some_and(|m| !m.disabled)
|
||||
&& doc
|
||||
.image_blob(im)
|
||||
.image_blob(&doc.shown_image(n, im))
|
||||
.is_some_and(|b| b.takes_colorize() && fits(&b.bytes) && b.coverage().is_some_and(|(_, _, c)| c.iter().all(|&v| v == 0 || v == 255)))
|
||||
}
|
||||
|
||||
|
||||
@@ -283,7 +283,8 @@ impl<'a> Writer<'a> {
|
||||
}
|
||||
NodeKind::Image(im) => {
|
||||
self.lock(locked);
|
||||
let blob = self.doc.image_blob(im);
|
||||
let shown = self.doc.shown_image(n, im);
|
||||
let blob = self.doc.image_blob(&shown);
|
||||
// A colorize color the renderer draws the image in.
|
||||
let colorize =
|
||||
n.extras().colorize.as_deref().filter(|p| matches!(p, Paint::Solid { .. }) && blob.is_some_and(ImageBlob::takes_colorize));
|
||||
|
||||
@@ -367,6 +367,8 @@ impl<'a> Scene<'a> {
|
||||
}
|
||||
NodeKind::Text(t) => self.text(n, t),
|
||||
NodeKind::Image(im) => {
|
||||
let shown = self.doc.shown_image(n, im);
|
||||
let im = &*shown;
|
||||
let blob = self.doc.image_blob(im);
|
||||
match n.extras().colorize.as_deref() {
|
||||
Some(p @ Paint::Solid { .. }) if blob.is_none_or(|b| b.gray_source().is_some()) => {
|
||||
|
||||
@@ -213,7 +213,7 @@ impl<'a> Scene<'a> {
|
||||
self.node(&o, true);
|
||||
}
|
||||
}
|
||||
NodeKind::Image(im) => self.image(im),
|
||||
NodeKind::Image(im) => self.image(&self.doc.shown_image(n, im)),
|
||||
NodeKind::SymbolInstance { symbol, xf } => {
|
||||
if let Some(sym) = self.doc.symbols.iter().find(|s| &s.name == symbol)
|
||||
&& self.nest < MAX_NEST
|
||||
|
||||
@@ -165,6 +165,9 @@ impl Exporter<'_> {
|
||||
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 };
|
||||
// A color-masked image in the mask's art masks with the pixels it shows.
|
||||
let shown = self.doc.shown_image(art, mi);
|
||||
let mi = &*shown;
|
||||
let plain =
|
||||
art.opacity >= 1.0 && art.blend == BlendMode::Normal && art.extras().mask.is_none() && !art.appearance.effects.iter().any(|e| e.visible);
|
||||
if m.invert || !plain || (mi.xf, mi.width, mi.height) != (im.xf, im.width, im.height) {
|
||||
|
||||
@@ -249,8 +249,9 @@ impl Exporter<'_> {
|
||||
self.shape(s, n, &bp, *rule, page);
|
||||
}
|
||||
NodeKind::Text(t) => self.text(s, n, t, page),
|
||||
NodeKind::Image(im) => match n.extras().colorize.as_deref() {
|
||||
Some(p @ Paint::Solid { .. }) if self.doc.image_blob(im).is_none_or(vectorcraft_doc::ImageBlob::takes_colorize) => {
|
||||
NodeKind::Image(im) => match (n.extras().colorize.as_deref(), self.doc.shown_image(n, im)) {
|
||||
(Some(p @ Paint::Solid { .. }), im) if self.doc.image_blob(&im).is_none_or(vectorcraft_doc::ImageBlob::takes_colorize) => {
|
||||
let im = &*im;
|
||||
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);
|
||||
@@ -263,7 +264,7 @@ impl Exporter<'_> {
|
||||
self.warn(super::exporter::UNWRITABLE_IMAGE);
|
||||
}
|
||||
}
|
||||
_ => self.image(s, im, alpha, n.extras().pixelated),
|
||||
(_, im) => self.image(s, &im, alpha, n.extras().pixelated),
|
||||
},
|
||||
NodeKind::SymbolInstance { symbol, xf } => {
|
||||
// Within the objects a frame may draw of symbol instances, as on screen: nested
|
||||
|
||||
@@ -153,25 +153,34 @@ fn colorized_key(key: &str) -> String {
|
||||
}
|
||||
|
||||
/// Point the colorized images in `n`'s subtree at [`colorized_key`], collecting their keys in
|
||||
/// `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.
|
||||
/// `keys`. A colorized image with a color mask is separated as it shows: its masked pixels
|
||||
/// ([`Document::shown_image`]). An image whose pixels it shows 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())
|
||||
{
|
||||
let shown = match &node.kind {
|
||||
NodeKind::Image(im) if colorized(node) => {
|
||||
let mask = Document::color_mask_of(node).filter(|m| m.made_for(im) && images.contains_key(&m.key));
|
||||
Some(mask.map_or(&im.key, |m| &m.key).clone())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if shown.as_ref().and_then(|key| images.get(key)).is_some_and(|b| !b.takes_colorize()) {
|
||||
node.extras_mut().colorize = None;
|
||||
}
|
||||
if colorized(node)
|
||||
&& let NodeKind::Image(im) = &mut node.kind
|
||||
{
|
||||
keys.insert(im.key.clone());
|
||||
im.key = colorized_key(&im.key);
|
||||
if let Some(key) = shown.filter(|_| colorized(node)) {
|
||||
if let Some(e) = node.extras_if_any() {
|
||||
e.color_mask = None;
|
||||
}
|
||||
if let NodeKind::Image(im) = &mut node.kind {
|
||||
im.key = colorized_key(&key);
|
||||
keys.insert(key);
|
||||
}
|
||||
}
|
||||
if let Some(ch) = node.children_mut() {
|
||||
for c in ch.iter_mut() {
|
||||
|
||||
@@ -380,6 +380,11 @@ fn the_output_settings_convert_cmyk_images_and_compression_recompresses_them_in_
|
||||
/// A Press Quality export of a 2 × 2 CMYK image masked by PNG `mask` on its own pixel grid
|
||||
/// (`clip`, `invert`): the file, and the decoded soft mask of each CMYK image (`None`: it has none).
|
||||
fn export_masked(mask: Vec<u8>, clip: bool, invert: bool) -> (Vec<u8>, Vec<Option<Vec<u8>>>) {
|
||||
export_smasks(&masked_doc(mask, clip, invert))
|
||||
}
|
||||
|
||||
/// A document of a 2 × 2 CMYK image masked by PNG `mask` (key `alpha`) on its own pixel grid.
|
||||
fn masked_doc(mask: Vec<u8>, clip: bool, invert: bool) -> Document {
|
||||
let ink = [0u8, 102, 255, 0];
|
||||
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(ink, 4)).unwrap()).unwrap());
|
||||
@@ -400,8 +405,14 @@ fn export_masked(mask: Vec<u8>, clip: bool, invert: bool) -> (Vec<u8>, Vec<Optio
|
||||
let n = Node::new(d.alloc_id(), image("img")).with_mask(Some(Box::new(mask)));
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
/// A Press Quality export of `d`: the file, and the decoded soft mask of each CMYK image (`None`:
|
||||
/// it has none).
|
||||
fn export_smasks(d: &Document) -> (Vec<u8>, Vec<Option<Vec<u8>>>) {
|
||||
let press = builtin_presets().into_iter().find(|p| p.name == "Press Quality").unwrap().settings;
|
||||
let out = export_with_report(&d, &PdfOptions { settings: press, created: Some(1_700_000_000), ..Default::default() }).unwrap();
|
||||
let out = export_with_report(d, &PdfOptions { settings: press, created: Some(1_700_000_000), ..Default::default() }).unwrap();
|
||||
let file = Pdf::new(Arc::new(out.bytes.clone())).unwrap();
|
||||
let smasks = file
|
||||
.objects()
|
||||
@@ -440,6 +451,34 @@ 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 grey mask image with a color mask masks the CMYK image with the pixels it shows: its hidden
|
||||
/// pixels are clear in the image's `/SMask`.
|
||||
#[test]
|
||||
fn a_color_masked_grid_mask_image_writes_the_pixels_it_shows_as_the_smask() {
|
||||
let mut d = masked_doc(png(image::GrayImage::from_raw(2, 2, vec![255, 128, 0, 64]).unwrap()), true, false);
|
||||
// The last pixel hidden.
|
||||
d.images.insert(
|
||||
"shown".into(),
|
||||
ImageBlob::new("image/png", png(image::GrayAlphaImage::from_raw(2, 2, vec![255, 255, 128, 255, 0, 255, 64, 0]).unwrap())),
|
||||
);
|
||||
let id = d.layers[0].children().unwrap()[0].id;
|
||||
let mut mask = d.node(id).unwrap().extras().mask.clone().unwrap();
|
||||
let art = std::sync::Arc::make_mut(&mut mask.art);
|
||||
art.extras_mut().color_mask = Some(Box::new(vectorcraft_doc::color_mask::ColorMask {
|
||||
key: "shown".into(),
|
||||
from: "alpha".into(),
|
||||
background: vectorcraft_doc::color_mask::MaskBackground::Colors,
|
||||
colors: vec![[64, 64, 64]],
|
||||
contiguous: false,
|
||||
measure: vectorcraft_doc::color_mask::MaskMatch::Distance,
|
||||
tolerance: 0.0,
|
||||
softness: 0.0,
|
||||
}));
|
||||
d.node_mut(id).unwrap().extras_mut().mask = Some(mask);
|
||||
let (_, smasks) = export_smasks(&d);
|
||||
assert_eq!(smasks, [Some(vec![255, 128, 0, 0])]);
|
||||
}
|
||||
|
||||
/// 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]
|
||||
|
||||
@@ -168,6 +168,76 @@ fn separations_print_the_image_on_its_ink_plate() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Printed separations separate a colorized image with a color mask as it shows: the pixels the
|
||||
/// mask hides print on no plate.
|
||||
#[test]
|
||||
fn separations_print_a_color_masked_image_as_it_shows() {
|
||||
let spot = Paint::Solid { color: Color::cmyk(0.0, 1.0, 1.0, 0.0), swatch: Some("Ink".into()), tint: 1.0 };
|
||||
let mut d = doc(ColorMode::Cmyk, Some(spot));
|
||||
// The black pixel hidden.
|
||||
let mut png = vec![];
|
||||
image::GrayAlphaImage::from_raw(2, 1, vec![0, 0, 255, 255])
|
||||
.unwrap()
|
||||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
d.images.insert("shown".into(), ImageBlob::png(png));
|
||||
let id = d.layers[0].children().unwrap()[0].id;
|
||||
d.node_mut(id).unwrap().extras_mut().color_mask = Some(Box::new(vectorcraft_doc::color_mask::ColorMask {
|
||||
key: "shown".into(),
|
||||
from: "pic".into(),
|
||||
background: vectorcraft_doc::color_mask::MaskBackground::Colors,
|
||||
colors: vec![[0, 0, 0]],
|
||||
contiguous: false,
|
||||
measure: vectorcraft_doc::color_mask::MaskMatch::Distance,
|
||||
tolerance: 0.0,
|
||||
softness: 0.0,
|
||||
}));
|
||||
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!("an image") };
|
||||
assert_eq!(p.image_blob(im).and_then(|b| b.coverage()).unwrap().2, [0, 0], "the hidden black pixel prints on no plate");
|
||||
}
|
||||
}
|
||||
|
||||
/// A colorized image with a color mask keeps its colorize paint on the plates when its masked
|
||||
/// pixels take it, as on the canvas, though its own pixels are a preview that doesn't.
|
||||
#[test]
|
||||
fn separations_colorize_masked_pixels_whose_own_pixels_are_a_preview() {
|
||||
let spot = Paint::Solid { color: Color::cmyk(0.0, 1.0, 1.0, 0.0), swatch: Some("Ink".into()), tint: 1.0 };
|
||||
let mut d = doc(ColorMode::Cmyk, Some(spot));
|
||||
let preview = Arc::new(vectorcraft_doc::links::missing_placeholder().unwrap());
|
||||
d.images.insert("pic".into(), ImageBlob { mime: "image/png".into(), bytes: preview.clone(), proxy: Some(preview) });
|
||||
let mut png = vec![];
|
||||
image::GrayAlphaImage::from_raw(2, 1, vec![0, 0, 255, 255])
|
||||
.unwrap()
|
||||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
d.images.insert("shown".into(), ImageBlob::png(png));
|
||||
let id = d.layers[0].children().unwrap()[0].id;
|
||||
d.node_mut(id).unwrap().extras_mut().color_mask = Some(Box::new(vectorcraft_doc::color_mask::ColorMask {
|
||||
key: "shown".into(),
|
||||
from: "pic".into(),
|
||||
background: vectorcraft_doc::color_mask::MaskBackground::Colors,
|
||||
colors: vec![[0, 0, 0]],
|
||||
contiguous: false,
|
||||
measure: vectorcraft_doc::color_mask::MaskMatch::Distance,
|
||||
tolerance: 0.0,
|
||||
softness: 0.0,
|
||||
}));
|
||||
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!("an image") };
|
||||
assert!(n.extras().colorize.is_some(), "the masked pixels take the colorize paint");
|
||||
assert_eq!(p.image_blob(im).and_then(|b| b.coverage()).unwrap().2, [0, 0], "separated as the masked pixels");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
@@ -662,14 +662,20 @@ impl Renderer {
|
||||
// shortcut: art kept by `fx_arts` isn't made again when only a swatch's kind (spot or
|
||||
// process) changes, which Duotone reads; editing the object makes it again.
|
||||
let swatches = |name: &str| effects::addons::duotone::swatch_kind(doc, name);
|
||||
let adjusted = effects::adjust(&art, outer, &swatches, &mut |key, map| {
|
||||
let mut h = std::hash::DefaultHasher::new();
|
||||
std::hash::Hash::hash(&format!("{map:?}"), &mut h);
|
||||
let hash = std::hash::Hasher::finish(&h);
|
||||
let derived = format!("{key}\u{0}adjust\u{0}{hash:016x}");
|
||||
wanted.push((key.to_string(), derived.clone(), hash, map.clone()));
|
||||
Some(derived)
|
||||
});
|
||||
let adjusted = effects::adjust(
|
||||
&art,
|
||||
outer,
|
||||
&swatches,
|
||||
&mut |key, map| {
|
||||
let mut h = std::hash::DefaultHasher::new();
|
||||
std::hash::Hash::hash(&format!("{map:?}"), &mut h);
|
||||
let hash = std::hash::Hasher::finish(&h);
|
||||
let derived = format!("{key}\u{0}adjust\u{0}{hash:016x}");
|
||||
wanted.push((key.to_string(), derived.clone(), hash, map.clone()));
|
||||
Some(derived)
|
||||
},
|
||||
&|key| doc.images.contains_key(key),
|
||||
);
|
||||
for (key, derived, hash, map) in wanted {
|
||||
self.adjusted_image(&key, derived, hash, &map);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::images;
|
||||
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())
|
||||
&& doc.image_blob(&doc.shown_image(n, im)).is_some_and(|b| !b.takes_colorize())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -250,7 +250,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(f.doc, a, f.ink), a.extras().pixelated);
|
||||
self.draw_image_in(ctx, f, &f.doc.shown_image(a, im), Some(®ion), images::image_tint(f.doc, a, f.ink), a.extras().pixelated);
|
||||
self.stats.drawn += 1;
|
||||
return;
|
||||
}
|
||||
@@ -518,7 +518,8 @@ 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(f.doc, image, f.ink), image.extras().pixelated);
|
||||
let im = f.doc.shown_image(image, im);
|
||||
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;
|
||||
}
|
||||
@@ -568,7 +569,9 @@ 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(f.doc, n, f.ink), n.extras().pixelated),
|
||||
NodeKind::Image(im) => {
|
||||
self.draw_image_in(ctx, f, &f.doc.shown_image(n, 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
|
||||
|
||||
@@ -767,6 +767,8 @@ impl Writer<'_> {
|
||||
}
|
||||
NodeKind::Text(t) => self.text_node(n, t),
|
||||
NodeKind::Image(im) => {
|
||||
let shown = self.doc.shown_image(n, im);
|
||||
let im = &*shown;
|
||||
if let Some(Paint::Solid { color, .. }) = n.extras().colorize.as_deref()
|
||||
&& let Some(blob) = self.doc.image_blob(im)
|
||||
&& blob.gray_source().is_some()
|
||||
|
||||
@@ -42,6 +42,8 @@ impl Writer<'_> {
|
||||
art.walk(&mut |n| {
|
||||
let NodeKind::Image(im) = &n.kind else { return };
|
||||
let Some(Paint::Solid { color, .. }) = n.extras().colorize.as_deref() else { return };
|
||||
let shown = self.doc.shown_image(n, im);
|
||||
let im = &*shown;
|
||||
if self.doc.image_blob(im).is_none_or(|blob| blob.gray_source().is_none()) {
|
||||
return;
|
||||
}
|
||||
@@ -176,12 +178,14 @@ impl Writer<'_> {
|
||||
let mut seen: HashMap<(String, u32, u32), (usize, vectorcraft_doc::ImageObject)> = HashMap::new();
|
||||
art.walk(&mut |c| {
|
||||
if let NodeKind::Image(im) = &c.kind {
|
||||
let shown = self.doc.shown_image(c, im);
|
||||
let im = &*shown;
|
||||
if matches!(c.extras().colorize.as_deref(), Some(Paint::Solid { .. }))
|
||||
&& self.doc.image_blob(im).is_none_or(|b| b.gray_source().is_some())
|
||||
{
|
||||
return;
|
||||
}
|
||||
seen.entry((im.key.clone(), im.width, im.height)).or_insert_with(|| (0, (**im).clone())).0 += 1;
|
||||
seen.entry((im.key.clone(), im.width, im.height)).or_insert_with(|| (0, im.clone())).0 += 1;
|
||||
}
|
||||
});
|
||||
let mut repeated: Vec<_> = seen.into_iter().filter(|(k, (n, _))| *n > 1 && !self.shared_images.contains_key(k)).collect();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Image Tools in the interface: its submenu (Object › Plug-ins › Image Tools). Remove Background's
|
||||
//! menu item opens the generic command dialog (`ui.paramDialog`) on its options, with a Preview
|
||||
//! (`dialogs::command`).
|
||||
//! (`dialogs::command`). Color Mask's opens the same dialog on the selected image's mask settings
|
||||
//! (the command's report, see `menus::invoke`), or the defaults for an image without one.
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
@@ -8,18 +9,29 @@ use crate::menus::Item;
|
||||
|
||||
/// Remove Background's command.
|
||||
pub const REMOVE_BACKGROUND: &str = "addon.image.removeBackground";
|
||||
/// Remove Background › Background: what the command's `background` takes, by name.
|
||||
/// Color Mask's command.
|
||||
pub const COLOR_MASK: &str = "addon.image.colorMask";
|
||||
/// Remove Color Mask's command.
|
||||
pub const REMOVE_COLOR_MASK: &str = "addon.image.removeColorMask";
|
||||
/// Apply Color Mask's command.
|
||||
pub const APPLY_COLOR_MASK: &str = "addon.image.applyColorMask";
|
||||
/// Remove Background and Color Mask › Background: what the commands' `background` takes, by name.
|
||||
pub const BACKGROUNDS: &[(&str, &str)] = &[("Edge Color", "edges"), ("Corner Color", "corners"), ("Picked Colors", "colors")];
|
||||
/// Remove Background › Match: what the command's `match` takes, by name.
|
||||
/// Remove Background and Color Mask › Match: what the commands' `match` takes, by name.
|
||||
pub const MATCHES: &[(&str, &str)] = &[("Distance", "distance"), ("Each Channel", "channels")];
|
||||
|
||||
/// Object › Plug-ins › Image Tools.
|
||||
pub fn menu() -> Vec<Item> {
|
||||
vec![Item::Cmd(
|
||||
"Remove Background…",
|
||||
REMOVE_BACKGROUND,
|
||||
json!({"background": "edges", "colors": "#ffffff", "contiguous": true, "match": "distance", "tolerance": 10, "softness": 10}),
|
||||
)]
|
||||
vec![
|
||||
Item::Cmd(
|
||||
"Remove Background…",
|
||||
REMOVE_BACKGROUND,
|
||||
json!({"background": "edges", "colors": "#ffffff", "contiguous": true, "match": "distance", "tolerance": 10, "softness": 10}),
|
||||
),
|
||||
Item::Cmd("Color Mask…", COLOR_MASK, json!({})),
|
||||
Item::Cmd("Remove Color Mask", REMOVE_COLOR_MASK, json!({})),
|
||||
Item::Cmd("Apply Color Mask", APPLY_COLOR_MASK, json!({})),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -41,13 +41,75 @@ fn alphas(app: &VectorcraftApp) -> [u8; 3] {
|
||||
}
|
||||
|
||||
fn open(app: &mut VectorcraftApp) {
|
||||
let entries = menus::menu_entries(app);
|
||||
let item = entries.iter().find(|e| e.path == ["Object", "Plug-ins", "Image Tools"] && e.label == "Remove Background…").unwrap();
|
||||
assert!(item.enabled);
|
||||
click(app, "Remove Background…");
|
||||
}
|
||||
|
||||
/// Choose Object › Plug-ins › Image Tools › `label`, which must be enabled.
|
||||
fn click(app: &mut VectorcraftApp, label: &str) {
|
||||
let item = menu_item(app, label);
|
||||
assert!(item.enabled, "{label} enabled");
|
||||
let (id, p) = menus::click_target(&item.label, item.command.as_deref().unwrap(), &item.params);
|
||||
menus::invoke(app, &id, p);
|
||||
}
|
||||
|
||||
fn menu_item(app: &VectorcraftApp, label: &str) -> menus::MenuEntry {
|
||||
menus::menu_entries(app).into_iter().find(|e| e.path == ["Object", "Plug-ins", "Image Tools"] && e.label == label).unwrap()
|
||||
}
|
||||
|
||||
/// The selected image's color mask, if it has one.
|
||||
fn mask(app: &VectorcraftApp) -> Option<vectorcraft_doc::color_mask::ColorMask> {
|
||||
let st = app.session.doc().unwrap();
|
||||
st.selection.objects.first().and_then(|id| st.doc.node(*id)).and_then(vectorcraft_doc::Document::color_mask_of).cloned()
|
||||
}
|
||||
|
||||
/// The alpha of the selected image's masked pixels at the corner, on the ring and in the block
|
||||
/// (all 255 without a mask).
|
||||
fn masked_alphas(app: &VectorcraftApp) -> [u8; 3] {
|
||||
let Some(m) = mask(app) else { return [255; 3] };
|
||||
let px = image::load_from_memory(&app.session.doc().unwrap().doc.images[&m.key].bytes).unwrap().to_rgba8();
|
||||
[(0, 0), (1, 1), (4, 4)].map(|(x, y)| px.get_pixel(x, y).0[3])
|
||||
}
|
||||
|
||||
/// Object › Plug-ins › Image Tools › Color Mask… opens the command dialog on the defaults, previews
|
||||
/// a mask while the image's pixels stay, and keeps it on OK as one step; opened again it shows the
|
||||
/// image's mask settings. Remove Color Mask and Apply Color Mask are enabled only for an image
|
||||
/// with a mask.
|
||||
#[test]
|
||||
fn color_mask_previews_keeps_its_settings_and_comes_off() {
|
||||
let mut app = app();
|
||||
let steps = |app: &VectorcraftApp| app.session.doc().unwrap().history.undo.len();
|
||||
let (start, n) = (image(&app), steps(&app));
|
||||
assert!(!menu_item(&app, "Remove Color Mask").enabled && !menu_item(&app, "Apply Color Mask").enabled);
|
||||
|
||||
click(&mut app, "Color Mask…");
|
||||
let d = app.ui.dialog.as_ref().expect("a dialog opens");
|
||||
assert_eq!((d.str("__command"), d.str("__label")), (super::COLOR_MASK.to_string(), "Color Mask".to_string()));
|
||||
assert_eq!((d.str("background"), d.f64("tolerance", -1.0), d.bool("preview")), ("edges".to_string(), 10.0, false));
|
||||
assert!(!d.fields.contains_key("masked"));
|
||||
let shown = show(&mut app);
|
||||
for s in ["Color Mask", "Edge Color", "Contiguous", "Match", "Tolerance", "Softness", "Preview"] {
|
||||
assert!(shown.contains(s), "{s} in {shown}");
|
||||
}
|
||||
set(&mut app, "preview", json!(true));
|
||||
set(&mut app, "tolerance", json!(20));
|
||||
show(&mut app);
|
||||
assert_eq!(masked_alphas(&app), [0, 0, 255], "the preview hides the white and the ring");
|
||||
assert_eq!(image(&app), start, "the image's pixels stay");
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert_eq!((steps(&app), image(&app)), (n + 1, start.clone()), "one undo step; the pixels as they were");
|
||||
assert_eq!(mask(&app).map(|m| m.tolerance), Some(20.0));
|
||||
|
||||
// Opened again: the image's mask settings.
|
||||
click(&mut app, "Color Mask…");
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().f64("tolerance", -1.0), 20.0);
|
||||
crate::dialogs::cancel(&mut app);
|
||||
assert_eq!(steps(&app), n + 1);
|
||||
|
||||
assert!(menu_item(&app, "Apply Color Mask").enabled);
|
||||
click(&mut app, "Remove Color Mask");
|
||||
assert_eq!((mask(&app), image(&app), steps(&app)), (None, start, n + 2));
|
||||
}
|
||||
|
||||
fn show(app: &mut VectorcraftApp) -> String {
|
||||
painted_text(app, |app, ui| crate::dialogs::show(app, ui.ctx()))
|
||||
}
|
||||
|
||||
@@ -64,12 +64,16 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
|
||||
/// Does this dialog show a Preview checkbox? Grid Repeat Options does: the public page for the
|
||||
/// grid options lists one, and the radial and mirror pages do not. Type on a Path Options does
|
||||
/// too (#5360), as public tutorials of the dialog show, and Type Tools › Release Type on a Path
|
||||
/// works the same way (#5532), and so do Image Tools › Remove Background and Cutting Tools › Weed
|
||||
/// Box and Lines. (Cancel rolls a preview back whichever dialog started it.)
|
||||
/// works the same way (#5532), and so do Image Tools › Remove Background and Color Mask and
|
||||
/// Cutting Tools › Weed Box and Lines. (Cancel rolls a preview back whichever dialog started it.)
|
||||
fn previews(d: &Dialog) -> bool {
|
||||
match d.str("__command").as_str() {
|
||||
"object.repeat.options" => d.fields.contains_key("gridType"),
|
||||
"type.pathOptions" | crate::addons::typetools::RELEASE | crate::addons::image::REMOVE_BACKGROUND | crate::addons::cutting::WEED => true,
|
||||
"type.pathOptions"
|
||||
| crate::addons::typetools::RELEASE
|
||||
| crate::addons::image::REMOVE_BACKGROUND
|
||||
| crate::addons::image::COLOR_MASK
|
||||
| crate::addons::cutting::WEED => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -134,7 +138,9 @@ fn order(command: &str) -> &'static [&'static str] {
|
||||
"object.pathfinder.trap" => &["thickness", "heightWidth", "tintReduction", "trapsWithProcessColor", "reverseTraps"],
|
||||
"addon.color.printFixes" => &["inkLimit", "excludePureBlack", "removeWhiteOverprint", "overprintBlack", "textOnly", "fill", "stroke"],
|
||||
crate::addons::typetools::RELEASE => &["mode", "keepOriginal"],
|
||||
crate::addons::image::REMOVE_BACKGROUND => &["background", "colors", "contiguous", "match", "tolerance", "softness"],
|
||||
crate::addons::image::REMOVE_BACKGROUND | crate::addons::image::COLOR_MASK => {
|
||||
&["background", "colors", "contiguous", "match", "tolerance", "softness"]
|
||||
}
|
||||
crate::addons::cutting::BRIDGES => &["width", "count", "direction", "keepOriginal"],
|
||||
_ => &[],
|
||||
}
|
||||
@@ -157,8 +163,8 @@ fn choices(command: &str, key: &str) -> Option<form::Choices> {
|
||||
("graph.setType", "pieLegend") => Some(PIE_LEGEND),
|
||||
("graph.setType", "piePosition") => Some(PIE_POSITION),
|
||||
("graph.setType", "pieSort") => Some(PIE_SORT),
|
||||
(crate::addons::image::REMOVE_BACKGROUND, "background") => Some(crate::addons::image::BACKGROUNDS),
|
||||
(crate::addons::image::REMOVE_BACKGROUND, "match") => Some(crate::addons::image::MATCHES),
|
||||
(crate::addons::image::REMOVE_BACKGROUND | crate::addons::image::COLOR_MASK, "background") => Some(crate::addons::image::BACKGROUNDS),
|
||||
(crate::addons::image::REMOVE_BACKGROUND | crate::addons::image::COLOR_MASK, "match") => Some(crate::addons::image::MATCHES),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,6 +696,10 @@ font-technology Variable Променливи
|
||||
Corner Color Цвят на ъглите
|
||||
Contiguous Съседни
|
||||
Each Channel Всеки канал
|
||||
Color Mask… Цветова маска…
|
||||
Color Mask Цветова маска
|
||||
Remove Color Mask Премахване на цветовата маска
|
||||
Apply Color Mask Прилагане на цветовата маска
|
||||
Edit Path Редактиране на контура
|
||||
Mask Маска
|
||||
Crop Marks Знаци за изрязване
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -206,6 +206,10 @@
|
||||
Corner Color Boja uglova
|
||||
Contiguous Susjedni
|
||||
Each Channel Svaki kanal
|
||||
Color Mask… Maska boja…
|
||||
Color Mask Maska boja
|
||||
Remove Color Mask Ukloni masku boja
|
||||
Apply Color Mask Primijeni masku boja
|
||||
Edit Path Uredi putanju
|
||||
Mask Maska
|
||||
Rasterize… Rasteriziraj…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1011,6 +1011,10 @@ antiAlias Sharp Nítid
|
||||
Corner Color Color de les cantonades
|
||||
Contiguous Contigu
|
||||
Each Channel Cada canal
|
||||
Color Mask… Màscara de color…
|
||||
Color Mask Màscara de color
|
||||
Remove Color Mask Elimina la màscara de color
|
||||
Apply Color Mask Aplica la màscara de color
|
||||
Crop Marks Marques de retall
|
||||
Crop To Retalla fins a
|
||||
Cross Creu
|
||||
@@ -5967,6 +5971,10 @@ digits Malayalam Malaiàlam
|
||||
@msg a CMYK image has no transparency to set una imatge CMYK no té transparència per establir
|
||||
@msg every pixel matches the background: no image would be left cada píxel coincideix amb el fons: no quedaria cap imatge
|
||||
@msg an image of {w} × {h} pixels is too large to change una imatge de {w} × {h} píxels és massa gran per canviar-la
|
||||
@msg select an image with a color mask selecciona una imatge amb una màscara de color
|
||||
@msg the color mask's settings are out of range la configuració de la màscara de color és fora de l’interval
|
||||
@msg a CMYK image can't take a color mask una imatge CMYK no pot tenir una màscara de color
|
||||
@msg the mask would hide every pixel la màscara amagaria tots els píxels
|
||||
@msg select an Image Trace object selecciona un objecte d’Image Trace
|
||||
@msg select an Intertwine group selecciona un grup Intertwine
|
||||
@msg select an object in perspective selecciona un objecte en perspectiva
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -212,6 +212,10 @@
|
||||
Corner Color Barva rohů
|
||||
Contiguous Souvislé
|
||||
Each Channel Každý kanál
|
||||
Color Mask… Barevná maska…
|
||||
Color Mask Barevná maska
|
||||
Remove Color Mask Odstranit barevnou masku
|
||||
Apply Color Mask Použít barevnou masku
|
||||
Edit Path Upravit cestu
|
||||
Mask Maska
|
||||
Rasterize… Rastrovat…
|
||||
@@ -5096,6 +5100,10 @@ imageTrace Outline Obrys
|
||||
@msg a CMYK image has no transparency to set obraz CMYK nemá průhlednost, kterou by bylo možné nastavit
|
||||
@msg every pixel matches the background: no image would be left každý obrazový bod odpovídá pozadí: nezbyl by žádný obraz
|
||||
@msg an image of {w} × {h} pixels is too large to change obraz o velikosti {w} × {h} obrazových bodů je příliš velký na změnu
|
||||
@msg select an image with a color mask vyberte obraz s barevnou maskou
|
||||
@msg the color mask's settings are out of range nastavení barevné masky jsou mimo rozsah
|
||||
@msg a CMYK image can't take a color mask obraz CMYK nemůže mít barevnou masku
|
||||
@msg the mask would hide every pixel maska by skryla všechny obrazové body
|
||||
@msg select an object in perspective vyberte objekt v perspektivě
|
||||
@msg select artwork (or a colour group) to recolor vyberte kresbu (nebo skupinu barev) k přebarvení
|
||||
@msg Recolor completed; {locked} and {hidden} were left unchanged Přebarvení dokončeno; beze změny: {locked} a {hidden}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -199,6 +199,10 @@
|
||||
Corner Color Hjørnefarve
|
||||
Contiguous Sammenhængende
|
||||
Each Channel Hver kanal
|
||||
Color Mask… Farvemaske…
|
||||
Color Mask Farvemaske
|
||||
Remove Color Mask Fjern farvemaske
|
||||
Apply Color Mask Anvend farvemaske
|
||||
Edit Path Redigér kurve
|
||||
Mask Maskér
|
||||
Rasterize… Rasterisér…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -689,6 +689,10 @@ font-technology Variable Variabel
|
||||
Corner Color Eckfarbe
|
||||
Contiguous Zusammenhängend
|
||||
Each Channel Jeder Kanal
|
||||
Color Mask… Farbmaske…
|
||||
Color Mask Farbmaske
|
||||
Remove Color Mask Farbmaske entfernen
|
||||
Apply Color Mask Farbmaske anwenden
|
||||
Edit Path Pfad bearbeiten
|
||||
Mask Maske
|
||||
Crop Marks Schnittmarken
|
||||
@@ -4713,6 +4717,10 @@ theme Appearance Erscheinungsbild
|
||||
@msg a CMYK image has no transparency to set ein CMYK-Bild hat keine Transparenz, die festgelegt werden kann
|
||||
@msg every pixel matches the background: no image would be left jedes Pixel entspricht dem Hintergrund: Es würde kein Bild übrig bleiben
|
||||
@msg an image of {w} × {h} pixels is too large to change ein Bild mit {w} × {h} Pixeln ist zu groß, um geändert zu werden
|
||||
@msg select an image with a color mask wählen Sie ein Bild mit einer Farbmaske aus
|
||||
@msg the color mask's settings are out of range die Einstellungen der Farbmaske liegen außerhalb des zulässigen Bereichs
|
||||
@msg a CMYK image can't take a color mask ein CMYK-Bild kann keine Farbmaske erhalten
|
||||
@msg the mask would hide every pixel die Maske würde jedes Pixel ausblenden
|
||||
@msg select an object in perspective wählen Sie ein Objekt in der Perspektive aus
|
||||
@msg select artwork (or a colour group) to recolor wählen Sie eine Grafik (oder eine Farbgruppe) zum Neufärben aus
|
||||
@msg Recolor completed; {locked} and {hidden} were left unchanged Neufärben abgeschlossen; unverändert: {locked} und {hidden}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -670,6 +670,10 @@ font-technology Variable Μεταβλητές
|
||||
Corner Color Χρώμα γωνιών
|
||||
Contiguous Συνεχόμενα
|
||||
Each Channel Κάθε κανάλι
|
||||
Color Mask… Μάσκα χρώματος…
|
||||
Color Mask Μάσκα χρώματος
|
||||
Remove Color Mask Αφαίρεση μάσκας χρώματος
|
||||
Apply Color Mask Εφαρμογή μάσκας χρώματος
|
||||
Edit Path Επεξεργασία διαδρομής
|
||||
Mask Μάσκα
|
||||
Crop Marks Σημάδια ξακρίσματος
|
||||
@@ -4735,6 +4739,10 @@ digits Malayalam Μαλαγιάλαμ
|
||||
@msg a CMYK image has no transparency to set μια εικόνα CMYK δεν έχει διαφάνεια για να οριστεί
|
||||
@msg every pixel matches the background: no image would be left κάθε εικονοστοιχείο ταιριάζει με το φόντο: δεν θα έμενε καμία εικόνα
|
||||
@msg an image of {w} × {h} pixels is too large to change η εικόνα {w} × {h} εικονοστοιχείων είναι υπερβολικά μεγάλη για να αλλάξει
|
||||
@msg select an image with a color mask επιλέξτε εικόνα με μάσκα χρώματος
|
||||
@msg the color mask's settings are out of range οι ρυθμίσεις της μάσκας χρώματος είναι εκτός εύρους
|
||||
@msg a CMYK image can't take a color mask μια εικόνα CMYK δεν μπορεί να έχει μάσκα χρώματος
|
||||
@msg the mask would hide every pixel η μάσκα θα έκρυβε κάθε εικονοστοιχείο
|
||||
@msg select an object in perspective επιλέξτε αντικείμενο σε προοπτική
|
||||
@msg select artwork (or a colour group) to recolor επιλέξτε σχέδιο (ή ομάδα χρωμάτων) για επαναχρωματισμό
|
||||
@msg Recolor completed; {locked} and {hidden} were left unchanged Ο επαναχρωματισμός ολοκληρώθηκε· χωρίς αλλαγή: {locked} και {hidden}
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -710,6 +710,10 @@ font-technology Variable Variables
|
||||
Corner Color Color de las esquinas
|
||||
Contiguous Contiguo
|
||||
Each Channel Cada canal
|
||||
Color Mask… Máscara de color…
|
||||
Color Mask Máscara de color
|
||||
Remove Color Mask Quitar máscara de color
|
||||
Apply Color Mask Aplicar máscara de color
|
||||
Edit Path Editar trazado
|
||||
Mask Máscara
|
||||
Crop Marks Marcas de corte
|
||||
@@ -4788,6 +4792,10 @@ axis Both Ambos
|
||||
@msg a CMYK image has no transparency to set una imagen CMYK no tiene transparencia que establecer
|
||||
@msg every pixel matches the background: no image would be left todos los píxeles coinciden con el fondo: no quedaría ninguna imagen
|
||||
@msg an image of {w} × {h} pixels is too large to change una imagen de {w} × {h} píxeles es demasiado grande para cambiarla
|
||||
@msg select an image with a color mask selecciona una imagen con una máscara de color
|
||||
@msg the color mask's settings are out of range los ajustes de la máscara de color están fuera de rango
|
||||
@msg a CMYK image can't take a color mask una imagen CMYK no puede tener una máscara de color
|
||||
@msg the mask would hide every pixel la máscara ocultaría todos los píxeles
|
||||
@msg select an object in perspective selecciona un objeto en perspectiva
|
||||
@msg select artwork (or a colour group) to recolor selecciona la ilustración (o un grupo de colores) que quieres volver a colorear
|
||||
@msg Recolor completed; {locked} and {hidden} were left unchanged Recoloreado completado; sin cambios: {locked} y {hidden}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -219,6 +219,10 @@
|
||||
Corner Color Kulmien väri
|
||||
Contiguous Yhtenäinen
|
||||
Each Channel Jokainen kanava
|
||||
Color Mask… Värimaski…
|
||||
Color Mask Värimaski
|
||||
Remove Color Mask Poista värimaski
|
||||
Apply Color Mask Käytä värimaskia
|
||||
Edit Path Muokkaa polkua
|
||||
Mask Maski
|
||||
Rasterize… Rasteroi…
|
||||
@@ -4996,6 +5000,10 @@ imageTrace Outline Ääriviiva
|
||||
@msg a CMYK image has no transparency to set CMYK-kuvalla ei ole asetettavaa läpinäkyvyyttä
|
||||
@msg every pixel matches the background: no image would be left jokainen pikseli vastaa taustaa: kuvaa ei jäisi jäljelle
|
||||
@msg an image of {w} × {h} pixels is too large to change kuva, jonka koko on {w} × {h} pikseliä, on liian suuri muutettavaksi
|
||||
@msg select an image with a color mask valitse kuva, jossa on värimaski
|
||||
@msg the color mask's settings are out of range värimaskin asetukset ovat sallitun alueen ulkopuolella
|
||||
@msg a CMYK image can't take a color mask CMYK-kuvalla ei voi olla värimaskia
|
||||
@msg the mask would hide every pixel maski piilottaisi jokaisen pikselin
|
||||
@msg select an object in perspective valitse perspektiivissä oleva objekti
|
||||
@msg select artwork (or a colour group) to recolor valitse uudelleenväritettävä kuvitus (tai väriryhmä)
|
||||
@msg Recolor completed; {locked} and {hidden} were left unchanged Uudelleenväritys valmis; muuttamatta jäi: {locked} ja {hidden}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -712,6 +712,10 @@ font-technology Variable Variables
|
||||
Corner Color Couleur des angles
|
||||
Contiguous Contigu
|
||||
Each Channel Chaque canal
|
||||
Color Mask… Masque de couleur…
|
||||
Color Mask Masque de couleur
|
||||
Remove Color Mask Supprimer le masque de couleur
|
||||
Apply Color Mask Appliquer le masque de couleur
|
||||
Edit Path Modifier le tracé
|
||||
Mask Masque
|
||||
Crop Marks Traits de coupe
|
||||
@@ -4777,6 +4781,10 @@ axis Both Les deux
|
||||
@msg a CMYK image has no transparency to set une image CMJN n’a pas de transparence à définir
|
||||
@msg every pixel matches the background: no image would be left chaque pixel correspond à l’arrière-plan : il ne resterait aucune image
|
||||
@msg an image of {w} × {h} pixels is too large to change une image de {w} × {h} pixels est trop grande pour être modifiée
|
||||
@msg select an image with a color mask sélectionnez une image avec un masque de couleur
|
||||
@msg the color mask's settings are out of range les réglages du masque de couleur sont hors limites
|
||||
@msg a CMYK image can't take a color mask une image CMJN ne peut pas recevoir de masque de couleur
|
||||
@msg the mask would hide every pixel le masque cacherait tous les pixels
|
||||
@msg select an object in perspective sélectionnez un objet en perspective
|
||||
@msg select artwork (or a colour group) to recolor sélectionnez l’illustration (ou un groupe de couleurs) dont redéfinir les couleurs
|
||||
@msg Recolor completed; {locked} and {hidden} were left unchanged Recoloration terminée ; sans changement : {locked} et {hidden}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1129,6 +1129,10 @@ antiAlias Sharp शार्प
|
||||
Corner Color कोनों का रंग
|
||||
Contiguous सन्निहित
|
||||
Each Channel प्रत्येक चैनल
|
||||
Color Mask… रंग मास्क…
|
||||
Color Mask रंग मास्क
|
||||
Remove Color Mask रंग मास्क हटाएँ
|
||||
Apply Color Mask रंग मास्क लागू करें
|
||||
Crop Marks क्रॉप मार्क
|
||||
Crop To करने के लिए फसल
|
||||
Cross क्रॉस
|
||||
@@ -7665,6 +7669,10 @@ antiAlias Sharp शार्प
|
||||
@msg a CMYK image has no transparency to set CMYK इमेज में सेट करने के लिए कोई पारदर्शिता नहीं है
|
||||
@msg every pixel matches the background: no image would be left हर पिक्सेल पृष्ठभूमि से मेल खाता है: कोई इमेज नहीं बचेगी
|
||||
@msg an image of {w} × {h} pixels is too large to change {w} × {h} पिक्सेल की इमेज बदलने के लिए बहुत बड़ी है
|
||||
@msg select an image with a color mask रंग मास्क वाली एक इमेज चुनें
|
||||
@msg the color mask's settings are out of range रंग मास्क की सेटिंग सीमा से बाहर हैं
|
||||
@msg a CMYK image can't take a color mask CMYK इमेज पर रंग मास्क नहीं लगाया जा सकता
|
||||
@msg the mask would hide every pixel मास्क हर पिक्सेल को छिपा देगा
|
||||
@msg select an image (or an Image Trace group) एक इमेज सिलेक्ट करें (या एक इमेज ट्रेस समूह)
|
||||
@msg select an Image Trace object एक इमेज ट्रेस ऑब्जेक्ट चुनें
|
||||
@msg select an image, or pass id एक इमेज सिलेक्ट करें, या आईडी पास करें
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -911,6 +911,10 @@ font-technology Variable Változó
|
||||
Corner Color Sarokszín
|
||||
Contiguous Összefüggő
|
||||
Each Channel Minden csatorna
|
||||
Color Mask… Színmaszk…
|
||||
Color Mask Színmaszk
|
||||
Remove Color Mask Színmaszk eltávolítása
|
||||
Apply Color Mask Színmaszk alkalmazása
|
||||
Edit Path Útvonal szerkesztése
|
||||
Crop Marks Vágójelek
|
||||
Crop To Körülvágás
|
||||
@@ -5539,6 +5543,10 @@ imageTrace Outline Körvonal
|
||||
@msg a CMYK image has no transparency to set a CMYK képnek nincs beállítható átlátszósága
|
||||
@msg every pixel matches the background: no image would be left minden képpont egyezik a háttérrel: nem maradna kép
|
||||
@msg an image of {w} × {h} pixels is too large to change egy {w} × {h} pixeles kép túl nagy a módosításhoz
|
||||
@msg select an image with a color mask jelölj ki egy színmaszkos képet
|
||||
@msg the color mask's settings are out of range a színmaszk beállításai a tartományon kívül esnek
|
||||
@msg a CMYK image can't take a color mask CMYK képre nem tehető színmaszk
|
||||
@msg the mask would hide every pixel a maszk minden képpontot elrejtene
|
||||
@msg select an Image Trace object jelölj ki egy képvektorizálási objektumot
|
||||
@msg select an object in perspective jelölj ki egy perspektivikus objektumot
|
||||
@msg select artwork (or a colour group) to recolor jelölj ki átszínezendő grafikát (vagy színcsoportot)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -199,6 +199,10 @@
|
||||
Corner Color Warna sudut
|
||||
Contiguous Bersambung
|
||||
Each Channel Setiap kanal
|
||||
Color Mask… Masker warna…
|
||||
Color Mask Masker warna
|
||||
Remove Color Mask Hapus masker warna
|
||||
Apply Color Mask Terapkan masker warna
|
||||
Edit Path Edit jalur
|
||||
Mask Masker
|
||||
Rasterize… Rasterisasi…
|
||||
|
||||
|
@@ -710,6 +710,10 @@ font-technology Variable Variabili
|
||||
Corner Color Colore degli angoli
|
||||
Contiguous Contiguo
|
||||
Each Channel Ogni canale
|
||||
Color Mask… Maschera colore…
|
||||
Color Mask Maschera colore
|
||||
Remove Color Mask Rimuovi maschera colore
|
||||
Apply Color Mask Applica maschera colore
|
||||
Edit Path Modifica tracciato
|
||||
Mask Maschera
|
||||
Crop Marks Segni di ritaglio
|
||||
@@ -4839,6 +4843,10 @@ axis Both Entrambi
|
||||
@msg a CMYK image has no transparency to set un'immagine CMYK non ha trasparenza da impostare
|
||||
@msg every pixel matches the background: no image would be left ogni pixel corrisponde allo sfondo: non resterebbe alcuna immagine
|
||||
@msg an image of {w} × {h} pixels is too large to change un'immagine di {w} × {h} pixel è troppo grande per essere modificata
|
||||
@msg select an image with a color mask seleziona un'immagine con una maschera colore
|
||||
@msg the color mask's settings are out of range le impostazioni della maschera colore sono fuori intervallo
|
||||
@msg a CMYK image can't take a color mask un'immagine CMYK non può avere una maschera colore
|
||||
@msg the mask would hide every pixel la maschera nasconderebbe ogni pixel
|
||||
@msg select an object in perspective seleziona un oggetto in prospettiva
|
||||
@msg select artwork (or a colour group) to recolor seleziona la grafica (o un gruppo colori) da ricolorare
|
||||
@msg Recolor completed; {locked} and {hidden} were left unchanged Ricolorazione completata; senza modifiche: {locked} e {hidden}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -243,6 +243,10 @@ digits Malayalam マラヤーラム数字
|
||||
Corner Color 角の色
|
||||
Contiguous 隣接
|
||||
Each Channel 各チャンネル
|
||||
Color Mask… カラーマスク…
|
||||
Color Mask カラーマスク
|
||||
Remove Color Mask カラーマスクを削除
|
||||
Apply Color Mask カラーマスクを適用
|
||||
Edit Path パスを編集
|
||||
Mask マスク
|
||||
Rasterize… ラスタライズ…
|
||||
@@ -4963,6 +4967,10 @@ swatch Bright Violet ビビッドバイオレット
|
||||
@msg a CMYK image has no transparency to set CMYK 画像には設定できる透明部分がありません
|
||||
@msg every pixel matches the background: no image would be left すべてのピクセルが背景に一致します: 画像が残りません
|
||||
@msg an image of {w} × {h} pixels is too large to change {w} × {h} ピクセルの画像は大きすぎて変更できません
|
||||
@msg select an image with a color mask カラーマスクのある画像を選択してください
|
||||
@msg the color mask's settings are out of range カラーマスクの設定が範囲外です
|
||||
@msg a CMYK image can't take a color mask CMYK 画像にはカラーマスクを設定できません
|
||||
@msg the mask would hide every pixel マスクがすべてのピクセルを隠してしまいます
|
||||
@msg the document has no artboards ドキュメントにアートボードがありません
|
||||
@msg tool (a tool id) is required tool (ツール ID) を指定してください
|
||||
@msg unknown tool `{t}` `{t}` は不明なツールです
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -171,6 +171,10 @@
|
||||
Corner Color კუთხეების ფერი
|
||||
Contiguous მომიჯნავე
|
||||
Each Channel თითოეული არხი
|
||||
Color Mask… ფერის ნიღაბი…
|
||||
Color Mask ფერის ნიღაბი
|
||||
Remove Color Mask ფერის ნიღბის მოშორება
|
||||
Apply Color Mask ფერის ნიღბის გამოყენება
|
||||
Mask ნიღაბი
|
||||
Rasterize… რასტრირება…
|
||||
Create Gradient Mesh… გრადიენტული ბადის შექმნა…
|
||||
@@ -4454,6 +4458,10 @@ font-technology Variable ცვლადი
|
||||
@msg a CMYK image has no transparency to set CMYK გამოსახულებას არ აქვს დასაყენებელი გამჭვირვალობა
|
||||
@msg every pixel matches the background: no image would be left ყველა პიქსელი ემთხვევა ფონს: გამოსახულება აღარ დარჩებოდა
|
||||
@msg an image of {w} × {h} pixels is too large to change {w} × {h} პიქსელის გამოსახულება შესაცვლელად ზედმეტად დიდია
|
||||
@msg select an image with a color mask მონიშნეთ ფერის ნიღბიანი გამოსახულება
|
||||
@msg the color mask's settings are out of range ფერის ნიღბის პარამეტრები დასაშვებ დიაპაზონს სცდება
|
||||
@msg a CMYK image can't take a color mask CMYK გამოსახულებას ფერის ნიღაბი ვერ დაედება
|
||||
@msg the mask would hide every pixel ნიღაბი ყველა პიქსელს დამალავდა
|
||||
@msg select an object in perspective მონიშნეთ ობიექტი პერსპექტივაში
|
||||
@msg select artwork (or a colour group) to recolor მონიშნეთ ხელახლა შესაფერადებელი ნახატი (ან ფერთა ჯგუფი)
|
||||
@msg Recolor completed; {locked} and {hidden} were left unchanged ხელახლა შეფერადება დასრულდა; უცვლელი დარჩა: {locked} და {hidden}
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -236,6 +236,10 @@ digits Malayalam 말라얄람어
|
||||
Corner Color 모퉁이 색상
|
||||
Contiguous 인접
|
||||
Each Channel 각 채널
|
||||
Color Mask… 색상 마스크…
|
||||
Color Mask 색상 마스크
|
||||
Remove Color Mask 색상 마스크 제거
|
||||
Apply Color Mask 색상 마스크 적용
|
||||
Edit Path 패스 편집
|
||||
Mask 마스크
|
||||
Rasterize… 래스터화…
|
||||
@@ -4890,6 +4894,10 @@ theme Appearance 외관
|
||||
@msg a CMYK image has no transparency to set CMYK 이미지에는 설정할 투명도가 없습니다
|
||||
@msg every pixel matches the background: no image would be left 모든 픽셀이 배경과 일치합니다: 남는 이미지가 없습니다
|
||||
@msg an image of {w} × {h} pixels is too large to change {w} × {h}픽셀 이미지는 너무 커서 변경할 수 없습니다
|
||||
@msg select an image with a color mask 색상 마스크가 있는 이미지를 선택하세요
|
||||
@msg the color mask's settings are out of range 색상 마스크 설정이 범위를 벗어났습니다
|
||||
@msg a CMYK image can't take a color mask CMYK 이미지에는 색상 마스크를 적용할 수 없습니다
|
||||
@msg the mask would hide every pixel 마스크가 모든 픽셀을 숨기게 됩니다
|
||||
@msg the document has no artboards 문서에 아트보드가 없습니다
|
||||
@msg tool (a tool id) is required 도구(도구 id)가 필요합니다
|
||||
@msg unknown tool `{t}` 알 수 없는 도구 `{t}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -213,6 +213,10 @@
|
||||
Corner Color Hoekkleur
|
||||
Contiguous Aaneengesloten
|
||||
Each Channel Elk kanaal
|
||||
Color Mask… Kleurmasker…
|
||||
Color Mask Kleurmasker
|
||||
Remove Color Mask Kleurmasker verwijderen
|
||||
Apply Color Mask Kleurmasker toepassen
|
||||
Edit Path Pad bewerken
|
||||
Mask Masker
|
||||
Rasterize… Rasteriseren…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -176,6 +176,10 @@
|
||||
Corner Color Kolor narożników
|
||||
Contiguous Ciągły
|
||||
Each Channel Każdy kanał
|
||||
Color Mask… Maska koloru…
|
||||
Color Mask Maska koloru
|
||||
Remove Color Mask Usuń maskę koloru
|
||||
Apply Color Mask Zastosuj maskę koloru
|
||||
Edit Path Edytuj ścieżkę
|
||||
Mask Maska
|
||||
Rasterize… Rasteryzuj…
|
||||
@@ -4922,6 +4926,10 @@ imageTrace Outline Kontur
|
||||
@msg a CMYK image has no transparency to set obraz CMYK nie ma przezroczystości, którą można ustawić
|
||||
@msg every pixel matches the background: no image would be left każdy piksel pasuje do tła: nie zostałby żaden obraz
|
||||
@msg an image of {w} × {h} pixels is too large to change obraz o wymiarach {w} × {h} pikseli jest zbyt duży, aby go zmienić
|
||||
@msg select an image with a color mask zaznacz obraz z maską koloru
|
||||
@msg the color mask's settings are out of range ustawienia maski koloru są poza zakresem
|
||||
@msg a CMYK image can't take a color mask obraz CMYK nie może mieć maski koloru
|
||||
@msg the mask would hide every pixel maska ukryłaby każdy piksel
|
||||
@msg select an object in perspective zaznacz obiekt w perspektywie
|
||||
@msg select artwork (or a colour group) to recolor zaznacz kompozycję (lub grupę kolorów), aby zmienić kolory
|
||||
@msg Recolor completed; {locked} and {hidden} were left unchanged Zmiana kolorów zakończona; bez zmian: {locked} i {hidden}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -636,6 +636,10 @@ antiAlias Sharp Nítido
|
||||
Corner Color Cor dos Cantos
|
||||
Contiguous Contíguo
|
||||
Each Channel Cada Canal
|
||||
Color Mask… Máscara de Cor…
|
||||
Color Mask Máscara de Cor
|
||||
Remove Color Mask Remover Máscara de Cor
|
||||
Apply Color Mask Aplicar Máscara de Cor
|
||||
Edit Path Editar caminho
|
||||
Mask Máscara
|
||||
Crop Marks Marcas de Corte
|
||||
@@ -4244,6 +4248,10 @@ axis Both Ambos
|
||||
@msg a CMYK image has no transparency to set uma imagem CMYK não tem transparência para definir
|
||||
@msg every pixel matches the background: no image would be left todos os pixels correspondem ao fundo: nenhuma imagem restaria
|
||||
@msg an image of {w} × {h} pixels is too large to change uma imagem de {w} × {h} pixels é grande demais para ser alterada
|
||||
@msg select an image with a color mask selecione uma imagem com uma máscara de cor
|
||||
@msg the color mask's settings are out of range as configurações da máscara de cor estão fora do intervalo
|
||||
@msg a CMYK image can't take a color mask uma imagem CMYK não pode ter uma máscara de cor
|
||||
@msg the mask would hide every pixel a máscara ocultaria todos os pixels
|
||||
@msg select an object in perspective selecione um objeto em perspectiva
|
||||
@msg select artwork (or a colour group) to recolor selecione uma arte-final (ou um grupo de cores) para recolorir
|
||||
@msg Recolor completed; {locked} and {hidden} were left unchanged Recoloração concluída; sem alteração: {locked} e {hidden}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -708,6 +708,10 @@ font-technology Variable Вариативные
|
||||
Corner Color Цвет углов
|
||||
Contiguous Смежные
|
||||
Each Channel Каждый канал
|
||||
Color Mask… Цветовая маска…
|
||||
Color Mask Цветовая маска
|
||||
Remove Color Mask Удалить цветовую маску
|
||||
Apply Color Mask Применить цветовую маску
|
||||
Edit Path Редактировать контур
|
||||
Mask Маска
|
||||
Crop Marks Метки обрезки
|
||||
@@ -4825,6 +4829,10 @@ axis Both Оба
|
||||
@msg a CMYK image has no transparency to set у изображения CMYK нет прозрачности, которую можно задать
|
||||
@msg every pixel matches the background: no image would be left каждый пиксель совпадает с фоном: от изображения ничего не осталось бы
|
||||
@msg an image of {w} × {h} pixels is too large to change изображение размером {w} × {h} пикселей слишком велико для изменения
|
||||
@msg select an image with a color mask выделите изображение с цветовой маской
|
||||
@msg the color mask's settings are out of range настройки цветовой маски вне допустимого диапазона
|
||||
@msg a CMYK image can't take a color mask к изображению CMYK нельзя применить цветовую маску
|
||||
@msg the mask would hide every pixel маска скрыла бы все пиксели
|
||||
@msg select an object in perspective выделите объект в перспективе
|
||||
@msg select artwork (or a colour group) to recolor выделите рисунок (или цветовую группу) для перекрашивания
|
||||
@msg Recolor completed; {locked} and {hidden} were left unchanged Перекрашивание завершено; без изменений: {locked} и {hidden}
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -698,6 +698,10 @@ font-technology Variable Variabla
|
||||
Corner Color Hörnfärg
|
||||
Contiguous Sammanhängande
|
||||
Each Channel Varje kanal
|
||||
Color Mask… Färgmask…
|
||||
Color Mask Färgmask
|
||||
Remove Color Mask Ta bort färgmask
|
||||
Apply Color Mask Tillämpa färgmask
|
||||
Edit Path Redigera bana
|
||||
Mask Maskera
|
||||
Crop Marks Skärmärken
|
||||
@@ -4683,6 +4687,10 @@ axis Both Båda
|
||||
@msg a CMYK image has no transparency to set en CMYK-bild har ingen transparens att ställa in
|
||||
@msg every pixel matches the background: no image would be left varje pixel matchar bakgrunden: ingen bild skulle bli kvar
|
||||
@msg an image of {w} × {h} pixels is too large to change en bild på {w} × {h} pixlar är för stor för att ändras
|
||||
@msg select an image with a color mask markera en bild med en färgmask
|
||||
@msg the color mask's settings are out of range färgmaskens inställningar ligger utanför intervallet
|
||||
@msg a CMYK image can't take a color mask en CMYK-bild kan inte ha en färgmask
|
||||
@msg the mask would hide every pixel masken skulle dölja varje pixel
|
||||
@msg select an object in perspective markera ett objekt i perspektiv
|
||||
@msg select artwork (or a colour group) to recolor markera grafik (eller en färggrupp) att färga om
|
||||
@msg Recolor completed; {locked} and {hidden} were left unchanged Omfärgning klar; utan ändring: {locked} och {hidden}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -690,6 +690,10 @@ font-technology Variable Değişken
|
||||
Corner Color Köşe Rengi
|
||||
Contiguous Bitişik
|
||||
Each Channel Her Kanal
|
||||
Color Mask… Renk Maskesi…
|
||||
Color Mask Renk Maskesi
|
||||
Remove Color Mask Renk Maskesini Kaldır
|
||||
Apply Color Mask Renk Maskesini Uygula
|
||||
Mask Maske
|
||||
Crop Marks Kırpma İşaretleri
|
||||
Crop To Kırpma Hedefi
|
||||
@@ -4536,6 +4540,10 @@ axis Both Her İkisi
|
||||
@msg a CMYK image has no transparency to set CMYK görüntüsünde ayarlanacak saydamlık yok
|
||||
@msg every pixel matches the background: no image would be left her piksel arka planla eşleşiyor: hiç görüntü kalmaz
|
||||
@msg an image of {w} × {h} pixels is too large to change {w} × {h} piksellik bir görüntü değiştirilemeyecek kadar büyük
|
||||
@msg select an image with a color mask renk maskesi olan bir resim seç
|
||||
@msg the color mask's settings are out of range renk maskesinin ayarları izin verilen aralığın dışında
|
||||
@msg a CMYK image can't take a color mask CMYK görüntüsüne renk maskesi uygulanamaz
|
||||
@msg the mask would hide every pixel maske her pikseli gizlerdi
|
||||
@msg select an object in perspective perspektifte bir nesne seç
|
||||
@msg select artwork (or a colour group) to recolor yeniden renklendirilecek resmi (veya bir renk grubunu) seçin
|
||||
@msg Recolor completed; {locked} and {hidden} were left unchanged Yeniden renklendirme tamamlandı; değiştirilmeden kalan: {locked} ve {hidden}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -704,6 +704,10 @@ font-technology Variable Варіативні
|
||||
Corner Color Колір кутів
|
||||
Contiguous Суміжні
|
||||
Each Channel Кожен канал
|
||||
Color Mask… Колірна маска…
|
||||
Color Mask Колірна маска
|
||||
Remove Color Mask Видалити колірну маску
|
||||
Apply Color Mask Застосувати колірну маску
|
||||
Edit Path Редагувати контур
|
||||
Mask Маска
|
||||
Crop Marks Мітки обрізання
|
||||
@@ -4823,6 +4827,10 @@ axis Both Обидві
|
||||
@msg a CMYK image has no transparency to set зображення CMYK не має прозорості, яку можна задати
|
||||
@msg every pixel matches the background: no image would be left кожен піксель збігається з фоном: від зображення нічого не залишилося б
|
||||
@msg an image of {w} × {h} pixels is too large to change зображення розміром {w} × {h} пікселів завелике для зміни
|
||||
@msg select an image with a color mask виділіть зображення з колірною маскою
|
||||
@msg the color mask's settings are out of range параметри колірної маски поза допустимим діапазоном
|
||||
@msg a CMYK image can't take a color mask до зображення CMYK не можна застосувати колірну маску
|
||||
@msg the mask would hide every pixel маска приховала б усі пікселі
|
||||
@msg select an object in perspective виділіть об’єкт у перспективі
|
||||
@msg select artwork (or a colour group) to recolor виділіть малюнок (або колірну групу) для перефарбовування
|
||||
@msg Recolor completed; {locked} and {hidden} were left unchanged Перефарбовування завершено; без змін: {locked} і {hidden}
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -811,6 +811,10 @@ font-technology Variable Biến thiên
|
||||
Corner Color Màu góc
|
||||
Contiguous Liền kề
|
||||
Each Channel Từng kênh
|
||||
Color Mask… Mặt nạ màu…
|
||||
Color Mask Mặt nạ màu
|
||||
Remove Color Mask Xóa mặt nạ màu
|
||||
Apply Color Mask Áp dụng mặt nạ màu
|
||||
Edit Path Sửa đường dẫn
|
||||
Crop Marks Dấu cắt xén
|
||||
Crop To Cắt xén đến
|
||||
@@ -5202,6 +5206,10 @@ theme Appearance Giao diện
|
||||
@msg a CMYK image has no transparency to set ảnh CMYK không có độ trong suốt để đặt
|
||||
@msg every pixel matches the background: no image would be left mọi điểm ảnh đều khớp với nền: sẽ không còn lại ảnh nào
|
||||
@msg an image of {w} × {h} pixels is too large to change ảnh {w} × {h} điểm ảnh quá lớn nên không thể thay đổi
|
||||
@msg select an image with a color mask hãy chọn ảnh có mặt nạ màu
|
||||
@msg the color mask's settings are out of range cài đặt của mặt nạ màu nằm ngoài phạm vi cho phép
|
||||
@msg a CMYK image can't take a color mask ảnh CMYK không thể có mặt nạ màu
|
||||
@msg the mask would hide every pixel mặt nạ sẽ ẩn mọi điểm ảnh
|
||||
@msg select an Image Trace object hãy chọn đối tượng vector hóa ảnh
|
||||
@msg select an object in perspective hãy chọn đối tượng trong phối cảnh
|
||||
@msg select artwork (or a colour group) to recolor hãy chọn tác phẩm (hoặc nhóm màu) để tô lại
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 204 and column 5.
|
@@ -655,6 +655,10 @@ antiAlias Sharp 锐利
|
||||
Corner Color 转角颜色
|
||||
Contiguous 连续
|
||||
Each Channel 每个通道
|
||||
Color Mask… 颜色蒙版…
|
||||
Color Mask 颜色蒙版
|
||||
Remove Color Mask 移除颜色蒙版
|
||||
Apply Color Mask 应用颜色蒙版
|
||||
Edit Path 编辑路径
|
||||
Mask 蒙版
|
||||
Crop Marks 裁切标记
|
||||
@@ -4655,6 +4659,10 @@ imageTrace Outline 轮廓
|
||||
@msg a CMYK image has no transparency to set CMYK 图像没有可设置的透明度
|
||||
@msg every pixel matches the background: no image would be left 每个像素都与背景匹配:不会留下任何图像
|
||||
@msg an image of {w} × {h} pixels is too large to change {w} × {h} 像素的图像过大,无法更改
|
||||
@msg select an image with a color mask 请选择一个带颜色蒙版的图像
|
||||
@msg the color mask's settings are out of range 颜色蒙版的设置超出范围
|
||||
@msg a CMYK image can't take a color mask CMYK 图像不能使用颜色蒙版
|
||||
@msg the mask would hide every pixel 蒙版会隐藏所有像素
|
||||
@msg the document has no artboards 文档没有画板
|
||||
@msg tool (a tool id) is required 需要 tool (工具 id)
|
||||
@msg unknown tool `{t}` 未知工具 `{t}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -672,6 +672,10 @@ font-technology Variable 可變
|
||||
Corner Color 轉角顏色
|
||||
Contiguous 連續
|
||||
Each Channel 每個通道
|
||||
Color Mask… 顏色遮色片…
|
||||
Color Mask 顏色遮色片
|
||||
Remove Color Mask 移除顏色遮色片
|
||||
Apply Color Mask 套用顏色遮色片
|
||||
Edit Path 編輯路徑
|
||||
Mask 遮色片
|
||||
Crop Marks 裁切標記
|
||||
@@ -2424,6 +2428,10 @@ file-filter Templates 範本
|
||||
@msg a CMYK image has no transparency to set CMYK 影像沒有可設定的透明度
|
||||
@msg every pixel matches the background: no image would be left 每個像素都與背景相符:不會留下任何影像
|
||||
@msg an image of {w} × {h} pixels is too large to change {w} × {h} 像素的影像過大,無法變更
|
||||
@msg select an image with a color mask 請選取帶有顏色遮色片的影像
|
||||
@msg the color mask's settings are out of range 顏色遮色片的設定超出範圍
|
||||
@msg a CMYK image can't take a color mask CMYK 影像無法使用顏色遮色片
|
||||
@msg the mask would hide every pixel 遮色片會隱藏所有像素
|
||||
@msg the document has no artboards 文件沒有工作區域
|
||||
@msg tool (a tool id) is required 需要 tool (工具 id)
|
||||
@msg unknown tool `{t}` 未知的工具 `{t}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -29,12 +29,13 @@ pub(crate) fn with_held_keys(target: (String, Value), held: egui::Modifiers) ->
|
||||
|
||||
/// The dialog of command `id` that shows the selection's current values (its query): its heading
|
||||
/// and the queried values it doesn't show. Type on a Path Options leaves the brackets where they
|
||||
/// are.
|
||||
/// are. Color Mask opens on the selected image's mask settings (the defaults without one).
|
||||
fn queried_dialog(id: &str) -> Option<(&'static str, &'static [&'static str])> {
|
||||
Some(match id {
|
||||
// Whether the text overflows, and Shrink Text's factor, are read-only facts.
|
||||
"text.areaOptions" => ("Area Type Options", &["overflow", "fitScale"]),
|
||||
"type.pathOptions" => ("Type on a Path Options", &["start", "end"]),
|
||||
crate::addons::image::COLOR_MASK => ("Color Mask", &["masked"]),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
+56
-2
@@ -10567,8 +10567,9 @@ there is no step. → `{symbols: [name…], ids: [instance id…], skipped: [{la
|
||||
|
||||
## Image Tools add-on
|
||||
|
||||
Image Tools is a built-in add-on for placed images: its command is `addon.image.removeBackground`, and in the desktop
|
||||
app it lives in Object › Plug-ins › Image Tools. It is a VectorCraft addition.
|
||||
Image Tools is a built-in add-on for placed images: its commands are `addon.image.removeBackground`,
|
||||
`addon.image.colorMask`, `addon.image.removeColorMask` and `addon.image.applyColorMask`, and in the desktop app it
|
||||
lives in Object › Plug-ins › Image Tools. It is a VectorCraft addition.
|
||||
|
||||
### Remove Background
|
||||
|
||||
@@ -10615,6 +10616,59 @@ a preview step does not keep them.
|
||||
{"name":"run_command","arguments":{"command":"addon.image.removeBackground","params":{"background":"corners","contiguous":false,"match":"channels","tolerance":12.55,"softness":0}}}
|
||||
```
|
||||
|
||||
### Color Mask
|
||||
|
||||
`addon.image.colorMask` hides colors of the selected images, or of the images in `ids`, and leaves their pixels as
|
||||
they are. It takes `background`, `colors`, `contiguous`, `match`, `tolerance` and `softness` with the same values,
|
||||
defaults and refusals as `addon.image.removeBackground`, and matches colors the same way. The image keeps its own
|
||||
pixels. The mask is kept with the image object and saved with the document: its settings, and a PNG of the image's
|
||||
pixels with the hidden colors made clear (16-bit and gray images keep their depth and channels, with the source's color
|
||||
profile and resolution). PDF's color key masking hides chosen colors of an image in a similar way
|
||||
([ISO 32000-1 §8.9.6.4](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf)).
|
||||
|
||||
- The canvas, printing and separations, PDF (PDF/X-1a and PDF/X-3 flatten the hidden pixels as transparency), SVG,
|
||||
EPS, the raster formats, DXF, EMF, WMF, the legacy .ai, the .ai editing data, copy and paste, and placing a document
|
||||
show the masked pixels. A linked image stays linked. An output that refers to linked files instead of embedding them
|
||||
(SVG with `images: "link"`) writes the masked pixels in place of the link.
|
||||
- A grayscale image's masked pixels have an alpha channel when the mask hides pixels. A colorize color on such an image
|
||||
follows the rule for grayscale images with transparency (a spot color, black or white), and Color Mask refuses a mask
|
||||
that hides pixels of a grayscale image colorized with a process color. A mask that hides no pixels shows the image's
|
||||
own pixels, so a grayscale image with such a mask takes a process color. Each image is checked on the pixels its new
|
||||
mask shows, whatever other images in the document hold. Remove Background checks the image's own pixels, which it
|
||||
clears.
|
||||
- A click anywhere in the image's frame selects it, hidden pixels included, as it does for an image with clear pixels.
|
||||
The Eyedropper, Object Mosaic, Create Gradient Mesh, Crop Image's trim and Image Trace read the image's own pixels,
|
||||
hidden colors included.
|
||||
- Running the command again on a masked image replaces its mask. All the images change in one undo step. An image whose
|
||||
mask does not change is left as it is, and when none changes the command adds no undo step and returns `changed: 0`.
|
||||
`hidden` counts the pixels made fully clear in the images that changed.
|
||||
- When the image's own pixels are replaced later (Update Link, Relink, Crop Image, a recolor, or a linked file read
|
||||
again when the document opens), the mask is made again for the new pixels. When the new pixels can't be masked (CMYK
|
||||
pixels, missing pixels, every pixel would be hidden, or the image is grayscale and colorized with a process color
|
||||
and the mask would hide some of its pixels), the mask keeps its settings and hides nothing.
|
||||
- The command refuses, and changes nothing, when an object in `ids` is not an image, an image is CMYK, a linked image
|
||||
shows its preview (embed or relink it first), an image would need more than the decode allowance, or the mask would
|
||||
hide every pixel of an image.
|
||||
- With none of `background`, `colors`, `contiguous`, `match`, `tolerance` and `softness` given, nothing changes, and
|
||||
the command returns the mask settings of the first image (the defaults when it has no mask) with `masked`. Color
|
||||
Mask… opens the command's dialog on those settings, with a Preview that works as Remove Background's does.
|
||||
|
||||
→ `{changed, ids, hidden}`, or without settings `{background, colors, contiguous, match, tolerance, softness, masked}`
|
||||
|
||||
`addon.image.removeColorMask` takes the mask off the selected images, or the images in `ids`, in one undo step, and
|
||||
the images show their own pixels again. `addon.image.applyColorMask` makes each mask part of its image: the masked
|
||||
pixels become the image's own, embedded as a PNG with an alpha channel (a linked image is embedded), and the mask is
|
||||
taken off, in one undo step. A mask that hides nothing is only taken off, and the image keeps its pixels and its link.
|
||||
Images without a mask are left as they are, and an object in `ids` that is not an image is refused. Apply Color Mask
|
||||
also refuses, and changes nothing, when a mask made again for new pixels would give a grayscale image colorized with a
|
||||
process color an alpha channel. Both commands are
|
||||
enabled only when a selected image has a mask. → `{changed, ids}`
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"addon.image.colorMask","params":{"background":"colors","colors":"#00ff00","contiguous":false,"tolerance":15,"softness":10}}}
|
||||
{"name":"run_command","arguments":{"command":"addon.image.applyColorMask","params":{}}}
|
||||
```
|
||||
|
||||
## Type Tools add-on
|
||||
|
||||
Type Tools is a built-in add-on for type: its commands are `addon.type.textTweaker` and `addon.type.releasePath`, and
|
||||
|
||||
@@ -2075,6 +2075,10 @@ A VectorCraft addition (it replaces a script some users ran), so no area score m
|
||||
|
||||
A VectorCraft addition, so no area score moves. Object › Plug-ins › Image Tools › Remove Background… (`addon.image.removeBackground`) makes the background of the selected images transparent in their own pixels, in the app with no service or model. The background is the most common edge color (nothing is cleared when the edges hold at least as many clear pixels as pixels of that color), the average of the corners that are not clear, or picked colors. Contiguous (on by default) clears it only where it reaches in from an edge; off, it is cleared everywhere. Tolerance and Softness (0 to 100, 10 by default, VectorCraft's own values) set how near a color is cleared and the band over which pixels fade out, measured as RGB distance or, with Match set to Each Channel, as the largest difference on one channel. The run is one undo step, and none is recorded when no pixel changes. The image stays an image: a PNG with alpha at its own bit depth, gray or color as it was, with its color profile and resolution, and a linked image is embedded. CMYK images, linked images that show their preview, images that would need more than the decode allowance, results with no pixel left and grayscale images colorized with a process color are refused (the result has an alpha channel, and a process color can't colorize a grayscale image with one: [Convert color modes](https://web.archive.org/web/20251220223850/https://helpx.adobe.com/illustrator/desktop/manage-colors/select-and-adjust-colors/convert-color-modes.html), "Colorize Grayscale or 1-bit images"). The dialog's Preview works as Release Type on a Path's does; its preview steps decode an image once, and the decoded pixels are released when the dialog closes. Tests: `cmd::addons::image::tests` in `vectorcraft-engine` (edge, corner and picked colors, contiguous on and off, a second run, tolerance and softness by distance and by channel, 16-bit and gray images, the color profile and resolution kept, an image read at another size, linked images, refusals and bad params, and decoded pixels reused across preview steps and released at OK and Cancel), `addons::image::tests::remove_background_previews_cancels_and_runs_on_ok` in `vectorcraft-ui-egui` and `command_sweep`. Commands: `docs/mcp.md` › Image Tools add-on. Since 2026-11-12 a selected image also offers Remove Background in the Contextual Task Bar, Properties › Quick Actions and the Control bar, where the reference app offers it (<https://community.adobe.com/announcements-651/ai-powered-image-editing-features-in-illustrator-1624061>); a click runs it at once with the defaults, as one undo step (tests `canvas::tests::the_task_bar_removes_a_selected_images_background` and `addons::image::tests::properties_and_the_control_bar_remove_the_background_at_once`).
|
||||
|
||||
## Image Tools add-on: Color Mask (2026-11-12)
|
||||
|
||||
A VectorCraft addition, so no area score moves. Object › Plug-ins › Image Tools › Color Mask… (`addon.image.colorMask`) hides colors of the selected images while their pixels stay as they are. It matches colors as Remove Background does (edge, corner or picked colors, Contiguous, Match, Tolerance and Softness, VectorCraft's own values). The mask's settings and the masked pixels are kept with the image object and saved with the document. The canvas (clip groups and white-shape opacity masks included), printing and separations, PDF (PDF/X-1a and PDF/X-3 flatten the hidden pixels), SVG, EPS, PNG, DXF, EMF, WMF, the legacy .ai, the .ai editing data, copy and paste, and placing a document show the masked pixels. A linked image stays linked, and outputs that would refer to its file write the masked pixels instead. A masked grayscale image whose mask hides pixels takes only a spot, black or white colorize color; one whose mask hides no pixels takes a process color too. The dialog opens on the image's mask settings. Remove Color Mask takes the mask off, and Apply Color Mask makes it part of the image's pixels. Each is one undo step, and none is recorded when nothing changes. When the image's own pixels are replaced (a link updated or read again on open, the image cropped or recolored), the mask is made again for the new pixels. A click anywhere in the image's frame selects it. Tests: `cmd::addons::image::mask::tests` in `vectorcraft-engine` (hidden colors drawn, pixels kept, edit, report, remove and apply, the outputs listed, save and open, copy and paste, placing a document, a linked image, cropped and replaced pixels, colorize colors, refusals), `color_mask::tests` and `placed_document::tests` in `vectorcraft-doc`, the separations and CMYK soft-mask tests in `vectorcraft-pdf`, and `addons::image::tests::color_mask_previews_keeps_its_settings_and_comes_off` in `vectorcraft-ui-egui`. Commands: `docs/mcp.md` › Image Tools add-on › Color Mask.
|
||||
|
||||
## Color Effects add-on: Find Nearest Swatch (2026-11-01)
|
||||
|
||||
A VectorCraft addition, not a feature of the reference app, so no area score moves. Object › Plug-ins › Color Effects › Find Nearest Swatch… lists each colour of the selection (fills and strokes, as ticked; solid colours and gradient stops, inside groups and type) with the three swatches of the chosen library closest to it by CIEDE2000 difference, with that difference (`addon.color.nearestSwatch`, #4923). The library is the document's swatches, a built-in library, or a library or colour book the user saved or loaded (no colour book data ships with VectorCraft). Apply gives each colour its nearest swatch as one undo step (`addon.color.applyNearestSwatch`): global and spot swatches link, and a library's swatches the colours take are added to the document first, so a spot colour stays spot. Remaining: typing a colour to look up in the dialog (the command takes `color`), and applying a match other than the nearest. Tests: `cmd::addons::color::nearest::tests` (engine), `addons::color::nearest::tests::find_nearest_swatch_lists_the_matches_and_applies_the_nearest` (ui-egui).
|
||||
|
||||
Reference in New Issue
Block a user