mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 11:54:03 +00:00
Merge main into feat/plugin-importers
This commit is contained in:
@@ -144,7 +144,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Make",
|
||||
["Object", "Image Trace"],
|
||||
None,
|
||||
"{id?: image or Image Trace group (default: selection), preset?: name (imageTrace.presets), savedPreset?: {settings} (a saved preset's settings, which the journal records so a replay traces with them after the preset is changed, renamed or deleted; used in place of the saved preset `preset` names), enhanced?: false (the preset's enhanced version: with Auto Grouping, and with Shapes in all but High Fidelity Photo; Flat Logo has none and is traced as it is), params?: {mode: \"blackAndWhite\"|\"grayscale\"|\"color\"|\"logo\", threshold: 0-255, palette: \"limited\"|\"fullTone\"|\"automatic\"|\"documentLibrary\" (color mode; limited: the `colors` best colours, fullTone: as many as the tones need, more with colorDetail, automatic: flat art with its own colours, else fullTone, documentLibrary: at most `colors` of the library's colours, as they are), colors: 2-256, colorDetail: 0-100 (fullTone, automatic), library: \"document\" (the document's swatches) or a swatch library id or name (swatch.library.list; documentLibrary), colorGroup?: name (color mode, documentLibrary with library \"document\": the colors of that document color group instead of every swatch; \"\" is every swatch), paths: 0-100, corners: 0-100, noise: px, method: \"abutting\"|\"overlapping\", ignoreWhite (leave out white areas), ignoreColor?: \"#rrggbb\" (with ignoreWhite: the colour left out instead of white), snapCurvesToLines, shapes: false (filled outlines that are a circle, an ellipse, a rectangle or a regular polygon come out as that live shape), autoGrouping: false (each path that has others inside it is grouped with them), gradients: false (grayscale and color: an area whose colour changes evenly along one direction is one path with a linear gradient fill), fills: true, strokes: false (Create: lines up to strokeWidth wide become stroked centre lines; without fills, wider areas become stroked outlines), strokeWidth: px (10), logoColors?: [\"#rrggbb\"…] (mode logo: flat-colour logos only; colours found automatically or named here, white is always its own colour, at most ten; noise = areas under noise/10 px ignored; edges to a fraction of a pixel, sharp corners restored, lines and circles snapped; enclosed transparent areas are painted white unless ignoreWhite makes them holes; fills/strokes are ignored; gradients and photos are refused)}, view?: as imageTrace.setView (default: the traced object's own, else tracingResult), ids?: [id…] (instead of id)} → {id, paths, anchors, colors, warnings: [text…] (the image is over 1500 px on a side, so it traces more slowly; or it was traced scaled down; Flat Logo names only the size it traced a larger image at)}",
|
||||
"{id?: image or Image Trace group (default: selection), preset?: name (imageTrace.presets), savedPreset?: {settings} (a saved preset's settings, which the journal records so a replay traces with them after the preset is changed, renamed or deleted; used in place of the saved preset `preset` names), enhanced?: false (the preset's enhanced version: with Auto Grouping, and with Shapes in all but High Fidelity Photo; Flat Logo has none and is traced as it is), params?: {mode: \"blackAndWhite\"|\"grayscale\"|\"color\"|\"logo\", threshold: 0-255, palette: \"limited\"|\"fullTone\"|\"automatic\"|\"documentLibrary\" (color mode; limited: the `colors` best colours, fullTone: as many as the tones need, more with colorDetail, automatic: flat art with its own colours, else fullTone, documentLibrary: at most `colors` of the library's colours, as they are), colors: 2-256, colorDetail: 0-100 (fullTone, automatic), library: \"document\" (the document's swatches) or a swatch library id or name (swatch.library.list; documentLibrary), colorGroup?: name (color mode, documentLibrary with library \"document\": the colors of that document color group instead of every swatch; \"\" is every swatch), paths: 0-100, corners: 0-100, noise: px, method: \"abutting\"|\"overlapping\", ignoreWhite (leave out white areas), ignoreColor?: \"#rrggbb\" (with ignoreWhite: the colour left out instead of white), snapCurvesToLines, shapes: false (filled outlines that are a circle, an ellipse, a rectangle or a regular polygon come out as that live shape), autoGrouping: false (each path that has others inside it is grouped with them), gradients: false (grayscale and color: an area whose colour changes evenly along one direction is one path with a linear gradient fill), fills: true, strokes: false (Create: lines up to strokeWidth wide become stroked centre lines; without fills, wider areas become stroked outlines), strokeWidth: px (10), logoColors?: [\"#rrggbb\"…] (mode logo: flat-colour logos only; colours found automatically or named here, at most ten; white is its own color, and a named color with every channel 245 or more is taken as that white, except with logoColors named, logoWhite false and no ignoreWhite; noise = areas under noise/10 px ignored; edges to a fraction of a pixel, sharp corners restored, lines and circles snapped; enclosed transparent areas are painted white while white is its own color, and stay holes with ignoreWhite or without a white of its own; fills/strokes are ignored; gradients and photos are refused), logoWhite: true (mode logo with logoColors named; false traces the named colors only: white areas take the nearest of them, a named white is kept and enclosed transparent areas stay holes; with ignoreWhite, white areas are left out either way)}, view?: as imageTrace.setView (default: the traced object's own, else tracingResult), ids?: [id…] (instead of id)} → {id, paths, anchors, colors, warnings: [text…] (the image is over 1500 px on a side, so it traces more slowly; or it was traced scaled down; Flat Logo names only the size it traced a larger image at)}",
|
||||
has_selection_or_ids,
|
||||
|s, p| trace_make(s, p, false)
|
||||
),
|
||||
|
||||
@@ -41,6 +41,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
cmd!(query "document.inspect", "Inspect Document", [], None, "{depth?, childLimit?, ids?: [id…] (list just these nodes, of any level, in this order, under `nodes` in place of `layers`; an unknown id is an error; at most 10000), kind?: panel label such as Layer; Group; Path; Type or Image (exact, ignoring case; list every such node under `nodes`, each with `path`, its ancestor ids with the layer first, searching the subtrees of `ids` when given), limit?: at most this many entries of the list, offset?: entries skipped first (default 0; with `ids`, `kind`, `limit` or `offset` the reply adds `total`, the entries before paging)} → layer tree (each node: id, name, kind, visible, locked, bounds, opacity (0–1) and opacityPct (0–100 %, the scale object.setProps takes) when below 1, blend when not Normal, fill, stroke and strokeWidth of objects and compound paths; a compound path's members have compoundMember: true instead of paint, which is the compound's), artboards, selection, history (a sliced tree reports childCount); without the scope options the reply is the whole layer tree", has_doc, doc_inspect),
|
||||
cmd!(query "document.node", "Inspect Object", [], None, "{id, summary?: compact summary, depth?: child levels in the summary (default all), childLimit?: children shown per node (default all; a level that shows fewer reports childCount)} → one object", has_doc, doc_node),
|
||||
cmd!(query "document.find", "Find Objects", [], None, "{name?: substring of the Layers panel name, kind?: panel label such as Group; Path; Type or Image (exact), text?: substring of type content, within?: [x, y, width, height] (an object's geometric bounds inside this rectangle, edges included; document points as document.inspect's bounds; layers never match this or the size filters), intersects?: [x, y, width, height] (bounds touching or overlapping it), minWidth?, maxWidth?, minHeight?, maxHeight?: pt (the bounds' size), minThickness?, maxThickness?: pt (the smaller of the bounds' width and height), minArea?, maxArea?: pt² (the bounds' width × height), minAnchors?, maxAnchors?: anchor points of a path (a compound path's members together; other objects never match), select?: bool (also select the matches, leaving out hidden and locked ones, those inside another match, and a compound path's members unless the compound path is isolated; Select > Reselect repeats it), limit?: max matches listed (default 100, 0 counts only)} → {matches: [{id, name, kind, path}], total, selected (with select: the count selected)}; at least one filter, all of them matching", has_doc, doc_find),
|
||||
cmd!(
|
||||
query "document.compare",
|
||||
"Compare with Image",
|
||||
[],
|
||||
None,
|
||||
"{path: an image file (PNG, JPEG, WebP, TIFF or BMP), artboard?: index=0, background?: \"#rrggbb\"=\"#ffffff\" (the render and the image are both flattened on it), threshold?: 0.08 (0–1), diffPath?: a PNG to write (each pixel's absolute R, G and B differences)} render the artboard at the image's size (the image's width in pixels over the artboard's width in points), drawn as document.export draws a PNG (linked images and placed documents from their files), and compare it with the image; an image whose height differs by more than a pixel from the artboard's height at that scale is refused (not in the browser) → {width, height, rmse: the root mean square of the R, G and B differences, 0–1 (6 decimals), over: the pixels with a channel that differs by more than threshold, total: the pixels, threshold, diffPath?, warnings?: as a PNG export's (type that doesn't fit its frame, an image that couldn't be decoded, type in a font or style that isn't available, previews drawn because a linked file can't be read)}",
|
||||
has_doc,
|
||||
doc_compare
|
||||
),
|
||||
cmd!(query "document.json", "Document JSON", [], None, "{} → complete document model", has_doc, |s, _| Ok(serde_json::to_value(&*s.doc()?.doc).unwrap_or(Value::Null))),
|
||||
cmd!(
|
||||
"document.setUnits",
|
||||
@@ -385,6 +394,155 @@ fn select_found(s: &mut Session, p: &Value, ids: &[NodeId]) -> Result<Value> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Render an artboard at an image's size, drawn as a PNG export draws it (linked images and placed
|
||||
/// documents from their files, with the export's warnings), and measure how far the
|
||||
/// two are apart: both flattened on `background`, the root mean square of the R, G and B
|
||||
/// differences (0–1), and the pixels with a channel that differs by more than `threshold`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn doc_compare(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
use vectorcraft_render::encode::{RasterExportOptions, RasterFormat, png};
|
||||
const C: &str = "document.compare";
|
||||
let path = str_param(p, "path").ok_or_else(|| bad(C, "missing `path`"))?;
|
||||
let diff_path = match p.get("diffPath") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(Value::String(v)) => Some(v.as_str()),
|
||||
Some(_) => {
|
||||
let key = "diffPath";
|
||||
return Err(bad(C, format!("`{key}` must be a string")));
|
||||
}
|
||||
};
|
||||
let background = match p.get("background") {
|
||||
None | Some(Value::Null) => [255; 3],
|
||||
Some(v) => match super::fileio::background(v) {
|
||||
Ok(Some(rgb)) => rgb,
|
||||
_ => return Err(bad(C, format!("background {v}: a color such as \"#808080\""))),
|
||||
},
|
||||
};
|
||||
let threshold = match p.get("threshold") {
|
||||
None | Some(Value::Null) => 0.08,
|
||||
Some(v) => v.as_f64().filter(|t| (0.0..=1.0).contains(t)).ok_or_else(|| bad(C, "`threshold` must be a number from 0 to 1"))?,
|
||||
};
|
||||
let doc = &s.doc()?.doc;
|
||||
let count = doc.artboards.len();
|
||||
let i = opt_u64(p, C, "artboard")?.unwrap_or(0);
|
||||
let region = usize::try_from(i)
|
||||
.ok()
|
||||
.and_then(|i| doc.artboards.get(i))
|
||||
.map(|a| a.rect)
|
||||
.ok_or_else(|| bad(C, format!("no artboard {i} (0-based; the document has {count})")))?;
|
||||
if let Some(d) = diff_path {
|
||||
crate::file_access::check_write(d).map_err(EngineError::Other)?;
|
||||
}
|
||||
let image = compare_image(path)?;
|
||||
let (iw, ih) = image.dimensions();
|
||||
// The image's width sets the scale; its height may differ from the artboard's at that scale by
|
||||
// the pixel the rounding of a render takes.
|
||||
let scale = f64::from(iw) / region.width();
|
||||
let rendered_height = region.height() * scale;
|
||||
if iw == 0 || ih == 0 || !rendered_height.is_finite() || (rendered_height - f64::from(ih)).abs() > 1.0 {
|
||||
let (rw, rh) = (iw, rendered_height.round());
|
||||
return Err(bad(
|
||||
C,
|
||||
format!("the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ"),
|
||||
));
|
||||
}
|
||||
vectorcraft_render::raster_size(region, scale).map_err(|e| bad(C, e))?;
|
||||
vectorcraft_render::raster_size(vectorcraft_geom::Rect::new(0.0, 0.0, f64::from(iw), f64::from(ih)), 1.0).map_err(|e| bad(C, e))?;
|
||||
let view = Affine::scale(scale) * Affine::translate((-region.x0, -region.y0));
|
||||
let opts = RasterExportOptions { background: Some(background), ..Default::default() }.render_options(RasterFormat::Png);
|
||||
// As a PNG export: linked images and placed documents drawn from their files, not the
|
||||
// previews saved for them.
|
||||
let (full, linked) = super::place::document::full_documents(doc);
|
||||
let failed = vectorcraft_render::decode_failures();
|
||||
let rendered = vectorcraft_render::Renderer::new().render(&full, iw, ih, view, &opts);
|
||||
// A canceled render stops drawing: the picture is not whole.
|
||||
vectorcraft_doc::cancel::check()?;
|
||||
// As a PNG export says, in its order: type on the artboard that doesn't fit its frame, an image
|
||||
// the renderer couldn't decode (missing from the render), type in a missing font or style (drawn
|
||||
// in another one), and linked files drawn from their previews.
|
||||
let mut warnings: Vec<String> = super::fonts::export_overflow_warning(&full, &full.layers, Some(&[region])).into_iter().collect();
|
||||
if let Some(w) = super::rasterfx::undecoded_warning(failed) {
|
||||
warnings.push(w.into());
|
||||
}
|
||||
if let Some(w) = super::fonts::substitution_warning(&full) {
|
||||
warnings.push(w);
|
||||
}
|
||||
warnings.extend(linked);
|
||||
let rendered = rendered.to_straight();
|
||||
let flat = |px: &[u8; 4]| {
|
||||
let a = u32::from(px[3]);
|
||||
[0, 1, 2].map(|k| ((u32::from(px[k]) * a + u32::from(background[k]) * (255 - a) + 127) / 255) as u8)
|
||||
};
|
||||
let row = iw as usize * 4;
|
||||
let limit = threshold * 255.0;
|
||||
let (mut squares, mut over) = (0.0f64, 0u64);
|
||||
let mut diff = diff_path.map(|_| Vec::with_capacity(row * ih as usize));
|
||||
for (a, b) in rendered.chunks_exact(row).zip(image.as_raw().chunks_exact(row)) {
|
||||
vectorcraft_doc::cancel::check()?;
|
||||
for (pa, pb) in a.as_chunks::<4>().0.iter().zip(b.as_chunks::<4>().0) {
|
||||
let (fa, fb) = (flat(pa), flat(pb));
|
||||
let d = [0, 1, 2].map(|k| fa[k].abs_diff(fb[k]));
|
||||
squares += d.iter().map(|&x| f64::from(x) * f64::from(x)).sum::<f64>();
|
||||
if d.iter().any(|&x| f64::from(x) > limit) {
|
||||
over += 1;
|
||||
}
|
||||
if let Some(out) = &mut diff {
|
||||
out.extend_from_slice(&[d[0], d[1], d[2], 255]);
|
||||
}
|
||||
}
|
||||
}
|
||||
let total = u64::from(iw) * u64::from(ih);
|
||||
let rmse = (squares / (total as f64 * 3.0)).sqrt() / 255.0;
|
||||
let mut out = json!({
|
||||
"width": iw,
|
||||
"height": ih,
|
||||
"rmse": (rmse * 1e6).round() / 1e6,
|
||||
"over": over,
|
||||
"total": total,
|
||||
"threshold": threshold,
|
||||
});
|
||||
if let (Some(d), Some(pixels)) = (diff_path, diff) {
|
||||
let bytes = png::encode(&pixels, iw, ih, &png::PngOptions { ppi: None, interlaced: false }).map_err(EngineError::Other)?;
|
||||
super::fileio::write_file(d, &bytes)?;
|
||||
out["diffPath"] = json!(d);
|
||||
}
|
||||
if !warnings.is_empty() {
|
||||
out["warnings"] = json!(warnings);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The image `document.compare` reads from `path` (through the automation read root), as straight
|
||||
/// RGBA: a PNG, JPEG, WebP, TIFF or BMP within the allocation an opened image may take.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn compare_image(path: &str) -> Result<image::RgbaImage> {
|
||||
use image::ImageDecoder as _;
|
||||
const C: &str = "document.compare";
|
||||
let bytes = super::fileio::read_file(path)?;
|
||||
if let Some((w, h)) = vectorcraft_doc::cmyk::oversized_tiff(&bytes) {
|
||||
return Err(bad(C, format!("an image of {w} × {h} pixels is too large to draw")));
|
||||
}
|
||||
let mut reader = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().map_err(|e| bad(C, format!("{path}: {e}")))?;
|
||||
let readable = [image::ImageFormat::Png, image::ImageFormat::Jpeg, image::ImageFormat::WebP, image::ImageFormat::Tiff, image::ImageFormat::Bmp];
|
||||
if !reader.format().is_some_and(|f| readable.contains(&f)) {
|
||||
return Err(bad(C, format!("{path}: not a PNG, JPEG, WebP, TIFF or BMP image")));
|
||||
}
|
||||
let mut limits = image::Limits::default();
|
||||
limits.max_alloc = Some(vectorcraft_doc::MAX_RASTER_ALLOC);
|
||||
reader.limits(limits);
|
||||
let decoder = reader.into_decoder().map_err(|e| bad(C, format!("{path}: {e}")))?;
|
||||
let (w, h) = decoder.dimensions();
|
||||
if decoder.total_bytes() > vectorcraft_doc::MAX_RASTER_ALLOC || u64::from(w) * u64::from(h) * 4 > vectorcraft_doc::MAX_RASTER_ALLOC {
|
||||
return Err(bad(C, format!("an image of {w} × {h} pixels is too large to draw")));
|
||||
}
|
||||
Ok(image::DynamicImage::from_decoder(decoder).map_err(|e| bad(C, format!("{path}: {e}")))?.to_rgba8())
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn doc_compare(_: &mut Session, _: &Value) -> Result<Value> {
|
||||
Err(bad("document.compare", "comparing with an image file isn't available in the browser"))
|
||||
}
|
||||
|
||||
/// Document Setup's units (also `document.setup {units}`).
|
||||
fn set_units(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let u = str_param(p, "units").and_then(Unit::named).ok_or_else(|| bad("document.setUnits", "unknown units"))?;
|
||||
|
||||
@@ -31,6 +31,7 @@ mod colorize;
|
||||
pub(super) mod colormgmt;
|
||||
mod colorthemes;
|
||||
mod colourfonts;
|
||||
mod compare;
|
||||
mod compound_shape;
|
||||
mod compoundpaint;
|
||||
mod constrain_new;
|
||||
|
||||
@@ -3825,6 +3825,43 @@ fn image_trace_flat_logo_refuses_a_gradient_and_a_bad_palette() {
|
||||
assert_eq!(r["colors"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_trace_flat_logo_can_trace_only_the_named_colors() {
|
||||
// A light disc in a black ring, a blue square on it, and a white spot.
|
||||
let art = vectorcraft_trace::Raster::from_fn(120, 120, |x, y| {
|
||||
let (fx, fy) = (f64::from(x) + 0.5, f64::from(y) + 0.5);
|
||||
let d = (fx - 60.0).hypot(fy - 60.0);
|
||||
if (fx - 32.0).hypot(fy - 40.0) <= 6.0 {
|
||||
[255, 255, 255, 255]
|
||||
} else if (45..75).contains(&x) && (45..75).contains(&y) {
|
||||
[40, 120, 240, 255]
|
||||
} else if d <= 46.0 {
|
||||
[240, 240, 220, 255]
|
||||
} else if d <= 50.0 {
|
||||
[0, 0, 0, 255]
|
||||
} else {
|
||||
[0, 0, 0, 0]
|
||||
}
|
||||
});
|
||||
let fills = |logo_white: bool| {
|
||||
let mut s = session();
|
||||
add_image(&mut s, &art, Affine::IDENTITY);
|
||||
let params = json!({"params": {"mode": "logo", "logoColors": ["#000000", "#F0F0DC", "#2878F0"], "logoWhite": logo_white}});
|
||||
let r = s.execute("imageTrace.makeAndExpand", ¶ms).unwrap();
|
||||
let g = node(&s, NodeId(r["id"].as_u64().unwrap()));
|
||||
let mut hex: Vec<String> = g.children().unwrap().iter().filter_map(|n| n.appearance.fill_paint().color()).map(|c| c.to_hex()).collect();
|
||||
hex.sort();
|
||||
hex.dedup();
|
||||
(r["colors"].clone(), hex)
|
||||
};
|
||||
let (colors, hex) = fills(true);
|
||||
assert_eq!(colors, 4);
|
||||
assert!(hex.contains(&"#ffffff".to_string()), "{hex:?}");
|
||||
let (colors, hex) = fills(false);
|
||||
assert_eq!(colors, 3);
|
||||
assert_eq!(hex, ["#000000", "#2878f0", "#f0f0dc"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_trace_object_remembers_its_settings() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
//! `document.compare`: an artboard rendered at an image's size, as a PNG export draws it, and
|
||||
//! measured against the image (RMSE, the pixels over a threshold, a difference image), with the
|
||||
//! image and the render flattened on one background; images of other proportions, files that
|
||||
//! can't be read and paths outside the automation roots are errors.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::Session;
|
||||
|
||||
/// A 40 × 20 pt document with a black 10 × 10 pt square at (10, 5), no stroke.
|
||||
fn session() -> (Session, Value) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 40, "height": 20})).unwrap();
|
||||
s.execute("paint.setFill", &json!({"color": "#000000"})).unwrap();
|
||||
s.execute("paint.setStroke", &json!({"none": true})).unwrap();
|
||||
let id = s.execute("shape.rectangle", &json!({"x": 10, "y": 5, "width": 10, "height": 10})).unwrap()["id"].clone();
|
||||
(s, id)
|
||||
}
|
||||
|
||||
fn path_in(dir: &std::path::Path, name: &str) -> String {
|
||||
dir.join(name).to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
/// The artboard exported as a PNG at 2 pixels a point on white.
|
||||
fn export(s: &mut Session, path: &str) {
|
||||
s.execute("document.export", &json!({"path": path, "format": "png", "scale": 2, "background": "white"})).unwrap();
|
||||
}
|
||||
|
||||
fn compare(s: &mut Session, p: Value) -> Value {
|
||||
s.execute("document.compare", &p).unwrap_or_else(|e| panic!("document.compare {p}: {e}"))
|
||||
}
|
||||
|
||||
fn error(s: &mut Session, p: Value) -> String {
|
||||
s.execute("document.compare", &p).map(|v| panic!("document.compare {p} answered {v}")).unwrap_err().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_export_compares_equal_and_a_moved_square_counts_the_pixels_it_changed() {
|
||||
let dir = vectorcraft_testkit::temp_dir("compare-moved");
|
||||
let png = path_in(&dir, "art.png");
|
||||
let (mut s, id) = session();
|
||||
export(&mut s, &png);
|
||||
let before = (s.doc().unwrap().revision, s.doc().unwrap().history.undo.len());
|
||||
let r = compare(&mut s, json!({"path": png}));
|
||||
assert_eq!(r, json!({"width": 80, "height": 40, "rmse": 0.0, "over": 0, "total": 3200, "threshold": 0.08}));
|
||||
assert_eq!((s.doc().unwrap().revision, s.doc().unwrap().history.undo.len()), before, "comparing doesn't edit the document");
|
||||
// 4 pt is 8 pixels: 8 columns of the square's 20 rows turn white and 8 turn black, every
|
||||
// channel by 255.
|
||||
s.execute("object.move", &json!({"ids": [id], "dx": 4, "dy": 0})).unwrap();
|
||||
let r = compare(&mut s, json!({"path": png}));
|
||||
let over = 2 * 8 * 20;
|
||||
assert_eq!((r["over"].as_u64(), r["total"].as_u64()), (Some(over), Some(3200)), "{r}");
|
||||
let rmse = ((over as f64 / 3200.0).sqrt() * 1e6).round() / 1e6;
|
||||
assert_eq!(r["rmse"].as_f64(), Some(rmse), "{r}");
|
||||
// Above every difference, nothing is over.
|
||||
assert_eq!(compare(&mut s, json!({"path": png, "threshold": 1}))["over"], json!(0));
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_image_and_the_render_are_flattened_on_the_background() {
|
||||
let dir = vectorcraft_testkit::temp_dir("compare-background");
|
||||
let png = path_in(&dir, "clear.png");
|
||||
// Transparent everywhere, with red in the color channels that the alpha hides.
|
||||
image::RgbaImage::from_pixel(80, 40, image::Rgba([255, 0, 0, 0])).save(&png).unwrap();
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 40, "height": 20})).unwrap();
|
||||
// Nothing drawn: both are the background alone.
|
||||
for background in ["#808080", "#ffffff"] {
|
||||
let r = compare(&mut s, json!({"path": png, "background": background}));
|
||||
assert_eq!((r["rmse"].as_f64(), r["over"].as_u64()), (Some(0.0), Some(0)), "{background}: {r}");
|
||||
}
|
||||
// A black left half: 128 from gray in every channel of half the pixels, 255 from white.
|
||||
s.execute("paint.setFill", &json!({"color": "#000000"})).unwrap();
|
||||
s.execute("paint.setStroke", &json!({"none": true})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 20, "height": 20})).unwrap();
|
||||
let r = compare(&mut s, json!({"path": png, "background": "#808080"}));
|
||||
let rmse = |d: f64| ((0.5f64).sqrt() * d / 255.0 * 1e6).round() / 1e6;
|
||||
assert_eq!((r["rmse"].as_f64(), r["over"].as_u64()), (Some(rmse(128.0)), Some(1600)), "{r}");
|
||||
let r = compare(&mut s, json!({"path": png}));
|
||||
assert_eq!((r["rmse"].as_f64(), r["over"].as_u64()), (Some(rmse(255.0)), Some(1600)), "{r}");
|
||||
// A threshold just above 128/255 leaves the gray differences out.
|
||||
assert_eq!(compare(&mut s, json!({"path": png, "background": "#808080", "threshold": 0.51}))["over"], json!(0));
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_image_of_other_proportions_is_refused_and_one_pixel_is_allowed() {
|
||||
let dir = vectorcraft_testkit::temp_dir("compare-size");
|
||||
let (mut s, _) = session();
|
||||
let square = path_in(&dir, "square.png");
|
||||
image::RgbaImage::from_pixel(30, 30, image::Rgba([255; 4])).save(&square).unwrap();
|
||||
let e = error(&mut s, json!({"path": square}));
|
||||
assert!(e.contains("30 × 30 pixels") && e.contains("30 × 15 pixels"), "{e}");
|
||||
let taller = path_in(&dir, "taller.png");
|
||||
image::RgbaImage::from_pixel(80, 41, image::Rgba([255; 4])).save(&taller).unwrap();
|
||||
let r = compare(&mut s, json!({"path": taller}));
|
||||
assert_eq!((r["width"].as_u64(), r["height"].as_u64(), r["total"].as_u64()), (Some(80), Some(41), Some(3280)), "{r}");
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_export_whose_height_was_rounded_compares_equal() {
|
||||
let dir = vectorcraft_testkit::temp_dir("compare-rounded");
|
||||
let png = path_in(&dir, "art.png");
|
||||
// 20.25 pt at 2 pixels a point is 40.5 rows, which the export rounds to 41.
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 40, "height": 20.25})).unwrap();
|
||||
s.execute("paint.setFill", &json!({"color": "#000000"})).unwrap();
|
||||
s.execute("paint.setStroke", &json!({"none": true})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 5, "width": 10, "height": 10})).unwrap();
|
||||
export(&mut s, &png);
|
||||
let r = compare(&mut s, json!({"path": png}));
|
||||
assert_eq!(r, json!({"width": 80, "height": 41, "rmse": 0.0, "over": 0, "total": 3280, "threshold": 0.08}));
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_linked_image_is_drawn_from_its_file_as_a_png_export_draws_it() {
|
||||
use crate::tests_links::{BLUE, Folder, RED, open, place, png, save, session, write};
|
||||
let dir = Folder::new("compare-linked");
|
||||
let pic = dir.file("photo.png");
|
||||
write(&pic, &png(600, 300, RED));
|
||||
let mut s = session();
|
||||
place(&mut s, &pic);
|
||||
let doc_path = dir.file("poster.vectorcraft");
|
||||
save(&mut s, &doc_path);
|
||||
// The file changes after the save: the reopened document shows the red preview saved for it,
|
||||
// and a PNG export reads the blue file.
|
||||
write(&pic, &png(600, 300, BLUE));
|
||||
open(&mut s, &doc_path);
|
||||
let out = dir.file("export.png");
|
||||
s.execute("document.export", &json!({"path": out, "format": "png", "background": "white"})).unwrap();
|
||||
let r = compare(&mut s, json!({"path": out}));
|
||||
assert_eq!((r["rmse"].as_f64(), r["over"].as_u64(), r.get("warnings")), (Some(0.0), Some(0), None), "{r}");
|
||||
// Without the file the preview is drawn, and the reply says so.
|
||||
std::fs::remove_file(&pic).unwrap();
|
||||
let r = compare(&mut s, json!({"path": out}));
|
||||
assert_eq!(r["over"].as_u64(), r["total"].as_u64(), "{r}");
|
||||
let warnings = r["warnings"].as_array().unwrap();
|
||||
assert!(warnings.len() == 1 && warnings[0].as_str().unwrap().contains("1 linked image output as its preview"), "{r}");
|
||||
// With type that doesn't fit its frame as well, the warnings come in the export's order.
|
||||
let overset =
|
||||
s.execute("text.create", &json!({"x": 10, "y": 10, "text": "Ab\nCd\nEf\nGh", "size": 12, "area": {"width": 100, "height": 20}})).unwrap();
|
||||
assert_eq!(overset["overflow"], json!([overset["id"]]), "{overset}");
|
||||
let exported = s.execute("document.export", &json!({"path": dir.file("now.png"), "format": "png", "background": "white"})).unwrap();
|
||||
let r = compare(&mut s, json!({"path": out}));
|
||||
assert_eq!(r["warnings"].as_array().map(Vec::len), Some(2), "{r}");
|
||||
assert_eq!(r["warnings"], exported["warnings"], "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_image_that_cannot_be_decoded_and_a_missing_font_warn_as_a_png_export_does() {
|
||||
let dir = vectorcraft_testkit::temp_dir("compare-warnings");
|
||||
let png = path_in(&dir, "art.png");
|
||||
let (mut s, _) = session();
|
||||
export(&mut s, &png);
|
||||
assert_eq!(compare(&mut s, json!({"path": png})).get("warnings"), None);
|
||||
crate::tests_menucmds::place_undecodable_png(&mut s);
|
||||
s.execute("text.create", &json!({"x": 2, "y": 18, "text": "Ab", "font": "Nonexistent Sans Probe"})).unwrap();
|
||||
let overset = s.execute("text.create", &json!({"x": 22, "y": 2, "text": "Ab\nCd\nEf", "size": 6, "area": {"width": 15, "height": 8}})).unwrap();
|
||||
assert_eq!(overset["overflow"], json!([overset["id"]]), "{overset}");
|
||||
let exported = s.execute("document.export", &json!({"path": path_in(&dir, "now.png"), "format": "png", "scale": 2})).unwrap();
|
||||
let r = compare(&mut s, json!({"path": png}));
|
||||
assert_eq!(r["warnings"], exported["warnings"], "{r}");
|
||||
let warnings = r["warnings"].as_array().unwrap();
|
||||
assert!(warnings.iter().any(|w| w == "an image couldn't be decoded and is missing from the picture"), "{r}");
|
||||
assert!(warnings.iter().any(|w| w.as_str().unwrap().contains("Nonexistent Sans Probe")), "{r}");
|
||||
assert!(warnings.iter().any(|w| w.as_str().unwrap().contains("doesn't fit its frame")), "{r}");
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_path_writes_each_pixels_differences() {
|
||||
let dir = vectorcraft_testkit::temp_dir("compare-diff");
|
||||
let png = path_in(&dir, "art.png");
|
||||
let diff = path_in(&dir, "diff.png");
|
||||
let (mut s, id) = session();
|
||||
export(&mut s, &png);
|
||||
s.execute("object.move", &json!({"ids": [id], "dx": 4, "dy": 0})).unwrap();
|
||||
let r = compare(&mut s, json!({"path": png, "diffPath": diff}));
|
||||
assert_eq!(r["diffPath"], json!(diff));
|
||||
let img = image::open(&diff).unwrap().to_rgba8();
|
||||
assert_eq!(img.dimensions(), (80, 40));
|
||||
// The square spanned pixels 20–39 and now spans 28–47, rows 10–29.
|
||||
for (x, y, want) in [(24, 20, 255), (44, 20, 255), (30, 20, 0), (24, 5, 0), (60, 20, 0)] {
|
||||
assert_eq!(img.get_pixel(x, y).0, [want, want, want, 255], "({x}, {y})");
|
||||
}
|
||||
let changed = img.pixels().filter(|p| p.0[0] != 0).count();
|
||||
assert_eq!(changed as u64, r["over"].as_u64().unwrap());
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_unreadable_and_malformed_inputs_are_errors() {
|
||||
let dir = vectorcraft_testkit::temp_dir("compare-errors");
|
||||
let (mut s, _) = session();
|
||||
let png = path_in(&dir, "art.png");
|
||||
export(&mut s, &png);
|
||||
let missing = path_in(&dir, "missing.png");
|
||||
assert!(error(&mut s, json!({"path": missing})).contains(&missing));
|
||||
let text = path_in(&dir, "notes.png");
|
||||
std::fs::write(&text, "not an image\n").unwrap();
|
||||
assert!(error(&mut s, json!({"path": text})).contains("not a PNG, JPEG, WebP, TIFF or BMP image"));
|
||||
let gif = path_in(&dir, "art.gif");
|
||||
image::RgbaImage::from_pixel(80, 40, image::Rgba([255; 4])).save(&gif).unwrap();
|
||||
assert!(error(&mut s, json!({"path": gif})).contains("not a PNG, JPEG, WebP, TIFF or BMP image"));
|
||||
for (p, want) in [
|
||||
(json!({}), "missing `path`"),
|
||||
(json!({"path": png, "artboard": 3}), "no artboard 3"),
|
||||
(json!({"path": png, "threshold": 2}), "`threshold` must be a number from 0 to 1"),
|
||||
(json!({"path": png, "threshold": "low"}), "`threshold` must be a number from 0 to 1"),
|
||||
(json!({"path": png, "background": "transparent"}), "a color such as"),
|
||||
(json!({"path": png, "diffPath": 5}), "`diffPath` must be a string"),
|
||||
] {
|
||||
let e = error(&mut s, p.clone());
|
||||
assert!(e.contains(want), "{p}: {e}");
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
/// A planar TIFF whose header claims more strips than the decoder can count (2^31 + 1 rows a strip
|
||||
/// each, times 5 planes) is refused as too large to draw, without a panic.
|
||||
#[test]
|
||||
fn a_tiff_with_more_strips_than_can_be_counted_is_too_large() {
|
||||
let dir = vectorcraft_testkit::temp_dir("compare-strips");
|
||||
let (mut s, _) = session();
|
||||
let plane = |v: u16| vec![v; 6];
|
||||
let channels = [plane(200), plane(40), plane(10), plane(0), plane(90)];
|
||||
let t = vectorcraft_testkit::tiff::Tiff {
|
||||
width: 3,
|
||||
height: 2,
|
||||
photometric: 5,
|
||||
bits: 8,
|
||||
extra: &[0],
|
||||
planar: true,
|
||||
ppi: Some(150),
|
||||
channels: &channels,
|
||||
};
|
||||
let mut file = vectorcraft_testkit::tiff::tiff(&t);
|
||||
// ImageLength (the directory's second entry, its value at offset 30) and RowsPerStrip (the
|
||||
// eighth, at offset 102).
|
||||
file[30..34].copy_from_slice(&(1u32 << 31 | 1).to_le_bytes());
|
||||
file[102..106].copy_from_slice(&1u32.to_le_bytes());
|
||||
assert_eq!(vectorcraft_doc::cmyk::oversized_tiff(&file), Some((3, 1 << 31 | 1)));
|
||||
let tif = path_in(&dir, "strips.tif");
|
||||
std::fs::write(&tif, &file).unwrap();
|
||||
let e = error(&mut s, json!({"path": tif}));
|
||||
assert_eq!(e, "invalid parameters for `document.compare`: an image of 3 × 2147483649 pixels is too large to draw");
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_image_and_the_difference_image_stay_inside_the_automation_roots() {
|
||||
let dir = vectorcraft_testkit::temp_dir("compare-roots");
|
||||
for d in ["in", "out", "elsewhere"] {
|
||||
std::fs::create_dir_all(dir.join(d)).unwrap();
|
||||
}
|
||||
let (mut s, _) = session();
|
||||
let inside = path_in(&dir.join("in"), "art.png");
|
||||
let outside = path_in(&dir.join("elsewhere"), "art.png");
|
||||
export(&mut s, &inside);
|
||||
export(&mut s, &outside);
|
||||
let roots = crate::file_access::AutomationRoots::new(&[&dir.join("in")], &[&dir.join("out")]).unwrap().unwrap();
|
||||
let mut run = |p: Value| crate::file_access::confine(Some(&roots), || s.execute("document.compare", &p));
|
||||
assert!(run(json!({"path": inside})).is_ok());
|
||||
let e = run(json!({"path": outside})).unwrap_err().to_string();
|
||||
assert!(e.contains("outside the read root"), "{e}");
|
||||
let stray = path_in(&dir.join("elsewhere"), "diff.png");
|
||||
let e = run(json!({"path": inside, "diffPath": stray})).unwrap_err().to_string();
|
||||
assert!(e.contains("outside the write root"), "{e}");
|
||||
assert!(!std::path::Path::new(&stray).exists());
|
||||
let kept = path_in(&dir.join("out"), "diff.png");
|
||||
assert_eq!(run(json!({"path": inside, "diffPath": kept})).unwrap()["diffPath"], json!(kept));
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
@@ -353,22 +353,38 @@ fn plane_at(planes: &[Vec<f32>], class: usize, i: usize) -> f32 {
|
||||
planes.get(class).and_then(|p| p.get(i)).copied().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// The unmixing classes for a palette: each colour, then white, then transparent.
|
||||
fn classes_for(palette: &[[u8; 3]]) -> Vec<[f64; 4]> {
|
||||
/// The unmixing classes for a palette: each colour, then white (when `own_white`), then transparent.
|
||||
fn classes_for(palette: &[[u8; 3]], own_white: bool) -> Vec<[f64; 4]> {
|
||||
let mut classes: Vec<[f64; 4]> =
|
||||
palette.iter().map(|c| [f64::from(c[0]) / 255.0, f64::from(c[1]) / 255.0, f64::from(c[2]) / 255.0, 1.0]).collect();
|
||||
classes.push([1.0, 1.0, 1.0, 1.0]);
|
||||
if own_white {
|
||||
classes.push([1.0, 1.0, 1.0, 1.0]);
|
||||
}
|
||||
classes.push([0.0, 0.0, 0.0, 0.0]);
|
||||
classes
|
||||
}
|
||||
|
||||
/// Whether white is traced as a class of its own: always, unless the caller named the colors and
|
||||
/// turned [`TraceParams::logo_white`] off. With Ignore White it is, so white areas are left out.
|
||||
fn own_white(params: &TraceParams) -> bool {
|
||||
params.logo_white || params.logo_colors.is_empty() || params.ignore_white
|
||||
}
|
||||
|
||||
/// Without a white class of its own: the palette color that takes white's coverage, the one
|
||||
/// nearest white (the first of equally near ones). `None` when a named color is white already
|
||||
/// (closer than [`SAME_COLOUR`]), so white needs no class to unmix with.
|
||||
fn white_goes_to(palette: &[[u8; 3]]) -> Option<usize> {
|
||||
let (i, d) = palette.iter().enumerate().map(|(i, &c)| (i, distance(c, [255; 3]))).min_by(|a, b| a.1.total_cmp(&b.1))?;
|
||||
(d >= SAME_COLOUR).then_some(i)
|
||||
}
|
||||
|
||||
fn distance(a: [u8; 3], b: [u8; 3]) -> f64 {
|
||||
(0..3).map(|k| (f64::from(a[k]) - f64::from(b[k])).powi(2)).sum::<f64>().sqrt()
|
||||
}
|
||||
|
||||
type Colours = Vec<[u8; 3]>;
|
||||
|
||||
/// The colours to trace: those the caller named (white is always its own class so it is skipped;
|
||||
/// The colours to trace: those the caller named (white is skipped when it is its own class;
|
||||
/// colours closer than [`SAME_COLOUR`] count as one; at most [`MAX_COLOURS`]) or the ones found,
|
||||
/// the tones of one colour folded into it ([`fold_tones`]); and with them the colours before folding.
|
||||
fn choose_palette(params: &TraceParams, pix: &[Px], w: usize, h: usize) -> Result<(Colours, Colours), TraceError> {
|
||||
@@ -380,7 +396,7 @@ fn choose_palette(params: &TraceParams, pix: &[Px], w: usize, h: usize) -> Resul
|
||||
for s in ¶ms.logo_colors {
|
||||
// Every entry is checked, but only the first few distinct colours are kept.
|
||||
let c = parse_hex(s).ok_or_else(|| TraceError::BadPalette(s.clone()))?;
|
||||
let white = c.iter().all(|&v| v >= WHITE_MIN);
|
||||
let white = own_white(params) && c.iter().all(|&v| v >= WHITE_MIN);
|
||||
if palette.len() < MAX_COLOURS && !white && !palette.iter().any(|&q| distance(q, c) < SAME_COLOUR) {
|
||||
palette.push(c);
|
||||
}
|
||||
@@ -405,7 +421,11 @@ pub(crate) fn trace_logo(img: &Raster, params: &TraceParams, max_anchors: usize)
|
||||
if fraction < MIN_FLAT {
|
||||
return Err(TraceError::NotFlat { fraction });
|
||||
}
|
||||
let mut classes = classes_for(&palette);
|
||||
let own_white = own_white(params);
|
||||
// Without a white of its own, white is still unmixed as a class, so blended edges split where
|
||||
// they are, and its coverage is then added to the color it goes to.
|
||||
let merge_white = if own_white { None } else { white_goes_to(&palette) };
|
||||
let mut classes = classes_for(&palette, own_white || merge_white.is_some());
|
||||
// A very busy image is averaged down further until the unmixing work is bounded.
|
||||
for _ in 0..6 {
|
||||
if count_ambiguous(&pix, &classes) <= EDGE_BUDGET || w * h <= 16_384 {
|
||||
@@ -423,15 +443,28 @@ pub(crate) fn trace_logo(img: &Raster, params: &TraceParams, max_anchors: usize)
|
||||
palette.extend(extra.into_iter().take(MAX_COLOURS - palette.len()));
|
||||
// (A colour nothing explained may be one more tone of a colour that is there.)
|
||||
palette = fold_tones(&pix, palette);
|
||||
classes = classes_for(&palette);
|
||||
classes = classes_for(&palette, own_white);
|
||||
unmix(&pix, w, h, &classes).0
|
||||
}
|
||||
} else {
|
||||
std::mem::take(&mut planes)
|
||||
};
|
||||
let (white, transparent) = (palette.len(), palette.len() + 1);
|
||||
if let Some(to) = merge_white
|
||||
&& palette.len() < planes.len()
|
||||
{
|
||||
let white_plane = planes.remove(palette.len());
|
||||
if let Some(target) = planes.get_mut(to) {
|
||||
for (t, v) in target.iter_mut().zip(&white_plane) {
|
||||
*t += *v;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Without a white class, `white` is an index no class has.
|
||||
let (white, transparent) = if own_white { (palette.len(), palette.len() + 1) } else { (usize::MAX, palette.len()) };
|
||||
let aliased = soften_if_aliased(&mut planes, w, h);
|
||||
fill_knockouts(&mut planes, w, h, white, transparent);
|
||||
if own_white {
|
||||
fill_knockouts(&mut planes, w, h, white, transparent);
|
||||
}
|
||||
let n = w * h;
|
||||
let dominant: Vec<u8> = (0..n)
|
||||
.map(|i| (0..planes.len()).max_by(|&a, &b| plane_at(&planes, a, i).total_cmp(&plane_at(&planes, b, i))).unwrap_or(transparent) as u8)
|
||||
@@ -474,7 +507,7 @@ pub(crate) fn trace_logo(img: &Raster, params: &TraceParams, max_anchors: usize)
|
||||
let to_source = ToSource { factor: factor as f64, work: (w, h), source: (w0, h0) };
|
||||
let mut shapes: Vec<Kept> = Vec::new();
|
||||
let mut anchors_made = 0usize;
|
||||
for class in 0..=white {
|
||||
for class in 0..transparent {
|
||||
if class == white && params.ignore_white {
|
||||
continue;
|
||||
}
|
||||
@@ -488,8 +521,9 @@ pub(crate) fn trace_logo(img: &Raster, params: &TraceParams, max_anchors: usize)
|
||||
if a2.abs() / 2.0 < min_area {
|
||||
continue;
|
||||
}
|
||||
// Holes are cut only under Ignore White; otherwise white and the other colors are traced as their own shapes.
|
||||
if a2 >= 0.0 && !params.ignore_white {
|
||||
// Holes are cut only under Ignore White or with no white class; otherwise white and the other colors are
|
||||
// traced as their own shapes.
|
||||
if a2 >= 0.0 && !(params.ignore_white || !own_white) {
|
||||
continue;
|
||||
}
|
||||
// More real outlines than we will fit is refused, not silently truncated.
|
||||
@@ -499,8 +533,9 @@ pub(crate) fn trace_logo(img: &Raster, params: &TraceParams, max_anchors: usize)
|
||||
let pts = to_pixels(&lp, u);
|
||||
if a2 < 0.0 { outers.push((a2.abs() / 2.0, pts)) } else { holes.push(pts) }
|
||||
}
|
||||
// Each hole belongs to the smallest outline that contains it, and to no other.
|
||||
let owner: Vec<Option<usize>> = if params.ignore_white {
|
||||
// Each hole belongs to the smallest outline that contains it, and to no other. (Holes are
|
||||
// cut where white does not paint over them: with Ignore White, or with no white class.)
|
||||
let owner: Vec<Option<usize>> = if params.ignore_white || !own_white {
|
||||
holes
|
||||
.iter()
|
||||
.map(|hole| {
|
||||
|
||||
@@ -62,6 +62,9 @@ fn render(w: u32, h: u32, blur: f64, shade: impl Fn(f64, f64) -> Option<[u8; 3]>
|
||||
|
||||
const RED: [u8; 3] = [210, 40, 50];
|
||||
const BLUE: [u8; 3] = [40, 80, 200];
|
||||
const LIGHT: [u8; 3] = [240, 240, 220];
|
||||
const SKY: [u8; 3] = [40, 120, 240];
|
||||
const WHITE: [u8; 3] = [255, 255, 255];
|
||||
|
||||
fn logo_params() -> TraceParams {
|
||||
TraceParams {
|
||||
@@ -205,6 +208,99 @@ fn the_named_palette_is_used_exactly_and_a_bad_one_is_an_error() {
|
||||
assert!(matches!(trace_within(&img, &bad, 1000), Err(TraceError::BadPalette(s)) if s == "red"));
|
||||
}
|
||||
|
||||
/// The color of the topmost path at `p` (paths are listed bottom first).
|
||||
fn top_color(res: &TraceResult, p: Point) -> Option<[u8; 3]> {
|
||||
res.paths.iter().rev().find(|t| t.path.to_bezpath().winding(p) != 0).map(|t| t.color)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logo_white_off_traces_only_the_named_colors() {
|
||||
// A light disc in a black ring, a blue square on it, and two white spots.
|
||||
let img = render(120, 120, 0.7, |x, y| {
|
||||
let d = (x - 60.0).hypot(y - 60.0);
|
||||
if (x - 32.0).hypot(y - 40.0) <= 6.0 || (x - 78.0).hypot(y - 86.0) <= 6.0 {
|
||||
Some(WHITE)
|
||||
} else if (45.0..=75.0).contains(&x) && (45.0..=75.0).contains(&y) {
|
||||
Some(SKY)
|
||||
} else if d <= 46.0 {
|
||||
Some(LIGHT)
|
||||
} else if d <= 50.0 {
|
||||
Some([0, 0, 0])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let named = TraceParams { logo_colors: vec!["#000000".into(), "#F0F0DC".into(), "#2878F0".into()], ..logo_params() };
|
||||
// By default white is a fourth color.
|
||||
let res = trace(&img, &named);
|
||||
assert_eq!(res.palette.len(), 4, "{:?}", res.palette);
|
||||
assert_eq!(top_color(&res, Point::new(32.0, 40.0)), Some(WHITE));
|
||||
// Without it, the three named colors only: the white spots take the light color.
|
||||
let res = trace(&img, &TraceParams { logo_white: false, ..named.clone() });
|
||||
assert_eq!(res.palette.len(), 3, "{:?}", res.palette);
|
||||
assert!(!res.palette.contains(&WHITE));
|
||||
for p in [Point::new(32.0, 40.0), Point::new(78.0, 86.0), Point::new(20.0, 60.0)] {
|
||||
assert_eq!(top_color(&res, p), Some(LIGHT), "at {p:?}");
|
||||
}
|
||||
assert_eq!(top_color(&res, Point::new(60.0, 60.0)), Some(SKY));
|
||||
assert_eq!(top_color(&res, Point::new(60.0, 12.0)), Some([0, 0, 0]));
|
||||
assert!(!covered(&res, Point::new(5.0, 5.0)));
|
||||
// Ignore White still leaves the white spots out.
|
||||
let res = trace(&img, &TraceParams { logo_white: false, ignore_white: true, ..named.clone() });
|
||||
for p in [Point::new(32.0, 40.0), Point::new(78.0, 86.0)] {
|
||||
assert!(!covered(&res, p), "at {p:?}");
|
||||
}
|
||||
assert_eq!(top_color(&res, Point::new(20.0, 60.0)), Some(LIGHT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logo_white_off_keeps_a_named_white_and_knockouts_as_holes() {
|
||||
let disc = render(64, 64, 0.7, |x, y| ((x - 32.0).hypot(y - 32.0) <= 20.0).then_some(WHITE));
|
||||
let only_white = TraceParams { logo_colors: vec!["#ffffff".into()], logo_white: false, ..logo_params() };
|
||||
let res = trace(&disc, &only_white);
|
||||
assert_eq!(res.paths.len(), 1);
|
||||
assert_eq!(res.paths[0].color, WHITE);
|
||||
// An enclosed transparent area stays a hole: there is no white to paint it with.
|
||||
let ring = render(64, 64, 0.7, |x, y| (12.0..=25.0).contains(&(x - 32.0).hypot(y - 32.0)).then_some(BLUE));
|
||||
let res = trace(&ring, &TraceParams { logo_colors: vec!["#2850c8".into()], logo_white: false, ..logo_params() });
|
||||
assert_eq!(res.paths.len(), 1);
|
||||
assert_eq!(res.paths[0].path.subpaths.len(), 2, "outer outline and a hole");
|
||||
assert!(!covered(&res, Point::new(32.0, 32.0)), "the hole must be empty");
|
||||
assert!(covered(&res, Point::new(32.0, 32.0 + 18.0)), "the ring itself is filled");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logo_white_off_gives_each_white_the_nearest_named_color() {
|
||||
// Red and blue are equally far from white: the white disc takes the first one named.
|
||||
let disc = render(64, 64, 0.7, |x, y| ((x - 32.0).hypot(y - 32.0) <= 20.0).then_some(WHITE));
|
||||
let params = TraceParams { logo_colors: vec!["#ff0000".into(), "#0000ff".into()], logo_white: false, ..logo_params() };
|
||||
let res = trace(&disc, ¶ms);
|
||||
assert_eq!(top_color(&res, Point::new(32.0, 32.0)), Some([255, 0, 0]));
|
||||
assert!(!covered(&res, Point::new(5.0, 5.0)));
|
||||
// Each white takes the named color nearest it, so two named whites both stay.
|
||||
const OFF: [u8; 3] = [245, 245, 245];
|
||||
let two = render(96, 64, 0.7, |x, y| {
|
||||
if (x - 24.0).hypot(y - 32.0) <= 16.0 {
|
||||
Some(OFF)
|
||||
} else if (x - 72.0).hypot(y - 32.0) <= 16.0 {
|
||||
Some(WHITE)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let params = TraceParams { logo_colors: vec!["#f5f5f5".into(), "#ffffff".into()], logo_white: false, ..logo_params() };
|
||||
let res = trace(&two, ¶ms);
|
||||
assert_eq!(top_color(&res, Point::new(24.0, 32.0)), Some(OFF));
|
||||
assert_eq!(top_color(&res, Point::new(72.0, 32.0)), Some(WHITE));
|
||||
// A white square on blue keeps its edges where they are when it turns red.
|
||||
let square = render(100, 100, 0.7, |x, y| Some(if (30.0..70.0).contains(&x) && (30.0..70.0).contains(&y) { WHITE } else { [0, 0, 255] }));
|
||||
let res = trace(&square, &TraceParams { logo_colors: vec!["#ff0000".into(), "#0000ff".into()], logo_white: false, ..logo_params() });
|
||||
for (inside, outside) in [(30.3, 29.7), (69.7, 70.3)] {
|
||||
assert_eq!(top_color(&res, Point::new(inside, 50.0)), Some([255, 0, 0]), "at x {inside}");
|
||||
assert_eq!(top_color(&res, Point::new(outside, 50.0)), Some([0, 0, 255]), "at x {outside}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_large_image_is_traced_at_reduced_size_and_scaled_back() {
|
||||
let (cx, cy, r) = (1200.0, 600.0, 400.0);
|
||||
|
||||
@@ -110,9 +110,16 @@ pub struct TraceParams {
|
||||
/// Stroke: the widest feature (px) traced as a stroke.
|
||||
pub stroke_width: f64,
|
||||
/// Flat Logo mode: the flat colours (`#rrggbb`) to use; empty finds them automatically. White is
|
||||
/// always traced on its own and cannot be named, colours closer than about 12 (RGB distance)
|
||||
/// count as one, and at most ten are used. (Not `palette`: that is Color mode's colour source.)
|
||||
/// traced on its own and cannot be named unless [`logo_white`](Self::logo_white) is off, colours
|
||||
/// closer than about 12 (RGB distance) count as one, and at most ten are used. (Not `palette`:
|
||||
/// that is Color mode's colour source.)
|
||||
pub logo_colors: Vec<String>,
|
||||
/// Flat Logo mode with named `logo_colors`: `true` traces white (every channel 245 or more) as a
|
||||
/// color of its own beside the named ones. `false` traces the named colors only: white areas
|
||||
/// take the nearest of them, a named white or near-white is used as named, and enclosed
|
||||
/// transparent areas stay holes. With the colors found automatically, white is always its own,
|
||||
/// and with [`ignore_white`](Self::ignore_white) white areas are left out whatever this says.
|
||||
pub logo_white: bool,
|
||||
}
|
||||
|
||||
impl Default for TraceParams {
|
||||
@@ -141,6 +148,7 @@ impl Default for TraceParams {
|
||||
strokes: false,
|
||||
stroke_width: 10.0,
|
||||
logo_colors: Vec::new(),
|
||||
logo_white: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1140,6 +1140,7 @@ font-technology Variable Променливи
|
||||
Find Font… Търсене на шрифт…
|
||||
Find Next Търсене на следващ
|
||||
Find Objects Търсене на обекти
|
||||
Compare with Image Сравняване с изображение
|
||||
First Page Първа страница
|
||||
First-line Left Indent Ляв отстъп на първи ред
|
||||
Fisheye… Рибено око…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1580,6 +1580,7 @@ digits Malayalam Malaiàlam
|
||||
Find Linked Files in Folder… Cerca fitxers enllaçats en una carpeta…
|
||||
Find Next Cerca el següent
|
||||
Find Objects Cerca objectes
|
||||
Compare with Image Compara amb una imatge
|
||||
Fine Dots Punts fins
|
||||
Fine noise hides banding in the gradient fill El soroll fi amaga l’efecte de bandes del farciment de degradat
|
||||
First Primer
|
||||
@@ -5006,6 +5007,11 @@ digits Malayalam Malaiàlam
|
||||
@msg an image has no pixels and was left out una imatge no té píxels i s’ha omès
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size una imatge de {w} × {h} píxels es mostra a mida reduïda
|
||||
@msg an image of {w} × {h} pixels is too large to draw una imatge de {w} × {h} píxels és massa gran per dibuixar-la
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: no és una imatge PNG, JPEG, WebP, TIFF ni BMP
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ la imatge fa {iw} × {ih} píxels i el tauler a aquesta amplada fa {rw} × {rh} píxels: les proporcions són diferents
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` ha de ser un nombre de 0 a 1
|
||||
@msg background {v}: a color such as "#808080" fons {v}: un color com "#808080"
|
||||
@msg comparing with an image file isn't available in the browser la comparació amb un fitxer d’imatge no està disponible al navegador
|
||||
@msg an image VectorCraft can't read was left out s’ha omès una imatge que VectorCraft no pot llegir
|
||||
@msg graphs come in as plain art, without their data or settings els gràfics s’importen com a il·lustració normal, sense les seves dades ni la seva configuració
|
||||
@msg an image whose data is cut short was left out s’ha omès una imatge les dades de la qual estan tallades
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3316,6 +3316,7 @@ theme Appearance Motiv
|
||||
Document JSON JSON dokumentu
|
||||
Duplicate Duplikovat
|
||||
Find Objects Najít objekty
|
||||
Compare with Image Porovnat s obrazem
|
||||
Inspect Document Prozkoumat dokument
|
||||
Inspect Object Prozkoumat objekt
|
||||
Begin Undo Group Zahájit skupinu kroků zpět
|
||||
@@ -5512,6 +5513,11 @@ mark-style Japanese Japonské
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} neznámý parametr `{key}`; příkaz přijímá: {keys}
|
||||
Load EPS into Clipboard Načíst EPS do schránky
|
||||
@msg an image of {w} × {h} pixels is too large to draw obraz o velikosti {w} × {h} obrazových bodů je příliš velký na vykreslení
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: není obraz PNG, JPEG, WebP, TIFF ani BMP
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ obraz má {iw} × {ih} obrazových bodů a pracovní plocha v této šířce má {rw} × {rh} obrazových bodů: jejich proporce se liší
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` musí být číslo od 0 do 1
|
||||
@msg background {v}: a color such as "#808080" pozadí {v}: barva jako "#808080"
|
||||
@msg comparing with an image file isn't available in the browser porovnání se souborem obrazu není v prohlížeči k dispozici
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size obraz o velikosti {w} × {h} obrazových bodů se zobrazuje ve zmenšené velikosti
|
||||
@msg the image can't be read as a texture obraz nelze načíst jako texturu
|
||||
Fill and Stroke Controls Ovládací prvky výplně a tahu
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3058,6 +3058,7 @@ theme Appearance Udseende
|
||||
Document JSON Dokument-JSON
|
||||
Duplicate Duplikér
|
||||
Find Objects Find objekter
|
||||
Compare with Image Sammenlign med billede
|
||||
Inspect Document Inspicér dokument
|
||||
Inspect Object Inspicér objekt
|
||||
Begin Undo Group Start fortrydelsesgruppe
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1155,6 +1155,7 @@ font-technology Variable Variabel
|
||||
Find Font… Schrift suchen…
|
||||
Find Next Weitersuchen
|
||||
Find Objects Objekte suchen
|
||||
Compare with Image Mit Bild vergleichen
|
||||
First Page Erste Seite
|
||||
First-line Left Indent Einzug links in erster Zeile
|
||||
Fisheye… Fischauge…
|
||||
@@ -5078,6 +5079,11 @@ lighting Light Licht
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} unbekannter Parameter `{key}`; der Befehl akzeptiert: {keys}
|
||||
Load EPS into Clipboard EPS in die Zwischenablage laden
|
||||
@msg an image of {w} × {h} pixels is too large to draw ein Bild mit {w} × {h} Pixeln ist zu groß, um gezeichnet zu werden
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: kein PNG-, JPEG-, WebP-, TIFF- oder BMP-Bild
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ das Bild hat {iw} × {ih} Pixel und die Zeichenfläche in dieser Breite {rw} × {rh} Pixel: ihre Proportionen unterscheiden sich
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` muss eine Zahl von 0 bis 1 sein
|
||||
@msg background {v}: a color such as "#808080" background {v}: eine Farbe wie "#808080"
|
||||
@msg comparing with an image file isn't available in the browser der Vergleich mit einer Bilddatei ist im Browser nicht verfügbar
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size ein Bild mit {w} × {h} Pixeln wird verkleinert angezeigt
|
||||
@msg the image can't be read as a texture das Bild kann nicht als Struktur gelesen werden
|
||||
Basic Basis
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1113,6 +1113,7 @@ font-technology Variable Μεταβλητές
|
||||
Find Font… Εύρεση γραμματοσειράς…
|
||||
Find Next Εύρεση επόμενου
|
||||
Find Objects Εύρεση αντικειμένων
|
||||
Compare with Image Σύγκριση με εικόνα
|
||||
First Page Πρώτη σελίδα
|
||||
First-line Left Indent Αριστερή εσοχή πρώτης γραμμής
|
||||
Fisheye… Εφέ ματιού ψαριού…
|
||||
@@ -5256,6 +5257,11 @@ effect Points Σημεία
|
||||
Load EPS into Clipboard Φόρτωση EPS στο πρόχειρο
|
||||
@msg unknown control `{name}`: fillStroke, coloring, drawMode or screenMode Άγνωστο στοιχείο ελέγχου `{name}`: fillStroke, coloring, drawMode ή screenMode
|
||||
@msg an image of {w} × {h} pixels is too large to draw Η εικόνα {w} × {h} εικονοστοιχείων είναι υπερβολικά μεγάλη για σχεδίαση
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: δεν είναι εικόνα PNG, JPEG, WebP, TIFF ή BMP
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ η εικόνα είναι {iw} × {ih} εικονοστοιχεία και ο πίνακας σχεδίασης σε αυτό το πλάτος είναι {rw} × {rh} εικονοστοιχεία: οι αναλογίες τους διαφέρουν
|
||||
@msg `threshold` must be a number from 0 to 1 Το `threshold` πρέπει να είναι αριθμός από 0 έως 1
|
||||
@msg background {v}: a color such as "#808080" φόντο {v}: χρώμα όπως "#808080"
|
||||
@msg comparing with an image file isn't available in the browser η σύγκριση με αρχείο εικόνας δεν είναι διαθέσιμη στο πρόγραμμα περιήγησης
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size Η εικόνα {w} × {h} εικονοστοιχείων εμφανίζεται σε μειωμένο μέγεθος
|
||||
Open Recent File 1 Άνοιγμα πρόσφατου αρχείου 1
|
||||
Open Recent File 2 Άνοιγμα πρόσφατου αρχείου 2
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 168 and column 5.
|
@@ -1184,6 +1184,7 @@ font-technology Variable Variables
|
||||
Find Font… Buscar fuente…
|
||||
Find Next Buscar siguiente
|
||||
Find Objects Buscar objetos
|
||||
Compare with Image Comparar con imagen
|
||||
First Page Primera página
|
||||
First-line Left Indent Sangría izquierda en primera línea
|
||||
Fisheye… Ojo de pez…
|
||||
@@ -5452,6 +5453,11 @@ mark-style Japanese Japonesas
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} parámetro desconocido `{key}`; el comando acepta: {keys}
|
||||
Load EPS into Clipboard Cargar EPS en el portapapeles
|
||||
@msg an image of {w} × {h} pixels is too large to draw una imagen de {w} × {h} píxeles es demasiado grande para dibujarse
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: no es una imagen PNG, JPEG, WebP, TIFF ni BMP
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ la imagen mide {iw} × {ih} píxeles y la mesa de trabajo a ese ancho mide {rw} × {rh} píxeles: sus proporciones difieren
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` debe ser un número de 0 a 1
|
||||
@msg background {v}: a color such as "#808080" fondo {v}: un color como "#808080"
|
||||
@msg comparing with an image file isn't available in the browser la comparación con un archivo de imagen no está disponible en el navegador
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size una imagen de {w} × {h} píxeles se muestra a tamaño reducido
|
||||
@msg the image can't be read as a texture la imagen no se puede leer como textura
|
||||
Fill and Stroke Controls Controles de relleno y trazo
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3254,6 +3254,7 @@ theme Appearance Ulkoasu
|
||||
Document JSON Asiakirjan JSON
|
||||
Duplicate Monista
|
||||
Find Objects Etsi objekteja
|
||||
Compare with Image Vertaa kuvaan
|
||||
Inspect Document Tarkastele asiakirjaa
|
||||
Inspect Object Tarkastele objektia
|
||||
Begin Undo Group Aloita kumoamisryhmä
|
||||
@@ -5396,6 +5397,11 @@ mark-style Japanese Japanilainen
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} tuntematon parametri `{key}`; komennon parametrit: {keys}
|
||||
Load EPS into Clipboard Lataa EPS leikepöydälle
|
||||
@msg an image of {w} × {h} pixels is too large to draw kuva, jonka koko on {w} × {h} pikseliä, on liian suuri piirrettäväksi
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: ei ole PNG-, JPEG-, WebP-, TIFF- tai BMP-kuva
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ kuva on {iw} × {ih} pikseliä ja piirtoalusta tällä leveydellä {rw} × {rh} pikseliä: niiden mittasuhteet eroavat
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` on oltava luku väliltä 0–1
|
||||
@msg background {v}: a color such as "#808080" tausta {v}: väri, kuten "#808080"
|
||||
@msg comparing with an image file isn't available in the browser vertailu kuvatiedostoon ei ole käytettävissä selaimessa
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size kuva, jonka koko on {w} × {h} pikseliä, näytetään pienennettynä
|
||||
Fill and Stroke Controls Täytön ja viivan säätimet
|
||||
Coloring Controls Värityksen säätimet
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1178,6 +1178,7 @@ font-technology Variable Variables
|
||||
Find Font… Rechercher une police…
|
||||
Find Next Rechercher le suivant
|
||||
Find Objects Rechercher des objets
|
||||
Compare with Image Comparer à une image
|
||||
First Page Première page
|
||||
First-line Left Indent Retrait gauche de première ligne
|
||||
Fisheye… Fish-eye…
|
||||
@@ -5508,6 +5509,11 @@ mark-style Japanese Japonais
|
||||
theme Appearance Apparence
|
||||
Load EPS into Clipboard Charger un EPS dans le presse-papiers
|
||||
@msg an image of {w} × {h} pixels is too large to draw une image de {w} × {h} pixels est trop grande pour être dessinée
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path} : ce n’est pas une image PNG, JPEG, WebP, TIFF ou BMP
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ l’image fait {iw} × {ih} pixels et le plan de travail à cette largeur fait {rw} × {rh} pixels : leurs proportions diffèrent
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` doit être un nombre de 0 à 1
|
||||
@msg background {v}: a color such as "#808080" arrière-plan {v} : une couleur comme "#808080"
|
||||
@msg comparing with an image file isn't available in the browser la comparaison avec un fichier image n’est pas disponible dans le navigateur
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size une image de {w} × {h} pixels est affichée en taille réduite
|
||||
@msg the image can't be read as a texture l'image ne peut pas être lue comme texture
|
||||
Fill and Stroke Controls Commandes de fond et de contour
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1484,6 +1484,7 @@ digits Malayalam Malajálam
|
||||
Find in Folder… Keresés mappában…
|
||||
Find Next Következő keresése
|
||||
Find Objects Objektumok keresése
|
||||
Compare with Image Összehasonlítás képpel
|
||||
Fine Dots Finom pontok
|
||||
|
||||
Simple Egyszerű
|
||||
@@ -5495,6 +5496,11 @@ imageTrace Outline Körvonal
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} ismeretlen paraméter: `{key}`; a parancs ezeket fogadja el: {keys}
|
||||
Load EPS into Clipboard EPS betöltése a vágólapra
|
||||
@msg an image of {w} × {h} pixels is too large to draw egy {w} × {h} pixeles kép túl nagy a kirajzoláshoz
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: nem PNG, JPEG, WebP, TIFF vagy BMP kép
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ a kép {iw} × {ih} pixeles, a rajztábla ilyen szélességben {rw} × {rh} pixeles: az arányaik eltérnek
|
||||
@msg `threshold` must be a number from 0 to 1 a `threshold` értéke 0 és 1 közötti szám lehet
|
||||
@msg background {v}: a color such as "#808080" background {v}: egy szín, például "#808080"
|
||||
@msg comparing with an image file isn't available in the browser a képfájllal való összehasonlítás a böngészőben nem érhető el
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size egy {w} × {h} pixeles kép csökkentett méretben jelenik meg
|
||||
@msg the image can't be read as a texture a kép nem olvasható be textúraként
|
||||
Fill and Stroke Controls Kitöltés és körvonal vezérlői
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3058,6 +3058,7 @@ theme Appearance Tampilan
|
||||
Document JSON JSON dokumen
|
||||
Duplicate Duplikatkan
|
||||
Find Objects Cari objek
|
||||
Compare with Image Bandingkan dengan gambar
|
||||
Inspect Document Periksa dokumen
|
||||
Inspect Object Periksa objek
|
||||
Begin Undo Group Mulai grup urungkan
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2260 and column 5.
|
@@ -1185,6 +1185,7 @@ font-technology Variable Variabili
|
||||
Find Font… Trova font…
|
||||
Find Next Trova successivo
|
||||
Find Objects Trova oggetti
|
||||
Compare with Image Confronta con immagine
|
||||
First Page Prima pagina
|
||||
First-line Left Indent Rientro sinistro prima riga
|
||||
Fisheye… Occhio di pesce…
|
||||
@@ -5120,6 +5121,11 @@ lighting Light Luce
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} parametro sconosciuto `{key}`; il comando accetta: {keys}
|
||||
Load EPS into Clipboard Carica EPS negli appunti
|
||||
@msg an image of {w} × {h} pixels is too large to draw un'immagine di {w} × {h} pixel è troppo grande per essere disegnata
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: non è un'immagine PNG, JPEG, WebP, TIFF o BMP
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ l'immagine è di {iw} × {ih} pixel e la tavola da disegno a quella larghezza è di {rw} × {rh} pixel: le proporzioni sono diverse
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` deve essere un numero da 0 a 1
|
||||
@msg background {v}: a color such as "#808080" sfondo {v}: un colore come "#808080"
|
||||
@msg comparing with an image file isn't available in the browser il confronto con un file immagine non è disponibile nel browser
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size un'immagine di {w} × {h} pixel viene mostrata a dimensioni ridotte
|
||||
@msg the image can't be read as a texture l'immagine non può essere letta come texture
|
||||
Basic Base
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -154,6 +154,7 @@ digits Malayalam マラヤーラム数字
|
||||
Find and Replace… 検索と置換…
|
||||
Find Next 次を検索
|
||||
Find Objects オブジェクトを検索
|
||||
Compare with Image 画像と比較
|
||||
Spelling スペルチェック
|
||||
Auto Spell Check 自動スペルチェック
|
||||
Check Spelling… スペルチェック…
|
||||
@@ -5162,6 +5163,11 @@ lighting Light ライト
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} `{key}` は不明なパラメーターです。このコマンドで使えるパラメーター: {keys}
|
||||
Load EPS into Clipboard EPS をクリップボードに読み込む
|
||||
@msg an image of {w} × {h} pixels is too large to draw {w} × {h} ピクセルの画像は大きすぎて描画できません
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: PNG、JPEG、WebP、TIFF、BMP のいずれの画像でもありません
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ 画像は {iw} × {ih} ピクセルで、その幅でのアートボードは {rw} × {rh} ピクセルです: 縦横比が異なります
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` には 0 から 1 までの数値を指定してください
|
||||
@msg background {v}: a color such as "#808080" 背景に {v} は指定できません: 「#808080」のようなカラーを指定してください
|
||||
@msg comparing with an image file isn't available in the browser 画像ファイルとの比較はブラウザーでは使用できません
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size {w} × {h} ピクセルの画像は縮小して表示されます
|
||||
@msg the image can't be read as a texture この画像はテクスチャとして読み込めません
|
||||
Basic 基本
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2876,6 +2876,7 @@ theme Appearance იერსახე
|
||||
Document JSON დოკუმენტის JSON
|
||||
Duplicate დუბლირება
|
||||
Find Objects ობიექტების ძებნა
|
||||
Compare with Image გამოსახულებასთან შედარება
|
||||
Inspect Document დოკუმენტის შემოწმება
|
||||
Inspect Object ობიექტის შემოწმება
|
||||
Begin Undo Group დაბრუნების ჯგუფის დაწყება
|
||||
@@ -5049,6 +5050,11 @@ imageTrace Outline კონტური
|
||||
@msg {path}: not a file {path}: ფაილი არ არის
|
||||
Load EPS into Clipboard EPS-ის ბუფერში ჩატვირთვა
|
||||
@msg an image of {w} × {h} pixels is too large to draw {w} × {h} პიქსელის გამოსახულება დასახატად ზედმეტად დიდია
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: არ არის PNG, JPEG, WebP, TIFF ან BMP გამოსახულება
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ გამოსახულება არის {iw} × {ih} პიქსელი, ხოლო სამუშაო დაფა ამ სიგანეზე არის {rw} × {rh} პიქსელი: მათი პროპორციები განსხვავდება
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` უნდა იყოს რიცხვი 0-დან 1-მდე
|
||||
@msg background {v}: a color such as "#808080" ფონი {v}: ფერი, მაგალითად "#808080"
|
||||
@msg comparing with an image file isn't available in the browser გამოსახულების ფაილთან შედარება ბრაუზერში მიუწვდომელია
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size {w} × {h} პიქსელის გამოსახულება შემცირებული ზომით ჩანს
|
||||
A later row takes this shortcut ამ მალსახმობს ქვემოთა სტრიქონი იყენებს
|
||||
Add to the Current Shortcuts მიმდინარე მალსახმობებზე დამატება
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -147,6 +147,7 @@ digits Malayalam 말라얄람어
|
||||
Find and Replace… 찾기 및 바꾸기…
|
||||
Find Next 다음 찾기
|
||||
Find Objects 개체 찾기
|
||||
Compare with Image 이미지와 비교
|
||||
Spelling 맞춤법
|
||||
Auto Spell Check 자동 맞춤법 검사
|
||||
Check Spelling… 맞춤법 검사…
|
||||
@@ -5081,6 +5082,11 @@ lighting Light 광원
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} 알 수 없는 매개변수 `{key}`: 이 명령이 받는 매개변수: {keys}
|
||||
Load EPS into Clipboard EPS를 클립보드로 불러오기
|
||||
@msg an image of {w} × {h} pixels is too large to draw {w} × {h}픽셀 이미지는 너무 커서 그릴 수 없습니다
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: PNG, JPEG, WebP, TIFF 또는 BMP 이미지가 아닙니다
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ 이미지는 {iw} × {ih}픽셀이고 그 너비에서 아트보드는 {rw} × {rh}픽셀입니다: 비율이 다릅니다
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold`은(는) 0에서 1 사이의 숫자여야 합니다
|
||||
@msg background {v}: a color such as "#808080" 배경 {v}: "#808080" 같은 색상이어야 합니다
|
||||
@msg comparing with an image file isn't available in the browser 이미지 파일과 비교하는 기능은 브라우저에서 사용할 수 없습니다
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size {w} × {h}픽셀 이미지는 축소된 크기로 표시됩니다
|
||||
@msg the image can't be read as a texture 이미지를 텍스처로 읽을 수 없습니다
|
||||
Basic 기본
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3072,6 +3072,7 @@ theme Appearance Vormgeving
|
||||
Document JSON Document-JSON
|
||||
Duplicate Dupliceren
|
||||
Find Objects Objecten zoeken
|
||||
Compare with Image Vergelijken met afbeelding
|
||||
Inspect Document Document inspecteren
|
||||
Inspect Object Object inspecteren
|
||||
Begin Undo Group Groep voor ongedaan maken beginnen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3195,6 +3195,7 @@ theme Appearance Wygląd
|
||||
Document JSON JSON dokumentu
|
||||
Duplicate Powiel
|
||||
Find Objects Znajdź obiekty
|
||||
Compare with Image Porównaj z obrazem
|
||||
Inspect Document Zbadaj dokument
|
||||
Inspect Object Zbadaj obiekt
|
||||
Begin Undo Group Rozpocznij grupę cofania
|
||||
@@ -5350,6 +5351,11 @@ mark-style Japanese Japońskie
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} nieznany parametr `{key}`; polecenie przyjmuje: {keys}
|
||||
Load EPS into Clipboard Wczytaj EPS do schowka
|
||||
@msg an image of {w} × {h} pixels is too large to draw obraz o wymiarach {w} × {h} pikseli jest zbyt duży, aby go narysować
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: to nie jest obraz PNG, JPEG, WebP, TIFF ani BMP
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ obraz ma {iw} × {ih} pikseli, a obszar kompozycji przy tej szerokości ma {rw} × {rh} pikseli: ich proporcje się różnią
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` musi być liczbą od 0 do 1
|
||||
@msg background {v}: a color such as "#808080" tło {v}: kolor, np. "#808080"
|
||||
@msg comparing with an image file isn't available in the browser porównywanie z plikiem obrazu nie jest dostępne w przeglądarce
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size obraz o wymiarach {w} × {h} pikseli jest wyświetlany w zmniejszonym rozmiarze
|
||||
Basic Podstawowy
|
||||
Fill and Stroke Controls Elementy sterujące wypełnieniem i obrysem
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3343,6 +3343,7 @@ font-technology Variable Variáveis
|
||||
Favorites Favoritos
|
||||
Fill (X), double-click for the Color Picker Preenchimento (X), clique duas vezes para o seletor de cores
|
||||
Find Objects Localizar objetos
|
||||
Compare with Image Comparar com imagem
|
||||
Flare Tool Options Opções da ferramenta Reflexo
|
||||
Halo Halo
|
||||
Rays Raios
|
||||
@@ -5447,6 +5448,11 @@ mark-style Japanese Japonesas
|
||||
@msg The shape is too complex for a 3D effect A forma é complexa demais para um efeito 3D
|
||||
@msg The 3D surface is too complex; simplify the shape A superfície 3D é complexa demais; simplifique a forma
|
||||
@msg an image of {w} × {h} pixels is too large to draw uma imagem de {w} × {h} pixels é grande demais para ser desenhada
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: não é uma imagem PNG, JPEG, WebP, TIFF ou BMP
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ a imagem tem {iw} × {ih} pixels e a prancheta nessa largura tem {rw} × {rh} pixels: as proporções são diferentes
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` deve ser um número de 0 a 1
|
||||
@msg background {v}: a color such as "#808080" plano de fundo {v}: uma cor como "#808080"
|
||||
@msg comparing with an image file isn't available in the browser a comparação com um arquivo de imagem não está disponível no navegador
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size uma imagem de {w} × {h} pixels é exibida em tamanho reduzido
|
||||
Fill and Stroke Controls Controles de Preenchimento e Traçado
|
||||
Coloring Controls Controles de Coloração
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1182,6 +1182,7 @@ font-technology Variable Вариативные
|
||||
Find Font… Найти шрифт…
|
||||
Find Next Найти далее
|
||||
Find Objects Найти объекты
|
||||
Compare with Image Сравнить с изображением
|
||||
First Page Первая страница
|
||||
First-line Left Indent Отступ слева в первой строке
|
||||
Fisheye… Рыбий глаз…
|
||||
@@ -5115,6 +5116,11 @@ lighting Light Свет
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} неизвестный параметр `{key}`; команда принимает: {keys}
|
||||
Load EPS into Clipboard Загрузить EPS в буфер обмена
|
||||
@msg an image of {w} × {h} pixels is too large to draw изображение размером {w} × {h} пикселей слишком велико для отрисовки
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: это не изображение PNG, JPEG, WebP, TIFF или BMP
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ размер изображения {iw} × {ih} пикселей, а монтажной области при этой ширине {rw} × {rh} пикселей: их пропорции различаются
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` должно быть числом от 0 до 1
|
||||
@msg background {v}: a color such as "#808080" фон {v}: цвет, например "#808080"
|
||||
@msg comparing with an image file isn't available in the browser сравнение с файлом изображения недоступно в браузере
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size изображение размером {w} × {h} пикселей показано в уменьшенном размере
|
||||
@msg the image can't be read as a texture изображение не удаётся прочитать как текстуру
|
||||
Basic Базовый
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1148,6 +1148,7 @@ font-technology Variable Variabla
|
||||
Find Font… Sök typsnitt…
|
||||
Find Next Sök nästa
|
||||
Find Objects Sök objekt
|
||||
Compare with Image Jämför med bild
|
||||
First Page Första sidan
|
||||
First-line Left Indent Vänsterindrag för första raden
|
||||
Fisheye… Fisköga…
|
||||
@@ -5412,6 +5413,11 @@ mark-style Japanese Japanska
|
||||
theme Appearance Utseende
|
||||
Load EPS into Clipboard Läs in EPS till urklipp
|
||||
@msg an image of {w} × {h} pixels is too large to draw en bild på {w} × {h} pixlar är för stor för att ritas
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: inte en PNG-, JPEG-, WebP-, TIFF- eller BMP-bild
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ bilden är {iw} × {ih} pixlar och ritytan i den bredden är {rw} × {rh} pixlar: proportionerna skiljer sig åt
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` måste vara ett tal från 0 till 1
|
||||
@msg background {v}: a color such as "#808080" background {v}: en färg som "#808080"
|
||||
@msg comparing with an image file isn't available in the browser jämförelse med en bildfil är inte tillgänglig i webbläsaren
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size en bild på {w} × {h} pixlar visas i förminskad storlek
|
||||
Fill and Stroke Controls Kontroller för fyllning och kontur
|
||||
Coloring Controls Kontroller för färgläggning
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -1137,6 +1137,7 @@ font-technology Variable Değişken
|
||||
Find Font… Yazı Tipi Bul…
|
||||
Find Next Sonrakini Bul
|
||||
Find Objects Nesneleri Bul
|
||||
Compare with Image Görüntüyle Karşılaştır
|
||||
First Page İlk Sayfa
|
||||
First-line Left Indent Birinci Satırda Sol Girinti
|
||||
Fisheye… Balıkgözü…
|
||||
@@ -4017,6 +4018,11 @@ axis Both Her İkisi
|
||||
@msg invalid image data for `{0}` `{0}` için geçersiz resim verileri
|
||||
@msg invalid parameters for `{cmd}`: {msg} `{cmd}` için geçersiz parametreler: {msg}
|
||||
@msg is not an image VectorCraft reads VectorCraft'ın okuduğu bir resim değil
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: PNG, JPEG, WebP, TIFF veya BMP resmi değil
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ resim {iw} × {ih} piksel, çalışma yüzeyi bu genişlikte {rw} × {rh} piksel: oranları farklı
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` 0 ile 1 arasında bir sayı olmalıdır
|
||||
@msg background {v}: a color such as "#808080" background {v}: "#808080" gibi bir renk
|
||||
@msg comparing with an image file isn't available in the browser bir resim dosyasıyla karşılaştırma tarayıcıda kullanılamaz
|
||||
@msg it uses `{at}`, which VectorCraft's PostScript reader doesn't know VectorCraft'ın PostScript okuyucusunun bilmediği `{at}`yi kullanıyor
|
||||
@msg it uses `{at}`, which VectorCraft's PostScript reader doesn't know, in {procs} {procs}'de VectorCraft'ın PostScript okuyucusunun bilmediği `{at}`yi kullanıyor
|
||||
@msg its Windows metafile preview was opened instead, which may simplify the art bunun yerine Windows meta dosyası önizlemesi açıldı, bu da işi basitleştirebilir
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -1179,6 +1179,7 @@ font-technology Variable Варіативні
|
||||
Find Font… Знайти шрифт…
|
||||
Find Next Знайти далі
|
||||
Find Objects Знайти об’єкти
|
||||
Compare with Image Порівняти із зображенням
|
||||
First Page Перша сторінка
|
||||
First-line Left Indent Відступ ліворуч у першому рядку
|
||||
Fisheye… Риб’яче око…
|
||||
@@ -5100,6 +5101,11 @@ lighting Light Світло
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} невідомий параметр `{key}`; команда приймає: {keys}
|
||||
Load EPS into Clipboard Завантажити EPS у буфер обміну
|
||||
@msg an image of {w} × {h} pixels is too large to draw зображення розміром {w} × {h} пікселів завелике для відображення
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: це не зображення PNG, JPEG, WebP, TIFF чи BMP
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ розмір зображення {iw} × {ih} пікселів, а монтажної області за цієї ширини {rw} × {rh} пікселів: їхні пропорції відрізняються
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` має бути числом від 0 до 1
|
||||
@msg background {v}: a color such as "#808080" фон {v}: колір, наприклад "#808080"
|
||||
@msg comparing with an image file isn't available in the browser порівняння з файлом зображення недоступне в браузері
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size зображення розміром {w} × {h} пікселів показано в зменшеному розмірі
|
||||
@msg the image can't be read as a texture зображення не вдається прочитати як текстуру
|
||||
Basic Базовий
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 182 and column 5.
|
@@ -1389,6 +1389,7 @@ digits Malayalam Malayalam
|
||||
Find Linked Files in Folder… Tìm tệp liên kết trong thư mục…
|
||||
Find Next Tìm tiếp
|
||||
Find Objects Tìm đối tượng
|
||||
Compare with Image So sánh với ảnh
|
||||
Fine Dots Chấm mịn
|
||||
|
||||
Simple Đơn giản
|
||||
@@ -5104,6 +5105,11 @@ theme Appearance Giao diện
|
||||
@msg {tool} isn't in a toolbar group of several tools {tool} không nằm trong nhóm thanh công cụ có nhiều công cụ
|
||||
Load EPS into Clipboard Tải EPS vào bảng tạm
|
||||
@msg an image of {w} × {h} pixels is too large to draw ảnh {w} × {h} điểm ảnh quá lớn nên không thể vẽ
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: không phải ảnh PNG, JPEG, WebP, TIFF hay BMP
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ ảnh có kích thước {iw} × {ih} điểm ảnh, còn bảng vẽ ở chiều rộng đó có kích thước {rw} × {rh} điểm ảnh: tỉ lệ của chúng khác nhau
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` phải là một số từ 0 đến 1
|
||||
@msg background {v}: a color such as "#808080" background {v}: màu như "#808080"
|
||||
@msg comparing with an image file isn't available in the browser không thể so sánh với tệp ảnh trong trình duyệt
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size ảnh {w} × {h} điểm ảnh được hiển thị ở kích thước thu nhỏ
|
||||
Basic Cơ bản
|
||||
Fill and Stroke Controls Điều khiển tô và nét
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 204 and column 5.
|
@@ -1125,6 +1125,7 @@
|
||||
Find Font… 查找字体…
|
||||
Find Next 查找下一个
|
||||
Find Objects 查找对象
|
||||
Compare with Image 与图像比较
|
||||
First Page 第一页
|
||||
First-line Left Indent 首行左缩进
|
||||
Fisheye… 鱼眼…
|
||||
@@ -5386,6 +5387,11 @@ lighting Light 光源
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} 未知参数 `{key}`;该命令接受:{keys}
|
||||
Load EPS into Clipboard 将 EPS 载入剪贴板
|
||||
@msg an image of {w} × {h} pixels is too large to draw {w} × {h} 像素的图像过大,无法绘制
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}:不是 PNG、JPEG、WebP、TIFF 或 BMP 图像
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ 图像为 {iw} × {ih} 像素,而画板在该宽度下为 {rw} × {rh} 像素:两者的比例不同
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` 必须是 0 到 1 之间的数字
|
||||
@msg background {v}: a color such as "#808080" background {v}:颜色,例如 "#808080"
|
||||
@msg comparing with an image file isn't available in the browser 浏览器中无法与图像文件进行比较
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size {w} × {h} 像素的图像以缩小的尺寸显示
|
||||
Fill and Stroke Controls 填色和描边控件
|
||||
Coloring Controls 颜色控件
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1150,6 +1150,7 @@ font-technology Variable 可變
|
||||
Find Font… 尋找字體…
|
||||
Find Next 尋找下一個
|
||||
Find Objects 尋找物件
|
||||
Compare with Image 與影像比較
|
||||
First Page 第一頁
|
||||
First-line Left Indent 首行左縮排
|
||||
Fisheye… 魚眼…
|
||||
@@ -5200,6 +5201,11 @@ lighting Light 光源
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} 未知的參數 `{key}`;此指令接受:{keys}
|
||||
Load EPS into Clipboard 將 EPS 載入剪貼簿
|
||||
@msg an image of {w} × {h} pixels is too large to draw {w} × {h} 像素的影像過大,無法繪製
|
||||
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}:不是 PNG、JPEG、WebP、TIFF 或 BMP 影像
|
||||
@msg the image is {iw} × {ih} pixels and the artboard at that width is {rw} × {rh} pixels: their proportions differ 影像為 {iw} × {ih} 像素,而工作區域在該寬度下為 {rw} × {rh} 像素:兩者的比例不同
|
||||
@msg `threshold` must be a number from 0 to 1 `threshold` 必須是 0 到 1 之間的數字
|
||||
@msg background {v}: a color such as "#808080" background {v}:顏色,例如 "#808080"
|
||||
@msg comparing with an image file isn't available in the browser 瀏覽器中無法與影像檔案進行比較
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size {w} × {h} 像素的影像以縮小的尺寸顯示
|
||||
Basic 基本
|
||||
Fill and Stroke Controls 填色和筆畫控制項
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -32,6 +32,15 @@ white beside a cream), at most six; scattered specks of a colour add none. Adobe
|
||||
its own palettes are chosen; this is VectorCraft's own method (see
|
||||
[image-trace-quality.md](image-trace-quality.md#auto-color-keeps-rare-colours)).
|
||||
|
||||
**Flat Logo colors.** Flat Logo traces white as a color of its own beside the colors named in
|
||||
`logoColors`, and takes a named color with every channel 245 or more as that white. Each pixel is read
|
||||
as a blend of the traced colors and goes to the color with the largest share, so a near-white area can
|
||||
go to a named light color instead of white. With `"logoWhite": false` in the params of `imageTrace.make` or
|
||||
`imageTrace.makeAndExpand`, it traces the named colors only: white areas take the nearest named color,
|
||||
a named white or near-white is used as named, and enclosed transparent areas stay holes instead of
|
||||
being painted white. When Flat Logo finds the colors itself, `logoWhite` has no effect, and with
|
||||
`ignoreWhite` white areas are left out whatever `logoWhite` says.
|
||||
|
||||
**Document Library color groups.** With Palette › Document Library and Library › Document Swatches,
|
||||
the panel's **Colors** menu traces with every document swatch (**All**) or with the colors of one
|
||||
color group from the Swatches panel, as the public
|
||||
|
||||
+16
@@ -269,6 +269,22 @@ chain (layer first) and `limit` caps `matches` (default 100; `0` only counts); w
|
||||
`selected`, the number of objects selected. `document.json` stays whole: it is the fidelity path, not the way to
|
||||
look around.
|
||||
|
||||
`run_command document.compare {path, artboard?, background?, threshold?, diffPath?}` measures how far an
|
||||
artboard is from an image file (PNG, JPEG, WebP, TIFF or BMP). It renders the
|
||||
artboard (default 0) at the image's size, as `export` draws a PNG: the scale is the image's width in pixels over
|
||||
the artboard's width in points, and an image whose height differs from the artboard's height at that scale by
|
||||
more than one pixel is an error that names both sizes. The render and the image are both flattened on
|
||||
`background` (`"#rrggbb"`, default `"#ffffff"`). The reply is `{width, height, rmse, over, total, threshold}`:
|
||||
`rmse` is the root mean square of the R, G and B differences on a 0 to 1 scale, rounded to 6 decimals, and
|
||||
`over` counts the pixels with a channel that differs by more than `threshold` (0 to 1, default 0.08) of the
|
||||
total `total`. `diffPath` writes a PNG of each pixel's absolute R, G and B differences (black where the two
|
||||
match) and adds `diffPath` to the reply. As in a PNG export, linked images and placed documents are drawn
|
||||
from their files rather than from the previews saved for them. The reply's `warnings` are a PNG export's, in
|
||||
the same order: type on the artboard that doesn't fit its frame, an embedded image that couldn't be decoded (it
|
||||
is missing from the render), type in a font or style that isn't available, and a linked file that can't be read
|
||||
(its preview is drawn). `path` is read through the automation read root, and `diffPath` is written through the
|
||||
write root. The command does not change the document, and it is not available in the web app.
|
||||
|
||||
### Completions
|
||||
|
||||
`completion/complete` answers for a prompt argument (`ref/prompt`) or a template variable
|
||||
|
||||
@@ -136,6 +136,7 @@ native wording review, layout checks and a web Devanagari font remain. [Notes](l
|
||||
| M11 | Artboards & views (selected-set deletion shared by Artboards menu, trash and Properties, retaining artboard 1 when all are selected; Artboard deletion keys keep the first of an all-selected set and protect artwork; history prunes invalid indices while preserving valid multiselection (history tests and full workspace CI pass); click-to-fit objects with the Artboard tool (#1227), artboard escape routing and document setup modal handoff done; artboard panel/tool with Move Artwork for X/Y fields and Artboard Options, Trim View, middle-button pan, print tiling and the status bar's artboard navigator with synchronized Artboard tool selection done; multiple windows, presentation polish) (cached canvas reprojection preserves scale during resize, rotation and DPI changes; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) (cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; regression-tested; estimates unchanged) | 🟡; Artboard Options shares existing presets and orientation controls and can fit artwork without moving it; artwork fitting includes all drawn document art across artboards and off-board; artwork bounds are cached by active document and revision, and selection uses the shared bounds cache across artboards | 15–25 |
|
||||
| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG `<pattern>`/PDF export) and live Repeat (radial/grid/mirror) done; interactive selected mirror axis implemented (focused and native checks passed; full CI blocked by an unrelated text test); symbol and brush libraries done; symbol editing shows draggable 9-slice guides (#5280), with guides outside the artwork still unsupported; brushes/symbols in progress | 23–37 |
|
||||
| M11 | Artboards & views (selected-set deletion shared by Artboards menu, trash and Properties, retaining artboard 1 when all are selected; Artboard deletion keys keep the first of an all-selected set and protect artwork; history prunes invalid indices while preserving valid multiselection (history tests and full workspace CI pass); click-to-fit objects with the Artboard tool (#1227), artboard escape routing and document setup modal handoff done; artboard panel/tool with Move Artwork for X/Y fields and Artboard Options, Trim View, middle-button pan, print tiling and the status bar's artboard navigator with synchronized Artboard tool selection done; multiple windows, presentation polish) (cached canvas reprojection preserves scale during resize, rotation and DPI changes; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) (cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; regression-tested; estimates unchanged); status-bar pointer coordinates follow the active ruler origin and General units | 🟡; Artboard Options shares existing presets and orientation controls and can fit artwork without moving it; artwork fitting includes all drawn document art across artboards and off-board; artwork bounds are cached by active document and revision, and selection uses the shared bounds cache across artboards | 15–25 |
|
||||
| M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (junction-returning pieces retain their positioned boundaries; a simpler contour walk with identical output; an edge's sub-pixel shift is the same from either side; notches read only their own piece; collapsed loops keep few segments; large images trace with a warning; Auto Color keeps rare distinct colours; Flat Logo with named colors can leave out white with `logoWhite: false`; quick presets trace at once and the Image Trace button opens the adjustment controls; Properties reopens them; tiny fitted contours retain islands and holes; Create Strokes: exact crop EDT, Guo–Hall thinning and maximal-disc graph simplification; 15 presets, the 14 documented ones each with an enhanced version; Ignore Color, Shapes, Auto Grouping and Gradients options; measured in `image-trace-quality.md`; closed outlines retain their interior after straightening; the Flat Logo mode places edges to a fraction of a pixel, restores sharp corners, snaps lines and circles, and refuses gradients; live trace result bounds and selection highlights exclude the retained bitmap except in source-image views), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, group-selected series colours persist through regenerate/save/open) done; Graph Design and Marker designs, Transpose and Switch x/y, `\|` label line breaks, the pie options, drop shadows, the legend across the top, overlapping widths, First Row/Column in Front, filled lines, kept label formatting and the Graph Data worksheet done; graph Column designs pending | 6–10 |
|
||||
| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG `<pattern>`/PDF export) and live Repeat (radial/grid/mirror) done; interactive selected mirror axis implemented (focused and native checks passed; full CI blocked by an unrelated text test); symbol libraries done (Window › Symbol Libraries, a generated Basic Shapes library, User Defined, Other Library…, Save Symbols); brush libraries done (Window › Brush Libraries, four generated libraries, User Defined, Other Library…, Save Brushes); brushes/symbols in progress | 23–37 |
|
||||
| M11 | Artboards & views (selected-set deletion shared by Artboards menu, trash and Properties, retaining artboard 1 when all are selected; Artboard deletion keys keep the first of an all-selected set and protect artwork; history prunes invalid indices while preserving valid multiselection (history tests and full workspace CI pass); click-to-fit objects with the Artboard tool (#1227), artboard escape routing and document setup modal handoff done; artboard panel/tool with Move Artwork for X/Y fields and Artboard Options, Trim View, middle-button pan, print tiling and the status bar's artboard navigator with synchronized Artboard tool selection done; Window › Arrange › Tile and Consolidate All Windows done; multiple windows, presentation polish) (cached canvas reprojection preserves scale during resize, rotation and DPI changes; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) (cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; regression-tested; estimates unchanged) | 🟡; Artboard Options shares existing presets and orientation controls and can fit artwork without moving it; artwork fitting includes all drawn document art across artboards and off-board; artwork bounds are cached by active document and revision, and selection uses the shared bounds cache across artboards | 15–25 |
|
||||
| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG `<pattern>`/PDF export) and live Repeat (radial/grid/mirror) done; interactive selected mirror axis implemented (focused and native checks passed; full CI blocked by an unrelated text test); symbol and brush libraries done; brushes/symbols in progress | 23–37 |
|
||||
|
||||
@@ -1090,6 +1090,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Gradient mesh from an image:** Object › Create Gradient Mesh (`object.mesh.create`) on an embedded image makes a mesh of 1 to 50 rows and columns over the picture's rectangle, placed with the image's transform; each point takes the alpha-weighted average colour and the opacity of the area around it, and Appearance and Highlight apply. A linked image is refused with a message to embed it (#1676). Scores and hours unchanged.
|
||||
- **Several mesh points at once (#4207, #4208, #4170):** the Mesh tool and Direct Selection pick several points of a gradient mesh or mesh envelope: Shift-click adds a point or takes a picked one away, Direct Selection's marquee picks the points inside it (Shift toggles them) and selects their meshes, and its click on a mesh line picks the line's two ends and one inside a patch the four points around it. The picked points show filled; dragging one moves them all as one undo step and the arrow keys nudge them (`object.mesh.movePoints {id, points, dx, dy}` or `{items, dx, dy}`), and a click on one of several picked points picks it alone. A solid fill colour (`paint.setFill`: Color panel, Swatches, Eyedropper) recolours every picked point, and the Transparency panel's Opacity fades them instead of the object, the panel showing their opacity (blank when it differs); `object.mesh.setPointColor` takes `points` or `items`. Public reference: [Edit mesh objects](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-meshes/edit-mesh-objects.html) (Shift selects multiple mesh points; a fill colour colours the selected point), [Macworld, gradient mesh](https://www.macworld.com/article/202693/gmesh2.html) (Direct Selection: a mesh line selects the points at both ends, a patch the points around it, Shift-click adds or removes points), [Peachpit](https://www.peachpit.com/articles/article.aspx?p=1638270) (each mesh point can have its own colour or transparency). Tests: `shift_clicks_pick_mesh_points_that_drag_and_nudge_together` and `direct_selection_picks_mesh_points_by_marquee_line_and_patch` in `crates/tools/src/meshedit.rs`, `picked_mesh_points_take_the_fill_opacity_and_drag_together` in `crates/engine/src/tests/editcolors.rs`, `mesh_points_move_and_colour_together` in `crates/engine/src/tests/live.rs`. Scores and hours unchanged.
|
||||
- **Image Trace Flat Logo mode:** a fourth mode beside Black and White, Grayscale and Color, for flat-colour logos. Each pixel is read as a blend of the logo's colours (found automatically or named), the blend is interpolated smoothly and every outline is placed where two colours are equally present, so edges are accurate to a fraction of a pixel. Outlines are then cleaned: corners the blur rounded are put back, near-straight runs and near-circular arcs become exact lines and arcs, the rest is fitted with as few Béziers as the tolerance allows, touching colours overlap by a hair so no gap opens, and an outline that strays from the raw contour is fitted plainly instead. Gradients and photos are refused with advice to use Color. Line art whose only solid area is white paper (strokes a pixel or two wide) takes its colours from the strokes' cores, with the lighter shades along a stroke's anti-aliased edge folded into its ink; a page whose ink (a channel at least 48 below white) covers under 1% of it traces to nothing, as blank paper does. *Measured* (a local experiment, not run in CI): a flat-colour logo's vector original was rendered down to a soft 199 × 100 px transparent PNG, traced, and the result compared with the original at 1600 px wide. The built-in colour presets (6 Colors, 16 Colors, High Fidelity Photo, Low Fidelity Photo) had a mean edge error of 2.5 to 3.4 px and region overlap of 0.88 to 0.91; Flat Logo had 0.78 px and 0.966 (0.48 px and 0.975 from a crisp source), in about 0.1 s. Limits: flat-colour art only; needle-thin sharp tips are shortened by about 1.7 source pixels because the blur erases them; it works at no more than 1024 px on the long side (larger images are averaged down first); only one logo family has been scored against a vector original.
|
||||
- **Image Trace, Flat Logo named colors only:** with `logoColors` named, `"logoWhite": false` traces those colors only, so white areas take the nearest of them and no white is added; a named white is kept, and enclosed transparent areas stay holes; code-drawn tests in `crates/trace` and `crates/engine`.
|
||||
- **Advanced art:** live Blends (steps/distance/smooth colour, editable spine, anchor-targeted Blend tool, faithful interpolation, knockout), Envelope Distort (warp/mesh/top object, Reset, full Envelope Options, mesh handles, Edit Contents; type, images, symbols, gradients, patterns and appearance distorted everywhere), Liquify tools with options and pen pressure, Puppet Warp with rotating pins, Perspective Grid with presets, movable planes and projected type (choosing a perspective tool shows the grid; Hide Grid hides it with the tool still chosen), Gradient Mesh, Shape Builder (stroke-only open paths and lines cut the regions they cross and enclose regions of their own, and regions they bound merge without seams where the lines cross at rounded coordinates (#893); filled open paths count as closed; erase mode while Alt/Option is held: a minus pointer, red highlights of what will go, a click or drag deletes the regions touched, the pieces of open lines and the edges (outline pieces between the points where paths meet, opening their paths there) it passes over, one undo step, `shapeBuilder.merge {erase}` and `shapeBuilder.regions` edges over MCP), Live Paint (every path an edge, open lines included), Image Trace (12 presets, colour layers traced in parallel; a trace past two million anchor points is refused with advice rather than running out of memory, an image over 64 megapixels is traced scaled down to that size (aspect kept, paths at the image's own size) and one over 192 megapixels is refused, and a selection too large to outline shows its bounds (#525); Object › Image Trace › Make and Make and Expand trace at once with the Default preset, #543; with an image selected the Control bar and the Properties panel show Image Trace, whose arrow traces with any preset, Mask (`object.maskImage`: a clipping path on the image's outline, selected so its handles crop), Crop Image (a crop box on the image, starting on its part over the artboard, dragged by its handles or inside it, the outside dimmed, with X/Y/W/H, Apply and Cancel in the Control bar, Enter and Escape, #734; `object.cropImage {rect}` for agents, `{trim: true}` cuts an image to its pixels that aren't fully transparent, pixel for pixel) and Embed or Unembed in place of Fill and Stroke; with an Image Trace object selected, its Preset (choosing another traces again), View, the Image Trace panel and Expand, plus Release in Properties; the five View modes (Tracing Result, with Outlines, Outlines, Outlines with Source Image, Source Image) redraw a trace on screen without tracing again, are saved with it and `imageTrace.setView` sets them, while exports and Expand keep the result; the panel's Advanced section is a header that stays open or closed as it was left (part of #641); Color mode's Palette: Limited, Full Tone (as many colours as the tones need, Less … More), Automatic (flat art with its own colours, else Full Tone; a heuristic) and Document Library (the most used colours of the document's swatches or a swatch library, filled with the swatch colours as they are), part of #641; Advanced › Create: Fills and/or Strokes, where lines up to the Stroke width trace as stroked centre lines (distance transform, thinning, a skeleton graph with spurs pruned, curve fitting; one width per line; without Fills, wider areas become stroked outlines), part of #641), pattern swatches with pattern editing mode, live Repeat (radial/grid/mirror). A single selected mirror repeat shows a prominent axis with a center move handle and two rotation handles. Drag the line or center to change spacing, or an end to change angle; Shift snaps rotation to 45°. The original artwork stays in place, the reflected copy previews live, Escape cancels and each drag takes one undo step through `object.repeat.options`. Controls follow zoom/view rotation and hide with edges or the bounding box. Engine and headless canvas regressions passed for gestures, history and visibility; native move, rotation and Escape screenshots were reviewed. [Public mirror-repeat reference](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-patterns/set-mirror-repeat-options.html). A single selected grid repeat shows a handle below the grid for rows, one right of it for columns and one past its bottom-right corner for both, and sliders between its first two columns and rows for the horizontal and vertical spacing; the source keeps its size, each drag is one undo step and Escape cancels it ([public grid-repeat reference](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-patterns/set-grid-repeat-options.html), #2691). Object › Expand turns radial, grid and mirror repeats into groups of their instances (#1765, #1756).
|
||||
Choosing the Blend tool again while it is active (its toolbar button, W or `tool.select`) finishes the blend: the object clicked first is dropped and the next click no longer adds to the blend just made, so the click after it starts a new blend ([Blending objects](https://web.archive.org/web/20241217212108/https://helpx.adobe.com/illustrator/using/blending-objects.html)).
|
||||
Window › Blend exposes easing Type, Ramp and independent color Shift through `object.blend.options`.
|
||||
|
||||
Reference in New Issue
Block a user