Image Tools: Remove Background clears an image's background in its pixels

A new built-in add-on, Image Tools (Object › Plug-ins › Image Tools),
adds Remove Background… (`addon.image.removeBackground`). It makes the
background of the selected images transparent by lowering the alpha of
their own pixels, in one undo step, with no service or model. The
background is the most common color along the image's edges, the
average of the image's corners, or colors the user picks. Contiguous
(on by default) clears it only where it reaches in from an edge; off,
it is cleared wherever it is. Tolerance and Softness (0 to 100, 10 by
default) set how near a color must be to be cleared and the band over
which pixels fade out. Match measures that nearness as the RGB
distance or as the largest difference on one channel. It is a
VectorCraft addition.

- The image stays an image: an embedded PNG with alpha, the source's
  ICC profile and resolution, and 16 bits or gray when the source is.
  A linked image is embedded and keeps its Placement Options.
- When the edges hold at least as many clear pixels as pixels of their
  most common color, edge mode clears nothing, so a second run leaves
  the subject alone. Corner mode leaves clear corners out of the
  average and clears nothing when all four are clear.
- With Match set to Each Channel, a tolerance of t / 255 × 100 clears
  the colors within t 8-bit steps on every channel.
- A CMYK image, a linked image that shows its preview, an image that
  would need more than the decode allowance (checked from its header)
  and a result with no pixel left are refused, and the document stays
  as it was. When no pixel changes, no undo step is added. The result
  reports `cleared`, the pixels made fully clear.
- Picked colors are hex colors, with or without `#`, or black, white,
  red, green or blue.
- The dialog opens with Preview off; with Preview on, the canvas shows
  the result, and Cancel puts the image back. While the dialog
  previews, the session keeps the images' decoded pixels (at most
  128 MiB, 32 MiB in the web app) so each preview step does not decode
  them again, and releases them when the dialog closes with OK or
  Cancel. A run that is not a preview step keeps nothing.

Docs: docs/mcp.md (Image Tools add-on), docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-11-11 19:47:28 -08:00
parent 7dd71a6972
commit f65f3029a6
42 changed files with 1707 additions and 3 deletions
+1
View File
@@ -749,6 +749,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 use docmenu::guide_indexes;
pub(crate) use edit::{settle_interaction, typing_in_progress};
pub(crate) use object::bounds_rewrite;
+4
View File
@@ -6,12 +6,15 @@ mod cleanup;
mod color;
mod contour;
mod document;
mod image;
mod pathtools;
mod pen;
mod select;
mod shapes;
mod typetools;
pub(crate) use image::DecodedImages;
use super::CommandSpec;
/// The commands of every built-in add-on.
@@ -21,6 +24,7 @@ pub fn specs() -> Vec<CommandSpec> {
v.extend(color::specs());
v.extend(contour::specs());
v.extend(document::specs());
v.extend(image::specs());
v.extend(pathtools::specs());
v.extend(pen::specs());
v.extend(select::specs());
+598
View File
@@ -0,0 +1,598 @@
//! Image Tools (`addon.image.*`), a built-in add-on. Remove Background makes the background of the
//! 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.
#[cfg(test)]
mod tests;
use std::collections::{HashMap, HashSet};
use std::io::Cursor;
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_geom::Affine;
use super::super::fileio::{MAX_RASTER_ALLOC, ppi};
use super::super::menucmds::has_image;
use super::super::{CommandSpec, Result, Session, bad, checked_ids_param, cmd, hex_with_alpha, number_or, switch_or, text_param};
/// Remove Background's command.
pub const REMOVE_BACKGROUND: &str = "addon.image.removeBackground";
const C: &str = REMOVE_BACKGROUND;
/// The most colors `colors` lists.
const MAX_COLORS: usize = 16;
/// Bytes a pixel takes while it is worked on, beside its pixels: its level, whether the edge search
/// reached it, and its place in the search's stack (4 bytes, twice that while the stack grows).
const WORK_BYTES: u64 = 10;
pub fn specs() -> Vec<CommandSpec> {
vec![cmd!(
"addon.image.removeBackground",
"Remove Background…",
["Object", "Plug-ins", "Image Tools"],
None,
"{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, and a result with no pixel left 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
)]
}
/// Where the background's colors come from.
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum Background {
/// The most common color along the image's edges.
Edges,
/// The average of the image's four corner pixels that are not clear.
Corners,
/// These colors.
Colors(Vec<[u8; 3]>),
}
/// How near a color is to a background color.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Match {
/// The RGB distance, as a percentage of the largest one (black to white).
Distance,
/// The largest difference on one of red, green and blue, as a percentage of a channel's range.
Channels,
}
/// What Remove Background clears: the background, whether only where it reaches in from an edge,
/// and how near a color must be to it (a percentage, measured as `measure` says) to be cleared, and
/// to fade out past that.
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Settings {
pub background: Background,
pub contiguous: bool,
pub measure: Match,
pub tolerance: f64,
pub softness: f64,
}
impl Settings {
/// The settings `p` gives, each checked.
fn read(p: &Value) -> 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 (tolerance, softness) = (percent("tolerance")?, percent("softness")?);
let colors = match p.get("colors") {
None | Some(Value::Null) => None,
Some(v) => Some(colors(v)?),
};
let background = match text_param(p, "background", C)? {
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")),
};
let measure = match text_param(p, "match", C)? {
None | Some("distance") => Match::Distance,
Some("channels") => Match::Channels,
Some(_) => return Err(bad(C, "`match` must be distance or channels")),
};
let contiguous = switch_or(p, "contiguous", true, C)?;
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")
}
/// 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]>> {
// 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()),
};
if texts.is_empty() || texts.len() > MAX_COLORS {
return Err(colors_error());
}
texts.into_iter().map(|t| rgb8(t).ok_or_else(colors_error)).collect()
}
/// The 8-bit RGB of the color `t` writes.
fn rgb8(t: &str) -> Option<[u8; 3]> {
if hex_with_alpha(t) {
return None;
}
let [r, g, b] = Color::from_hex(t)?.to_rgb_uncalibrated();
Some([r, g, b].map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8))
}
fn remove_background(s: &mut Session, p: &Value) -> Result<Value> {
let settings = Settings::read(p)?;
// 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 = changes(s, p, &settings, previewing.then_some(&mut kept));
s.decoded_images = kept;
let changes = found?;
let ids: Vec<u64> = changes.iter().map(|(id, ..)| id.0).collect();
let made_clear: u64 = changes.iter().map(|(_, out, _)| out.cleared).sum();
if !changes.is_empty() {
s.edit("Remove Background", |d, _| {
for (id, out, xf) in changes {
let key = out.blob.content_key();
d.images.entry(key.clone()).or_insert(out.blob);
if let Some(NodeKind::Image(im)) = d.node_mut(id).map(|n| &mut n.kind) {
im.key = key;
(im.width, im.height, im.xf) = (out.width, out.height, xf);
im.link = None;
}
}
Ok(())
})?;
}
Ok(json!({ "changed": ids.len(), "ids": ids, "cleared": made_clear }))
}
/// The images `p` names (`ids`, or the selected images) whose pixels change, each with its pixels
/// 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 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(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(C, "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(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();
let mut changes = vec![];
for (id, im, blob) in images {
let out = match cleared.get(im.key.as_str()) {
Some(out) => out.clone(),
None => {
let out = clear(blob, settings, kept.as_deref_mut())?;
cleared.insert(&im.key, out.clone());
out
}
};
if let Some(out) = out {
// A file read at another size than the object's keeps the object's place and size.
let xf = if (out.width, out.height) == (im.width, im.height) {
im.xf
} else {
im.xf * Affine::scale_non_uniform(f64::from(im.width) / f64::from(out.width), f64::from(im.height) / f64::from(out.height))
};
changes.push((id, out, xf));
}
}
Ok(changes)
}
/// 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)]
pub(crate) struct Cleared {
pub blob: ImageBlob,
pub width: u32,
pub height: u32,
pub cleared: u64,
}
/// An image's pixels as straight RGBA, at 8 or 16 bits a sample.
#[derive(Clone)]
enum Pixels {
Eight(image::RgbaImage),
Sixteen(ImageBuffer<Rgba<u16>, Vec<u16>>),
}
/// The most bytes of decoded pixels and ICC profiles [`DecodedImages`] keeps (128 MiB, or 32 MiB in
/// the web app).
const KEEP_BYTES: u64 = MAX_RASTER_ALLOC / 16;
/// The images Remove Background decoded during an interaction's preview steps, the most recently
/// used first, at most [`KEEP_BYTES`] in all. The next preview step runs the command again on the
/// same bytes and takes their pixels from here instead of decoding them again. The session holds
/// it and empties it when the interaction ends (OK or Cancel). A run that is not a preview step
/// does not read or fill it.
#[derive(Default)]
pub(crate) struct DecodedImages(Vec<Decoded>);
/// An image decoded by [`clear`]: the bytes it was decoded from (held weakly, so they are not kept
/// alive for it, and an entry whose bytes are gone is dropped), its pixels, whether it was gray,
/// and its ICC profile.
struct Decoded {
source: Weak<Vec<u8>>,
pixels: Arc<Pixels>,
gray: bool,
icc: Option<Vec<u8>>,
}
#[cfg(test)]
thread_local! {
/// How many times this thread decoded an image in [`clear`].
pub(crate) static DECODES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
impl DecodedImages {
/// Drops every image kept.
pub(crate) fn empty(&mut self) {
self.0.clear();
}
/// How many images are kept.
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.0.len()
}
/// The pixels, gray flag and ICC profile kept for `bytes`, if they are kept. Images whose bytes
/// are gone are dropped first.
fn get(&mut self, bytes: &Arc<Vec<u8>>) -> Option<(Arc<Pixels>, bool, Option<Vec<u8>>)> {
self.0.retain(|d| d.source.strong_count() > 0);
let at = self.0.iter().position(|d| std::ptr::eq(d.source.as_ptr(), Arc::as_ptr(bytes)))?;
let d = self.0.remove(at);
let found = (Arc::clone(&d.pixels), d.gray, d.icc.clone());
self.0.insert(0, d);
Some(found)
}
/// Keeps `pixels` decoded from `bytes` first, in place of anything kept for the same bytes, and
/// drops images whose bytes are gone and the least recently used past [`KEEP_BYTES`].
fn keep(&mut self, bytes: &Arc<Vec<u8>>, pixels: Arc<Pixels>, gray: bool, icc: Option<Vec<u8>>) {
self.0.retain(|d| d.source.strong_count() > 0 && !std::ptr::eq(d.source.as_ptr(), Arc::as_ptr(bytes)));
self.0.insert(0, Decoded { source: Arc::downgrade(bytes), pixels, gray, icc });
let mut total = 0u64;
self.0.retain(|d| {
total = total.saturating_add(d.bytes());
total <= KEEP_BYTES
});
}
}
impl Decoded {
/// How many bytes the entry keeps: its pixels and its ICC profile.
fn bytes(&self) -> u64 {
self.pixels.bytes().saturating_add(self.icc.as_ref().map_or(0, |icc| icc.len() as u64))
}
}
impl Pixels {
/// How many bytes the pixels take.
fn bytes(&self) -> u64 {
match self {
Self::Eight(img) => img.as_raw().len() as u64,
Self::Sixteen(img) => img.as_raw().len() as u64 * 2,
}
}
/// The width and height in pixels.
fn dimensions(&self) -> (u32, u32) {
match self {
Self::Eight(img) => img.dimensions(),
Self::Sixteen(img) => img.dimensions(),
}
}
/// Pixel `i` (row by row from the top left) at 16 bits a sample (an 8-bit sample `v` is
/// `v * 257`).
fn rgba16(&self, i: usize) -> [u16; 4] {
let mut out = [0u16; 4];
match self {
Self::Eight(img) => {
for (o, v) in out.iter_mut().zip(img.as_raw().get(i * 4..i * 4 + 4).unwrap_or_default()) {
*o = u16::from(*v) * 257;
}
}
Self::Sixteen(img) => {
for (o, v) in out.iter_mut().zip(img.as_raw().get(i * 4..i * 4 + 4).unwrap_or_default()) {
*o = *v;
}
}
}
out
}
}
/// `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>> {
if blob.is_cmyk() {
return Err(bad(C, "a CMYK image has no transparency to set"));
}
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)?,
};
// 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));
let (w, h) = px.dimensions();
let levels = levels(w as usize, h as usize, &|i| px.rgba16(i), settings);
let Some(cleared) = apply(&mut px, &levels) else { return Ok(None) };
let left = match &px {
Pixels::Eight(img) => img.pixels().any(|p| p.0[3] > 0),
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"));
}
let out = match (px, gray) {
(Pixels::Eight(img), false) => DynamicImage::ImageRgba8(img),
(Pixels::Sixteen(img), false) => DynamicImage::ImageRgba16(img),
(Pixels::Eight(img), true) => {
DynamicImage::ImageLumaA8(ImageBuffer::from_fn(w, h, |x, y| LumaA([img.get_pixel(x, y).0[0], img.get_pixel(x, y).0[3]])))
}
(Pixels::Sixteen(img), true) => {
DynamicImage::ImageLumaA16(ImageBuffer::from_fn(w, h, |x, y| LumaA([img.get_pixel(x, y).0[0], img.get_pixel(x, y).0[3]])))
}
};
let mut png = Vec::new();
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()))?;
}
out.write_with_encoder(encoder).map_err(|e| bad(C, 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);
}
Ok(Some(Cleared { blob: ImageBlob::png(png), width: w, height: h, cleared }))
}
/// `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}")))?;
// The ICC profile the colors are in goes with them into the PNG.
let (w, h, kind, icc) = {
use image::ImageDecoder as _;
let (w, h) = decoder.dimensions();
(w, h, decoder.color_type(), decoder.icc_profile().ok().flatten())
};
drop(decoder);
// The file's pixels, those worked on (as many again for the PNG made of them) and the work,
// checked from the header before anything is decoded.
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")));
}
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}")))?;
#[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);
let px = Arc::new(if deep { Pixels::Sixteen(img.into_rgba16()) } else { Pixels::Eight(img.into_rgba8()) });
// A kept copy is worked on as a copy: one more set of the pixels worked on.
let with_copy = per_pixel + if deep { 8 } else { 4 };
if let Some(kept) = kept
&& u64::from(w).checked_mul(u64::from(h)).and_then(|n| n.checked_mul(with_copy)).is_some_and(|n| n <= MAX_RASTER_ALLOC)
&& px.bytes().saturating_add(icc.as_ref().map_or(0, |icc| icc.len() as u64)) <= KEEP_BYTES
{
kept.keep(&blob.bytes, Arc::clone(&px), gray, icc.clone());
}
Ok((px, gray, icc))
}
/// Each pixel's alpha scaled by its level (255 keeps it). Returns how many pixels were made fully
/// clear, or `None` when no pixel changed.
fn apply(px: &mut Pixels, levels: &[u8]) -> Option<u64> {
let (mut changed, mut cleared) = (false, 0u64);
match px {
Pixels::Eight(img) => {
for (p, &l) in img.pixels_mut().zip(levels) {
let a = ((u32::from(p.0[3]) * u32::from(l) + 127) / 255) as u8;
changed |= a != p.0[3];
cleared += u64::from(a == 0 && p.0[3] != 0);
p.0[3] = a;
}
}
Pixels::Sixteen(img) => {
for (p, &l) in img.pixels_mut().zip(levels) {
let a = ((u32::from(p.0[3]) * u32::from(l) + 127) / 255) as u16;
changed |= a != p.0[3];
cleared += u64::from(a == 0 && p.0[3] != 0);
p.0[3] = a;
}
}
}
changed.then_some(cleared)
}
/// How much of each of the `w` × `h` pixels `px` gives (row by row, straight RGBA at 16 bits a
/// sample) stays, from 0 (cleared) to 255 (kept), as `settings` say. Each pixel's distance to the
/// background is worked out once. A pixel past the first `u32::MAX` is left kept by the edge search.
pub(crate) fn levels(w: usize, h: usize, px: &dyn Fn(usize) -> [u16; 4], settings: &Settings) -> Vec<u8> {
let n = w.saturating_mul(h);
let refs: Vec<[u16; 3]> = match &settings.background {
Background::Colors(refs) => refs.iter().map(|c| c.map(|v| u16::from(v) * 257)).collect(),
Background::Edges => match edge_color(w, h, px) {
Some(edge) => vec![edge],
None => return vec![255; n],
},
Background::Corners => match corner_color(w, h, px) {
Some(corner) => vec![corner],
None => return vec![255; n],
},
};
let mut out: Vec<u8> = (0..n)
.map(|i| {
let [r, g, b, _] = px(i);
level([r, g, b], &refs, settings)
})
.collect();
if settings.contiguous {
// Out from the edges, through clear pixels and pixels that lose some alpha; the pixels it
// doesn't reach are kept.
let reaches = |out: &[u8], i: usize| out.get(i).is_some_and(|l| *l < 255) || px(i)[3] == 0;
let mut seen = vec![false; n];
let mut stack: Vec<u32> = vec![];
for i in border(w, h).filter(|&i| reaches(&out, i)) {
if let (Some(s), Ok(at)) = (seen.get_mut(i), u32::try_from(i)) {
*s = true;
stack.push(at);
}
}
while let Some(at) = stack.pop() {
let i = at as usize;
let (x, y) = (i % w, i / w);
let next = [(x > 0).then(|| i - 1), (x + 1 < w).then(|| i + 1), (y > 0).then(|| i - w), (y + 1 < h).then(|| i + w)];
for j in next.into_iter().flatten() {
if let Some(s) = seen.get_mut(j)
&& !*s
&& reaches(&out, j)
&& let Ok(at) = u32::try_from(j)
{
*s = true;
stack.push(at);
}
}
}
for (l, s) in out.iter_mut().zip(&seen) {
if !*s {
*l = 255;
}
}
}
out
}
/// The level of a pixel of color `c`: 0 within the tolerance of the nearest of `refs`, 255 from
/// the tolerance plus the softness on, and in proportion between them.
fn level(c: [u16; 3], refs: &[[u16; 3]], settings: &Settings) -> u8 {
let Settings { tolerance, softness, measure, .. } = *settings;
let d = refs.iter().map(|r| apart(c, *r, measure)).fold(f64::INFINITY, f64::min);
if d <= tolerance {
0
} else if d >= tolerance + softness {
255
} else {
((d - tolerance) / softness * 255.0).round().clamp(0.0, 255.0) as u8
}
}
/// How far apart `a` and `b` are, as a percentage, measured as `measure` says: the RGB distance of
/// the largest one (black to white), or the largest difference on one channel of the channel's range.
/// A difference of `k` 8-bit steps on one channel is exactly `k / 255 × 100` with channels.
fn apart(a: [u16; 3], b: [u16; 3], measure: Match) -> f64 {
match measure {
Match::Distance => {
let sq: f64 = a.iter().zip(b).map(|(x, y)| (f64::from(*x) - f64::from(y)).powi(2)).sum();
sq.sqrt() / (65_535.0 * 3f64.sqrt()) * 100.0
}
Match::Channels => f64::from(a.iter().zip(b).map(|(x, y)| x.abs_diff(y)).max().unwrap_or(0)) / 65_535.0 * 100.0,
}
}
/// The pixels on the edges of a `w` × `h` image, each once.
fn border(w: usize, h: usize) -> impl Iterator<Item = usize> {
let top = (0..w).filter(move |_| h > 0);
let bottom = (0..w).filter(move |_| h > 1).map(move |x| (h - 1) * w + x);
let sides = (1..h.saturating_sub(1)).flat_map(move |y| [Some(y * w), (w > 1).then(|| y * w + w - 1)].into_iter().flatten());
top.chain(bottom).chain(sides)
}
/// The most common color along an image's edges: its edge pixels grouped by 16 levels a channel,
/// with the clear ones as a group of their own, as the average of the largest group. `None` when
/// the clear edge pixels are at least as many as that group: the background is already cleared.
fn edge_color(w: usize, h: usize, px: &dyn Fn(usize) -> [u16; 4]) -> Option<[u16; 3]> {
let mut groups: HashMap<[u16; 3], (u64, [u64; 3])> = HashMap::new();
let mut clear = 0u64;
for i in border(w, h) {
let [r, g, b, a] = px(i);
if a == 0 {
clear += 1;
continue;
}
let e = groups.entry([r >> 12, g >> 12, b >> 12]).or_default();
e.0 += 1;
for (s, v) in e.1.iter_mut().zip([r, g, b]) {
*s += u64::from(v);
}
}
// Equal groups go by color, so the result doesn't depend on the map's order.
let (_, (n, sums)) = groups.into_iter().max_by(|a, b| a.1.0.cmp(&b.1.0).then(b.0.cmp(&a.0)))?;
if clear >= n {
return None;
}
Some(sums.map(|s| ((s + n / 2) / n.max(1)) as u16))
}
/// The average of the four corner pixels of a `w` × `h` image that are not clear (a corner of a
/// one-pixel-wide image counts twice). `None` when all four are clear: the background is already
/// cleared.
fn corner_color(w: usize, h: usize, px: &dyn Fn(usize) -> [u16; 4]) -> Option<[u16; 3]> {
if w == 0 || h == 0 {
return None;
}
let corners = [0, w - 1, (h - 1) * w, (h - 1) * w + w - 1];
let (mut n, mut sums) = (0u64, [0u64; 3]);
for [r, g, b, a] in corners.map(px) {
if a > 0 {
n += 1;
for (s, v) in sums.iter_mut().zip([r, g, b]) {
*s += u64::from(v);
}
}
}
(n > 0).then(|| sums.map(|s| ((s + n / 2) / n) as u16))
}
+480
View File
@@ -0,0 +1,480 @@
use std::io::Cursor;
use std::sync::Arc;
use image::{DynamicImage, ImageBuffer, Luma, Rgb};
use serde_json::{Value, json};
use vectorcraft_doc::{ImageBlob, ImageObject, LinkInfo, Node, NodeId, NodeKind};
use vectorcraft_geom::Affine;
use super::{Background, Match, REMOVE_BACKGROUND as C, Settings, level, levels};
use crate::Session;
use crate::cmd::fileio::ppi;
const WHITE: [u8; 4] = [255, 255, 255, 255];
const RED: [u8; 4] = [200, 0, 0, 255];
/// The command's defaults with `background`.
fn settings(background: Background) -> Settings {
Settings { background, contiguous: true, measure: Match::Distance, tolerance: 10.0, softness: 10.0 }
}
fn png(img: DynamicImage) -> Vec<u8> {
let mut out = Vec::new();
img.write_to(&mut Cursor::new(&mut out), image::ImageFormat::Png).unwrap();
out
}
/// 12 × 10 pixels: white, with a red block from (3, 3) to (9, 7) that holds one white pixel at
/// (6, 5), which no white edge pixel reaches.
fn framed() -> image::RgbaImage {
image::RgbaImage::from_fn(12, 10, |x, y| {
let block = (3..9).contains(&x) && (3..7).contains(&y);
image::Rgba(if block && (x, y) != (6, 5) { RED } else { WHITE })
})
}
/// A session with `bytes` opened as a document, its image selected.
fn open(bytes: &[u8]) -> (Session, NodeId) {
let mut s = Session::new();
s.execute("document.open", &json!({"name": "art.png", "dataBase64": vectorcraft_format::base64_encode(bytes)})).unwrap();
s.execute("select.all", &json!({})).unwrap();
let id = s.doc().unwrap().selection.objects[0];
(s, id)
}
fn image_of(s: &Session, id: NodeId) -> ImageObject {
let NodeKind::Image(im) = &s.doc().unwrap().doc.node(id).unwrap().kind else { panic!("an image") };
(**im).clone()
}
fn bytes_of(s: &Session, id: NodeId) -> Arc<Vec<u8>> {
s.doc().unwrap().doc.images[&image_of(s, id).key].bytes.clone()
}
fn pixels(s: &Session, id: NodeId) -> DynamicImage {
image::load_from_memory(&bytes_of(s, id)).unwrap()
}
fn alpha(img: &DynamicImage, x: u32, y: u32) -> u8 {
img.to_rgba8().get_pixel(x, y).0[3]
}
fn undo_len(s: &Session) -> usize {
s.doc().unwrap().history.undo.len()
}
#[test]
fn the_edge_colors_are_cleared_where_they_reach_in_from_an_edge() {
let (mut s, id) = open(&png(DynamicImage::ImageRgba8(framed())));
let before = (image_of(&s, id), undo_len(&s));
let r = s.execute(C, &json!({})).unwrap();
assert_eq!(r, json!({"changed": 1, "ids": [id.0], "cleared": 12 * 10 - 6 * 4}));
let out = pixels(&s, id);
assert_eq!(out.color(), image::ColorType::Rgba8);
assert_eq!((alpha(&out, 0, 0), alpha(&out, 11, 9), alpha(&out, 2, 4)), (0, 0, 0), "the white around the block");
assert_eq!(out.to_rgba8().get_pixel(4, 4).0, RED, "the block");
assert_eq!(alpha(&out, 6, 5), 255, "white inside the block, which no edge reaches");
let after = image_of(&s, id);
assert_eq!((after.width, after.height, after.xf), (12, 10, before.0.xf), "the same place and size");
assert_eq!(undo_len(&s), before.1 + 1, "one undo step");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(image_of(&s, id), before.0);
}
/// Picked colors, like the other backgrounds, are cleared where they reach in from an edge, and with
/// `contiguous: false` wherever they are.
#[test]
fn picked_colors_are_cleared_from_the_edges_or_wherever_they_are() {
let (mut s, id) = open(&png(DynamicImage::ImageRgba8(framed())));
s.execute(C, &json!({"background": "colors", "colors": ["#fff"], "tolerance": 0, "softness": 0})).unwrap();
let out = pixels(&s, id);
assert_eq!((alpha(&out, 0, 0), alpha(&out, 6, 5), alpha(&out, 4, 4)), (0, 255, 255), "not inside the block");
let (mut s, id) = open(&png(DynamicImage::ImageRgba8(framed())));
s.execute(C, &json!({"background": "colors", "colors": ["#fff"], "contiguous": false, "tolerance": 0, "softness": 0})).unwrap();
let out = pixels(&s, id);
assert_eq!((alpha(&out, 0, 0), alpha(&out, 6, 5), alpha(&out, 4, 4)), (0, 0, 255), "inside the block too; the red stays");
}
/// With `contiguous: false` the edge color is cleared inside the subject too.
#[test]
fn the_edge_color_can_be_cleared_wherever_it_is() {
let (mut s, id) = open(&png(DynamicImage::ImageRgba8(framed())));
let r = s.execute(C, &json!({"contiguous": false})).unwrap();
assert_eq!(r["cleared"], 12 * 10 - 6 * 4 + 1);
let out = pixels(&s, id);
assert_eq!((alpha(&out, 0, 0), alpha(&out, 6, 5), alpha(&out, 4, 4)), (0, 0, 255));
}
/// `colors` as one string split by commas and spaces, written as other color params take them.
#[test]
fn picked_colors_can_be_one_string_of_colors() {
for colors in ["#00ff00, ffffff", "green white", "#0f0,#FFF"] {
let (mut s, id) = open(&png(DynamicImage::ImageRgba8(framed())));
s.execute(C, &json!({"background": "colors", "colors": colors, "contiguous": false, "tolerance": 0, "softness": 0})).unwrap();
let out = pixels(&s, id);
assert_eq!((alpha(&out, 0, 0), alpha(&out, 6, 5), alpha(&out, 4, 4)), (0, 0, 255), "{colors}");
}
}
/// A subject along a whole edge stays: only the most common edge color is the background.
#[test]
fn a_subject_along_one_edge_is_not_the_background() {
let img = image::RgbaImage::from_fn(10, 10, |_, y| image::Rgba(if y >= 7 { RED } else { WHITE }));
let (mut s, id) = open(&png(DynamicImage::ImageRgba8(img)));
s.execute(C, &json!({})).unwrap();
let out = pixels(&s, id);
assert_eq!((alpha(&out, 5, 0), alpha(&out, 5, 9), alpha(&out, 0, 8)), (0, 255, 255));
}
/// The preview steps of one interaction, as the dialog's are, decode an image once and give what a
/// run on freshly decoded pixels gives. The pixels are kept only until the interaction ends (OK or
/// Cancel), and a run that is not a preview step keeps none.
#[test]
fn preview_steps_on_the_same_pixels_decode_them_once() {
let decodes = || super::DECODES.with(std::cell::Cell::get);
let (mut s, id) = open(&png(DynamicImage::ImageRgba8(framed())));
let start = decodes();
let p = json!({"background": "colors", "colors": "#c80000", "contiguous": false, "tolerance": 0, "softness": 0});
s.begin_interaction("Remove Background").unwrap();
s.preview(C, &json!({"tolerance": 0, "softness": 0})).unwrap();
s.preview(C, &json!({"tolerance": 20, "softness": 0})).unwrap();
s.preview(C, &p).unwrap();
assert_eq!((decodes() - start, s.decoded_images.len()), (1, 1), "decoded once for three preview steps");
s.commit_interaction().unwrap();
assert_eq!(s.decoded_images.len(), 0, "OK lets the pixels go");
let (mut fresh, fresh_id) = open(&png(DynamicImage::ImageRgba8(framed())));
fresh.execute(C, &p).unwrap();
assert_eq!((decodes() - start, fresh.decoded_images.len()), (2, 0), "a run that is not a preview step keeps nothing");
assert_eq!(bytes_of(&s, id), bytes_of(&fresh, fresh_id), "the same result");
assert_eq!((alpha(&pixels(&s, id), 4, 4), alpha(&pixels(&s, id), 0, 0)), (0, 255), "the red cleared, the white kept");
s.execute("edit.undo", &json!({})).unwrap();
s.begin_interaction("Remove Background").unwrap();
s.preview(C, &json!({})).unwrap();
assert_eq!(s.decoded_images.len(), 1);
s.cancel_interaction().unwrap();
assert_eq!(s.decoded_images.len(), 0, "Cancel lets the pixels go");
s.execute(C, &json!({})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
s.execute(C, &json!({})).unwrap();
assert_eq!(decodes() - start, 5, "each run that is not a preview step decodes");
}
/// Pixels a preview step takes from the step before keep what they were decoded with: a 16-bit gray
/// image with an ICC profile comes out as a run on freshly decoded pixels gives it.
#[test]
fn reused_pixels_keep_their_depth_gray_and_profile() {
use image::{ImageDecoder as _, ImageEncoder as _};
let icc = b"a profile's bytes".to_vec();
let gray16 = ImageBuffer::from_fn(8, 8, |x, y| Luma([if (2..6).contains(&x) && (2..6).contains(&y) { 7_000u16 } else { 61_000 }]));
let mut bytes = Vec::new();
let mut encoder = image::codecs::png::PngEncoder::new(&mut bytes);
encoder.set_icc_profile(icc.clone()).unwrap();
DynamicImage::ImageLuma16(gray16).write_with_encoder(encoder).unwrap();
let decodes = || super::DECODES.with(std::cell::Cell::get);
let (mut s, id) = open(&bytes);
let start = decodes();
s.begin_interaction("Remove Background").unwrap();
s.preview(C, &json!({"tolerance": 0, "softness": 0})).unwrap();
s.preview(C, &json!({})).unwrap();
assert_eq!(decodes() - start, 1, "the second step took the pixels of the first");
let (mut fresh, fresh_id) = open(&bytes);
fresh.execute(C, &json!({})).unwrap();
assert_eq!(bytes_of(&s, id), bytes_of(&fresh, fresh_id), "the same result");
let DynamicImage::ImageLumaA16(out) = pixels(&s, id) else { panic!("16-bit gray with alpha") };
assert_eq!((out.get_pixel(0, 0).0, out.get_pixel(3, 3).0), ([61_000, 0], [7_000, 65_535]));
let result = bytes_of(&s, id);
let mut decoder = image::codecs::png::PngDecoder::new(Cursor::new(result.as_slice())).unwrap();
assert_eq!(decoder.icc_profile().unwrap(), Some(icc));
}
/// Once the background is cleared, most edge pixels are clear and a second run clears nothing: the
/// subject's color along an edge is not taken for the background.
#[test]
fn a_second_run_leaves_the_subject() {
let img = image::RgbaImage::from_fn(10, 10, |x, y| {
image::Rgba(if y >= 7 {
RED
} else if (4..6).contains(&x) && (3..5).contains(&y) {
[0, 0, 200, 255]
} else {
WHITE
})
});
let (mut s, id) = open(&png(DynamicImage::ImageRgba8(img)));
s.execute(C, &json!({})).unwrap();
let (first, steps) = (image_of(&s, id), undo_len(&s));
let r = s.execute(C, &json!({})).unwrap();
assert_eq!(r, json!({"changed": 0, "ids": [], "cleared": 0}));
assert_eq!((image_of(&s, id), undo_len(&s)), (first, steps), "no change and no undo step");
let out = pixels(&s, id);
assert_eq!((alpha(&out, 5, 0), alpha(&out, 5, 9), alpha(&out, 4, 3)), (0, 255, 255), "the red and the blue stay");
}
#[test]
fn tolerance_and_softness_set_how_near_a_color_is_cleared() {
// White; 1.96 % from white; 21.6 % from white.
let px = [WHITE, [250, 250, 250, 255], [200, 200, 200, 255]];
let at = |i: usize| px[i].map(|v| u16::from(v) * 257);
let run = |tolerance, softness| levels(3, 1, &at, &Settings { tolerance, softness, ..settings(Background::Colors(vec![[255; 3]])) });
assert_eq!(run(0.0, 0.0), [0, 255, 255], "only white itself at 0");
assert_eq!(run(2.0, 0.0), [0, 0, 255]);
assert_eq!(run(0.0, 10.0), [0, 50, 255], "1.96 % into a 10 % fade");
assert_eq!(run(100.0, 0.0), [0, 0, 0], "everything at 100");
}
#[test]
fn a_result_with_no_pixel_left_is_refused_and_changes_nothing() {
let (mut s, id) = open(&png(DynamicImage::ImageRgba8(framed())));
let before = (image_of(&s, id), undo_len(&s));
let e = s.execute(C, &json!({"background": "colors", "colors": "#ffffff", "tolerance": 100})).unwrap_err();
assert!(e.to_string().contains("every pixel matches the background"), "{e}");
assert_eq!((image_of(&s, id), undo_len(&s)), before);
}
#[test]
fn an_image_whose_pixels_do_not_change_is_left_with_no_undo_step() {
let (mut s, id) = open(&png(DynamicImage::ImageRgba8(framed())));
let before = (image_of(&s, id), undo_len(&s));
let r = s.execute(C, &json!({"background": "colors", "colors": "#00ff00", "tolerance": 0, "softness": 0})).unwrap();
assert_eq!(r, json!({"changed": 0, "ids": [], "cleared": 0}));
assert_eq!((image_of(&s, id), undo_len(&s)), before);
// An image whose edges and corners are already clear has no edge or corner color to clear,
// whatever color the clear pixels hold.
let ring =
image::RgbaImage::from_fn(6, 6, |x, y| image::Rgba(if (1..5).contains(&x) && (1..5).contains(&y) { WHITE } else { [255, 255, 255, 0] }));
for p in [json!({}), json!({"background": "corners", "contiguous": false})] {
let (mut s, _) = open(&png(DynamicImage::ImageRgba8(ring.clone())));
assert_eq!(s.execute(C, &p).unwrap()["changed"], 0, "{p}");
}
}
/// The corners' color: 40 × 30 pixels of light gray around a red block, cleared within 5 steps on
/// each channel (`match: channels`, a tolerance of `5 / 255 × 100`, no softness), from the edges
/// or wherever it is.
#[test]
fn the_corners_color_is_cleared() {
let img = image::RgbaImage::from_fn(40, 30, |x, y| {
image::Rgba(if (10..30).contains(&x) && (8..22).contains(&y) { [200, 40, 40, 255] } else { [240, 240, 240, 255] })
});
for contiguous in [false, true] {
let (mut s, id) = open(&png(DynamicImage::ImageRgba8(img.clone())));
let p = json!({"background": "corners", "contiguous": contiguous, "match": "channels", "tolerance": 5.0 / 255.0 * 100.0, "softness": 0});
let r = s.execute(C, &p).unwrap();
assert_eq!(r["cleared"], 40 * 30 - 14 * 20, "{p}");
let out = pixels(&s, id).to_rgba8();
assert_eq!((out.get_pixel(0, 0).0[3], out.get_pixel(15, 15).0), (0, [200, 40, 40, 255]), "{p}");
}
// A clear corner is left out of the average: the other three are white, and white is cleared.
let mut img = image::RgbaImage::from_pixel(8, 8, image::Rgba(WHITE));
img.put_pixel(0, 0, image::Rgba([0; 4]));
img.put_pixel(4, 4, image::Rgba(RED));
let (mut s, id) = open(&png(DynamicImage::ImageRgba8(img)));
let r = s.execute(C, &json!({"background": "corners", "tolerance": 0, "softness": 0})).unwrap();
let out = pixels(&s, id);
assert_eq!((alpha(&out, 7, 7), alpha(&out, 3, 3), alpha(&out, 4, 4)), (0, 0, 255));
assert_eq!(r["cleared"], 8 * 8 - 2, "the pixels made clear, not the corner that was clear already");
}
/// One picked color matched by channel at a tolerance of 0: cleared wherever it is, and a pixel a
/// long way from it kept.
#[test]
fn one_picked_color_is_cleared_by_channel() {
let mut img = image::RgbaImage::from_pixel(4, 4, image::Rgba([10, 20, 30, 255]));
img.put_pixel(2, 2, image::Rgba([250, 250, 250, 255]));
let (mut s, id) = open(&png(DynamicImage::ImageRgba8(img)));
let p = json!({"background": "colors", "colors": "#0a141e", "contiguous": false, "match": "channels", "tolerance": 0, "softness": 0});
assert_eq!(s.execute(C, &p).unwrap()["cleared"], 15);
let out = pixels(&s, id);
assert_eq!((alpha(&out, 0, 0), alpha(&out, 2, 2)), (0, 255));
}
/// With `match: channels`, a tolerance of `t / 255 × 100` clears a color `k` 8-bit steps from the
/// background on one channel exactly when `k ≤ t`, for every `t` and `k` from 0 to 255; with
/// `distance` the same color is nearer when the others match.
#[test]
fn channels_match_the_largest_difference_on_one_channel() {
for t in 0..=255u16 {
let s = Settings { measure: Match::Channels, tolerance: f64::from(t) / 255.0 * 100.0, softness: 0.0, ..settings(Background::Edges) };
for k in 0..=255u16 {
for c in [[k * 257, 0, 0], [0, k * 257, 0], [0, 0, k * 257]] {
assert_eq!(level(c, &[[0; 3]], &s) == 0, k <= t, "t {t}, k {k}");
}
}
}
let at = |measure| {
level([0, 40 * 257, 0], &[[0; 3]], &Settings { measure, tolerance: 32.0 / 255.0 * 100.0, softness: 0.0, ..settings(Background::Edges) })
};
assert_eq!((at(Match::Channels), at(Match::Distance)), (255, 0), "40 steps on green: past 32 on one channel; 9 % of the RGB distance");
}
#[test]
fn sixteen_bit_and_gray_images_keep_their_depth_and_channels() {
let deep = ImageBuffer::from_fn(8, 8, |x, y| Rgb(if (2..6).contains(&x) && (2..6).contains(&y) { [51_400, 0, 1] } else { [65_535; 3] }));
let (mut s, id) = open(&png(DynamicImage::ImageRgb16(deep)));
s.execute(C, &json!({})).unwrap();
let DynamicImage::ImageRgba16(out) = pixels(&s, id) else { panic!("16-bit RGBA") };
assert_eq!(out.get_pixel(0, 0).0[3], 0);
assert_eq!(out.get_pixel(3, 3).0, [51_400, 0, 1, 65_535], "16-bit samples kept as they were");
// Samples one 8-bit step apart are told apart: only the background is cleared at 0.
let near = ImageBuffer::from_fn(4, 4, |x, y| Rgb(if (1..3).contains(&x) && (1..3).contains(&y) { [65_280; 3] } else { [65_535; 3] }));
let (mut s, id) = open(&png(DynamicImage::ImageRgb16(near)));
s.execute(C, &json!({"tolerance": 0, "softness": 0})).unwrap();
let DynamicImage::ImageRgba16(out) = pixels(&s, id) else { panic!("16-bit RGBA") };
assert_eq!((out.get_pixel(0, 0).0[3], out.get_pixel(1, 1).0[3]), (0, 65_535));
let gray = ImageBuffer::from_fn(8, 8, |x, y| Luma([if (2..6).contains(&x) && (2..6).contains(&y) { 30u8 } else { 240 }]));
let (mut s, id) = open(&png(DynamicImage::ImageLuma8(gray)));
s.execute(C, &json!({})).unwrap();
let DynamicImage::ImageLumaA8(out) = pixels(&s, id) else { panic!("gray with alpha") };
assert_eq!((out.get_pixel(0, 0).0, out.get_pixel(3, 3).0), ([240, 0], [30, 255]));
let gray16 = ImageBuffer::from_fn(8, 8, |x, y| Luma([if (2..6).contains(&x) && (2..6).contains(&y) { 7_000u16 } else { 61_000 }]));
let (mut s, id) = open(&png(DynamicImage::ImageLuma16(gray16)));
s.execute(C, &json!({})).unwrap();
let DynamicImage::ImageLumaA16(out) = pixels(&s, id) else { panic!("16-bit gray with alpha") };
assert_eq!((out.get_pixel(0, 0).0, out.get_pixel(3, 3).0), ([61_000, 0], [7_000, 65_535]));
}
/// The ICC profile the colors are in stays with them.
#[test]
fn the_color_profile_is_kept() {
use image::{ImageDecoder as _, ImageEncoder as _};
let icc = b"a profile's bytes".to_vec();
let mut bytes = Vec::new();
let mut encoder = image::codecs::png::PngEncoder::new(&mut bytes);
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 mut decoder = image::codecs::png::PngDecoder::new(Cursor::new(out.blob.bytes.as_slice())).unwrap();
assert_eq!(decoder.icc_profile().unwrap(), Some(icc));
}
/// The resolution the file declares stays with the pixels, so a later Replace or relink sizes the
/// image as before.
#[test]
fn the_resolution_is_kept() {
let bytes = ppi::with_png_resolution(&png(DynamicImage::ImageRgba8(framed())), (300.0, 150.0));
let (mut s, id) = open(&bytes);
s.execute(C, &json!({})).unwrap();
let (x, y) = ppi::resolution(&bytes_of(&s, id)).expect("a resolution");
assert_eq!((x.round(), y.round()), (300.0, 150.0));
}
#[test]
fn a_cmyk_image_is_refused() {
let inks = vectorcraft_doc::cmyk::Inks::new(4, 2, [0, 0, 0, 0].repeat(8)).unwrap();
let tiff = ImageBlob::cmyk_tiff(&inks).unwrap();
let (mut s, id) = open(&tiff.bytes);
let before = (image_of(&s, id), undo_len(&s));
let e = s.execute(C, &json!({})).unwrap_err();
assert!(e.to_string().contains("a CMYK image has no transparency to set"), "{e}");
assert_eq!((image_of(&s, id), undo_len(&s)), before);
}
/// A linked image of `blob`, `width` × `height` pixels as the object records it, beside the open
/// document's image.
fn place_linked(s: &mut Session, key: &str, blob: ImageBlob, (width, height): (u32, u32)) -> NodeId {
s.edit("Place", |d, sel| {
d.images.insert(key.into(), blob);
let id = d.alloc_id();
let im = ImageObject {
key: key.into(),
width,
height,
xf: Default::default(),
link: Some(LinkInfo::new("/nowhere/art.png")),
placement: Default::default(),
pixels: None,
};
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(Box::new(im))))?;
sel.set([id]);
Ok(id)
})
.unwrap()
}
#[test]
fn a_linked_image_is_embedded_and_one_that_shows_its_preview_is_refused() {
let bytes = png(DynamicImage::ImageRgba8(framed()));
let (mut s, _) = open(&bytes);
let linked = place_linked(&mut s, "linked", ImageBlob::png(bytes.clone()), (12, 10));
let clip = vectorcraft_doc::links::PlacementOptions { clip: true, ..Default::default() };
s.edit("Placement", |d, _| {
if let Some(NodeKind::Image(im)) = d.node_mut(linked).map(|n| &mut n.kind) {
im.placement = clip;
}
Ok(())
})
.unwrap();
let r = s.execute(C, &json!({"ids": [linked.0, linked.0]})).unwrap();
assert_eq!(r, json!({"changed": 1, "ids": [linked.0], "cleared": 12 * 10 - 6 * 4}), "an image named twice changes once");
let im = image_of(&s, linked);
assert_eq!((im.link, alpha(&pixels(&s, linked), 0, 0)), (None, 0), "embedded, with its background cleared");
assert_eq!(im.placement, clip, "its Placement Options kept, as Embed keeps them");
// Pixels the file wasn't read for: the preview, as an image whose file is missing shows it.
let preview = Arc::new(bytes);
let missing = place_linked(&mut s, "missing", ImageBlob { mime: "image/png".into(), bytes: preview.clone(), proxy: Some(preview) }, (12, 10));
let before = (image_of(&s, missing), undo_len(&s));
let e = s.execute(C, &json!({})).unwrap_err();
assert!(e.to_string().contains("the linked image shows its preview: embed or relink it first"), "{e}");
assert_eq!((image_of(&s, missing), undo_len(&s)), before);
}
/// An object whose pixels were read at another size than it records keeps its place and size.
#[test]
fn an_image_read_at_another_size_keeps_its_place_and_size() {
let bytes = png(DynamicImage::ImageRgba8(framed()));
let (mut s, _) = open(&bytes);
let id = place_linked(&mut s, "linked", ImageBlob::png(bytes), (24, 20));
s.execute(C, &json!({"ids": [id.0]})).unwrap();
let im = image_of(&s, id);
assert_eq!((im.width, im.height, im.xf), (12, 10, Affine::scale_non_uniform(2.0, 2.0)));
}
#[test]
fn bad_params_are_refused_and_change_nothing() {
let (mut s, id) = open(&png(DynamicImage::ImageRgba8(framed())));
let rect = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 5, "height": 5})).unwrap()["id"].clone();
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
let before = (image_of(&s, id), undo_len(&s));
// With `ids`, the command is available when one of them is an image.
let not_image = format!("object {rect} is not an image");
let cases: [(Value, &str); 18] = [
(json!({"background": "sky"}), "`background` must be edges, corners or colors"),
(json!({"background": 5}), "`background` must be a string"),
(json!({"match": "rgb"}), "`match` must be distance or channels"),
(json!({"match": true}), "`match` must be a string"),
(json!({"contiguous": "yes"}), "`contiguous` must be true or false"),
(json!({"contiguous": 1}), "`contiguous` must be true or false"),
(json!({"tolerance": 101}), "`tolerance` must be from 0 to 100"),
(json!({"softness": -1}), "`softness` must be from 0 to 100"),
(json!({"tolerance": "5"}), "`tolerance` must be a number"),
(json!({"background": "colors"}), "`colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue"),
(json!({"background": "colors", "colors": []}), "`colors` must list"),
(json!({"background": "colors", "colors": "#12"}), "`colors` must list"),
(json!({"background": "colors", "colors": "#ffffff80"}), "`colors` must list"),
(json!({"colors": ["#ffffff", 7]}), "`colors` must list"),
(json!({"background": "colors", "colors": vec!["#000"; 17]}), "`colors` must list"),
(json!({"background": "colors", "colors": "#000 ".repeat(100_000)}), "`colors` must list"),
(json!({"ids": [id.0, rect]}), &not_image),
(json!({"ids": [rect]}), "select an image"),
];
for (p, msg) in cases {
let e = s.execute(C, &p).unwrap_err().to_string();
assert!(e.contains(msg), "{p}: {e}");
}
assert_eq!((image_of(&s, id), undo_len(&s)), before);
}
#[test]
fn an_image_too_large_to_change_is_refused_before_it_is_decoded() {
let run = |w: u32, h: u32| {
let (mut s, _) = open(&crate::tests_linkspanel::png_header(w, h));
s.execute(C, &json!({})).unwrap_err().to_string()
};
// As gray samples, RGBA worked on and kept for the PNG, and the work: 19 bytes a pixel.
let over = run(10_632, 10_631);
assert!(over.contains("an image of 10632 × 10631 pixels is too large to change"), "{over}");
// Within the allowance it is decoded, and its missing rows can't be read.
let within = run(10_631, 10_631);
assert!(within.contains("the image's pixels can't be read"), "{within}");
}
+1 -1
View File
@@ -871,7 +871,7 @@ fn rasterize(s: &mut Session, p: &Value) -> Result<Value> {
Ok(out)
}
fn has_image(s: &Session) -> std::result::Result<(), String> {
pub(crate) fn has_image(s: &Session) -> std::result::Result<(), String> {
has_selection(s)?;
let st = s.active().ok_or("no document open")?;
if st.selection.objects.iter().any(|id| matches!(st.doc.node(*id).map(|n| &n.kind), Some(NodeKind::Image(_)))) {
@@ -32,6 +32,7 @@ pub(super) const ALLOWED: &[&str] = &[
"addon.color.strokeToFill",
"addon.color.swatchLegend",
"addon.contour.set",
"addon.image.removeBackground",
"addon.pathtools.addPoints",
"addon.pathtools.close",
"addon.pathtools.corner",
+3
View File
@@ -282,6 +282,9 @@ pub struct Session {
pub(crate) in_tool_actions: bool,
/// While [`Session::preview`] runs its command: its edits are the interaction's own.
previewing: bool,
/// The images Remove Background decoded during the interaction's preview steps, emptied when
/// it ends ([`cmd::DecodedImages`]).
pub(crate) decoded_images: cmd::DecodedImages,
/// Envelope Options with no envelope selected: the options and fidelity new envelopes get
/// (`None`: the reference app's defaults, fidelity 50); not saved.
pub(crate) envelope_defaults: Option<(vectorcraft_doc::live::EnvelopeOptions, f64)>,
+1
View File
@@ -115,6 +115,7 @@ impl Session {
recovery: Default::default(),
in_tool_actions: false,
previewing: false,
decoded_images: Default::default(),
envelope_defaults: None,
liquify_stroke: None,
plane_widget_press: false,
+2
View File
@@ -335,6 +335,7 @@ impl Session {
pub fn commit_interaction(&mut self) -> Result<()> {
self.clear_deferred_overlay_cache();
self.decoded_images.empty();
if let Some(paint) = self.active().and_then(|st| st.interaction.as_ref()).and_then(|it| it.preview_paint.clone()) {
self.paint = paint;
}
@@ -414,6 +415,7 @@ impl Session {
pub fn cancel_interaction(&mut self) -> Result<()> {
self.clear_deferred_overlay_cache();
self.decoded_images.empty();
self.pending_paint = None;
if let Some(paint) = self
.active()
+2
View File
@@ -7,6 +7,7 @@ pub mod compounds;
pub mod contour;
pub mod document;
pub mod enclosing_lasso;
pub mod image;
pub mod pathtools;
pub mod pen;
pub mod select;
@@ -26,6 +27,7 @@ pub fn plugin_menu() -> Vec<Item> {
Item::Sub("Contour", contour::menu()),
Item::Sub("Document Tools", document::menu()),
Item::Sub("Enclosing Lasso", enclosing_lasso::menu()),
Item::Sub("Image Tools", image::menu()),
Item::Sub("Path Precision Tools", pathtools::menu()),
Item::Sub("Selection Tools", select::menu()),
Item::Sub("Smart Pen", pen::menu()),
+26
View File
@@ -0,0 +1,26 @@
//! 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`).
use serde_json::json;
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.
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.
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}),
)]
}
#[cfg(test)]
mod tests;
+111
View File
@@ -0,0 +1,111 @@
use std::sync::Arc;
use serde_json::json;
use vectorcraft_doc::NodeKind;
use vectorcraft_engine::Session;
use crate::tests_labels::painted_text;
use crate::{VectorcraftApp, menus};
/// An app with one image open and selected: 12 × 10 pixels, white, a near-white ring one pixel in
/// (13.7 % from white) and a red block inside it.
fn app() -> VectorcraftApp {
let img = image::RgbaImage::from_fn(12, 10, |x, y| {
image::Rgba(if (3..9).contains(&x) && (3..7).contains(&y) {
[200, 0, 0, 255]
} else if (1..11).contains(&x) && (1..9).contains(&y) && (x == 1 || x == 10 || y == 1 || y == 8) {
[220, 220, 220, 255]
} else {
[255; 4]
})
});
let mut png = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("document.open", json!({"name": "art.png", "dataBase64": vectorcraft_format::base64_encode(&png)})).unwrap();
app.run("select.all", json!({})).unwrap();
app
}
/// The selected image's pixel key and bytes.
fn image(app: &VectorcraftApp) -> (String, Arc<Vec<u8>>) {
let st = app.session.doc().unwrap();
let Some(NodeKind::Image(im)) = st.selection.objects.first().and_then(|id| st.doc.node(*id)).map(|n| &n.kind) else { panic!("an image") };
(im.key.clone(), st.doc.images[&im.key].bytes.clone())
}
/// The alpha of the selected image's pixels at the corner, on the ring and in the block.
fn alphas(app: &VectorcraftApp) -> [u8; 3] {
let px = image::load_from_memory(&image(app).1).unwrap().to_rgba8();
[(0, 0), (1, 1), (4, 4)].map(|(x, y)| px.get_pixel(x, y).0[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);
let (id, p) = menus::click_target(&item.label, item.command.as_deref().unwrap(), &item.params);
menus::invoke(app, &id, p);
}
fn show(app: &mut VectorcraftApp) -> String {
painted_text(app, |app, ui| crate::dialogs::show(app, ui.ctx()))
}
fn set(app: &mut VectorcraftApp, key: &str, v: serde_json::Value) {
crate::dialogs::set_field(app.ui.dialog.as_mut().unwrap(), key.into(), v);
}
/// Object › Plug-ins › Image Tools › Remove Background… opens the command dialog on Edge Color
/// with Preview off. With Preview on the canvas shows the result for the values in the dialog,
/// each change worked out again from the image as it was; Cancel puts the image back with no step
/// of history, and OK keeps the result for the values in the dialog as one step.
#[test]
fn remove_background_previews_cancels_and_runs_on_ok() {
let mut app = app();
let steps = |app: &VectorcraftApp| app.session.doc().unwrap().history.undo.len();
let (start, n) = (image(&app), steps(&app));
open(&mut app);
let d = app.ui.dialog.as_ref().expect("a dialog opens");
assert_eq!((d.str("__command"), d.str("__label")), (super::REMOVE_BACKGROUND.to_string(), "Remove Background".to_string()));
assert_eq!((d.str("background"), d.bool("preview")), ("edges".to_string(), false));
assert!(crate::dialogs::accepts_field(d, "preview"));
assert_eq!((d.bool("contiguous"), d.str("match")), (true, "distance".to_string()));
let shown = show(&mut app);
for s in ["Remove Background", "Edge Color", "Contiguous", "Match", "Distance", "Tolerance", "Softness", "Preview"] {
assert!(shown.contains(s), "{s} in {shown}");
}
assert_eq!(image(&app), start, "no preview without the checkbox");
// Preview on: at tolerance 10 the ring fades; at 20 it is cleared; at 0, with no softness,
// only the white is cleared and the ring is whole again.
set(&mut app, "preview", json!(true));
show(&mut app);
let [corner, ring, block] = alphas(&app);
assert!(corner == 0 && ring > 0 && ring < 255 && block == 255, "{:?}", alphas(&app));
set(&mut app, "tolerance", json!(20));
show(&mut app);
assert_eq!(alphas(&app), [0, 0, 255]);
set(&mut app, "tolerance", json!(0));
set(&mut app, "softness", json!(0));
show(&mut app);
assert_eq!(alphas(&app), [0, 255, 255], "worked out again from the image as it was");
// Cancel: as it was, with no step of history.
crate::dialogs::cancel(&mut app);
assert_eq!((image(&app), steps(&app)), (start.clone(), n), "Cancel restores the image and adds no undo step");
assert!(!app.session.in_interaction());
// OK: the values in the dialog, as one step.
open(&mut app);
set(&mut app, "preview", json!(true));
set(&mut app, "tolerance", json!(20));
show(&mut app);
crate::dialogs::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
assert_eq!(steps(&app), n + 1, "one undo step");
assert_eq!(alphas(&app), [0, 0, 255]);
app.run("edit.undo", json!({})).unwrap();
assert_eq!(image(&app), start);
}
+6 -2
View File
@@ -64,11 +64,12 @@ 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). (Cancel rolls a preview back whichever dialog started it.)
/// works the same way (#5532), and so does Image Tools › Remove Background. (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 => true,
"type.pathOptions" | crate::addons::typetools::RELEASE | crate::addons::image::REMOVE_BACKGROUND => true,
_ => false,
}
}
@@ -117,6 +118,7 @@ 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"],
_ => &[],
}
}
@@ -136,6 +138,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),
_ => None,
}
}
+9
View File
@@ -686,6 +686,15 @@ font-technology Variable Променливи
Created: Създадено:
Crop Изрязване
Crop Image Изрязване на изображение
Image Tools Инструменти за изображения
Remove Background… Премахване на фона…
Remove Background Премахване на фона
Background Фон
Edge Color Цвят на ръба
Picked Colors Избрани цветове
Corner Color Цвят на ъглите
Contiguous Съседни
Each Channel Всеки канал
Edit Path Редактиране на контура
Mask Маска
Crop Marks Знаци за изрязване
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -196,6 +196,15 @@
Expand… Proširi…
Expand Appearance Proširi izgled
Crop Image Izreži sliku
Image Tools Alati za slike
Remove Background… Ukloni pozadinu…
Remove Background Ukloni pozadinu
Background Pozadina
Edge Color Boja ruba
Picked Colors Odabrane boje
Corner Color Boja uglova
Contiguous Susjedni
Each Channel Svaki kanal
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.
+17
View File
@@ -985,6 +985,15 @@ antiAlias Sharp Nítid
CRLF (Windows) CRLF (Windows)
Crop Retalla
Crop Image Retalla la imatge
Image Tools Eines d’imatge
Remove Background… Elimina el fons…
Remove Background Elimina el fons
Background Fons
Edge Color Color de la vora
Picked Colors Colors triats
Corner Color Color de les cantonades
Contiguous Contigu
Each Channel Cada canal
Crop Marks Marques de retall
Crop To Retalla fins a
Cross Creu
@@ -5738,6 +5747,14 @@ digits Malayalam Malaiàlam
@msg exit isolation mode first surt primer del mode d’aïllament
@msg select an envelope selecciona un embolcall
@msg select an image selecciona una imatge
@msg `background` must be edges, corners or colors `background` ha de ser edges, corners o colors
@msg `match` must be distance or channels `match` ha de ser distance o channels
@msg `{key}` must be from 0 to 100 `{key}` ha d’estar entre 0 i 100
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors` ha de llistar d’1 a 16 colors, cadascun en hexadecimal (#rrggbb o #rgb) o com a black, white, red, green o blue
@msg the linked image shows its preview: embed or relink it first la imatge enllaçada mostra la previsualització: incrusta-la o torna a enllaçar-la primer
@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 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.
+17
View File
@@ -202,6 +202,15 @@
Expand… Rozložit…
Expand Appearance Rozložit vzhled
Crop Image Oříznout obraz
Image Tools Nástroje obrazu
Remove Background… Odstranit pozadí…
Remove Background Odstranit pozadí
Background Pozadí
Edge Color Barva okraje
Picked Colors Vybrané barvy
Corner Color Barva rohů
Contiguous Souvislé
Each Channel Každý kanál
Edit Path Upravit cestu
Mask Maska
Rasterize… Rastrovat…
@@ -4916,6 +4925,14 @@ imageTrace Outline Obrys
@msg this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset tato vektorizace by vytvořila více než {max} kotevních bodů, víc, než dokument plynule zobrazí: zvyšte Šum, snižte Cesty nebo Barvy, nebo zvolte přednastavení s nižší věrností
@msg select an envelope vyberte obálku
@msg select an image vyberte obraz
@msg `background` must be edges, corners or colors `background` musí být edges, corners nebo colors
@msg `match` must be distance or channels `match` musí být distance nebo channels
@msg `{key}` must be from 0 to 100 `{key}` musí být od 0 do 100
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors` musí obsahovat 1 až 16 barev, každou šestnáctkově (#rrggbb nebo #rgb) nebo jako black, white, red, green či blue
@msg the linked image shows its preview: embed or relink it first propojený obraz zobrazuje náhled: nejprve jej vložte nebo znovu propojte
@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 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.
+9
View File
@@ -189,6 +189,15 @@
Expand… Udvid…
Expand Appearance Udvid udseende
Crop Image Beskær billede
Image Tools Billedværktøjer
Remove Background… Fjern baggrund…
Remove Background Fjern baggrund
Background Baggrund
Edge Color Kantfarve
Picked Colors Valgte farver
Corner Color Hjørnefarve
Contiguous Sammenhængende
Each Channel Hver kanal
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.
+17
View File
@@ -679,6 +679,15 @@ font-technology Variable Variabel
Created: Erstellt:
Crop Schnittmengenfläche
Crop Image Bild beschneiden
Image Tools Bildwerkzeuge
Remove Background… Hintergrund entfernen…
Remove Background Hintergrund entfernen
Background Hintergrund
Edge Color Randfarbe
Picked Colors Ausgewählte Farben
Corner Color Eckfarbe
Contiguous Zusammenhängend
Each Channel Jeder Kanal
Edit Path Pfad bearbeiten
Mask Maske
Crop Marks Schnittmarken
@@ -4525,6 +4534,14 @@ theme Appearance Erscheinungsbild
@msg this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset diese Nachzeichnung würde mehr als {max} Ankerpunkte erzeugen, mehr als das Dokument flüssig darstellen kann: Erhöhen Sie „Rauschen“, verringern Sie „Pfade“ oder „Farben“ oder wählen Sie eine Vorgabe mit geringerer Genauigkeit
@msg select an envelope wählen Sie eine Hülle aus
@msg select an image wählen Sie ein Bild aus
@msg `background` must be edges, corners or colors `background` muss edges, corners oder colors sein
@msg `match` must be distance or channels `match` muss distance oder channels sein
@msg `{key}` must be from 0 to 100 `{key}` muss von 0 bis 100 sein
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors` muss 1 bis 16 Farben enthalten, jede hexadezimal (#rrggbb oder #rgb) oder als black, white, red, green oder blue
@msg the linked image shows its preview: embed or relink it first das verknüpfte Bild zeigt seine Vorschau: Betten Sie es zuerst ein oder verknüpfen Sie es neu
@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 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.
+17
View File
@@ -660,6 +660,15 @@ font-technology Variable Μεταβλητές
Created: Δημιουργήθηκε:
Crop Περικοπή
Crop Image Περικοπή εικόνας
Image Tools Εργαλεία εικόνας
Remove Background… Αφαίρεση φόντου…
Remove Background Αφαίρεση φόντου
Background Φόντο
Edge Color Χρώμα άκρης
Picked Colors Επιλεγμένα χρώματα
Corner Color Χρώμα γωνιών
Contiguous Συνεχόμενα
Each Channel Κάθε κανάλι
Edit Path Επεξεργασία διαδρομής
Mask Μάσκα
Crop Marks Σημάδια ξακρίσματος
@@ -4557,6 +4566,14 @@ digits Malayalam Μαλαγιάλαμ
@msg this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset αυτή η διανυσματοποίηση θα δημιουργούσε πάνω από {max} σημεία αγκύρωσης, περισσότερα από όσα μπορεί να εμφανίσει ομαλά το έγγραφο: αυξήστε τον Θόρυβο, μειώστε τις Διαδρομές ή τα Χρώματα ή επιλέξτε προεπιλογή χαμηλότερης πιστότητας
@msg select an envelope επιλέξτε περίβλημα
@msg select an image επιλέξτε εικόνα
@msg `background` must be edges, corners or colors το `background` πρέπει να είναι edges, corners ή colors
@msg `match` must be distance or channels το `match` πρέπει να είναι distance ή channels
@msg `{key}` must be from 0 to 100 το `{key}` πρέπει να είναι από 0 έως 100
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue το `colors` πρέπει να περιέχει 1 έως 16 χρώματα, το καθένα σε δεκαεξαδική μορφή (#rrggbb ή #rgb) ή ως black, white, red, green ή blue
@msg the linked image shows its preview: embed or relink it first η συνδεδεμένη εικόνα εμφανίζει την προεπισκόπησή της: ενσωματώστε ή επανασυνδέστε την πρώτα
@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 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.
+17
View File
@@ -700,6 +700,15 @@ font-technology Variable Variables
Created: Creado:
Crop Recortar
Crop Image Recortar imagen
Image Tools Herramientas de imagen
Remove Background… Quitar fondo…
Remove Background Quitar fondo
Background Fondo
Edge Color Color del borde
Picked Colors Colores elegidos
Corner Color Color de las esquinas
Contiguous Contiguo
Each Channel Cada canal
Edit Path Editar trazado
Mask Máscara
Crop Marks Marcas de corte
@@ -4608,6 +4617,14 @@ axis Both Ambos
@msg this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset este calco crearía más de {max} puntos de ancla, más de los que el documento puede mostrar con fluidez: sube Ruido, baja Trazados o Colores, o elige un preajuste de menor fidelidad
@msg select an envelope selecciona una envolvente
@msg select an image selecciona una imagen
@msg `background` must be edges, corners or colors `background` debe ser edges, corners o colors
@msg `match` must be distance or channels `match` debe ser distance o channels
@msg `{key}` must be from 0 to 100 `{key}` debe estar entre 0 y 100
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors` debe contener de 1 a 16 colores, cada uno en hexadecimal (#rrggbb o #rgb) o como black, white, red, green o blue
@msg the linked image shows its preview: embed or relink it first la imagen vinculada muestra su vista previa: incrústala o vuelve a vincularla primero
@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 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.
+17
View File
@@ -209,6 +209,15 @@
Expand… Laajenna…
Expand Appearance Laajenna ulkoasu
Crop Image Rajaa kuva
Image Tools Kuvatyökalut
Remove Background… Poista tausta…
Remove Background Poista tausta
Background Tausta
Edge Color Reunan väri
Picked Colors Valitut värit
Corner Color Kulmien väri
Contiguous Yhtenäinen
Each Channel Jokainen kanava
Edit Path Muokkaa polkua
Mask Maski
Rasterize… Rasteroi…
@@ -4818,6 +4827,14 @@ imageTrace Outline Ääriviiva
@msg this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset tämä jäljitys tuottaisi yli {max} ankkuripistettä, enemmän kuin asiakirja näyttää sujuvasti: nosta kohinaa, vähennä polkuja tai värejä tai valitse epätarkempi esiasetus
@msg select an envelope valitse vääristyskuori
@msg select an image valitse kuva
@msg `background` must be edges, corners or colors `background`-arvon on oltava edges, corners tai colors
@msg `match` must be distance or channels `match`-arvon on oltava distance tai channels
@msg `{key}` must be from 0 to 100 `{key}`-arvon on oltava väliltä 0–100
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors`-arvossa on oltava 1–16 väriä, kukin heksadesimaalina (#rrggbb tai #rgb) tai muodossa black, white, red, green tai blue
@msg the linked image shows its preview: embed or relink it first linkitetty kuva näyttää esikatselunsa: upota tai linkitä se ensin uudelleen
@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 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.
+17
View File
@@ -702,6 +702,15 @@ font-technology Variable Variables
Created: Création :
Crop Recadrer
Crop Image Recadrer l’image
Image Tools Outils d’image
Remove Background… Supprimer l’arrière-plan…
Remove Background Supprimer l’arrière-plan
Background Arrière-plan
Edge Color Couleur du bord
Picked Colors Couleurs choisies
Corner Color Couleur des angles
Contiguous Contigu
Each Channel Chaque canal
Edit Path Modifier le tracé
Mask Masque
Crop Marks Traits de coupe
@@ -4597,6 +4606,14 @@ axis Both Les deux
@msg this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset cette vectorisation créerait plus de {max} points d’ancrage, plus que le document ne peut afficher de façon fluide : augmentez Bruit, réduisez Tracés ou Couleurs, ou choisissez un paramètre prédéfini moins fidèle
@msg select an envelope sélectionnez une enveloppe
@msg select an image sélectionnez une image
@msg `background` must be edges, corners or colors `background` doit valoir edges, corners ou colors
@msg `match` must be distance or channels `match` doit valoir distance ou channels
@msg `{key}` must be from 0 to 100 `{key}` doit être compris entre 0 et 100
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors` doit contenir de 1 à 16 couleurs, chacune en hexadécimal (#rrggbb ou #rgb) ou sous la forme black, white, red, green ou blue
@msg the linked image shows its preview: embed or relink it first l’image liée affiche son aperçu : incorporez-la ou rétablissez d’abord le lien
@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 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.
+17
View File
@@ -1099,6 +1099,15 @@ antiAlias Sharp शार्प
Croatian क्रोएशियाई
Crop क्रॉप करें
Crop Image फसल की इमेज
Image Tools इमेज टूल
Remove Background… पृष्ठभूमि हटाएँ…
Remove Background पृष्ठभूमि हटाएँ
Background पृष्ठभूमि
Edge Color किनारे का रंग
Picked Colors चुने गए रंग
Corner Color कोनों का रंग
Contiguous सन्निहित
Each Channel प्रत्येक चैनल
Crop Marks क्रॉप मार्क
Crop To करने के लिए फसल
Cross क्रॉस
@@ -7355,6 +7364,14 @@ antiAlias Sharp शार्प
@msg select a text object एक टेक्स्ट ऑब्जेक्ट चुनें
@msg select an envelope एक लिफाफे सिलेक्ट करें
@msg select an image एक इमेज चुनें
@msg `background` must be edges, corners or colors `background` edges, corners या colors होना चाहिए
@msg `match` must be distance or channels `match` distance या channels होना चाहिए
@msg `{key}` must be from 0 to 100 `{key}` 0 से 100 तक होना चाहिए
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors` में 1 से 16 रंग होने चाहिए, हर एक हेक्स (#rrggbb या #rgb) में या black, white, red, green या blue के रूप में
@msg the linked image shows its preview: embed or relink it first लिंक्ड इमेज अपना पूर्वावलोकन दिखा रही है: पहले इसे एम्बेड करें या फिर से लिंक करें
@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 (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.
+17
View File
@@ -901,6 +901,15 @@ font-technology Variable Változó
CRLF (Windows) CRLF (Windows)
Crop Körülvágás
Crop Image Kép körülvágása
Image Tools Képeszközök
Remove Background… Háttér eltávolítása…
Remove Background Háttér eltávolítása
Background Háttér
Edge Color Szélszín
Picked Colors Kiválasztott színek
Corner Color Sarokszín
Contiguous Összefüggő
Each Channel Minden csatorna
Edit Path Útvonal szerkesztése
Crop Marks Vágójelek
Crop To Körülvágás
@@ -5359,6 +5368,14 @@ imageTrace Outline Körvonal
@msg a symbol can't hold an instance of itself egy szimbólum nem tartalmazhatja a saját példányát
@msg select an envelope jelölj ki egy burkolót
@msg select an image jelölj ki egy képet
@msg `background` must be edges, corners or colors a `background` értéke edges, corners vagy colors lehet
@msg `match` must be distance or channels a `match` értéke distance vagy channels lehet
@msg `{key}` must be from 0 to 100 a `{key}` értéke 0 és 100 között lehet
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue a `colors` 1–16 színt sorolhat fel, mindegyiket hexadecimálisan (#rrggbb vagy #rgb) vagy black, white, red, green vagy blue alakban
@msg the linked image shows its preview: embed or relink it first a csatolt kép az előnézetét mutatja: előbb ágyazd be vagy csatold újra
@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 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.
+9
View File
@@ -189,6 +189,15 @@
Expand… Perluas…
Expand Appearance Perluas tampilan
Crop Image Pangkas gambar
Image Tools Alat Gambar
Remove Background… Hapus latar belakang…
Remove Background Hapus latar belakang
Background Latar belakang
Edge Color Warna tepi
Picked Colors Warna pilihan
Corner Color Warna sudut
Contiguous Bersambung
Each Channel Setiap kanal
Edit Path Edit jalur
Mask Masker
Rasterize… Rasterisasi…
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
+17
View File
@@ -700,6 +700,15 @@ font-technology Variable Variabili
Created: Creato:
Crop Ritaglia
Crop Image Ritaglia immagine
Image Tools Strumenti immagine
Remove Background… Rimuovi sfondo…
Remove Background Rimuovi sfondo
Background Sfondo
Edge Color Colore del bordo
Picked Colors Colori scelti
Corner Color Colore degli angoli
Contiguous Contiguo
Each Channel Ogni canale
Edit Path Modifica tracciato
Mask Maschera
Crop Marks Segni di ritaglio
@@ -4651,6 +4660,14 @@ axis Both Entrambi
@msg this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset questo ricalco creerebbe più di {max} punti di ancoraggio, più di quanti il documento possa mostrare in modo fluido: aumenta Disturbo, riduci Tracciati o Colori, oppure scegli un preset a fedeltà inferiore
@msg select an envelope seleziona un involucro
@msg select an image seleziona un'immagine
@msg `background` must be edges, corners or colors `background` deve essere edges, corners o colors
@msg `match` must be distance or channels `match` deve essere distance o channels
@msg `{key}` must be from 0 to 100 `{key}` deve essere compreso tra 0 e 100
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors` deve elencare da 1 a 16 colori, ciascuno in esadecimale (#rrggbb o #rgb) o come black, white, red, green o blue
@msg the linked image shows its preview: embed or relink it first l’immagine collegata mostra la sua anteprima: incorporala o ricollegala prima
@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 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.
+17
View File
@@ -231,6 +231,15 @@ digits Malayalam マラヤーラム数字
Expand… 分割・拡張…
Expand Appearance アピアランスを分割
Crop Image 画像の切り抜き
Image Tools 画像ツール
Remove Background… 背景を削除…
Remove Background 背景を削除
Background 背景
Edge Color 端の色
Picked Colors 選択したカラー
Corner Color 角の色
Contiguous 隣接
Each Channel 各チャンネル
Edit Path パスを編集
Mask マスク
Rasterize… ラスタライズ…
@@ -4853,6 +4862,14 @@ swatch Bright Violet ビビッドバイオレット
@msg no such view 指定された表示が見つかりません
@msg printing isn't available here この環境ではプリントできません
@msg select an image 画像を選択してください
@msg `background` must be edges, corners or colors `background` は edges、corners または colors にしてください
@msg `match` must be distance or channels `match` は distance または channels にしてください
@msg `{key}` must be from 0 to 100 `{key}` は 0 から 100 までの値にしてください
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors` には 16 進数(#rrggbb または #rgb)か black、white、red、green、blue で書いたカラーを 1〜16 個指定してください
@msg the linked image shows its preview: embed or relink it first リンク画像はプレビューを表示しています: 先に埋め込むか再リンクしてください
@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 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.
+17
View File
@@ -161,6 +161,15 @@
Expand… გაშლა…
Expand Appearance იერსახის გაშლა
Crop Image გამოსახულების ჩამოჭრა
Image Tools გამოსახულების ხელსაწყოები
Remove Background… ფონის მოშორება…
Remove Background ფონის მოშორება
Background ფონი
Edge Color კიდის ფერი
Picked Colors არჩეული ფერები
Corner Color კუთხეების ფერი
Contiguous მომიჯნავე
Each Channel თითოეული არხი
Mask ნიღაბი
Rasterize… რასტრირება…
Create Gradient Mesh… გრადიენტული ბადის შექმნა…
@@ -4356,6 +4365,14 @@ font-technology Variable ცვლადი
@msg this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset ეს ვექტორიზაცია {max}-ზე მეტ საყრდენ წერტილს შექმნიდა, რასაც დოკუმენტი შეუფერხებლად ვერ აჩვენებს: გაზარდეთ ხმაური, შეამცირეთ ტრაექტორიები ან ფერები, ან აირჩიეთ ნაკლები სიზუსტის პრესეტი
@msg select an envelope მონიშნეთ გარსი
@msg select an image მონიშნეთ გამოსახულება
@msg `background` must be edges, corners or colors `background` უნდა იყოს edges, corners ან colors
@msg `match` must be distance or channels `match` უნდა იყოს distance ან channels
@msg `{key}` must be from 0 to 100 `{key}` უნდა იყოს 0-დან 100-მდე
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors` უნდა შეიცავდეს 1-დან 16-მდე ფერს, თითოეულს თექვსმეტობით ფორმატში (#rrggbb ან #rgb) ან როგორც black, white, red, green ან blue
@msg the linked image shows its preview: embed or relink it first დაკავშირებული გამოსახულება გადახედვას აჩვენებს: ჯერ ჩააშენეთ ან ხელახლა მიაბით
@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 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.
+17
View File
@@ -224,6 +224,15 @@ digits Malayalam 말라얄람어
Expand… 확장…
Expand Appearance 모양새 확장
Crop Image 이미지 자르기
Image Tools 이미지 도구
Remove Background… 배경 제거…
Remove Background 배경 제거
Background 배경
Edge Color 가장자리 색상
Picked Colors 선택한 색상
Corner Color 모퉁이 색상
Contiguous 인접
Each Channel 각 채널
Edit Path 패스 편집
Mask 마스크
Rasterize… 래스터화…
@@ -4780,6 +4789,14 @@ theme Appearance 외관
@msg no such view 해당 보기가 없습니다
@msg printing isn't available here 여기서는 인쇄할 수 없습니다
@msg select an image 이미지를 선택하세요
@msg `background` must be edges, corners or colors `background`는 edges, corners 또는 colors여야 합니다
@msg `match` must be distance or channels `match`는 distance 또는 channels여야 합니다
@msg `{key}` must be from 0 to 100 `{key}`는 0에서 100 사이여야 합니다
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors`에는 16진수(#rrggbb 또는 #rgb)나 black, white, red, green, blue로 쓴 색상이 1~16개 있어야 합니다
@msg the linked image shows its preview: embed or relink it first 연결된 이미지가 미리 보기를 표시하고 있습니다: 먼저 포함하거나 다시 링크하세요
@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 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.
+9
View File
@@ -203,6 +203,15 @@
Expand… Uitbreiden…
Expand Appearance Uiterlijk uitbreiden
Crop Image Afbeelding bijsnijden
Image Tools Afbeeldingshulpmiddelen
Remove Background… Achtergrond verwijderen…
Remove Background Achtergrond verwijderen
Background Achtergrond
Edge Color Randkleur
Picked Colors Gekozen kleuren
Corner Color Hoekkleur
Contiguous Aaneengesloten
Each Channel Elk kanaal
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.
+17
View File
@@ -166,6 +166,15 @@
Expand… Rozszerz…
Expand Appearance Rozszerz wygląd
Crop Image Kadruj obraz
Image Tools Narzędzia obrazu
Remove Background… Usuń tło…
Remove Background Usuń tło
Background Tło
Edge Color Kolor krawędzi
Picked Colors Wybrane kolory
Corner Color Kolor narożników
Contiguous Ciągły
Each Channel Każdy kanał
Edit Path Edytuj ścieżkę
Mask Maska
Rasterize… Rasteryzuj…
@@ -4742,6 +4751,14 @@ imageTrace Outline Kontur
@msg this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset ten obrys utworzyłby ponad {max} punktów kontrolnych, więcej niż dokument może płynnie wyświetlić: zwiększ Szum, zmniejsz Ścieżki lub Kolory albo wybierz ustawienie o niższej wierności
@msg select an envelope zaznacz kopertę
@msg select an image zaznacz obraz
@msg `background` must be edges, corners or colors `background` musi mieć wartość edges, corners lub colors
@msg `match` must be distance or channels `match` musi mieć wartość distance lub channels
@msg `{key}` must be from 0 to 100 `{key}` musi mieścić się w zakresie od 0 do 100
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors` musi zawierać od 1 do 16 kolorów, każdy szesnastkowo (#rrggbb lub #rgb) albo jako black, white, red, green lub blue
@msg the linked image shows its preview: embed or relink it first połączony obraz pokazuje podgląd: najpierw go osadź lub połącz ponownie
@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 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.
+17
View File
@@ -626,6 +626,15 @@ antiAlias Sharp Nítido
Created: Criado:
Crop Recortar
Crop Image Recortar Imagem
Image Tools Ferramentas de imagem
Remove Background… Remover Fundo…
Remove Background Remover Fundo
Background Fundo
Edge Color Cor da Borda
Picked Colors Cores Escolhidas
Corner Color Cor dos Cantos
Contiguous Contíguo
Each Channel Cada Canal
Edit Path Editar caminho
Mask Máscara
Crop Marks Marcas de Corte
@@ -4148,6 +4157,14 @@ axis Both Ambos
@msg this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset esta vetorização geraria mais de {max} pontos de ancoragem, mais do que o documento consegue exibir com fluidez: aumente Ruído, reduza Traçados ou Cores, ou escolha uma predefinição de menor fidelidade
@msg select an envelope selecione um envelope
@msg select an image selecione uma imagem
@msg `background` must be edges, corners or colors `background` deve ser edges, corners ou colors
@msg `match` must be distance or channels `match` deve ser distance ou channels
@msg `{key}` must be from 0 to 100 `{key}` deve estar entre 0 e 100
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors` deve listar de 1 a 16 cores, cada uma em hexadecimal (#rrggbb ou #rgb) ou como black, white, red, green ou blue
@msg the linked image shows its preview: embed or relink it first a imagem vinculada mostra sua prévia: incorpore-a ou revincule-a primeiro
@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 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.
+17
View File
@@ -698,6 +698,15 @@ font-technology Variable Вариативные
Created: Создано:
Crop Обрезать
Crop Image Обрезать изображение
Image Tools Инструменты изображения
Remove Background… Удалить фон…
Remove Background Удалить фон
Background Фон
Edge Color Цвет края
Picked Colors Выбранные цвета
Corner Color Цвет углов
Contiguous Смежные
Each Channel Каждый канал
Edit Path Редактировать контур
Mask Маска
Crop Marks Метки обрезки
@@ -4645,6 +4654,14 @@ axis Both Оба
@msg this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset эта трассировка создала бы более {max} опорных точек, что больше, чем документ может плавно отобразить: увеличьте «Шум», уменьшите «Контуры» или «Цвета» либо выберите набор с меньшей точностью
@msg select an envelope выделите оболочку
@msg select an image выделите изображение
@msg `background` must be edges, corners or colors `background` должен быть edges, corners или colors
@msg `match` must be distance or channels `match` должен быть distance или channels
@msg `{key}` must be from 0 to 100 `{key}` должен быть от 0 до 100
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors` должен содержать от 1 до 16 цветов, каждый в шестнадцатеричном виде (#rrggbb или #rgb) или как black, white, red, green или blue
@msg the linked image shows its preview: embed or relink it first связанное изображение показывает предпросмотр: сначала внедрите его или восстановите связь
@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 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.
+17
View File
@@ -688,6 +688,15 @@ font-technology Variable Variabla
Created: Skapad:
Crop Beskär
Crop Image Beskär bild
Image Tools Bildverktyg
Remove Background… Ta bort bakgrund…
Remove Background Ta bort bakgrund
Background Bakgrund
Edge Color Kantfärg
Picked Colors Valda färger
Corner Color Hörnfärg
Contiguous Sammanhängande
Each Channel Varje kanal
Edit Path Redigera bana
Mask Maskera
Crop Marks Skärmärken
@@ -4506,6 +4515,14 @@ axis Both Båda
@msg this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset spårningen skulle ge fler än {max} ankarpunkter, fler än dokumentet kan visa smidigt: höj Brus, sänk Banor eller Färger, eller välj en förinställning med lägre återgivning
@msg select an envelope markera ett kuvert
@msg select an image markera en bild
@msg `background` must be edges, corners or colors `background` måste vara edges, corners eller colors
@msg `match` must be distance or channels `match` måste vara distance eller channels
@msg `{key}` must be from 0 to 100 `{key}` måste vara från 0 till 100
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors` måste lista 1 till 16 färger, var och en hexadecimalt (#rrggbb eller #rgb) eller som black, white, red, green eller blue
@msg the linked image shows its preview: embed or relink it first den länkade bilden visar sin förhandsvisning: bädda in eller länka om den först
@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 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.
+17
View File
@@ -680,6 +680,15 @@ font-technology Variable Değişken
Created: Oluşturuldu:
Crop Kırpma
Crop Image Resmi Kırp
Image Tools Resim Araçları
Remove Background… Arka Planı Kaldır…
Remove Background Arka Planı Kaldır
Background Arka Plan
Edge Color Kenar Rengi
Picked Colors Seçilen Renkler
Corner Color Köşe Rengi
Contiguous Bitişik
Each Channel Her Kanal
Mask Maske
Crop Marks Kırpma İşaretleri
Crop To Kırpma Hedefi
@@ -4358,6 +4367,14 @@ axis Both Her İkisi
@msg this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset bu iz, {max} bağlantı noktalarından daha fazlasını, yani belgenin düzgün bir şekilde gösterebileceğinden daha fazlasını oluşturur: Gürültüyü artırın, Yolları veya Renkleri düşürün veya aslına uygunluğu daha düşük bir ön ayar seçin
@msg select an envelope bir zarf seç
@msg select an image bir resim seç
@msg `background` must be edges, corners or colors `background` edges, corners veya colors olmalıdır
@msg `match` must be distance or channels `match` distance veya channels olmalıdır
@msg `{key}` must be from 0 to 100 `{key}` 0 ile 100 arasında olmalıdır
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors`, her biri onaltılık (#rrggbb veya #rgb) ya da black, white, red, green veya blue olarak yazılmış 1 ile 16 arasında renk içermelidir
@msg the linked image shows its preview: embed or relink it first bağlantılı resim önizlemesini gösteriyor: önce gömün veya yeniden bağlayın
@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 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.
+17
View File
@@ -694,6 +694,15 @@ font-technology Variable Варіативні
Created: Створено:
Crop Обрізати
Crop Image Обрізати зображення
Image Tools Інструменти зображення
Remove Background… Видалити фон…
Remove Background Видалити фон
Background Фон
Edge Color Колір краю
Picked Colors Вибрані кольори
Corner Color Колір кутів
Contiguous Суміжні
Each Channel Кожен канал
Edit Path Редагувати контур
Mask Маска
Crop Marks Мітки обрізання
@@ -4635,6 +4644,14 @@ axis Both Обидві
@msg this trace would make more than {max} anchor points, more than the document can show smoothly: raise Noise, lower Paths or Colors, or pick a lower-fidelity preset це трасування створило б більше {max} опорних точок, що більше, ніж документ може плавно відобразити: збільште «Шум», зменште «Контури» або «Кольори» або виберіть набір з меншою точністю
@msg select an envelope виділіть оболонку
@msg select an image виділіть зображення
@msg `background` must be edges, corners or colors `background` має бути edges, corners або colors
@msg `match` must be distance or channels `match` має бути distance або channels
@msg `{key}` must be from 0 to 100 `{key}` має бути від 0 до 100
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors` має містити від 1 до 16 кольорів, кожен у шістнадцятковому вигляді (#rrggbb або #rgb) або як black, white, red, green чи blue
@msg the linked image shows its preview: embed or relink it first пов’язане зображення показує попередній перегляд: спочатку вбудуйте його або відновіть зв’язок
@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 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 contains an unexpected character in line 182 and column 5.
+17
View File
@@ -801,6 +801,15 @@ font-technology Variable Biến thiên
Created: Ngày tạo:
Crop Cắt xén
Crop Image Cắt xén ảnh
Image Tools Công cụ hình ảnh
Remove Background… Xóa nền…
Remove Background Xóa nền
Background Nền
Edge Color Màu cạnh
Picked Colors Màu đã chọn
Corner Color Màu góc
Contiguous Liền kề
Each Channel Từng kênh
Edit Path Sửa đường dẫn
Crop Marks Dấu cắt xén
Crop To Cắt xén đến
@@ -5014,6 +5023,14 @@ theme Appearance Giao diện
@msg a symbol can't hold an instance of itself ký hiệu không thể chứa đối tượng của chính nó
@msg select an envelope hãy chọn một Envelope
@msg select an image hãy chọn ảnh
@msg `background` must be edges, corners or colors `background` phải là edges, corners hoặc colors
@msg `match` must be distance or channels `match` phải là distance hoặc channels
@msg `{key}` must be from 0 to 100 `{key}` phải nằm trong khoảng 0 đến 100
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors` phải liệt kê từ 1 đến 16 màu, mỗi màu viết dạng hex (#rrggbb hoặc #rgb) hoặc là black, white, red, green hay blue
@msg the linked image shows its preview: embed or relink it first hình ảnh được liên kết đang hiển thị bản xem trước: hãy nhúng hoặc liên kết lại trước
@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 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.
+17
View File
@@ -645,6 +645,15 @@ antiAlias Sharp 锐利
Created: 创建时间:
Crop 裁剪
Crop Image 裁剪图像
Image Tools 图像工具
Remove Background… 移除背景…
Remove Background 移除背景
Background 背景
Edge Color 边缘颜色
Picked Colors 选取的颜色
Corner Color 转角颜色
Contiguous 连续
Each Channel 每个通道
Edit Path 编辑路径
Mask 蒙版
Crop Marks 裁切标记
@@ -4553,6 +4562,14 @@ imageTrace Outline 轮廓
@msg no such view 没有此视图
@msg printing isn't available here 此处无法打印
@msg select an image 请选择一个图像
@msg `background` must be edges, corners or colors `background` 必须为 edges、corners 或 colors
@msg `match` must be distance or channels `match` 必须为 distance 或 channels
@msg `{key}` must be from 0 to 100 `{key}` 必须介于 0 到 100 之间
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors` 必须列出 1 到 16 种颜色,每种写为十六进制(#rrggbb 或 #rgb)或 black、white、red、green、blue
@msg the linked image shows its preview: embed or relink it first 链接图像显示的是预览:请先嵌入或重新链接
@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 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.
+17
View File
@@ -662,6 +662,15 @@ font-technology Variable 可變
Created: 建立時間:
Crop 裁切
Crop Image 裁切影像
Image Tools 影像工具
Remove Background… 移除背景…
Remove Background 移除背景
Background 背景
Edge Color 邊緣顏色
Picked Colors 選取的顏色
Corner Color 轉角顏色
Contiguous 連續
Each Channel 每個通道
Edit Path 編輯路徑
Mask 遮色片
Crop Marks 裁切標記
@@ -2367,6 +2376,14 @@ file-filter Templates 範本
@msg no such view 沒有這個檢視
@msg printing isn't available here 此處無法列印
@msg select an image 請選取影像
@msg `background` must be edges, corners or colors `background` 必須為 edges、corners 或 colors
@msg `match` must be distance or channels `match` 必須為 distance 或 channels
@msg `{key}` must be from 0 to 100 `{key}` 必須介於 0 到 100 之間
@msg `colors` must list 1 to 16 colors, each hex (#rrggbb or #rgb) or black, white, red, green or blue `colors` 必須列出 1 到 16 種顏色,每種寫為十六進位(#rrggbb 或 #rgb)或 black、white、red、green、blue
@msg the linked image shows its preview: embed or relink it first 連結影像顯示的是預覽:請先嵌入或重新連結
@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 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.
+48
View File
@@ -9291,6 +9291,54 @@ there is no step. → `{symbols: [name…], ids: [instance id…], skipped: [{la
{"name":"run_command","arguments":{"command":"addon.document.symbolsFromLayers","params":{"replace":true}}}
```
## 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.
### Remove Background
`addon.image.removeBackground` makes the background of the selected images, or of the images in `ids`, transparent by
lowering the alpha of their own pixels. The work runs in the app, and no service or model is used.
- With `background: "edges"` (the default), the background is the most common color along the image's edges. When the
edges hold at least as many clear pixels as pixels of that color, as after an earlier run, nothing is cleared.
- With `background: "corners"`, the background is the average color of the image's four corner pixels. A clear corner
is left out of the average, and when all four are clear, nothing is cleared.
- With `background: "colors"`, the background is the colors in `colors`. `colors` lists 1 to 16 colors, as an array or
as one string split by commas or spaces. Each is written as other color params take a hex color (`#rrggbb` or `#rgb`,
with or without the `#`) or named black, white, red, green or blue.
- With `contiguous: true` (the default), the background is cleared only where it reaches in from an edge, so the same
color inside the subject stays. With `contiguous: false`, it is cleared wherever it is.
- `tolerance` (0 to 100, default 10) is how near a pixel's color must be to a background color to be cleared, as a
percentage. With `match: "distance"` (the default), it is a percentage of the RGB distance from black to white. With
`match: "channels"`, it is a percentage of 255 steps on the one of red, green and blue that differs most, so a
tolerance of `t / 255 × 100` clears the colors within `t` 8-bit steps on every channel. `softness` (0 to 100,
default 10) is the band past the tolerance over which pixels fade from cleared to kept. These ranges and defaults
are VectorCraft's own.
- A value outside 0 to 100, a `background` other than `edges`, `corners` or `colors`, a `match` other than `distance`
or `channels`, a `contiguous` that is not true or false, or a `colors` that lists no color, more than 16, or one that
is not a color or has an alpha part is refused, and the document stays as it was.
The result stays an image, embedded as a PNG with an alpha channel and the source's color profile and resolution. An
image of 16-bit samples keeps 16 bits, a grayscale image stays gray, and a linked image is embedded. All the images
change in one undo step, and an image that `ids` names twice changes once. An image whose pixels do not change is left
as it is, and when none changes the command adds no undo step and returns `changed: 0`. `cleared` counts the pixels
made fully clear in the images that changed. The command refuses, and changes nothing, when an image is CMYK, a linked
image shows its preview (embed or relink it first), an image would need more than the decode allowance while it is
worked on (2 GiB, or 512 MiB in the web app, counted from the file's header before it is decoded), or every pixel of an
image would be cleared. Remove Background… opens the command's dialog with Preview off; with Preview on, the canvas
shows the result for the values in the dialog, and Cancel puts the image back. While a dialog previews the command,
the session keeps the decoded pixels of the images it changes (at most 128 MiB, or 32 MiB in the web app), so the next
preview step does not decode them again; it releases them when the dialog closes with OK or Cancel. A run that is not
a preview step does not keep them.
→ `{changed, ids, cleared}`
```json
{"name":"run_command","arguments":{"command":"addon.image.removeBackground","params":{"background":"colors","colors":["#ffffff","#f0f0f0"],"tolerance":5,"softness":15}}}
{"name":"run_command","arguments":{"command":"addon.image.removeBackground","params":{"background":"corners","contiguous":false,"match":"channels","tolerance":12.55,"softness":0}}}
```
## 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
+4
View File
@@ -1941,6 +1941,10 @@ A VectorCraft addition, not a feature of the reference app, so no area score mov
A VectorCraft addition (it replaces a script some users ran), so no area score moves. Object › Plug-ins › Document Tools › Batch Trace Folder (`addon.document.batchTraceFolder`) asks for a folder of images and a destination folder, traces each image with a preset (optionally expanded) and saves it there under its own name as `.vectorcraft`, `.svg`, `.pdf`, `.ai` or `.eps`, without replacing files, closing each image's document again and reporting the images it couldn't trace (#4921). A folder with more than 1,000 entries is refused rather than traced in part. Optional settings reject null and values of the wrong type before opening images. Tests: `cmd::addons::document::tests::batch_trace_folder_traces_and_saves_each_image`, `batch_trace_folder_refuses_a_folder_it_cannot_list_in_full`, and `batch_trace_folder_rejects_wrong_typed_options` (engine), `addons::document::tests::batch_trace_folder_asks_for_both_folders` (ui-egui).
## Image Tools add-on: Remove Background (2026-11-11)
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 and results with no pixel left are refused. 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.
## 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).