diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index dbe3d4509..21bd4f9bc 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -41,6 +41,15 @@ pub fn specs() -> Vec { 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 { }) } +/// 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 { + 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 = 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::(); + 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 { + 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 { + 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 { let u = str_param(p, "units").and_then(Unit::named).ok_or_else(|| bad("document.setUnits", "unknown units"))?; diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index b9e247ed9..b708c25ca 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -31,6 +31,7 @@ mod colorize; pub(super) mod colormgmt; mod colorthemes; mod colourfonts; +mod compare; mod compound_shape; mod compoundpaint; mod constrain_new; diff --git a/crates/engine/src/tests/compare.rs b/crates/engine/src/tests/compare.rs new file mode 100644 index 000000000..3bd4446d6 --- /dev/null +++ b/crates/engine/src/tests/compare.rs @@ -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); +} diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index e9a219bd5..8bdbf65a9 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -1140,6 +1140,7 @@ font-technology Variable Променливи Find Font… Търсене на шрифт… Find Next Търсене на следващ Find Objects Търсене на обекти + Compare with Image Сравняване с изображение First Page Първа страница First-line Left Indent Ляв отстъп на първи ред Fisheye… Рибено око… diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index ea246b35c..68c1e81b0 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 7e51de244..e74e240d7 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 84134c893..d50e862bb 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 7d0e48fb8..2da424151 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index be001e2fb..4d713dfed 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 930ecf090..8c00c78d2 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 99594975d..3491ca30a 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 1aebc348d..539c0f3ae 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index a400c495f..247ffc64e 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 0134649ea..4d0377083 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index b19e01f96..44da4c994 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 77d50e0a8..07577badf 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -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 基本 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index c710ee3c5..6a5581ed4 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -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 მიმდინარე მალსახმობებზე დამატება diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index c0545a3da..cce6ce736 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -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 기본 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 72d4ea21a..bd525526b 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index ff2b9bd6b..adfb669e8 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 3340597d8..183d41dc1 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 3732558f7..544050c9a 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -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 Базовый diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 58ceab701..f007400d9 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 9385b30cc..3c28f018c 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 1b96c235e..7de17b0fc 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -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 Базовий diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index ce98bc978..678eddbce 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index a2cf40630..9a3346839 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -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 颜色控件 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 9440e225f..4b7107054 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -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 填色和筆畫控制項 diff --git a/docs/mcp.md b/docs/mcp.md index ae04c1647..6b4a6bbeb 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -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