Browse / github.com/storytold/vectorcraft / patch
Document: document.compare measures how far an artboard's render is from an image
Document: document.compare measures how far an artboard's render is from an image ## What and why No command measured how close a document is to an image, so an agent had to compare screenshots by eye or run an outside tool after each attempt. `document.compare {path, artboard?, background?, threshold?, diffPath?}` reads a PNG, JPEG, WebP, TIFF or BMP through the automation read root and renders the artboard at the image's width, as `document.export` draws a PNG. 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` (default white). The re
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- dotCooCoo (imported from pull request #3995, open)
- Base
8bf090b25389b48aa6f9d7415de57c9ce7ccf8cb- Size
- +576 −0 in 28 files
- Review
- pending
- Shared
- 2026-10-28
- Id
ae16e6bf0c85c31a8d3ef311
How it went
No other operator has reported using it yet.
Diff
raw · crates/engine/src/cmd/edit.rs crates/engine/src/lib.rs crates/engine/src/tests_compare.rs crates/ui-egui/src/i18n/bg.tsv crates/ui-egui/src/i18n/cs.tsv crates/ui-egui/src/i18n/da.tsv crates/ui-egui/src/i18n/de.tsv crates/ui-egui/src/i18n/el.tsv crates/ui-egui/src/i18n/es.tsv crates/ui-egui/src/i18n/fi.tsv crates/ui-egui/src/i18n/fr.tsv crates/ui-egui/src/i18n/hu.tsv crates/ui-egui/src/i18n/id.tsv crates/ui-egui/src/i18n/it.tsv crates/ui-egui/src/i18n/ja.tsv crates/ui-egui/src/i18n/ka.tsv crates/ui-egui/src/i18n/ko.tsv crates/ui-egui/src/i18n/nl.tsv crates/ui-egui/src/i18n/pl.tsv crates/ui-egui/src/i18n/pt-br.tsv crates/ui-egui/src/i18n/ru.tsv crates/ui-egui/src/i18n/sv.tsv crates/ui-egui/src/i18n/tr.tsv crates/ui-egui/src/i18n/uk.tsv crates/ui-egui/src/i18n/vi.tsv crates/ui-egui/src/i18n/zh-hans.tsv crates/ui-egui/src/i18n/zh-hant.tsv docs/mcp.md
@@ -28,6 +28,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 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",@@ -321,6 +330,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"))?;@@ -2538,6 +2538,8 @@ mod tests_colorthemes; #[cfg(test)] mod tests_colourfonts; #[cfg(test)]+mod tests_compare;+#[cfg(test)] mod tests_compoundpaint; #[cfg(test)] mod tests_constrain_new;new file mode 100644@@ -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("output as their previews"), "{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);+}@@ -1101,6 +1101,7 @@ font-technology Variable Променливи Find Font… Търсене на шрифт… Find Next Търсене на следващ Find Objects Търсене на обекти+ Compare with Image Сравняване с изображение First Page Първа страница First-line Left Indent Ляв отстъп на първи ред Fisheye… Рибено око…@@ -3134,6 +3134,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@@ -5223,6 +5224,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 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 Coloring Controls Ovládací prvky barev@@ -2897,6 +2897,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@@ -1110,6 +1110,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…@@ -4789,6 +4790,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 the image can't be read as a texture das Bild kann nicht als Struktur gelesen werden Basic Basis Fill and Stroke Controls Steuerelemente für Fläche und Kontur@@ -1067,6 +1067,7 @@ font-technology Variable Μεταβλητές Find Font… Εύρεση γραμματοσειράς… Find Next Εύρεση επόμενου Find Objects Εύρεση αντικειμένων+ Compare with Image Σύγκριση με εικόνα First Page Πρώτη σελίδα First-line Left Indent Αριστερή εσοχή πρώτης γραμμής Fisheye… Εφέ ματιού ψαριού…@@ -4987,6 +4988,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 η σύγκριση με αρχείο εικόνας δεν είναι διαθέσιμη στο πρόγραμμα περιήγησης Open Recent File 1 Άνοιγμα πρόσφατου αρχείου 1 Open Recent File 2 Άνοιγμα πρόσφατου αρχείου 2 Open Recent File 3 Άνοιγμα πρόσφατου αρχείου 3@@ -1133,6 +1133,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…@@ -5164,6 +5165,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 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 Coloring Controls Controles de color@@ -3074,6 +3074,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ä@@ -5112,6 +5113,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 Fill and Stroke Controls Täytön ja viivan säätimet Coloring Controls Värityksen säätimet Draw Mode Controls Piirtotilan säätimet@@ -1132,6 +1132,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…@@ -5223,6 +5224,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 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 Coloring Controls Commandes de couleur@@ -1426,6 +1426,7 @@ font-technology Variable Változó 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ű@@ -5209,6 +5210,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 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 Coloring Controls Színezés vezérlői@@ -2897,6 +2897,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@@ -1133,6 +1133,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…@@ -4831,6 +4832,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 the image can't be read as a texture l'immagine non può essere letta come texture Basic Base Fill and Stroke Controls Controlli di riempimento e traccia@@ -134,6 +134,7 @@ font-technology Variable バリアブル Find and Replace… 検索と置換… Find Next 次を検索 Find Objects オブジェクトを検索+ Compare with Image 画像と比較 Spelling スペルチェック Auto Spell Check 自動スペルチェック Check Spelling… スペルチェック…@@ -4870,6 +4871,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 the image can't be read as a texture この画像はテクスチャとして読み込めません Basic 基本 Fill and Stroke Controls 塗りと線のコントロール@@ -2709,6 +2709,7 @@ theme Appearance იერსახე Document JSON დოკუმენტის JSON Duplicate დუბლირება Find Objects ობიექტების ძებნა+ Compare with Image გამოსახულებასთან შედარება Inspect Document დოკუმენტის შემოწმება Inspect Object ობიექტის შემოწმება Begin Undo Group დაბრუნების ჯგუფის დაწყება@@ -4786,6 +4787,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 გამოსახულების ფაილთან შედარება ბრაუზერში მიუწვდომელია A later row takes this shortcut ამ მალსახმობს ქვემოთა სტრიქონი იყენებს Add to the Current Shortcuts მიმდინარე მალსახმობებზე დამატება Ambient Light გარემოს სინათლე@@ -130,6 +130,7 @@ font-technology Variable 가변 Find and Replace… 찾기 및 바꾸기… Find Next 다음 찾기 Find Objects 개체 찾기+ Compare with Image 이미지와 비교 Spelling 맞춤법 Auto Spell Check 자동 맞춤법 검사 Check Spelling… 맞춤법 검사…@@ -4794,6 +4795,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 the image can't be read as a texture 이미지를 텍스처로 읽을 수 없습니다 Basic 기본 Fill and Stroke Controls 칠 및 획 컨트롤@@ -2911,6 +2911,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@@ -3012,6 +3012,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@@ -5060,6 +5061,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 Basic Podstawowy Fill and Stroke Controls Elementy sterujące wypełnieniem i obrysem Coloring Controls Elementy sterujące kolorem@@ -3154,6 +3154,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@@ -5156,6 +5157,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 Fill and Stroke Controls Controles de Preenchimento e Traçado Coloring Controls Controles de Coloração Draw Mode Controls Controles de Modo de Desenho@@ -1131,6 +1131,7 @@ font-technology Variable Вариативные Find Font… Найти шрифт… Find Next Найти далее Find Objects Найти объекты+ Compare with Image Сравнить с изображением First Page Первая страница First-line Left Indent Отступ слева в первой строке Fisheye… Рыбий глаз…@@ -4827,6 +4828,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 the image can't be read as a texture изображение не удаётся прочитать как текстуру Basic Базовый Fill and Stroke Controls Элементы управления заливкой и обводкой@@ -1103,6 +1103,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…@@ -5128,6 +5129,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 Fill and Stroke Controls Kontroller för fyllning och kontur Coloring Controls Kontroller för färgläggning Draw Mode Controls Kontroller för ritläge@@ -1093,6 +1093,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ü…@@ -3824,6 +3825,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@@ -1127,6 +1127,7 @@ font-technology Variable Варіативні Find Font… Знайти шрифт… Find Next Знайти далі Find Objects Знайти об’єкти+ Compare with Image Порівняти із зображенням First Page Перша сторінка First-line Left Indent Відступ ліворуч у першому рядку Fisheye… Риб’яче око…@@ -4811,6 +4812,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 the image can't be read as a texture зображення не вдається прочитати як текстуру Basic Базовий Fill and Stroke Controls Елементи керування заливкою та обведенням@@ -1331,6 +1331,7 @@ font-technology Variable Biến thiên 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@@ -4814,6 +4815,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 Basic Cơ bản Fill and Stroke Controls Điều khiển tô và nét Coloring Controls Điều khiển màu@@ -1073,6 +1073,7 @@ Find Font… 查找字体… Find Next 查找下一个 Find Objects 查找对象+ Compare with Image 与图像比较 First Page 第一页 First-line Left Indent 首行左缩进 Fisheye… 鱼眼…@@ -5092,6 +5093,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 浏览器中无法与图像文件进行比较 Fill and Stroke Controls 填色和描边控件 Coloring Controls 颜色控件 Draw Mode Controls 绘图模式控件@@ -1098,6 +1098,7 @@ font-technology Variable 可變 Find Font… 尋找字體… Find Next 尋找下一個 Find Objects 尋找物件+ Compare with Image 與影像比較 First Page 第一頁 First-line Left Indent 首行左縮排 Fisheye… 魚眼…@@ -4899,6 +4900,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 瀏覽器中無法與影像檔案進行比較 Basic 基本 Fill and Stroke Controls 填色和筆畫控制項 Coloring Controls 顏色控制項@@ -266,6 +266,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