Merge pull request #6769 from dotCooCoo/fix/options-dialog-menus

EPS: Save As options are menus with readable names, and Export As's Range shows an example
This commit is contained in:
BFlat
2026-11-12 13:17:11 +07:00
committed by GitHub
42 changed files with 699 additions and 95 deletions
+32 -20
View File
@@ -48,7 +48,7 @@ pub(crate) const OPTIONS: &[FormatOption] = &[
name: "useArtboards",
ty: "boolean",
default: "null",
description: "true: one file per chosen artboard (default all), <file>_<artboard>.eps, and with two or more a central <file>.eps of the whole document (none when no art shows); absent: the same when artboard, artboards or range names artboards, else one file of the visible art; false: one file of the visible art, even when artboards are named",
description: "true: one file per chosen artboard (default all), <file>_<artboard>.eps, and with two or more a central <file>.eps of the whole document (when no art shows, Export writes none and Save writes it on the first artboard); absent: the same when artboard, artboards or range names artboards, else one file of the visible art; false: one file of the visible art, even when artboards are named (Save: the first artboard when no art shows)",
},
FormatOption { name: "level", ty: "integer", default: "3", description: "PostScript language level, 2 or 3" },
FormatOption {
@@ -203,10 +203,15 @@ fn expanded(cmd: &str, p: &Value, saved: &[super::super::FlattenerPreset]) -> Re
}
}
/// One file to write: its artboard, region and origin.
type Page = (Option<usize>, Rect, Point);
/// The files to write: `(artboard, region, origin)` each — every chosen artboard with its corner
/// as the origin, or the visible art's bounds with the first artboard's corner. With two or more
/// chosen artboards, a central file of the visible art comes first (none when no art shows).
fn pages(doc: &Document, set: &Settings) -> Result<Vec<(Option<usize>, Rect, Point)>> {
/// `saving`: Save always writes the file it names, so with no visible art that file's region is
/// the first artboard, and the second value is true.
fn pages(doc: &Document, set: &Settings, saving: bool) -> Result<(Vec<Page>, bool)> {
let n = doc.artboards.len();
let named = set.boards.resolve(n).map_err(|e| bad(C, e))?;
let boards = match (set.use_artboards, named) {
@@ -217,17 +222,25 @@ fn pages(doc: &Document, set: &Settings) -> Result<Vec<(Option<usize>, Rect, Poi
};
let corner = |r: Rect| Point::new(r.x0, r.y1);
let art = |art: Rect| (None, art, doc.artboards.first().map_or(corner(art), |a| corner(a.rect)));
// The visible art's bounds; for Save with no visible art, the first artboard (true).
let visible = || match vectorcraft_render::encode::art_bounds(doc) {
Some(bounds) => Some((bounds, false)),
None if saving => doc.artboards.first().map(|a| (a.rect, true)),
None => None,
};
match boards {
Some(boards) => {
let central = (boards.len() > 1).then(|| vectorcraft_render::encode::art_bounds(doc).map(art)).flatten();
let central = (boards.len() > 1).then(visible).flatten();
let each = boards
.into_iter()
.map(|b| doc.artboards.get(b).map(|a| (Some(b), a.rect, corner(a.rect))).ok_or_else(|| bad(C, format!("no artboard {}", b + 1))));
central.map(Ok).into_iter().chain(each).collect()
let pages = central.map(|(bounds, _)| Ok(art(bounds))).into_iter().chain(each).collect::<Result<_>>()?;
Ok((pages, central.is_some_and(|(_, blank)| blank)))
}
None => {
let bounds = vectorcraft_render::encode::art_bounds(doc).ok_or_else(|| bad(C, "nothing to export: the document has no visible art"))?;
Ok(vec![art(bounds)])
let missing = if saving { "the document has no artboard" } else { "nothing to export: the document has no visible art" };
let (bounds, blank) = visible().ok_or_else(|| bad(C, missing))?;
Ok((vec![art(bounds)], blank))
}
}
}
@@ -293,16 +306,17 @@ pub(super) fn encode(doc: &Document, p: &Value) -> Result<Encoded> {
encode_document(doc, p, false)
}
/// Save keeps a single page's original frame and all native editing data, unlike Export's
/// optional art-bound crop or separate file per artboard. Existing EPS loss warnings still apply.
/// Save writes the pages Export does (the visible art's bounds, or with Use Artboards the chosen
/// artboards), with a smaller preview. A single file, the one the document becomes, carries all
/// the native editing data; with no visible art its page is the first artboard.
pub(super) fn encode_save(doc: &Document, p: &Value) -> Result<Encoded> {
encode_document(doc, p, true)
}
fn encode_document(doc: &Document, p: &Value, saving: bool) -> Result<Encoded> {
let set = parse(p)?;
if saving && (set.use_artboards == Some(false) || bool_or(p, "selectedOnly", false)) {
return Err(bad(C, "EPS Save retains the artboard and all artwork: use document.export for cropped or selected-only EPS"));
if saving && bool_or(p, "selectedOnly", false) {
return Err(bad(C, "EPS Save writes all artwork: use document.export for selected-only EPS"));
}
let original = doc;
let (full, warnings) = crate::cmd::place::document::full_documents(doc);
@@ -315,18 +329,16 @@ fn encode_document(doc: &Document, p: &Value, saving: bool) -> Result<Encoded> {
} else {
Cow::Borrowed(original)
};
let pages = if saving {
let board = set.boards.one(doc.artboards.len()).map_err(|e| bad(C, e))?;
let rect = doc.artboards.get(board).ok_or_else(|| bad(C, "the document has no artboard"))?.rect;
vec![(Some(board), rect, Point::new(rect.x0, rect.y1))]
} else {
pages(doc, &set)?
};
let (pages, blank) = pages(doc, &set, saving)?;
if pages.is_empty() {
return Err(bad(C, "the document has no artboard"));
}
// The artboards' background colors, behind the art (the editing data keeps them as colors).
let filled = super::export::with_artboard_fills(doc, p, pages.iter().filter_map(|(b, ..)| *b)).map_err(|e| bad(C, e))?;
// The file Save writes alone becomes the document's own, so it carries every artboard.
let whole = saving && pages.len() == 1;
// The artboards' background colors, behind the art (the editing data keeps them as colors);
// a page on the first artboard because no art shows has that artboard's color.
let fills = pages.iter().filter_map(|(b, ..)| *b).chain(blank.then_some(0));
let filled = super::export::with_artboard_fills(doc, p, fills).map_err(|e| bad(C, e))?;
let doc = &*filled;
let regions: Vec<Rect> = pages.iter().map(|(_, region, _)| *region).collect();
let expanded = expand_transparent_tile_images(doc, &set.flatten)?;
@@ -360,7 +372,7 @@ fn encode_document(doc: &Document, p: &Value, saving: bool) -> Result<Encoded> {
origin,
title: doc.title.clone(),
created,
native: Some(native(&editing, if saving { None } else { board }, set.include_linked)),
native: Some(native(&editing, if whole { None } else { board }, set.include_linked)),
..set.eps.clone()
};
let out = vectorcraft_eps::export(drawn, &o, preview.as_ref(), thumbnail.as_ref()).map_err(|e| bad(C, e))?;
+25 -4
View File
@@ -870,8 +870,10 @@ impl SaveJob {
_ => encode_all(doc, f.id, &self.params),
}
.map_err(|e| named_for_save(cmd, e))?;
// A legacy .ai file per artboard.
let per_artboard = f.id == "ai" && super::legacy_ai::wanted(cmd, f, &self.params)?;
// A legacy .ai file per artboard, or the files the EPS encoder writes: one, or with Use
// Artboards and two or more artboards the central file at the path and a file per
// artboard beside it.
let per_artboard = f.id == "eps" || (f.id == "ai" && super::legacy_ai::wanted(cmd, f, &self.params)?);
if enc.files.len() != 1 && !per_artboard {
return Err(bad(cmd, "Save writes one artboard: name one, or export several with document.export"));
}
@@ -1092,6 +1094,25 @@ fn format_options(s: &mut Session, p: &Value) -> Result<Value> {
}
}
}
// An EPS save that named artboards without Use Artboards, or with it on, had it on (the
// encoder reads them so): it shows on, with those artboards as the range.
if f.id == "eps"
&& let Some(m) = &saved
&& m.get("useArtboards").and_then(Value::as_bool) != Some(false)
{
let range = m.get("range").and_then(Value::as_str).filter(|r| !r.trim().is_empty());
let boards = m
.get("artboards")
.and_then(|a| serde_json::from_value::<Vec<usize>>(a.clone()).ok())
.or_else(|| m.get("artboard").and_then(Value::as_u64).and_then(|i| usize::try_from(i).ok()).map(|i| vec![i]))
.filter(|b| !b.is_empty());
if range.is_some() || boards.is_some() {
v["options"]["useArtboards"]["value"] = json!(true);
if let (None, Some(boards)) = (range, boards) {
v["options"]["range"]["value"] = json!(range_text(&boards));
}
}
}
v["saveFormats"] =
SAVE_FORMATS.iter().filter_map(|id| format(id)).map(|f| json!({"id": f.id, "label": f.label, "extensions": f.extensions})).collect();
Ok(v)
@@ -1104,7 +1125,7 @@ pub(super) fn specs() -> Vec<CommandSpec> {
"Save Document",
[],
None,
"{path?, format?: vectorcraft|template|pdf|svg|svgz|ai|eps (default: the path's extension, else the document's own format), options?: {…the format's options, see file.formatOptions; default: as last saved}, svg?: {…SVG options} (an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), …the SVG and PDF options given flat instead (an option given flat or in svg and in options with different values is an error), compress?: bool (native, also in options: gzip; default: the useCompression preference; warnings say when the format doesn't read compress, version, preview or precision given flat, or a key given in options), version?: 4 (3, 2 or 1: for older VectorCraft versions; 1 and 2 are never compressed), preview?: false (embed a PNG of the first artboard, at most 256 px), precision?: 0..9 (native: round path anchors and handles to this many decimal places, a smaller file; colors, transforms and type stay exact; default: exact, and a later save without it writes exactly again), separateArtboards?: false (native and .ai: also save each artboard of `range` to <name>-<artboard>.<ext> beside the file, holding that artboard and the art touching it; the result's files lists every file written), range?: \"1-3, 5\"|\"all\" (default; with separateArtboards), includeLinked?: false (keep linked files' own pixels, not just their previews; they stay linked), embedProfiles?: true (carry the ICC profiles loaded from files that the document is tagged with; opening the file installs them where missing), pdfCompatible?: true (native: also carry a PDF of every artboard; false omits it) | true for .ai (false: blank pages, only the native document; a .ai file's compress, default true, is its compression.compressText), modified?: Unix seconds|null (the File Info modified date, and created date when there is none, a save to a file stamps; default now, recorded in the journal so a replay matches; null: leave the dates)} → {path, format, bytes, warnings, files?: [path…] (the file and its artboards' files), linked?: [path…] (images an SVG links to)}. Save writes one artboard, except a .ai file: a PDF-compatible file of every artboard carrying the native document (preserveEditing always on; PDF options flat or in options), which document.open restores exactly, on the active artboard (the app's, else the one the document opened on; with editingData: false, the first); separateArtboards writes more files, one per artboard. EPS saves the chosen artboard (first by default), retaining its page margins and the whole editable document; its PostScript appearance flattens transparency, as EPS requires. Without a path it writes the document's own file in its own format: a document opened from or saved as SVG/PDF/EPS saves as that again (warnings name what the format loses). No path known (never saved, converted from an older version, restored by Data Recovery, or another format) → {dataBase64, format, name, folder?, warnings} and the document stays modified. A key that no save format takes, beside the path or in options, and an unknown key inside a PDF option's object or svg, are refused",
"{path?, format?: vectorcraft|template|pdf|svg|svgz|ai|eps (default: the path's extension, else the document's own format), options?: {…the format's options, see file.formatOptions; default: as last saved}, svg?: {…SVG options} (an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), …the SVG and PDF options given flat instead (an option given flat or in svg and in options with different values is an error), compress?: bool (native, also in options: gzip; default: the useCompression preference; warnings say when the format doesn't read compress, version, preview or precision given flat, or a key given in options), version?: 4 (3, 2 or 1: for older VectorCraft versions; 1 and 2 are never compressed), preview?: false (embed a PNG of the first artboard, at most 256 px), precision?: 0..9 (native: round path anchors and handles to this many decimal places, a smaller file; colors, transforms and type stay exact; default: exact, and a later save without it writes exactly again), separateArtboards?: false (native and .ai: also save each artboard of `range` to <name>-<artboard>.<ext> beside the file, holding that artboard and the art touching it; the result's files lists every file written), range?: \"1-3, 5\"|\"all\" (default; with separateArtboards), includeLinked?: false (keep linked files' own pixels, not just their previews; they stay linked), embedProfiles?: true (carry the ICC profiles loaded from files that the document is tagged with; opening the file installs them where missing), pdfCompatible?: true (native: also carry a PDF of every artboard; false omits it) | true for .ai (false: blank pages, only the native document; a .ai file's compress, default true, is its compression.compressText), modified?: Unix seconds|null (the File Info modified date, and created date when there is none, a save to a file stamps; default now, recorded in the journal so a replay matches; null: leave the dates)} → {path, format, bytes, warnings, files?: [path…] (the file and its artboards' files), linked?: [path…] (images an SVG links to)}. Save writes one artboard, except a .ai file and EPS (below): a .ai file is a PDF-compatible file of every artboard carrying the native document (preserveEditing always on; PDF options flat or in options), which document.open restores exactly, on the active artboard (the app's, else the one the document opened on; with editingData: false, the first); separateArtboards writes more files, one per artboard. EPS takes the options of document.exportEps: by default one file whose bounding box fits the visible art (the first artboard when no art shows), carrying the whole editable document; useArtboards: true (also when artboard, artboards or range names artboards; file.formatOptions then reports it true) makes the chosen artboard the page, drawn with its background color (without Use Artboards, only the first artboard's page when no art shows has its color), and with two or more chosen the file at the path is the central file of the visible art (again the first artboard when none shows) and each artboard is also written to <name>_<artboard>.eps beside it, holding that artboard; selectedOnly is refused; its PostScript appearance flattens transparency, as EPS requires. Without a path it writes the document's own file in its own format: a document opened from or saved as SVG/PDF/EPS saves as that again (warnings name what the format loses). No path known (never saved, converted from an older version, restored by Data Recovery, or another format) → {dataBase64, format, name, folder?, warnings} and the document stays modified. A key that no save format takes, beside the path or in options, and an unknown key inside a PDF option's object or svg, are refused",
has_doc,
|s, p| save(s, SaveMode::Save, p)
),
@@ -1158,7 +1179,7 @@ pub(super) fn specs() -> Vec<CommandSpec> {
"Format Options",
[],
None,
"{format?: a writable format id or ait (default: the document's own)} → {id, label, extensions, mime, read, write, raster, options: {name: {type, default, description, value}}, saveFormats: [{id, label, extensions}]}; value = the document's option as last saved in that format, else (native, for a document not yet saved as one) as the last native save used it, else the default",
"{format?: a writable format id or ait (default: the document's own)} → {id, label, extensions, mime, read, write, raster, options: {name: {type, default, description, value}}, saveFormats: [{id, label, extensions}]}; value = the document's option as last saved in that format, else (native, for a document not yet saved as one) as the last native save used it, else the default (EPS: useArtboards is true when the last save named artboards without it, and range then names them)",
always,
format_options
),
@@ -109,23 +109,25 @@ fn the_ai_editing_copy_for_other_apps_draws_the_color_as_the_pages_do() {
}
}
/// Does PostScript `t` paint the only red the document has, the first artboard's color? RGB, or
/// CMYK PostScript's (its values depend on the color conversion the platform offers, so only its
/// hue is checked).
fn red(t: &str) -> bool {
t.lines().any(|l| {
let mut w: Vec<&str> = l.split_whitespace().collect();
let Some(op) = w.pop() else { return false };
let v: Vec<f64> = w.iter().filter_map(|n| n.parse().ok()).collect();
match (op, v.as_slice()) {
("rg" | "setrgbcolor", [r, g, b]) => *r > 0.9 && *g < 0.1 && *b < 0.1,
("k" | "setcmykcolor", [c, m, y, k]) => *c < 0.1 && *m > 0.8 && *y > 0.8 && *k < 0.1,
_ => false,
}
})
}
#[test]
fn eps_and_postscript_print_draw_the_color() {
let mut s = session();
// The only red the document has, the first artboard's color: RGB, or CMYK PostScript's (its
// values depend on the color conversion the platform offers, so only its hue is checked).
let red = |t: &str| {
t.lines().any(|l| {
let mut w: Vec<&str> = l.split_whitespace().collect();
let Some(op) = w.pop() else { return false };
let v: Vec<f64> = w.iter().filter_map(|n| n.parse().ok()).collect();
match (op, v.as_slice()) {
("rg" | "setrgbcolor", [r, g, b]) => *r > 0.9 && *g < 0.1 && *b < 0.1,
("k" | "setcmykcolor", [c, m, y, k]) => *c < 0.1 && *m > 0.8 && *y > 0.8 && *k < 0.1,
_ => false,
}
})
};
let eps = |s: &mut Session, more: Value| {
String::from_utf8_lossy(&file(s, with(json!({"format": "eps", "useArtboards": true, "artboard": 0}), &more))).into_owned()
};
@@ -138,6 +140,22 @@ fn eps_and_postscript_print_draw_the_color() {
assert!(!red(&ps(&mut s, json!({"includeArtboardFill": false}))), "print without it");
}
/// EPS Save draws the color on an artboard's page: with Use Artboards, or on the first artboard
/// when no art shows. The box of the visible art, as Export's, has none.
#[test]
fn eps_save_draws_the_color_on_an_artboard_page() {
let mut s = session();
let save = |s: &mut Session, more: Value| {
let options = with(json!({"previewFormat": "none", "thumbnails": false}), &more);
String::from_utf8_lossy(&b64(&s.execute("document.save", &json!({"format": "eps", "options": options})).unwrap())).into_owned()
};
assert!(!red(&save(&mut s, json!({}))), "the art's box");
assert!(red(&save(&mut s, json!({"useArtboards": true, "range": "1"}))), "the artboard's page");
s.execute("select.all", &json!({})).unwrap();
s.execute("edit.clear", &json!({})).unwrap();
assert!(red(&save(&mut s, json!({}))), "the first artboard when no art shows");
}
#[test]
fn export_for_screens_includes_the_fill_unless_unchecked() {
let mut s = session();
+97 -9
View File
@@ -425,8 +425,16 @@ fn transparency_grid_round_trips_without_dirtying() {
let _ = std::fs::remove_dir_all(d);
}
/// The `%%BoundingBox` of an EPS file's PostScript.
fn bounding_box(eps: &[u8]) -> [i64; 4] {
let ps = std::str::from_utf8(vectorcraft_eps::sections(eps).unwrap().0).unwrap();
let line = ps.lines().find_map(|l| l.strip_prefix("%%BoundingBox:")).expect("a bounding box");
let v: Vec<i64> = line.split_whitespace().map(|n| n.parse().unwrap()).collect();
[v[0], v[1], v[2], v[3]]
}
#[test]
fn opened_eps_saves_to_its_own_file_with_editable_data_and_page_margins() {
fn opened_eps_saves_to_its_own_file_around_its_art_or_on_its_page() {
let d = dir("eps-original-format");
let eps_path = path(&d, "Artwork.eps");
// Original, contributor-authored PostScript: the artwork occupies only part of the page.
@@ -440,11 +448,16 @@ fn opened_eps_saves_to_its_own_file_with_editable_data_and_page_margins() {
assert_eq!((board.width(), board.height()), (200.0, 100.0));
rect(&mut s);
let edited_layers = s.doc().unwrap().doc.layers.clone();
let r = s.execute("document.save", &json!({"options": {"previewFormat": "none", "thumbnails": false}})).unwrap();
// Without Use Artboards the bounding box fits the art, as Export draws it; the editing data
// keeps the page.
let quiet = json!({"previewFormat": "none", "thumbnails": false});
let r = s.execute("document.save", &json!({"options": quiet})).unwrap();
assert_eq!((r["path"].as_str(), r["format"].as_str()), (Some(eps_path.as_str()), Some("eps")));
let bytes = std::fs::read(&eps_path).unwrap();
assert!(bytes.starts_with(b"%!PS-Adobe-3.0 EPSF-3.0"));
assert!(std::str::from_utf8(vectorcraft_eps::sections(&bytes).unwrap().0).unwrap().contains("%%BoundingBox: 0 0 200 100"));
let exported = b64(&s.execute("document.export", &json!({"format": "eps", "previewFormat": "none", "thumbnails": false})).unwrap());
assert_eq!(bounding_box(&bytes), bounding_box(&exported));
assert_ne!(bounding_box(&bytes), [0, 0, 200, 100], "the art does not fill the page");
let editing = vectorcraft_format::load(&vectorcraft_eps::native(&bytes).unwrap()).unwrap();
assert_eq!(editing.artboards[0].rect, board);
assert_eq!(editing.layers, edited_layers);
@@ -453,26 +466,101 @@ fn opened_eps_saves_to_its_own_file_with_editable_data_and_page_margins() {
assert_eq!(opened["restored"], true);
assert_eq!(s.doc().unwrap().doc.layers, edited_layers);
assert_eq!(s.doc().unwrap().doc.artboards[0].rect, board);
// Use Artboards keeps the opened file's page.
rect(&mut s);
s.execute("document.save", &json!({"options": {"useArtboards": true, "previewFormat": "none", "thumbnails": false}})).unwrap();
assert_eq!(s.doc().unwrap().path.as_deref(), Some(eps_path.as_str()));
assert_eq!(bounding_box(&std::fs::read(&eps_path).unwrap()), [0, 0, 200, 100]);
// Save remembers it.
rect(&mut s);
s.execute("document.save", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().path.as_deref(), Some(eps_path.as_str()));
assert_eq!(bounding_box(&std::fs::read(&eps_path).unwrap()), [0, 0, 200, 100]);
std::fs::remove_dir_all(d).unwrap();
}
#[test]
fn eps_save_keeps_all_editing_artboards_while_printing_one_page() {
fn eps_save_without_use_artboards_fits_the_box_to_art_past_the_artboard() {
let mut s = session();
s.execute("shape.rectangle", &json!({"x": -50, "y": 20, "width": 300, "height": 40})).unwrap();
let quiet = json!({"previewFormat": "none", "thumbnails": false});
let r = s.execute("document.save", &json!({"format": "eps", "options": quiet})).unwrap();
let [x0, y0, x1, y1] = bounding_box(&b64(&r));
assert!(x0 <= -50 && x1 >= 250 && y0 >= 0 && y1 <= 100, "{:?}", [x0, y0, x1, y1]);
for (use_artboards, expected) in [(false, [x0, y0, x1, y1]), (true, [0, 0, 200, 100])] {
let options = json!({"useArtboards": use_artboards, "previewFormat": "none", "thumbnails": false});
let r = s.execute("document.save", &json!({"format": "eps", "options": options})).unwrap();
assert_eq!(bounding_box(&b64(&r)), expected, "useArtboards: {use_artboards}");
}
}
#[test]
fn eps_save_keeps_all_editing_artboards_in_the_file_of_one_page() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 100, "artboards": 2})).unwrap();
rect(&mut s);
let r = s.execute("document.save", &json!({"format": "eps", "artboard": 1, "previewFormat": "none", "thumbnails": false})).unwrap();
let bytes = b64(&r);
assert!(r.get("files").is_none(), "{r}");
let board = s.doc().unwrap().doc.artboards[1].rect;
let [x0, y0, x1, y1] = bounding_box(&bytes);
assert_eq!(((x1 - x0) as f64, (y1 - y0) as f64), (board.width(), board.height()));
let native = vectorcraft_format::load(&vectorcraft_eps::native(&bytes).unwrap()).unwrap();
assert_eq!(native.artboards.len(), 2);
assert_eq!(native.layers, s.doc().unwrap().doc.layers);
assert!(s.execute("document.save", &json!({"format": "eps", "range": "all"})).is_err(), "EPS is one printed page");
for options in [json!({"useArtboards": false}), json!({"selectedOnly": true})] {
assert!(s.execute("document.save", &json!({"format": "eps", "options": options})).is_err(), "cropping is Export's job");
assert!(s.execute("document.save", &json!({"format": "eps", "options": {"selectedOnly": true}})).is_err(), "selected-only is Export's job");
}
#[test]
fn eps_save_with_use_artboards_and_a_named_artboard_reports_it_as_the_range() {
let d = dir("eps-named-artboard");
let eps_path = path(&d, "Art.eps");
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 100, "artboards": 2})).unwrap();
rect(&mut s);
let options = json!({"useArtboards": true, "artboard": 1, "previewFormat": "none", "thumbnails": false});
s.execute("document.save", &json!({"path": eps_path, "options": options})).unwrap();
let o = s.execute("file.formatOptions", &json!({})).unwrap();
assert_eq!(o["options"]["useArtboards"]["value"], true);
assert_eq!(o["options"]["range"]["value"], "2", "the saved artboard, not All");
std::fs::remove_dir_all(d).unwrap();
}
#[test]
fn eps_save_with_use_artboards_writes_the_central_file_and_one_per_artboard() {
let d = dir("eps-artboards");
let eps_path = path(&d, "Art.eps");
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 100, "artboards": 2})).unwrap();
rect(&mut s);
let options = json!({"useArtboards": true, "previewFormat": "none", "thumbnails": false});
let r = s.execute("document.save", &json!({"path": eps_path, "options": options})).unwrap();
let files: Vec<String> = r["files"].as_array().expect("files").iter().map(|f| f.as_str().unwrap().to_string()).collect();
assert_eq!(files.len(), 3, "{r}");
assert_eq!(files[0], eps_path, "the file the document becomes comes first");
assert!(files[1..].iter().all(|f| f.starts_with(&path(&d, "Art_")) && f.ends_with(".eps")), "{files:?}");
// The central file: the visible art and the whole document.
let central = std::fs::read(&eps_path).unwrap();
let exported = b64(&s.execute("document.export", &json!({"format": "eps", "previewFormat": "none", "thumbnails": false})).unwrap());
assert_eq!(bounding_box(&central), bounding_box(&exported));
assert_eq!(vectorcraft_format::load(&vectorcraft_eps::native(&central).unwrap()).unwrap().artboards.len(), 2);
// Each artboard's file: its artboard as the page, holding that artboard.
let boards = s.doc().unwrap().doc.artboards.clone();
for (f, board) in files[1..].iter().zip(&boards) {
let bytes = std::fs::read(f).unwrap();
let [x0, y0, x1, y1] = bounding_box(&bytes);
assert_eq!(((x1 - x0) as f64, (y1 - y0) as f64), (board.rect.width(), board.rect.height()), "{f}");
let native = vectorcraft_format::load(&vectorcraft_eps::native(&bytes).unwrap()).unwrap();
assert_eq!(native.artboards.iter().map(|a| a.rect).collect::<Vec<_>>(), [board.rect], "{f}");
}
let st = s.doc().unwrap();
assert_eq!((st.path.as_deref(), st.is_dirty()), (Some(eps_path.as_str()), false));
// With no visible art the central file is still written, on the first artboard.
s.execute("select.all", &json!({})).unwrap();
s.execute("edit.clear", &json!({})).unwrap();
std::fs::remove_file(&eps_path).unwrap();
s.execute("document.save", &json!({})).unwrap();
assert_eq!(bounding_box(&std::fs::read(&eps_path).unwrap()), [0, 0, 200, 100]);
std::fs::remove_dir_all(d).unwrap();
}
#[test]
@@ -550,7 +638,7 @@ fn eps_save_bounds_large_page_preview_without_cropping_vector_or_editing_data()
s.execute("file.new", &json!({"width": 4000, "height": 4000})).unwrap();
rect(&mut s);
// Explicit colour preview remains a colour preview; Save bounds its resolution, not the art.
let r = s.execute("document.save", &json!({"format": "eps", "previewFormat": "tiffColor"})).unwrap();
let r = s.execute("document.save", &json!({"format": "eps", "useArtboards": true, "previewFormat": "tiffColor"})).unwrap();
let bytes = b64(&r);
let (ps, tiff) = vectorcraft_eps::sections(&bytes).unwrap();
let tiff = tiff.unwrap();
+2 -1
View File
@@ -103,7 +103,8 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
d.fields.insert("all".into(), json!(all));
}
let mut range = d.str("range");
if ui.add_enabled(!all, egui::TextEdit::singleline(&mut range).align(egui::Align2::LEFT_CENTER).desired_width(90.0)).changed() {
let field = egui::TextEdit::singleline(&mut range).align(egui::Align2::LEFT_CENTER).desired_width(90.0).hint_text("1-3, 5");
if ui.add_enabled(!all, field).changed() {
d.fields.insert("range".into(), json!(range));
}
});
+163 -26
View File
@@ -7,8 +7,13 @@
//! editable), `path`, `format`, and one field per option (agents set them with `ui.dialog.set`).
//! Native and `.ai` files show their save options (VectorCraft Options after Save As): each
//! artboard to a separate file, with `all` (true: every artboard) or `range`, Include Linked Files,
//! Embed ICC Profiles, Create PDF-Compatible File and Use Compression. A native file's Version is a
//! menu of the versions VectorCraft writes (`version`, named by the releases that read each). A
//! Embed ICC Profiles, Create PDF-Compatible File and Use Compression. EPS shows Use Artboards
//! (`useArtboards`, off by default: the bounding box fits the visible art) with the same `all` or
//! `range` under it, then the options Export's EPS Options shows: Preview, Overprints, Flattener
//! preset and PostScript are menus that write the option's value (`previewFormat: "tiffColor"`,
//! `overprints: "preserve"`, `flattenerPreset: "medium"` or a saved preset's name, `level: 3`).
//! A native file's Version is a menu of the versions VectorCraft writes
//! (`version`, named by the releases that read each). A
//! `.ai` file's Version comes first: PDF-compatible, or legacy version 7 (`aiVersion: "7"`, a file
//! per artboard), which takes only Transparent Images (`transparentImages`), Hidden Objects
//! (`hiddenObjects`: a hidden layer, omitted or made visible), Text (`outlineText`: editable or
@@ -18,7 +23,9 @@
use std::sync::OnceLock;
use serde_json::{Map, Value, json};
use vectorcraft_engine::cmd::FlattenOptions;
use vectorcraft_engine::cmd::fileio::{self, ArtboardPick, Format, FormatOption, SAVE_FORMATS, SaveMode, TEMPLATE_FORMATS};
use vectorcraft_eps::{Level, Overprint, Preview};
use super::{DialogResult, DialogSpec, form};
use crate::state::Dialog;
@@ -76,50 +83,130 @@ fn separates(f: &Format) -> bool {
f.options.iter().any(|o| o.name == "separateArtboards")
}
/// The checkbox that `f`'s All or Range shows under: Save each artboard to a separate file, or
/// for EPS Use Artboards (off: the visible art's bounds).
fn board_switch(f: &Format) -> Option<&'static str> {
if separates(f) {
Some("separateArtboards")
} else if f.id == "eps" {
Some("useArtboards")
} else {
None
}
}
/// The options the dialog shows: arrays and objects are for agents, as is a native file's
/// `precision`, and a page range replaces the single artboard choice; none for a format with its
/// own dialog. A `.ait` template has no Version: Save as Template writes it PDF-compatible only.
fn shown(f: &Format) -> impl Iterator<Item = &'static FormatOption> {
let range = f.options.iter().any(|o| o.name == "range");
let separate = separates(f);
let switched = board_switch(f).is_some();
let options = if own_dialog(f) { &[][..] } else { f.options };
options.iter().filter(move |o| {
!(matches!(o.ty, "array" | "object")
|| o.name == "precision"
|| (range && o.name == "artboard")
|| (separate && o.name == "range")
|| (switched && o.name == "range")
|| (matches!(f.id, "ai" | "ait") && o.name == "useArtboards")
// EPS Save keeps a page and the complete editing document; cropped and
// selected-only output belongs to Export, never this save dialog.
|| (f.id == "eps" && matches!(o.name, "useArtboards" | "selectedOnly"))
// EPS Save writes all the artwork: selected-only output belongs to Export.
|| (f.id == "eps" && o.name == "selectedOnly")
|| (f.id == "ait" && o.name == "aiVersion"))
})
}
/// An option's label: the save options as the reference app's options dialog names them, the
/// others from their names.
/// An option's label: its written label, or for an option without one, its name split into words.
fn label(o: &FormatOption) -> String {
match o.name {
written_label(o).unwrap_or_else(|| form::humanize(o.name))
}
/// The written label of option `o`, translated.
fn written_label(o: &FormatOption) -> Option<String> {
Some(match o.name {
"separateArtboards" => tl!("Save each artboard to a separate file").into(),
"includeLinked" => tl!("Include Linked Files").into(),
"useArtboards" => tl!("Use Artboards").into(),
"includeLinked" | "includeLinkedFiles" => tl!("Include Linked Files").into(),
"embedProfiles" => tl!("Embed ICC Profiles").into(),
"pdfCompatible" => tl!("Create PDF-Compatible File").into(),
"compress" => tl!("Use Compression").into(),
"editingData" => tl!("Include Editing Data for Other Apps").into(),
_ => form::humanize(o.name),
}
"version" | "aiVersion" => tl!("Version:").into(),
"transparentImages" => tl!("Transparent Images:").into(),
"hiddenObjects" => tl!("Hidden Objects:").into(),
"outlineText" => tl!("Text:").into(),
"preview" => tl!("Preview").into(),
"thumbnails" => tl!("Include Document Thumbnails").into(),
"previewFormat" => tl!("Preview:").into(),
"transparentPreview" => tl!("Transparent").into(),
"overprints" => tl!("Overprints:").into(),
"flattenerPreset" => tl!("Flattener preset:").into(),
"embedFonts" => tl!("Embed Fonts (for other applications)").into(),
"cmykPostScript" => tl!("Include CMYK PostScript in RGB Files").into(),
"compatibleGradients" => tl!("Compatible Gradient and Gradient Mesh Printing").into(),
"level" => tl!("PostScript:").into(),
_ => return None,
})
}
/// Every artboard (the All choice under Save each artboard to a separate file): `all` when set,
/// else no range given.
/// The values of an option that takes one of a fixed set, each with the name its menu shows, and
/// `at`, the entry the option's value picks as the save reads it (none when the save would reject
/// the value). The first `builtins` names are VectorCraft's own (translated where they are shown);
/// the others are names the user gave (saved flattener presets), shown as they are.
struct Choices {
values: Vec<Value>,
names: Vec<String>,
builtins: usize,
at: Option<usize>,
}
/// The choices of `f`'s option `o`, whose value is `value`, when it takes one of a fixed set of
/// values: EPS's preview format, overprints, PostScript level and flattener preset (the built-in
/// presets by id, then the saved ones by name).
fn choices(app: &VectorcraftApp, f: &Format, o: &FormatOption, value: &Value) -> Option<Choices> {
if f.id != "eps" {
return None;
}
let text = value.as_str().map_or_else(|| value.to_string(), str::to_string);
let (pairs, builtins, at): (Vec<(Value, String)>, Option<usize>, Option<usize>) = match o.name {
"previewFormat" => (
Preview::ALL.iter().map(|p| (json!(p.id()), p.label().to_string())).collect(),
None,
Preview::from_id(&text).and_then(|p| Preview::ALL.iter().position(|q| *q == p)),
),
"overprints" => (
Overprint::ALL.iter().map(|p| (json!(p.id()), p.label().to_string())).collect(),
None,
Overprint::from_id(&text).and_then(|p| Overprint::ALL.iter().position(|q| *q == p)),
),
// The level is a number in the save options.
"level" => (
Level::ALL.iter().filter_map(|l| Some((json!(l.id().parse::<u64>().ok()?), l.label().to_string()))).collect(),
None,
Level::from_id(&text).and_then(|l| Level::ALL.iter().position(|q| *q == l)),
),
"flattenerPreset" => {
let presets = app.session.flattener_presets();
let id = |name: &str| FlattenOptions::PRESETS.into_iter().find(|id| FlattenOptions::preset_label(id) == Some(name));
let pairs = presets.iter().map(|p| (id(&p.name).map_or_else(|| json!(p.name), |id| json!(id)), p.name.clone())).collect();
let at = app.session.flattener_preset(&text).and_then(|p| presets.iter().position(|q| q.name == p.name));
(pairs, Some(FlattenOptions::builtin_presets().len()), at)
}
_ => return None,
};
let builtins = builtins.unwrap_or(pairs.len());
let (values, names): (Vec<Value>, Vec<String>) = pairs.into_iter().unzip();
Some(Choices { values, names, builtins, at })
}
/// Every artboard (the All choice under Save each artboard to a separate file or Use Artboards):
/// `all` when set, else no range given.
fn all_artboards(d: &Dialog) -> bool {
d.fields.get("all").and_then(Value::as_bool).unwrap_or_else(|| d.str("range").trim().is_empty())
}
/// All or Range, under Save each artboard to a separate file (enabled while it is on).
fn artboard_range(ui: &mut egui::Ui, d: &mut Dialog) {
/// All or Range, under the checkbox `switch` (enabled while it is on).
fn artboard_range(ui: &mut egui::Ui, d: &mut Dialog, switch: &str) {
ui.label("");
ui.add_enabled_ui(d.bool("separateArtboards"), |ui| {
ui.add_enabled_ui(d.bool(switch), |ui| {
ui.horizontal(|ui| {
ui.add_space(22.0);
let mut all = all_artboards(d);
@@ -167,9 +254,10 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
// Version first; the legacy version takes only its own options.
let legacy = legacy_ai(d);
let mut rows: Vec<&FormatOption> = shown(f).filter(|o| LEGACY_ONLY.contains(&o.name) == legacy || o.name == "aiVersion").collect();
rows.sort_by_key(|o| o.name != "aiVersion");
// EPS's PostScript level comes last, as in Export's EPS Options.
rows.sort_by_key(|o| (o.name != "aiVersion", f.id == "eps" && o.name == "level"));
for o in rows {
option_row(app, ui, d, o);
option_row(app, ui, d, f, o);
}
});
if legacy_ai(d) && app.session.active().is_some_and(|st| st.doc.artboards.len() > 1) {
@@ -330,8 +418,9 @@ fn artboard_picker(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog)
}
}
/// One option: a checkbox, an artboard menu, a version menu, a number or a text field.
fn option_row(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, o: &FormatOption) {
/// One option of format `f`: a checkbox, an artboard menu, a version menu, a menu of its
/// [`choices`], a number or a text field.
fn option_row(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, f: &Format, o: &FormatOption) {
let t = Tokens::get(ui.ctx());
let label = label(o);
let value = d.fields.get(o.name).cloned().unwrap_or(Value::Null);
@@ -383,6 +472,40 @@ fn option_row(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, o: &Forma
ui.end_row();
return;
}
if let Some(c) = choices(app, f, o, &value) {
ui.label(egui::RichText::new(&label).color(t.text_dim)).on_hover_text(o.description);
// A value none of them names shows as it is.
let current = c.at.and_then(|i| c.names.get(i)).cloned().unwrap_or_else(|| value.as_str().map_or_else(|| value.to_string(), str::to_string));
let names: Vec<&str> = c.names.iter().map(String::as_str).collect();
if let Some(i) = super::mixed_dropdown(ui, ("save-option", o.name), &current, &names, 254.0, |k| k < c.builtins) {
choose(d, o, &c, i);
}
ui.end_row();
return;
}
if f.id == "eps" && o.name == "transparentPreview" {
// Only the color preview can keep transparency.
ui.label("");
let color = Preview::from_id(&d.str("previewFormat")) == Some(Preview::TiffColor);
let transparent = value.as_bool().unwrap_or(true);
ui.horizontal(|ui| {
for (name, on) in [(tl!("Transparent"), true), (tl!("Opaque"), false)] {
if widgets::radio(ui, name, color && transparent == on, color) {
d.fields.insert(o.name.into(), json!(on));
}
ui.add_space(12.0);
}
});
ui.end_row();
return;
}
if f.id == "eps" && o.name == "embedFonts" {
// Type is written as glyph outlines, which need no fonts.
ui.label("");
widgets::check_tip(ui, &label, true, false, o.description);
ui.end_row();
return;
}
if o.ty == "boolean" {
ui.label("");
let on = value.as_bool().unwrap_or(false);
@@ -392,8 +515,8 @@ fn option_row(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, o: &Forma
d.fields.insert(o.name.into(), json!(!on));
}
ui.end_row();
if o.name == "separateArtboards" {
artboard_range(ui, d);
if matches!(o.name, "separateArtboards" | "useArtboards") {
artboard_range(ui, d, o.name);
}
return;
}
@@ -421,6 +544,13 @@ fn option_row(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, o: &Forma
ui.end_row();
}
/// Option `o` set to the value of entry `i` of its menu `c`.
fn choose(d: &mut Dialog, o: &FormatOption, c: &Choices, i: usize) {
if let Some(v) = c.values.get(i) {
d.fields.insert(o.name.into(), v.clone());
}
}
/// The native versions VectorCraft writes, newest first, each with the first release that reads
/// it (the format version each release tag writes: 2 in 0.1, 3 from 0.2, 4 from 0.8).
const NATIVE_VERSIONS: [(u32, &str); 4] = [(4, "0.8"), (3, "0.2"), (2, "0.1"), (1, "0.1")];
@@ -505,8 +635,15 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> DialogResult {
if !boards.iter().all(|b| *b) {
options.insert("range".into(), json!(super::export_for_screens::range_text(&boards)));
}
} else if separates(f) {
if !d.bool("separateArtboards") || all_artboards(d) {
} else if let Some(switch) = board_switch(f) {
if switch == "useArtboards" {
// All or Range alone names the artboards: an artboard remembered from an earlier
// save would otherwise turn Use Artboards on, or narrow All to itself.
options.remove("artboard");
options.remove("artboards");
options.insert(switch.into(), json!(d.bool(switch)));
}
if !d.bool(switch) || all_artboards(d) {
options.remove("range");
} else {
// A bad range keeps the dialog open.
@@ -346,19 +346,213 @@ fn save_as_pdf_asks_with_the_save_pdf_dialog_and_remembers_its_options() {
assert_eq!(dialog(&app).str("__save"), "file.saveCopy");
}
/// The `%%BoundingBox` line of an EPS file.
fn bounding_box(eps: &[u8]) -> String {
let ps = std::str::from_utf8(vectorcraft_eps::sections(eps).unwrap().0).unwrap();
ps.lines().find(|l| l.starts_with("%%BoundingBox:")).expect("a bounding box").to_string()
}
#[test]
fn eps_save_as_keeps_page_options_and_writes_eps_without_export_only_switches() {
fn eps_save_as_shows_use_artboards_off_and_writes_the_art_or_the_artboard() {
let (mut app, written, _) = desktop("art.eps");
app.run("file.saveAs", json!({})).unwrap();
let names: Vec<_> = shown(fileio::format("eps").unwrap()).map(|option| option.name).collect();
assert!(!names.contains(&"useArtboards") && !names.contains(&"selectedOnly"));
assert!(names.contains(&"useArtboards") && !names.contains(&"selectedOnly") && !names.contains(&"range"), "{names:?}");
assert!(names.contains(&"previewFormat"));
// EPS alone gains the row: the other save formats keep their own checkbox or none.
for id in SAVE_FORMATS.iter().chain(TEMPLATE_FORMATS).filter(|id| **id != "eps") {
let f = fileio::format(id).unwrap();
assert!(!shown(f).any(|o| o.name == "useArtboards") && board_switch(f) != Some("useArtboards"), "{id}");
}
let text = crate::tests_labels::painted_text(&mut app, |app, ui| super::super::show(app, ui.ctx()));
assert!(text.contains("Use Artboards") && text.contains("All") && text.contains("Range:"), "{text}");
assert!(!dialog(&app).bool("useArtboards"), "Use Artboards starts off");
super::super::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
let saved = written.borrow();
assert_eq!(saved[0].0, "art.eps");
assert!(vectorcraft_eps::native(&saved[0].1).is_some());
{
let saved = written.borrow();
assert_eq!(saved[0].0, "art.eps");
assert!(vectorcraft_eps::native(&saved[0].1).is_some());
// The box Export writes of the visible art, not the artboard.
let exported = app.session.execute("document.export", &json!({"format": "eps"})).unwrap();
let exported = vectorcraft_format::base64_decode(exported["dataBase64"].as_str().unwrap()).unwrap();
assert_eq!(bounding_box(&saved[0].1), bounding_box(&exported));
assert_ne!(bounding_box(&saved[0].1), "%%BoundingBox: 0 0 100 80");
}
assert_eq!(app.session.active().unwrap().format, "eps");
// Checked, the artboard is the page, and an artboard remembered from an earlier save doesn't
// narrow All.
app.run("file.saveAs", json!({})).unwrap();
let d = app.ui.dialog.as_mut().unwrap();
d.fields.insert("useArtboards".into(), json!(true));
d.fields.insert("artboard".into(), json!(3));
super::super::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
{
let saved = written.borrow();
assert_eq!(saved.len(), 2, "one artboard: one file");
assert_eq!(bounding_box(&saved[1].1), "%%BoundingBox: 0 0 100 80");
}
// A save that named an artboard without Use Artboards (as document.save {artboard} does) had
// it on: the dialog shows it on, on that artboard, and OK writes the artboard again.
let st = app.session.active_mut().unwrap();
st.save_options.remove("useArtboards");
st.save_options.insert("artboard".into(), json!(0));
app.run("file.saveAs", json!({})).unwrap();
assert!(dialog(&app).bool("useArtboards"));
assert_eq!(dialog(&app).str("range"), "1");
super::super::confirm(&mut app).unwrap();
let saved = written.borrow();
assert_eq!(saved.len(), 3);
assert_eq!(bounding_box(&saved[2].1), "%%BoundingBox: 0 0 100 80");
}
/// Save As › EPS shows the options of Export's EPS Options: Preview, Overprints, Flattener preset
/// and PostScript as menus of readable names (not fields holding `tiffColor`, `preserve` and
/// `medium`), the checkboxes under written labels, and the PostScript level last.
#[test]
fn eps_save_options_are_menus_with_readable_names() {
let (mut app, _, _) = desktop("art.eps");
app.run("file.saveAs", json!({})).unwrap();
let text = crate::tests_labels::painted_text(&mut app, |app, ui| super::super::show(app, ui.ctx()));
let lines: Vec<&str> = text.lines().collect();
for shown in [
"Preview:",
"TIFF (8-bit Color)",
"Transparent",
"Opaque",
"Overprints:",
"Preserve",
"Flattener preset:",
"Medium Resolution",
"Embed Fonts (for other applications)",
"Include Linked Files",
"Include Document Thumbnails",
"Include CMYK PostScript in RGB Files",
"Compatible Gradient and Gradient Mesh Printing",
"PostScript:",
"Language Level 3",
] {
assert!(lines.contains(&shown), "{shown:?} in {text}");
}
for id in ["tiffColor", "preserve", "medium", "Cmyk Post Script", "Preview Format:", "Level:", "Thumbnails"] {
assert!(!lines.contains(&id), "{id:?} in {text}");
}
let at = |s: &str| lines.iter().position(|l| *l == s).unwrap();
assert!(at("PostScript:") > at("Compatible Gradient and Gradient Mesh Printing"), "{text}");
}
/// Every option the save options dialog shows, in every format it serves, has a written label:
/// none is painted as its name split into words.
#[test]
fn save_options_have_written_labels() {
// The web's Save As and Save as Template dialogs show the options of every format they pick.
let formats = SAVE_FORMATS.iter().map(|id| (*id, "file.saveAs")).chain(TEMPLATE_FORMATS.iter().map(|id| (*id, "file.saveAsTemplate")));
for (id, command) in formats {
let (mut app, _) = web();
app.run(command, json!({})).unwrap();
let mut d = app.ui.dialog.take().unwrap();
switch_format(&mut app, &mut d, id);
app.ui.dialog = Some(d);
assert_eq!((dialog(&app).kind.as_str(), dialog(&app).str("format").as_str()), (KIND, id));
let text = crate::tests_labels::painted_text(&mut app, |app, ui| super::super::show(app, ui.ctx()));
let f = fileio::format(id).unwrap();
for o in shown(f) {
let written = written_label(o);
assert!(written.is_some(), "{id}: {} has no written label", o.name);
let split = form::humanize(o.name);
let split = split.trim_end_matches(':');
assert!(
written.as_deref().map(|w| w.trim_end_matches(':')) == Some(split) || !text.lines().any(|l| l == split),
"{id}: {split:?} in {text}"
);
}
}
}
/// A pick from an EPS menu saves the option's value: the id, the level as a number, a saved
/// flattener preset by its name. The file is written with them.
#[test]
fn picking_eps_menu_items_saves_their_values() {
let (mut app, written, _) = desktop("art.eps");
app.session.prefs.flattener_presets.push(vectorcraft_engine::cmd::FlattenerPreset {
name: "Mine".into(),
options: vectorcraft_engine::cmd::FlattenOptions::preset("low").unwrap(),
});
app.run("file.saveAs", json!({})).unwrap();
let f = fileio::format("eps").unwrap();
let mut d = app.ui.dialog.take().unwrap();
for (name, pick, value) in [
("previewFormat", "None", json!("none")),
("overprints", "Discard", json!("discard")),
("flattenerPreset", "Mine", json!("Mine")),
("level", "Language Level 2", json!(2)),
] {
let o = f.options.iter().find(|o| o.name == name).unwrap();
let c = choices(&app, f, o, &Value::Null).unwrap_or_else(|| panic!("{name} is a menu"));
let i = c.names.iter().position(|n| n == pick).unwrap_or_else(|| panic!("{pick} in {name}: {:?}", c.names));
choose(&mut d, o, &c, i);
assert_eq!(d.fields[name], value, "{name}");
assert_eq!(choices(&app, f, o, &value).unwrap().at, Some(i), "{name}: the menu shows the pick");
}
// The built-in presets come first, by id. A value picks the entry the save reads it as: a
// preset by id or name in any case, a level by its digits, a preview or overprint by id only.
let at = |name: &str, value: Value| {
let o = f.options.iter().find(|o| o.name == name).unwrap();
choices(&app, f, o, &value).unwrap().at
};
let o = f.options.iter().find(|o| o.name == "flattenerPreset").unwrap();
let c = choices(&app, f, o, &Value::Null).unwrap();
assert_eq!(c.values, [json!("high"), json!("medium"), json!("low"), json!("Mine")]);
assert_eq!((c.builtins, c.at), (3, None));
assert_eq!(
(at("flattenerPreset", json!("MEDIUM RESOLUTION")), at("flattenerPreset", json!("mine")), at("flattenerPreset", json!("Other"))),
(Some(1), Some(3), None)
);
assert_eq!((at("level", json!("level2")), at("level", json!(3)), at("level", json!(4))), (Some(0), Some(1), None));
assert_eq!((at("previewFormat", json!("TIFFBW")), at("previewFormat", json!("TIFF (8-bit Color)"))), (Some(1), None));
assert_eq!((at("overprints", json!("Discard")), at("overprints", json!("keep"))), (Some(1), None));
app.ui.dialog = Some(d);
super::super::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none(), "saved: {}", app.ui.status);
let saved = &app.session.active().unwrap().save_options;
for (name, value) in [("previewFormat", json!("none")), ("overprints", json!("discard")), ("flattenerPreset", json!("Mine")), ("level", json!(2))]
{
assert_eq!(saved[name], value, "{name}");
}
let bytes = written.borrow()[0].1.clone();
let (ps, preview) = vectorcraft_eps::sections(&bytes).unwrap();
assert!(preview.is_none(), "no preview");
assert!(String::from_utf8_lossy(ps).contains("%%LanguageLevel: 2"));
}
/// Save As › EPS: Transparent and Opaque apply to the color TIFF preview only, and are disabled
/// with any other preview. Embed Fonts shows checked and disabled. A flattener preset that no
/// entry of the menu names shows as it is.
#[test]
fn eps_save_transparency_follows_the_color_preview() {
use crate::widgets::accessibility_tests::{named, role_and_state, tree};
use egui::accesskit::Toggled;
let (mut app, _, _) = desktop("art.eps");
app.run("file.saveAs", json!({})).unwrap();
let set = |app: &mut VectorcraftApp, k: &str, v: Value| {
app.ui.dialog.as_mut().unwrap().fields.insert(k.into(), v);
};
set(&mut app, "previewFormat", json!("tiffColor"));
set(&mut app, "transparentPreview", json!(false));
let t = tree(|ui| super::super::show(&mut app, ui.ctx()));
assert!(!named(&t, "Transparent").is_disabled() && !named(&t, "Opaque").is_disabled());
assert_eq!((role_and_state(&t, "Transparent").1, role_and_state(&t, "Opaque").1), (Some(Toggled::False), Some(Toggled::True)));
let fonts = named(&t, "Embed Fonts (for other applications)");
assert_eq!((fonts.is_disabled(), fonts.toggled()), (true, Some(Toggled::True)));
for preview in ["none", "tiffBw"] {
set(&mut app, "previewFormat", json!(preview));
let t = tree(|ui| super::super::show(&mut app, ui.ctx()));
assert!(named(&t, "Transparent").is_disabled() && named(&t, "Opaque").is_disabled(), "{preview}");
}
set(&mut app, "flattenerPreset", json!("Gone"));
let text = crate::tests_labels::painted_text(&mut app, |app, ui| super::super::show(app, ui.ctx()));
assert!(text.lines().any(|l| l == "Gone"), "{text}");
}
/// #4931: a native file's Version is a menu of the versions VectorCraft writes, the current one
@@ -180,6 +180,15 @@ fn a_bad_range_keeps_export_as_open() {
assert!(written.borrow().is_empty());
}
/// An emptied Range field shows an example of the artboard numbers it takes, as Save As's does.
#[test]
fn an_empty_export_as_range_shows_an_example() {
let (mut app, _) = app(2);
app.run("file.exportAs", json!({"useArtboards": true, "range": ""})).unwrap();
let text = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui.ctx()));
assert!(text.lines().any(|l| l == "1-3, 5"), "{text}");
}
#[test]
fn svg_continues_in_svg_options_and_pdf_art_is_written_directly() {
let (mut app, written) = app(2);
+1
View File
@@ -2473,6 +2473,7 @@ font-technology Variable Променливи
Presets: Предварителни настройки:
Press Натиснете
Preview Преглед
Preview: Преглед:
Preview All Layers Преглед на всички слоеве
Preview in Browser Преглед в браузър
Preview in Browser needs the desktop app (the web downloads files instead) Прегледът в браузър изисква настолното приложение (уеб версията изтегля файловете вместо това)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -556,6 +556,7 @@
Gaussian Blur… Gaussovo zamućenje…
View Prikaz
Preview Pregled
Preview: Pregled:
Overprint Preview Pregled pretiska
Pixel Preview Pregled piksela
Trim View Prikaz obrezivanja
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3207,6 +3207,7 @@ digits Malayalam Malaiàlam
Press Quality Qualitat d’impressió
Pressure Pressió
Preview Previsualització
Preview: Previsualització:
Preview (1 inch on the page) Previsualització (1 polzada a la pàgina)
Preview All Layers Previsualitza totes les capes
Preview in Browser Previsualitza al navegador
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -565,6 +565,7 @@
Gaussian Blur… Gaussovské rozostření…
View Zobrazení
Preview Náhled
Preview: Náhled:
Overprint Preview Náhled přetisků
Pixel Preview Náhled obrazových bodů
Trim View Oříznuté zobrazení
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -520,6 +520,7 @@
Gaussian Blur… Gaussisk sløring…
View Vis
Preview Forhåndsvisning
Preview: Forhåndsvisning:
Overprint Preview Overprint-forhåndsvisning
Pixel Preview Pixel-forhåndsvisning
Trim View Beskåret visning
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -808,6 +808,14 @@ font-technology Variable Variabel
Disable Auto Add/Delete Automatisches Hinzufügen/Löschen deaktivieren
Disable Opacity Mask Deckkraftmaske deaktivieren
Discard Verwerfen
Preserve Beibehalten
TIFF (Black & White) TIFF (Schwarzweiß)
TIFF (8-bit Color) TIFF (8-Bit-Farbe)
High Resolution Hohe Auflösung
Medium Resolution Mittlere Auflösung
Low Resolution Niedrige Auflösung
Language Level 2 Sprachebene 2
Language Level 3 Sprachebene 3
Discard Empty Type Leeren Text verwerfen
Discard Recovered Documents Wiederhergestellte Dokumente verwerfen
Discard White Overprint in Output Weißes Überdrucken in der Ausgabe verwerfen
@@ -2503,6 +2511,7 @@ diagnostics ui Oberfläche
Presets: Vorgaben:
Press Mit
Preview Vorschau
Preview: Vorschau:
Preview All Layers Vorschau aller Ebenen
Preview in Browser Vorschau im Browser
Preview in Browser needs the desktop app (the web downloads files instead) „Vorschau im Browser“ erfordert die Desktop-App (im Web werden stattdessen Dateien heruntergeladen)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1889,6 +1889,7 @@ font-technology Variable Μεταβλητές
Preset: Προεπιλογή:
Presets: Προεπιλογές:
Preview Προεπισκόπηση
Preview: Προεπισκόπηση:
Previous Προηγούμενο
Print Εκτύπωση
Printer Εκτυπωτής
Can't render this file because it is too large.
+1
View File
@@ -2547,6 +2547,7 @@ font-technology Variable Variables
Presets: Preajustes:
Press Presiona
Preview Vista previa
Preview: Vista previa:
Preview All Layers Previsualizar todas las capas
Preview in Browser Previsualizar en el navegador
Preview in Browser needs the desktop app (the web downloads files instead) Previsualizar en el navegador requiere la aplicación de escritorio (la versión web descarga los archivos)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -559,6 +559,7 @@
Gaussian Blur… Gaussin sumennus…
View Näytä
Preview Esikatselu
Preview: Esikatselu:
Overprint Preview Päällepainatuksen esikatselu
Pixel Preview Pikseliesikatselu
Trim View Rajattu näkymä
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2539,6 +2539,7 @@ font-technology Variable Variables
Presets: Paramètres prédéfinis :
Press Appuyez sur
Preview Aperçu
Preview: Aperçu :
Preview All Layers Aperçu de tous les calques
Preview in Browser Aperçu dans le navigateur
Preview in Browser needs the desktop app (the web downloads files instead) L’aperçu dans le navigateur nécessite l’application de bureau (la version web télécharge les fichiers à la place)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -3501,6 +3501,7 @@ antiAlias Sharp शार्प
Press Quality प्रेस गुणवत्ता
Pressure दबाव
Preview प्रीव्यू
Preview: प्रीव्यू:
Preview (1 inch on the page) प्रीव्यू (1 इंच पृष्ठ पर)
Preview All Layers सभी लेयर्स का प्रीव्यू करें
Preview in Browser ब्राउज़र में प्रीव्यू
@@ -6392,7 +6393,7 @@ antiAlias Sharp शार्प
@msg EPS {field} `{s}`: {allowed} EPS {field} `{s}`: {allowed}
@msg EPS flattener: {e} ईपीएस flattener: {e}
@msg EPS options: {e} ईपीएस ऑप्शन्स: {e}
@msg EPS Save retains the artboard and all artwork: use document.export for cropped or selected-only EPS EPS सेव आर्टबोर्ड और पूरा आर्टवर्क रखता है: क्रॉप किए या केवल सिलेक्ट किए आर्टवर्क के EPS के लिए document.export उपयोग करें
@msg EPS Save writes all artwork: use document.export for selected-only EPS EPS सेव पूरा आर्टवर्क लिखता है: केवल सिलेक्ट किए आर्टवर्क के EPS के लिए document.export उपयोग करें
@msg every numbered name for {name} is taken in the document's folder {name} के लिए हर नंबर का नाम डॉक्यूमेंट के फ़ोल्डर में लिया जाता है
@msg every numbered name for it is taken in the Fonts folder इसके लिए हर नंबर वाला नाम फ़ॉन्ट फ़ोल्डर में लिया जाता है
@msg every numbered name for the copy is taken प्रति के लिए हर नंबर का नाम लिया जाता है
Can't render this file because it is too large.
+1
View File
@@ -3095,6 +3095,7 @@ diagnostics ui felület
Press keys… Nyomd le a billentyűket…
Pressure Nyomás
Preview Előnézet
Preview: Előnézet:
Preview All Layers Összes réteg előnézetben
Preview in Browser Előnézet böngészőben
Preview in Browser needs the desktop app (the web downloads files instead) Az Előnézet böngészőben az asztali alkalmazásban érhető el (a webes változat helyette letölti a fájlokat)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -520,6 +520,7 @@
Gaussian Blur… Kabur Gaussian…
View Tampilan
Preview Pratinjau
Preview: Pratinjau:
Overprint Preview Pratinjau overprint
Pixel Preview Pratinjau piksel
Trim View Tampilan terpangkas
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
520
521
522
523
524
525
526
+9
View File
@@ -829,6 +829,14 @@ font-technology Variable Variabili
Disable Auto Add/Delete Disattiva aggiunta/eliminazione automatica
Disable Opacity Mask Disattiva maschera di opacità
Discard Scarta
Preserve Mantieni
TIFF (Black & White) TIFF (bianco e nero)
TIFF (8-bit Color) TIFF (colore a 8 bit)
High Resolution Alta risoluzione
Medium Resolution Media risoluzione
Low Resolution Bassa risoluzione
Language Level 2 Livello di linguaggio 2
Language Level 3 Livello di linguaggio 3
Discard Empty Type Scarta testo vuoto
Discard Recovered Documents Scarta documenti recuperati
Discard White Overprint in Output Scarta sovrastampa del bianco nell'output
@@ -2579,6 +2587,7 @@ diagnostics ui interfaccia
Presets: Preset:
Press Premi
Preview Anteprima
Preview: Anteprima:
Preview All Layers Anteprima di tutti i livelli
Preview in Browser Anteprima nel browser
Preview in Browser needs the desktop app (the web downloads files instead) Anteprima nel browser richiede l'app desktop (la versione web scarica invece i file)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -870,6 +870,7 @@ digits Malayalam マラヤーラム数字
Search Commands… コマンドを検索…
VectorCraft Help… VectorCraft ヘルプ…
Preview プレビュー
Preview: プレビュー:
Show Edges エッジを表示
Show Corner Widget コーナーウィジェットを表示
Show Template テンプレートを表示
@@ -1459,6 +1460,14 @@ antiAlias Sharp シャープ
Disable Auto Add/Delete 自動追加/削除機能を使用しない
Disable Opacity Mask 不透明マスクを無効にする
Discard 破棄
Preserve 保持
TIFF (Black & White) TIFF (白黒)
TIFF (8-bit Color) TIFF (8 bit カラー)
High Resolution 高解像度
Medium Resolution 中解像度
Low Resolution 低解像度
Language Level 2 言語レベル 2
Language Level 3 言語レベル 3
Discard Empty Type 空のテキストを破棄
Discard Recovered Documents 復帰したドキュメントを破棄
Discard White Overprint in Output 出力時に白のオーバープリントを破棄
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -495,6 +495,7 @@
Gaussian Blur… გაუსის გაბუნდოვნება…
View ხედი
Preview წინასწარი ნახვა
Preview: წინასწარი ნახვა:
Overprint Preview ზედდაბეჭდვის წინასწარი ნახვა
Pixel Preview პიქსელების წინასწარი ნახვა
Trim View მოჭრილი ხედი
Can't render this file because it is too large.
+9
View File
@@ -863,6 +863,7 @@ digits Malayalam 말라얄람어
Search Commands… 명령 검색…
VectorCraft Help… VectorCraft 도움말…
Preview 미리 보기
Preview: 미리 보기:
Show Edges 가장자리 표시
Show Corner Widget 모퉁이 위젯 표시
Show Template 템플릿 표시
@@ -1449,6 +1450,14 @@ antiAlias Sharp 날카롭게
Disable Auto Add/Delete 자동 추가/삭제 사용 안 함
Disable Opacity Mask 불투명 마스크 사용 안 함
Discard 삭제
Preserve 보존
TIFF (Black & White) TIFF (흑백)
TIFF (8-bit Color) TIFF (8비트 색상)
High Resolution 고해상도
Medium Resolution 중간 해상도
Low Resolution 저해상도
Language Level 2 언어 수준 2
Language Level 3 언어 수준 3
Discard Empty Type 빈 문자 삭제
Discard Recovered Documents 복구된 문서 삭제
Discard White Overprint in Output 출력에서 흰색 오버프린트 삭제
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -534,6 +534,7 @@
Gaussian Blur… Gaussiaanse vervaging…
View Beeld
Preview Voorbeeld
Preview: Voorbeeld:
Overprint Preview Overprintvoorbeeld
Pixel Preview Pixelvoorbeeld
Trim View Bijsnijweergave
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -524,6 +524,7 @@
Gaussian Blur… Rozmycie gaussowskie…
View Widok
Preview Podgląd
Preview: Podgląd:
Overprint Preview Podgląd nadruku
Pixel Preview Podgląd pikseli
Trim View Widok przycięty
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2120,6 +2120,7 @@ antiAlias Sharp Nítido
Presets: Predefinidos:
Press Pressione
Preview Visualização
Preview: Visualização:
Preview Mode Modo de Visualização
Preview in Browser Visualizar no Navegador
Preview the document as it is now Visualizar o documento como está agora
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2574,6 +2574,7 @@ diagnostics ui интерфейс
Presets: Наборы:
Press Нажмите
Preview Просмотр
Preview: Просмотр:
Preview All Layers Просмотреть все слои
Preview in Browser Просмотреть в браузере
Preview in Browser needs the desktop app (the web downloads files instead) Просмотр в браузере требует настольного приложения (web-версия скачивает файлы)
Can't render this file because it is too large.
+2 -1
View File
@@ -2484,6 +2484,7 @@ font-technology Variable Variabla
Presets: Förinställningar:
Press Tryck på
Preview Förhandsgranskning
Preview: Förhandsgranskning:
Preview All Layers Förhandsgranska alla lager
Preview in Browser Förhandsgranska i webbläsare
Preview in Browser needs the desktop app (the web downloads files instead) Förhandsgranska i webbläsare kräver skrivbordsappen (webbversionen hämtar filerna i stället)
@@ -7462,7 +7463,7 @@ digits Hindi Hindi
@msg EPS {field} `{s}`: {allowed} EPS {field} `{s}`: {allowed}
@msg EPS flattener: {e} EPS-utplattning: {e}
@msg EPS options: {e} EPS-alternativ: {e}
@msg EPS Save retains the artboard and all artwork: use document.export for cropped or selected-only EPS När du sparar som EPS behålls ritytan och allt bildmaterial: använd document.export för beskuren EPS eller EPS med bara det markerade
@msg EPS Save writes all artwork: use document.export for selected-only EPS När du sparar som EPS skrivs allt bildmaterial: använd document.export för EPS med bara det markerade
@msg every numbered name for the result is taken alla numrerade namn för resultatet är upptagna
@msg expanded SVG styles exceed the import size limit de expanderade SVG-stilarna överskrider storleksgränsen för import
@msg expected {"dataBase64"} förväntade {"dataBase64"}
Can't render this file because it contains an unexpected character in line 5 and column 25.
+21
View File
@@ -238,6 +238,27 @@ fn anti_aliasing_method_labels_are_in_complete_catalogs() {
}
}
/// The names the EPS options' menus show (preview, overprints, flattener preset and PostScript
/// level), and Save As's Preview label.
#[test]
fn eps_option_menu_names_are_in_complete_catalogs() {
use vectorcraft_engine::cmd::FlattenOptions;
use vectorcraft_eps::{Level, Overprint, Preview};
let names: Vec<&str> = Preview::ALL
.map(Preview::label)
.into_iter()
.chain(Overprint::ALL.map(Overprint::label))
.chain(Level::ALL.map(Level::label))
.chain(FlattenOptions::PRESETS.into_iter().filter_map(FlattenOptions::preset_label))
.chain(["Preview:"])
.collect();
for lang in LANGUAGES.iter().filter(|lang| lang.complete_menus) {
for label in &names {
assert!(lang.catalog().plain(label).is_some(), "{} is missing {label}", lang.code);
}
}
}
/// Each language counts with its own plural rule: the form a count picks (`one`, then `few`, then
/// `other`/`many`).
#[test]
+1
View File
@@ -2357,6 +2357,7 @@ shape Open Açık
Presets: Ön ayarlar:
Press Basın
Preview Önizleme
Preview: Önizleme:
Preview All Layers Tüm Katmanları Önizle
Preview in Browser Tarayıcıda Önizleme
Preview in Browser needs the desktop app (the web downloads files instead) Tarayıcıdaki önizleme için masaüstü uygulaması gerekir (bunun yerine web, dosyaları indirir)
Can't render this file because it contains an unexpected character in line 5 and column 25.
+9
View File
@@ -823,6 +823,14 @@ font-technology Variable Варіативні
Disable Auto Add/Delete Вимкнути авто-додавання/видалення
Disable Opacity Mask Вимкнути маску непрозорості
Discard Відкинути
Preserve Зберігати
TIFF (Black & White) TIFF (чорно-білий)
TIFF (8-bit Color) TIFF (8-бітний колір)
High Resolution Висока роздільна здатність
Medium Resolution Середня роздільна здатність
Low Resolution Низька роздільна здатність
Language Level 2 Рівень мови 2
Language Level 3 Рівень мови 3
Discard Empty Type Видалити порожній текст
Discard Recovered Documents Відкинути відновлені документи
Discard White Overprint in Output Скасувати накладення білого під час виведення
@@ -2571,6 +2579,7 @@ diagnostics ui інтерфейс
Presets: Набори:
Press Натисніть
Preview Перегляд
Preview: Перегляд:
Preview All Layers Переглянути всі шари
Preview in Browser Переглянути у браузері
Preview in Browser needs the desktop app (the web downloads files instead) Перегляд у браузері вимагає настільної програми (web-версія завантажує файли)
Can't render this file because it contains an unexpected character in line 182 and column 5.
+9
View File
@@ -964,6 +964,14 @@ font-technology Variable Biến thiên
Disable Auto Add/Delete Tắt tự động thêm/xóa
Disable Opacity Mask Tắt mặt nạ độ mờ
Discard Loại bỏ
Preserve Bảo lưu
TIFF (Black & White) TIFF (Đen trắng)
TIFF (8-bit Color) TIFF (Màu 8 bit)
High Resolution Độ phân giải cao
Medium Resolution Độ phân giải vừa
Low Resolution Độ phân giải thấp
Language Level 2 Cấp ngôn ngữ 2
Language Level 3 Cấp ngôn ngữ 3
Discard Empty Type Loại bỏ chữ trống
Discard Recovered Documents Loại bỏ tài liệu đã phục hồi
Discard White Overprint in Output Loại bỏ in chồng màu trắng khi xuất
@@ -2880,6 +2888,7 @@ diagnostics ui giao diện
Press Nhấn
Pressure Áp lực
Preview Xem trước
Preview: Xem trước:
Preview All Layers Xem trước tất cả lớp
Preview in Browser Xem trước trong trình duyệt
Preview in Browser needs the desktop app (the web downloads files instead) Xem trước trong trình duyệt cần ứng dụng máy tính để bàn (web sẽ tải tệp xuống thay thế)
Can't render this file because it contains an unexpected character in line 204 and column 5.
+1
View File
@@ -2445,6 +2445,7 @@ diagnostics ui 界面
Presets: 预设:
Press 按
Preview 预览
Preview: 预览:
Preview in Browser 在浏览器中预览
Preview in Browser needs the desktop app (the web downloads files instead) 在浏览器中预览需要桌面版应用程序 (网页版会改为下载文件)
Preview Mode 预览模式
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -791,6 +791,14 @@ font-technology Variable 可變
Disable Auto Add/Delete 停用自動增加/刪除
Disable Opacity Mask 停用不透明度遮色片
Discard 捨棄
Preserve 保留
TIFF (Black & White) TIFF (黑白)
TIFF (8-bit Color) TIFF (8 位元色彩)
High Resolution 高解析度
Medium Resolution 中解析度
Low Resolution 低解析度
Language Level 2 語言層級 2
Language Level 3 語言層級 3
Discard Empty Type 捨棄空白文字
Discard Recovered Documents 捨棄已復原的文件
Discard White Overprint in Output 輸出時捨棄白色疊印
@@ -3539,6 +3547,7 @@ diagnostics ui 介面
Presets: 預設集:
Press 按下
Preview 預覽
Preview: 預覽:
Preview All Layers 預覽所有圖層
Preview in Browser 在瀏覽器中預覽
Preview in Browser needs the desktop app (the web downloads files instead) 在瀏覽器中預覽需要桌面版應用程式 (網頁版會改為下載檔案)
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5 -1
View File
@@ -794,7 +794,11 @@ artboards' files. A `.ai` file's `aiVersion` comes first: `"pdf"` (the default)
Options"), which takes only `transparentImages` (`"flatten"` or `"omit"`), `hiddenObjects` (`"layer"`, `"omit"` or
`"visible"`, the Hidden Objects menu), `outlineText` (the Text menu: false Editable, true Outlines) and, with several artboards, the artboard
picker: `boards`, one bool per artboard (default: those last saved, else all), saved as `range`, one file per checked
artboard. OK is disabled with none checked.
artboard. OK is disabled with none checked. Save As to an EPS file opens the same dialog after the save panel (heading
"EPS Options") with `useArtboards`, `all` and `range`, then the options Export's EPS Options shows. Preview
(`previewFormat`), Overprints (`overprints`), Flattener preset (`flattenerPreset`) and PostScript (`level`) are menus:
a pick writes the option's value (`"tiffColor"`, `"preserve"`, a built-in preset's id such as `"medium"` or a saved
preset's name, the level as a number), and a value none of them names shows as it is.
EMF and WMF: Export As… lists both formats (`file.exportAs {format: "emf"}` or `"wmf"`); they have no options
dialog, so `ui.dialog.confirm` asks for the path and writes the file (one per artboard with Use Artboards). Open and
+22 -8
View File
@@ -488,25 +488,39 @@ Stop opacity on translated artboards remains outside this regression coverage.
Feature evidence updated 2026-10-26; scores and estimates unchanged.
EPS is offered by Save As, and an opened EPS keeps its own path for ordinary Save. Save writes
one artboard (the first by default, or the named artboard), retaining its page bounds and transparent
margins rather than cropping to the drawn artwork. The whole editable VectorCraft document remains
embedded in EPS comments, including other artboards. Save bounds the optional TIFF preview to
EPS is offered by Save As, and an opened EPS keeps its own path for ordinary Save. The EPS
Options of Save As show Use Artboards with All or Range under it, for a separate file per artboard
([saving in EPS](https://helpx.adobe.com/illustrator/desktop/save-and-export/export-files-to-different-formats/save-artwork-in-eps-format.html)),
unchecked by default (the scripting reference's EPS save options list `saveMultipleArtboards` with
a default of false,
[scripting reference](https://web.archive.org/web/20160803105056/https://wwwimages.adobe.com/content/dam/Adobe/en/devnet/illustrator/sdk/CC2015_3/Illustrator%20JavaScript%20Scripting%20Reference.pdf)).
Unchecked, Save writes one file whose bounding box fits all the visible art, as Export does,
without the artboards' background colors; an opened EPS saved back this way loses the margins its
page had. With no visible art the page is the first artboard, in its color. Naming artboards with
`artboard`, `artboards` or `range` turns Use Artboards on. Checked with two or more chosen, the
file at the path is the central file of the visible art and each artboard is also written to
`<name>_<artboard>.eps` beside it, holding that artboard. Checked with one chosen, VectorCraft
writes that artboard as the page of the file at the path alone, retaining its bounds and
transparent margins. The file the document becomes carries the whole editable VectorCraft document
in EPS comments, including other artboards. Save bounds the optional TIFF preview to
1024 pixels on its longest side (about 4 MiB of RGBA pixels), avoiding the roughly 16 MiB preview a
large artboard otherwise produces. Export keeps its existing 2048-pixel preview cap. Preview
format choices are unchanged; no public preview-resolution option exists. The EPS PostScript appearance still flattens
transparency; its existing warnings remain. This does not promise native Illustrator editing-data fidelity.
Cropped and selected-only EPS output remains available through Export. Save refuses those options.
Selected-only EPS output remains available through Export. Save refuses `selectedOnly`.
An EPS opened from a preview, a partially interpreted program or damaged native editing data keeps
its source-loss warning: writing over that original requires explicit confirmation. File › Save uses
the same engine path and write-failure handling as other document formats.
Synthetic regressions: `tests::save::opened_eps_saves_to_its_own_file_with_editable_data_and_page_margins`,
`eps_save_keeps_all_editing_artboards_while_printing_one_page`,
Synthetic regressions: `tests::save::opened_eps_saves_to_its_own_file_around_its_art_or_on_its_page`,
`eps_save_without_use_artboards_fits_the_box_to_art_past_the_artboard`,
`eps_save_keeps_all_editing_artboards_in_the_file_of_one_page`,
`eps_save_with_use_artboards_writes_the_central_file_and_one_per_artboard`,
`cmd::fileio::tests_artboard_fill::eps_save_draws_the_color_on_an_artboard_page`,
`eps_save_supports_an_empty_artboard_without_cropping_its_page`,
`partially_read_eps_requires_confirmation_before_save_back`,
`eps_with_damaged_editing_data_requires_confirmation_before_save_back`,
`eps_save_bounds_large_page_preview_without_cropping_vector_or_editing_data`, and the UI regressions
`file_save_writes_opened_eps_back_without_another_save_as_dialog` and
`eps_save_as_keeps_page_options_and_writes_eps_without_export_only_switches`.
`eps_save_as_shows_use_artboards_off_and_writes_the_art_or_the_artboard`.
+2 -2
View File
@@ -199,7 +199,7 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi
- **Done (2026-11-06):** an EPS page that paints its gradients as rows of abutting one-colour strips imports them as linear gradients, so the seams between strips no longer show and an Illustrator EPS drawn that way keeps its layers instead of opening as its page. This narrows G9; scores and estimates unchanged.
- **Done (2026-10-30):** from an audit of a user's `.ai`/EPS files: All Caps and Small Caps; placed files at their box's corner; pages cropped to the art; gradient mesh corners with handle weights of 0 and the transparency written after a mesh, a masked mesh and a blend; a layer's state block with the stroke style; type on a path spaced as the file lays it out (the distance from the path under Align to Path is measured from the top of each character's em box, half the height of the top of its H or the bottom of its font's bounding box, and the fit under Gravity does not reproduce the file's layout); EPS10 legacy-format type; a PDF of some artboards compared on the artboard its page shows. Comparison limits remain unchanged. This narrows G9; scores and estimates unchanged.
- **Save-back update (2026-10-26):** ordinary EPS Save retains page margins and the whole editable document; source-loss confirmation remains. Import fidelity gaps and estimates below are unchanged.
- **Save-back update (2026-10-26):** ordinary EPS Save retains page margins and the whole editable document; source-loss confirmation remains. Import fidelity gaps and estimates below are unchanged. Since 2026-11-12 Save keeps the page margins only with Use Artboards checked; unchecked, the default, the bounding box fits the visible art.
- **Done (2026-10-16):** EPS legacy mesh corners and vector opacity masks retained; winding-declared compound member groups keep shared paint. Raster print-page minification no longer causes a false editing-data fallback; comparison limits remain unchanged. Synthetic regressions cover damage and missing artwork. This narrows G9; scores and estimates unchanged.
@@ -1097,4 +1097,4 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv
2026-10-28 behavior correction: Flare ray rotation follows vertical travel at one degree per document point, independent of pointer event count. Shift freezes the current orientation; Space also freezes rotation on the first update. This is the chosen application interaction rate. Scores and estimates unchanged.
## EPS Save evidence (2026-10-26)
Ordinary Save and Save As keep EPS as EPS, with the chosen page frame, transparent margins and the complete editable VectorCraft document. Existing import-loss confirmation and transparency warnings remain; the Save TIFF preview is capped at 1024 pixels, while Export retains 2048. See [file-format-parity.md](file-format-parity.md#eps-save-and-source-protection) for limits and regression evidence. This narrows G9 save-back workflow coverage; native Illustrator editing-data fidelity, parity scores and hour estimates are unchanged.
Ordinary Save and Save As keep EPS as EPS, with the chosen page frame, transparent margins and the complete editable VectorCraft document. Existing import-loss confirmation and transparency warnings remain; the Save TIFF preview is capped at 1024 pixels, while Export retains 2048. See [file-format-parity.md](file-format-parity.md#eps-save-and-source-protection) for limits and regression evidence. This narrows G9 save-back workflow coverage; native Illustrator editing-data fidelity, parity scores and hour estimates are unchanged. Since 2026-11-12 the EPS Options of Save As show Use Artboards, unchecked by default: unchecked, the bounding box fits the visible art; checked, Save keeps the chosen page frame and margins described here.
+1 -1
View File
@@ -509,7 +509,7 @@ applied colors still join the list while it is hidden.
| `open_panel` | `{panel}` | Remote only. `panel` is a panel id (`layers`, `swatches`, `colorGuide`, …, as `window.panel` takes) or its display label (`"Color Guide"`), in any case. |
| `screenshot` | `{path?, scale?, artboard?, window?, max_side?, traceViews?}` | Returns MCP image content (`image/png`, base64) plus a text block. Renders the artboard with the pixels of a PNG `export` with `background: "white"` at the same scale; `traceViews:true` draws Image Trace objects in the view set with `imageTrace.setView`, as in the app's window; `window:true` captures the app window (remote only), as `ui_screenshot` does, and `max_side` (1–4096, window captures only) scales the returned image down as described for `ui_screenshot`. With `path` the backend (the headless session or the app) writes the PNG there too; a window capture written there keeps its full size with `max_side`. |
| `open_file` | `{path, importer?, params?}` | Opens any readable file as a new active document: `.vectorcraft`/`.drawcraft`, `.vctemplate`, `.svg`/`.svgz`, `.pdf`/`.ai`, `.ait`, `.eps`/`.epsf`, `.ps`, `.dxf`, `.emf`, `.wmf`, HP-GL (`.plt`/`.hpgl`/`.hgl`/`.hpg`, and a file of an extension no other format reads that starts with plotter instructions), PNG (`.png`/`.pns`), JPEG, GIF, WebP, TIFF, BMP (`.bmp`/`.dib`/`.rle`), Targa (`.tga`/`.vda`/`.icb`/`.vst`), PCX, Pixar (`.pxr`), JPEG 2000 (`.jpf`/`.jpx`/`.jp2`/`.j2k`/`.j2c`/`.jpc`), HEIC/HEIF (`.heic`/`.heif`), JPEG XL (`.jxl`), PSD (`.psd`/`.pdd`)/PSB (an image opens as a document of its pixel size; a PSD or PSB file with its layers as layers, or as its merged image through `run_command document.open` with `layers: false`; a JPEG XL file as its first frame). Templates (`.vctemplate`, `.ait`) open as a new untitled document. PDF, `.ai` and SVG files saved with Preserve Editing reopen as the document they carry. The reply is `document.open`'s: its `warnings` say what didn't come in as it was (an EPS whose PostScript can't be read opens as its preview, and the warning names the PostScript error, the operator and the procedure). A file of an extension only an installed importer plug-in lists opens with that importer ([Plug-ins](#plug-ins)); `importer: "plugin.<id>"` opens the file with the importer it names, which must list the file's extension, also one a built-in format reads (in the app, as File › Open With does), and `params` gives that importer's parameters (their defaults when omitted). `run_command document.formats` lists the formats. |
| `save_file` | `{path?, compress?, version?, preview?, precision?, options?}` | Runs `document.save`: the document's own file in its own format (native `.vectorcraft` unless it was opened from or saved as SVG, PDF or a restorable `.ai`; then `warnings` say what that format loses). A path's extension picks the format (`.vectorcraft`, `.vctemplate`, `.pdf`, `.svg`, `.svgz`, `.ai`: a PDF carrying the native document, which reopens editable). For a native file, `compress: true` gzips it (default: the `useCompression` preference, Preferences › File Handling › Use Compression), `version` (1 to 4) writes it for an older VectorCraft (1 and 2 are never compressed, and `version: 2` with `compress: true` is an error), `preview: true` embeds a PNG of the first artboard, and `precision` (0 to 9) rounds path anchors and handles to that many decimal places for that save only (colors, transforms and type stay exact). `options` passes the format's save options as `run_command document.formats` lists them. When `compress`, `version`, `preview` or `precision` is given both at the top level and in `options` with different values, the call is an error (`` `compress: true` and `options.compress: false` disagree: give the option once ``) and nothing is written. A key in `options` that no save format takes is an error, and nothing is written (see Parameter keys). The save drops each option the written format doesn't read. The reply's `warnings` then name each such option given (for example `` `compress` was not used: SVG files don't read it `` for an `.svg` path, and `` `options.preview` was not used: … `` for a key in `options`; a `.ai` file reads `compress` but not `version`, `preview` or `precision`). For a `.ai` file, these warnings name the kind of `.ai` written: a legacy .ai (version 7) for `aiVersion: "7"`, a non-PDF-compatible .ai for `pdfCompatible: false`, and a PDF-compatible .ai otherwise, including a save that reuses the options the document was last saved with. `run_command document.save` and the other save commands refuse any option given beside the path and in `options` with different values, and give the same warnings for a top-level `compress`, `version`, `preview` or `precision` and for each key in `options` that the format doesn't read. The headless session and the app receive the same `app.save` params, and the app is also sent `wait: true`: the call returns once the file is written, or with the error (such as a path outside the write root), even with Background Save on. When the save asks first (the question before replacing a file that opened with losses, or on macOS and Linux the file dialog for an untitled document without `path`), the reply is `{pending}` or `"cancelled"` as before, and the save that runs after the answer uses the Background Save preference. |
| `save_file` | `{path?, compress?, version?, preview?, precision?, options?}` | Runs `document.save`: the document's own file in its own format (native `.vectorcraft` unless it was opened from or saved as SVG, PDF or a restorable `.ai`; then `warnings` say what that format loses). A path's extension picks the format (`.vectorcraft`, `.vctemplate`, `.pdf`, `.svg`, `.svgz`, `.ai`: a PDF carrying the native document, which reopens editable; `.eps`: by default one file whose bounding box fits the visible art, the first artboard when no art shows; `options.useArtboards: true` makes the chosen artboard the page and, with two or more chosen, the file at the path is the central file of the visible art and each artboard is also written to `<name>_<artboard>.eps` beside it, listed in the reply's `files`). For a native file, `compress: true` gzips it (default: the `useCompression` preference, Preferences › File Handling › Use Compression), `version` (1 to 4) writes it for an older VectorCraft (1 and 2 are never compressed, and `version: 2` with `compress: true` is an error), `preview: true` embeds a PNG of the first artboard, and `precision` (0 to 9) rounds path anchors and handles to that many decimal places for that save only (colors, transforms and type stay exact). `options` passes the format's save options as `run_command document.formats` lists them. When `compress`, `version`, `preview` or `precision` is given both at the top level and in `options` with different values, the call is an error (`` `compress: true` and `options.compress: false` disagree: give the option once ``) and nothing is written. A key in `options` that no save format takes is an error, and nothing is written (see Parameter keys). The save drops each option the written format doesn't read. The reply's `warnings` then name each such option given (for example `` `compress` was not used: SVG files don't read it `` for an `.svg` path, and `` `options.preview` was not used: … `` for a key in `options`; a `.ai` file reads `compress` but not `version`, `preview` or `precision`). For a `.ai` file, these warnings name the kind of `.ai` written: a legacy .ai (version 7) for `aiVersion: "7"`, a non-PDF-compatible .ai for `pdfCompatible: false`, and a PDF-compatible .ai otherwise, including a save that reuses the options the document was last saved with. `run_command document.save` and the other save commands refuse any option given beside the path and in `options` with different values, and give the same warnings for a top-level `compress`, `version`, `preview` or `precision` and for each key in `options` that the format doesn't read. The headless session and the app receive the same `app.save` params, and the app is also sent `wait: true`: the call returns once the file is written, or with the error (such as a path outside the write root), even with Background Save on. When the save asks first (the question before replacing a file that opened with losses, or on macOS and Linux the file dialog for an untitled document without `path`), the reply is `{pending}` or `"cancelled"` as before, and the save that runs after the answer uses the Background Save preference. |
| `export` | `{path?, format?, scale?, artboard?, range?, selection?, outlineText?, options?}` | `svg`, `svgz`, `pdf`, `ai` (the PDF-compatible `.ai` that Save writes, which carries the document when every artboard is written in order), `eps`, `dxf`, `emf`, `wmf`, `png`, `jpg`, `webp`, `gif`, `png8` (an indexed `.png`), `tiff`, `bmp`, `tga`, `psd` (layered), `txt` (the document's text), `vectorcraft` or `template` (a native template). The tool's `format` enum and `document.formats` list them, generated from the engine's format table. When `format` is omitted, it comes from the path's extension. PDF and `.ai` write one page per artboard: all of them, or `artboard` (0-based) / `range` (`"1-3, 5"`, 1-based). SVG and SVGZ write one file per chosen artboard. EPS writes one file of the visible art; with `artboard`, `range`, `options.artboards` or `options: {"useArtboards": true}` it writes one file per chosen artboard, after a central file of the visible art when there are two or more, and `useArtboards: false` keeps the one file. Rasters, DXF, EMF and WMF write one artboard, or one file per chosen artboard with `options: {"useArtboards": true}`. Rasters also take `options: {"rect": [x, y, width, height]}` (page points, y down): that area of the page at the given `ppi`/`scale`, without an artboard and without changing the document; it is refused with `artboard`, `artboards`, `range` or `useArtboards`, with a non-positive or non-finite size, and for non-raster formats. `options` carries more format options (e.g. `{"quality": 80}` for JPEG). `selection: true` exports the selected objects cropped to their bounds (the reply adds their `bounds`, and reports `format` and the encoder's `warnings` as a whole-document export does); `outlineText: true` writes SVG text as paths. Template layers are left out, live effects are kept, and exporting `vectorcraft` never changes the document's path. Without `path` the bytes come back as `dataBase64`. Both backends run the same `document.export` call. |
| `add_text` | `{text, x?, y?, width?, height?, fit?, fitMinPercent?, path?, mode?, pathEffect?, size?, font?, color?, justify?, vertical?}` | Point type at (x, y) (`justify`: `left`, `center` or `right` starts, centres or ends each line on x); area type with `width`/`height` (`fit`: `none`, `autoHeight`, `shrinkText`, as `text.areaOptions`); or `path` + `mode` (`area`/`onPath`) to flow text in or along a path, with `pathEffect` (`rainbow`, `skew`, `3dRibbon`, `stairStep`, `gravity`; SVG export writes type on a path with any effect other than `rainbow` as outlines, with a warning); `justify` (also `auto`, `justifyAll`) aligns the paragraphs of every kind, as `text.setStyle` does later. `vertical: true` uses the existing vertical writing mode for every kind (default `false`, horizontal). Returns `{id}`, with `overflow: true` when the text doesn't fit its frame or path. |
| `apply_effect` | `{effect?, params?, ids?}` | Appends a live effect. Without `effect`, returns the effect catalogue with parameters and defaults. |
+1 -1
View File
@@ -381,4 +381,4 @@ separate. Scores and hours unchanged. [Behavior and command details](development
2026-10-28 behavior correction: Flare ray rotation follows vertical travel at one degree per document point, independent of pointer event count. Shift freezes the current orientation; Space also freezes rotation on the first update. This is the chosen application interaction rate. Scores and estimates unchanged.
## EPS Save evidence (2026-10-26)
Ordinary Save and Save As keep EPS as EPS, with the chosen page frame, transparent margins and the complete editable VectorCraft document. Existing import-loss confirmation and transparency warnings remain; the Save TIFF preview is capped at 1024 pixels, while Export retains 2048. See [file-format-parity.md](file-format-parity.md#eps-save-and-source-protection) for limits and regression evidence. This narrows G9 save-back workflow coverage; native Illustrator editing-data fidelity, parity scores and hour estimates are unchanged.
Ordinary Save and Save As keep EPS as EPS, with the chosen page frame, transparent margins and the complete editable VectorCraft document. Existing import-loss confirmation and transparency warnings remain; the Save TIFF preview is capped at 1024 pixels, while Export retains 2048. See [file-format-parity.md](file-format-parity.md#eps-save-and-source-protection) for limits and regression evidence. This narrows G9 save-back workflow coverage; native Illustrator editing-data fidelity, parity scores and hour estimates are unchanged. Since 2026-11-12 the EPS Options of Save As show Use Artboards, unchecked by default: unchecked, the bounding box fits the visible art; checked, Save keeps the chosen page frame and margins described here.
+3 -1
View File
@@ -619,6 +619,8 @@ since 2026-10-01.
- **Effects in the art's bounds:** Export with Use Artboards off, Fit to Artwork Bounds, and the other exports of the whole art (EPS without artboards, Export for Screens' Full Document, Save for Web (Legacy) with Clip to Artboard off) measure each visible object as the export crops above do. The drop shadows and other effects of groups, layers, type and images are inside the image or the artboard. A Shaper group without effects of its own is measured by the art it shows, and the hidden members of a group with a drop shadow, a glow or another raster effect do not count unless it is a Shaper group or also has a geometry, Pathfinder, Crop Marks or 3D effect or a visible effect on its own fill or stroke. The SVG of an artboard keeps an object whose effects alone reach the artboard. Tests measure a group, type and an image with a drop shadow, export a group and type with Use Artboards off and fit an artboard to them.
- **EPS Save with Use Artboards:** Save As › EPS Options show Use Artboards with All or Range, for a separate file per artboard ([saving in EPS](https://helpx.adobe.com/illustrator/desktop/save-and-export/export-files-to-different-formats/save-artwork-in-eps-format.html)), unchecked by default (the scripting reference's EPS save options list `saveMultipleArtboards` with a default of false, [scripting reference](https://web.archive.org/web/20160803105056/https://wwwimages.adobe.com/content/dam/Adobe/en/devnet/illustrator/sdk/CC2015_3/Illustrator%20JavaScript%20Scripting%20Reference.pdf)). Unchecked, Save writes one file whose bounding box fits all the visible art, as Export does, without the artboards' background colors; when no art shows, the page is the first artboard in its color. An opened EPS saved back this way loses its page margins. Checked with two or more chosen artboards, the file at the path is the central file of the visible art and each artboard is also written to `<name>_<artboard>.eps` beside it. Checked with one chosen artboard, VectorCraft writes that artboard as the page of the file at the path alone (the behavior Save had since #4293). Evidence: `cmd::fileio::tests_artboard_fill::eps_save_draws_the_color_on_an_artboard_page`, `tests::save::eps_save_without_use_artboards_fits_the_box_to_art_past_the_artboard`, `tests::save::opened_eps_saves_to_its_own_file_around_its_art_or_on_its_page`, `tests::save::eps_save_with_use_artboards_writes_the_central_file_and_one_per_artboard`, `dialogs::save_options::tests::eps_save_as_shows_use_artboards_off_and_writes_the_art_or_the_artboard`.
- **Raster export backgrounds (#6125):** a raster export draws its background under the finished art, and blend modes read only the art below them, as on the canvas. An Outer Glow in Screen mode on the bare page shows in a PNG on white and in a JPEG. Flatten Transparency puts its white under the finished art the same way. Evidence: `tests::file::a_screen_outer_glow_is_in_raster_exports`, `tests::objectfx::a_screen_outer_glow_on_the_bare_page_shows_on_a_background`, `tests::knockout::page_knockout_and_isolated_blending`, `tests::knockout::no_blend_mode_reads_the_background`, `tests::cmykflatten::every_blend_mode_flattens_to_the_canvas_pixels_in_cmyk`.
- **Selected artboard deletion:** the Artboards panel menu, panel trash button and Properties use the same selected set; selecting all retains artboard 1. Pointer-event tests cover nonadjacent and single selections, the original first-board survivor, ordinary artwork, Undo/Redo and the single-board guard. Public reference: [Delete artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/delete-artboards.html).
@@ -2433,4 +2435,4 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s
2026-10-28 Flare interaction: the center drag turns only the rays by one degree per document point of increasing pointer y and back at the same rate for decreasing y. This is an application rule independent of event frequency. Shift keeps the current turn; Space during either drag moves the flare without changing its size or turn, including the first drag sample. Releasing a modifier resumes from the current pointer position without catching up skipped rotation. Scores and remaining estimates unchanged.
## EPS Save evidence (2026-10-26)
Ordinary Save and Save As keep EPS as EPS, with the chosen page frame, transparent margins and the complete editable VectorCraft document. Existing import-loss confirmation and transparency warnings remain; the Save TIFF preview is capped at 1024 pixels, while Export retains 2048. See [file-format-parity.md](file-format-parity.md#eps-save-and-source-protection) for limits and regression evidence. This narrows G9 save-back workflow coverage; native Illustrator editing-data fidelity, parity scores and hour estimates are unchanged.
Ordinary Save and Save As keep EPS as EPS, with the chosen page frame, transparent margins and the complete editable VectorCraft document. Existing import-loss confirmation and transparency warnings remain; the Save TIFF preview is capped at 1024 pixels, while Export retains 2048. See [file-format-parity.md](file-format-parity.md#eps-save-and-source-protection) for limits and regression evidence. This narrows G9 save-back workflow coverage; native Illustrator editing-data fidelity, parity scores and hour estimates are unchanged. Since 2026-11-12 the EPS Options of Save As show Use Artboards, unchecked by default: unchecked, the bounding box fits the visible art; checked, Save keeps the chosen page frame and margins described here.