mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 03:37:00 -06:00
EPS: Use Artboards also writes a central file with every artboard
With Use Artboards and two or more artboards, EPS export now writes a central file under the path's own name before the file of each artboard. The central file contains the whole document, and its bounding box is the visible art, as in an export without Use Artboards. An export of a single chosen artboard still writes one file at the path's name, and Export for Screens still writes one EPS per artboard. When no art shows, only the artboard files are written. Docs: the useArtboards params text, the MCP export tool's range text, docs/mcp.md, docs/control-protocol.md and docs/target-app-parity.md.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
//! EPS export for every EPS path (`document.export {format: eps}`, `document.exportEps`, Export As
|
||||
//! → EPS Options): the options parsed from the params, transparency flattened with a flattener
|
||||
//! preset, one file of the art's bounds or one per artboard, the preview and thumbnail rendered,
|
||||
//! preset, one file of the art's bounds or one per artboard (with a central file of the whole
|
||||
//! document when there are several), the preview and thumbnail rendered,
|
||||
//! and the native document embedded for reopening.
|
||||
|
||||
use std::borrow::Cow;
|
||||
@@ -28,7 +29,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Export EPS",
|
||||
[],
|
||||
None,
|
||||
"{path?, level?: 2|3 (default; PostScript language level: 3 writes smooth gradients and masks transparent image pixels out, 2 steps gradients and writes those pixels white), previewFormat?: none|tiffBw|tiffColor (default: a TIFF preview other apps show), transparentPreview?: true (else on white), overprints?: preserve (default)|discard, flattenerPreset?: \"medium\" (default; high, low or a saved preset: flattener.presets.list), flattener?: {the options of object.flattenTransparency over the preset}, embedFonts?: true (type is always written as outlines, so no fonts are needed), includeLinkedFiles?: false (the embedded document keeps linked images whole, not just their previews), thumbnails?: true (a PNG thumbnail in the file), cmykPostScript?: true (RGB documents written in CMYK), compatibleGradients?: false (stepped gradients at level 3 too), selectedOnly?: false, useArtboards?: true (one file per chosen artboard, artboards?/range?, {stem}_{artboard}.eps, the bounding box the artboard) | false (default: one file, the bounding box the visible art, y up from the first artboard's bottom-left corner), format?: \"eps\", artboard?: i | artboards?: [i…] | range?: \"1-3, 5\" (with useArtboards: the artboards written)} → {path, bytes, warnings, files?}; no path → {dataBase64, …}. Transparency is flattened with the preset (EPS has none); the native document rides along in comments, for reopening the file as it was. Same as document.export {format: eps}",
|
||||
"{path?, level?: 2|3 (default; PostScript language level: 3 writes smooth gradients and masks transparent image pixels out, 2 steps gradients and writes those pixels white), previewFormat?: none|tiffBw|tiffColor (default: a TIFF preview other apps show), transparentPreview?: true (else on white), overprints?: preserve (default)|discard, flattenerPreset?: \"medium\" (default; high, low or a saved preset: flattener.presets.list), flattener?: {the options of object.flattenTransparency over the preset}, embedFonts?: true (type is always written as outlines, so no fonts are needed), includeLinkedFiles?: false (the embedded document keeps linked images whole, not just their previews), thumbnails?: true (a PNG thumbnail in the file), cmykPostScript?: true (RGB documents written in CMYK), compatibleGradients?: false (stepped gradients at level 3 too), selectedOnly?: false, useArtboards?: true (one file per chosen artboard, artboards?/range?, {stem}_{artboard}.eps, the bounding box the artboard; with two or more, also the central file {stem}.eps first: the whole document, the bounding box the visible art) | false (default: one file, the bounding box the visible art, y up from the first artboard's bottom-left corner), format?: \"eps\", artboard?: i | artboards?: [i…] | range?: \"1-3, 5\" (with useArtboards: the artboards written)} → {path, bytes, warnings, files?}; no path → {dataBase64, …}. Transparency is flattened with the preset (EPS has none); the native document rides along in comments, for reopening the file as it was. Same as document.export {format: eps}",
|
||||
has_doc,
|
||||
export_eps
|
||||
)]
|
||||
@@ -43,7 +44,7 @@ pub(super) const OPTIONS: &[FormatOption] = &[
|
||||
name: "useArtboards",
|
||||
ty: "boolean",
|
||||
default: "null",
|
||||
description: "true: one file per chosen artboard (default all), <file>_<artboard>.eps; false or absent: one file of the visible art (naming artboards also uses them)",
|
||||
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; false or absent: one file of the visible art (naming artboards also uses them)",
|
||||
},
|
||||
FormatOption { name: "level", ty: "integer", default: "3", description: "PostScript language level, 2 or 3" },
|
||||
FormatOption {
|
||||
@@ -197,7 +198,8 @@ fn expanded(cmd: &str, p: &Value, saved: &[super::super::FlattenerPreset]) -> Re
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// 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)>> {
|
||||
let n = doc.artboards.len();
|
||||
let named = set.boards.resolve(n).map_err(|e| bad(C, e))?;
|
||||
@@ -208,14 +210,18 @@ fn pages(doc: &Document, set: &Settings) -> Result<Vec<(Option<usize>, Rect, Poi
|
||||
(None, None) => None,
|
||||
};
|
||||
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)));
|
||||
match boards {
|
||||
Some(boards) => 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))))
|
||||
.collect(),
|
||||
Some(boards) => {
|
||||
let central = (boards.len() > 1).then(|| vectorcraft_render::encode::art_bounds(doc).map(art)).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()
|
||||
}
|
||||
None => {
|
||||
let art = vectorcraft_render::encode::art_bounds(doc).ok_or_else(|| bad(C, "nothing to export: the document has no visible art"))?;
|
||||
Ok(vec![(None, art, doc.artboards.first().map_or(corner(art), |a| corner(a.rect)))])
|
||||
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)])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,8 +257,8 @@ fn native(doc: &Document, board: Option<usize>, include_linked: bool) -> Vec<u8>
|
||||
}
|
||||
|
||||
/// Encode `doc` as EPS: one file of the visible art, or with Use Artboards one per chosen
|
||||
/// artboard. Transparency is flattened first (once for all of them); the previews show `doc`
|
||||
/// itself.
|
||||
/// artboard (after a central file of the whole document when there are several). Transparency is
|
||||
/// flattened first (once for all of them); the previews show `doc` itself.
|
||||
pub(super) fn encode(doc: &Document, p: &Value) -> Result<Encoded> {
|
||||
let set = parse(p)?;
|
||||
let original = doc;
|
||||
|
||||
@@ -128,7 +128,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Export Document",
|
||||
[],
|
||||
None,
|
||||
"{path?, format?: svg|svgz|pdf|ai|png|jpg|webp|gif|png8|txt|dxf|eps|emf|wmf|tiff|bmp|tga|psd|vectorcraft|template (default: from the path's extension, else png; png8 writes an indexed .png; ait, a .ait template, is written by file.saveAsTemplate; an exporter plug-in's format is plugin.<plug-in id>, which an extension only it lists picks too), params?: {…} (an exporter plug-in's parameters; missing ones take their defaults, see plugin.info), selectedOnly?: false (the selected objects alone, in their layers), artboard?: 0, artboards?: [i…], rect?: [x, y, width, height] (raster formats: that area of the page in points, y down, instead of an artboard; not with artboard, artboards, range or useArtboards), range?: \"1-3, 5\" | \"all\" (1-based; PDF and .ai write one page per artboard, default all; SVG writes one file per artboard, {stem}-{artboard}.svg; raster formats write one artboard), useArtboards?: true (raster: one file per chosen artboard, default all, {stem}-{artboard}.{ext}; pdf/ai: every page) | false (pdf/ai/raster: the bounds of the visible art; SVG has it as an SVG option), ppi?: 72 (raster formats: pixels per inch, stored in the file; wins over scale), scale?: 1 (pixels per point), background?: transparent|white|black|\"#rrggbb\" (jpg: white when transparent), antiAlias?: none|art (default)|type (text snapped to pixels), interlaced?: false (png, Adam7), embedIcc?: true (png: an sRGB chunk marks the colours as sRGB), quality?: 90 (jpg: 0–100), colorModel?: rgb|cmyk|gray (jpg), method?: baseline|optimized|progressive, scans?: 3 (3–5, progressive), embedIcc?: true, imageMap?: none|client|server (an HTML or NCSA map of the objects with a URL, written as <stem>.html / <stem>.map), colors?: 256 (gif/png8: 2–256), reduction?: perceptual|selective (default)|adaptive|web|blackWhite|gray, dither?: none|diffusion (default)|pattern|noise, ditherAmount?: 100, transparency?: true, matte?: white|\"#rrggbb\"|none, interlaced?, lossless?: true (webp: lossy WebP isn't available yet: written lossless, with a warning), encoding?: utf8|utf16 (txt: the stories in stacking order, back to front, a thread once; utf16 with a byte order mark), lineEndings?: lf|crlf, selectionOnly?: false, svg?: {styling, outlineText, images, objectIds, decimals, minify, responsive, useArtboards, preserveEditing, metadata, fewerTspans, hiddenLayers, encoding, profile, embedFonts} (see document.formats), …the SVG options given flat instead, …the PDF options of document.exportPdf, …the DXF options of document.exportDxf (useArtboards: one drawing per artboard), …the EPS options of document.exportEps (useArtboards: one file per artboard, {stem}_{artboard}.eps; else the visible art), …emf and wmf write one picture of the artboard (useArtboards: true one file per chosen artboard, false the bounds of the visible art; EMF keeps curves, clipping, transparent images and gradients as images clipped to their shape; WMF flattens curves into polygons behind a placeable header; what a format leaves out comes back in warnings), colorModel?: rgb|cmyk|gray (tiff: rgb keeps transparency as an alpha channel), lzw?: true, byteOrder?: little|big, embedIcc?: true, colorModel?: rgb|gray (bmp), depth?: 24 (bmp: 1 black and white, 4|8 a palette by reduction and dither, 16, 24, 32 with alpha), fileFormat?: windows|os2 (1, 4, 8 or 24 bits), rle?: false (4 and 8 bits, windows), flipRows?: false (top-down rows), depth?: 24 (tga: 16 one-bit alpha, 24, 32 with alpha), colorModel?: rgb|cmyk|gray (psd), layers?: true (each top-level layer a pixel layer with its opacity and blend mode; a background colour is a Background layer; false: one flat image, on white), maxEditability?: false (layers and sublayers become groups, each object a layer named as in the Layers panel, text by its text; clipping, masked or knockout layers stay one layer), hiddenLayers?: false (hidden layers and objects written hidden instead of left out), embedIcc?: true (at most 30000 pixels a side; formats without alpha are flattened on white)} → {path, format, bytes, warnings, files?: [path…] (several), linked?: [path…] (linked images, image maps)}; no path → {dataBase64, format, bytes, warnings, files?: [{name, dataBase64}], linked?: [{name, dataBase64}]}. Never changes the document's path",
|
||||
"{path?, format?: svg|svgz|pdf|ai|png|jpg|webp|gif|png8|txt|dxf|eps|emf|wmf|tiff|bmp|tga|psd|vectorcraft|template (default: from the path's extension, else png; png8 writes an indexed .png; ait, a .ait template, is written by file.saveAsTemplate; an exporter plug-in's format is plugin.<plug-in id>, which an extension only it lists picks too), params?: {…} (an exporter plug-in's parameters; missing ones take their defaults, see plugin.info), selectedOnly?: false (the selected objects alone, in their layers), artboard?: 0, artboards?: [i…], rect?: [x, y, width, height] (raster formats: that area of the page in points, y down, instead of an artboard; not with artboard, artboards, range or useArtboards), range?: \"1-3, 5\" | \"all\" (1-based; PDF and .ai write one page per artboard, default all; SVG writes one file per artboard, {stem}-{artboard}.svg; raster formats write one artboard), useArtboards?: true (raster: one file per chosen artboard, default all, {stem}-{artboard}.{ext}; pdf/ai: every page) | false (pdf/ai/raster: the bounds of the visible art; SVG has it as an SVG option), ppi?: 72 (raster formats: pixels per inch, stored in the file; wins over scale), scale?: 1 (pixels per point), background?: transparent|white|black|\"#rrggbb\" (jpg: white when transparent), antiAlias?: none|art (default)|type (text snapped to pixels), interlaced?: false (png, Adam7), embedIcc?: true (png: an sRGB chunk marks the colours as sRGB), quality?: 90 (jpg: 0–100), colorModel?: rgb|cmyk|gray (jpg), method?: baseline|optimized|progressive, scans?: 3 (3–5, progressive), embedIcc?: true, imageMap?: none|client|server (an HTML or NCSA map of the objects with a URL, written as <stem>.html / <stem>.map), colors?: 256 (gif/png8: 2–256), reduction?: perceptual|selective (default)|adaptive|web|blackWhite|gray, dither?: none|diffusion (default)|pattern|noise, ditherAmount?: 100, transparency?: true, matte?: white|\"#rrggbb\"|none, interlaced?, lossless?: true (webp: lossy WebP isn't available yet: written lossless, with a warning), encoding?: utf8|utf16 (txt: the stories in stacking order, back to front, a thread once; utf16 with a byte order mark), lineEndings?: lf|crlf, selectionOnly?: false, svg?: {styling, outlineText, images, objectIds, decimals, minify, responsive, useArtboards, preserveEditing, metadata, fewerTspans, hiddenLayers, encoding, profile, embedFonts} (see document.formats), …the SVG options given flat instead, …the PDF options of document.exportPdf, …the DXF options of document.exportDxf (useArtboards: one drawing per artboard), …the EPS options of document.exportEps (useArtboards: one file per artboard, {stem}_{artboard}.eps, after a central {stem}.eps of the whole document when there are two or more; else the visible art), …emf and wmf write one picture of the artboard (useArtboards: true one file per chosen artboard, false the bounds of the visible art; EMF keeps curves, clipping, transparent images and gradients as images clipped to their shape; WMF flattens curves into polygons behind a placeable header; what a format leaves out comes back in warnings), colorModel?: rgb|cmyk|gray (tiff: rgb keeps transparency as an alpha channel), lzw?: true, byteOrder?: little|big, embedIcc?: true, colorModel?: rgb|gray (bmp), depth?: 24 (bmp: 1 black and white, 4|8 a palette by reduction and dither, 16, 24, 32 with alpha), fileFormat?: windows|os2 (1, 4, 8 or 24 bits), rle?: false (4 and 8 bits, windows), flipRows?: false (top-down rows), depth?: 24 (tga: 16 one-bit alpha, 24, 32 with alpha), colorModel?: rgb|cmyk|gray (psd), layers?: true (each top-level layer a pixel layer with its opacity and blend mode; a background colour is a Background layer; false: one flat image, on white), maxEditability?: false (layers and sublayers become groups, each object a layer named as in the Layers panel, text by its text; clipping, masked or knockout layers stay one layer), hiddenLayers?: false (hidden layers and objects written hidden instead of left out), embedIcc?: true (at most 30000 pixels a side; formats without alpha are flattened on white)} → {path, format, bytes, warnings, files?: [path…] (several), linked?: [path…] (linked images, image maps)}; no path → {dataBase64, format, bytes, warnings, files?: [{name, dataBase64}], linked?: [{name, dataBase64}]}. Never changes the document's path",
|
||||
has_doc,
|
||||
export::export
|
||||
),
|
||||
|
||||
@@ -80,8 +80,13 @@ fn use_artboards_writes_a_file_per_artboard_named_with_an_underscore() {
|
||||
let r = s.execute("document.export", &json!({"format": "eps", "useArtboards": true, "previewFormat": "none"})).unwrap();
|
||||
let files = r["files"].as_array().unwrap();
|
||||
let names: Vec<&str> = files.iter().map(|f| f["name"].as_str().unwrap()).collect();
|
||||
assert_eq!(names, ["Untitled-1_Artboard-1.eps", "Untitled-1_Artboard-2.eps"]);
|
||||
for f in files {
|
||||
assert_eq!(names, ["Untitled-1.eps", "Untitled-1_Artboard-1.eps", "Untitled-1_Artboard-2.eps"]);
|
||||
// The central file comes first: the whole document, bounded by the visible art of both artboards.
|
||||
let central = b64(&files[0]);
|
||||
assert_eq!(dsc(&postscript(¢ral), "BoundingBox").as_deref(), Some(format!("10 60 {} 90", second + 40.0).as_str()));
|
||||
let native = vectorcraft_format::load(&vectorcraft_eps::native(¢ral).unwrap()).unwrap();
|
||||
assert_eq!((native.artboards.len(), native.layers[0].children().unwrap().len()), (2, 2));
|
||||
for f in &files[1..] {
|
||||
let bytes = b64(f);
|
||||
let ps = postscript(&bytes);
|
||||
assert_eq!(dsc(&ps, "BoundingBox").as_deref(), Some("0 0 100 100"));
|
||||
@@ -89,6 +94,10 @@ fn use_artboards_writes_a_file_per_artboard_named_with_an_underscore() {
|
||||
let native = vectorcraft_format::load(&vectorcraft_eps::native(&bytes).unwrap()).unwrap();
|
||||
assert_eq!(native.artboards.len(), 1);
|
||||
}
|
||||
// Named artboards, two or more, get the central file too.
|
||||
let r = s.execute("document.exportEps", &json!({"artboards": [1, 0], "previewFormat": "none"})).unwrap();
|
||||
let names: Vec<&str> = r["files"].as_array().unwrap().iter().map(|f| f["name"].as_str().unwrap()).collect();
|
||||
assert_eq!(names, ["Untitled-1.eps", "Untitled-1_Artboard-2.eps", "Untitled-1_Artboard-1.eps"]);
|
||||
// A range names the artboards (useArtboards implied); one file keeps the path's name.
|
||||
let r = s.execute("document.exportEps", &json!({"range": "2", "previewFormat": "none"})).unwrap();
|
||||
assert!(r.get("files").is_none());
|
||||
@@ -97,6 +106,30 @@ fn use_artboards_writes_a_file_per_artboard_named_with_an_underscore() {
|
||||
// Without art there is nothing to bound.
|
||||
let mut empty = session(100.0, 100.0, 1);
|
||||
assert!(empty.execute("document.exportEps", &json!({})).unwrap_err().to_string().contains("nothing to export"));
|
||||
// Artboards without art still get their files, but no central file.
|
||||
let mut empty = session(100.0, 100.0, 2);
|
||||
let r = empty.execute("document.exportEps", &json!({"useArtboards": true, "previewFormat": "none"})).unwrap();
|
||||
assert_eq!(r["files"].as_array().map(Vec::len), Some(2), "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_artboards_writes_the_central_file_beside_the_artboard_files() {
|
||||
let mut s = session(100.0, 100.0, 2);
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 30, "height": 30})).unwrap();
|
||||
let dir = std::env::temp_dir().join(format!("vc-epsboards-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("logo.eps").to_string_lossy().to_string();
|
||||
s.execute("document.exportEps", &json!({"path": path, "useArtboards": true, "previewFormat": "none"})).unwrap();
|
||||
let mut written: Vec<String> = std::fs::read_dir(&dir).unwrap().map(|e| e.unwrap().file_name().to_string_lossy().into_owned()).collect();
|
||||
written.sort();
|
||||
assert_eq!(written, ["logo.eps", "logo_Artboard-1.eps", "logo_Artboard-2.eps"]);
|
||||
let central = std::fs::read(&path).unwrap();
|
||||
assert_eq!(dsc(&postscript(¢ral), "BoundingBox").as_deref(), Some("10 60 40 90"));
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
// Export for Screens still writes one EPS per artboard, with no central file.
|
||||
let rows = json!({"formats": [{"format": "eps", "useArtboards": true}], "settings": {"eps": {"useArtboards": true}}});
|
||||
let r = s.execute("document.exportForScreens", &rows).unwrap();
|
||||
assert_eq!(r["files"].as_array().map(Vec::len), Some(2), "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user