mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 14:41:52 +00:00
Export Selection: the picked format's options dialog follows the save panel
document.exportSelection run with no params or {} (a shortcut, or an
automation call) wrote the picked file straight away with each format's
defaults: a PDF used the default preset, so Save PDF's options, such as
writing long pages in points, could not be chosen. It now takes Export
As's route after the save panel: Save PDF without its Artboards
section, SVG Options without its artboards, the raster options, EPS
Options, DXF Options, Text Export Options with Selection Only on, or an
exporter plug-in's parameters. OK writes the selection alone, cropped
to it, with the chosen options. While a dialog is open the call returns
null, and OK returns what file.exportAs returns, without format or
bounds.
With a path or any option it still writes straight away.
- file.exportAs takes exportSelection: true and then writes the
document that document.exportSelection writes
(fileio::selection_doc), without artboard choices. The options
dialogs and the replace-file questions pass it on.
- Nothing selected stops before the save panel.
- SVG Options opened by Export Selection leaves the remembered Use
Artboards as it was.
- Exports through file.exportAs (Export As and the options dialogs)
apply Appearance of Black's rich black, as document.export does.
Docs: docs/mcp.md, docs/control-protocol.md, docs/target-app-parity.md.
This commit is contained in:
@@ -70,7 +70,7 @@ pub use encode::{ARTBOARD_PARAMS, ArtboardPick, Encoded, Note, NoteKind, encode,
|
||||
pub(crate) use encode::{anti_alias, background, with_single_artboard};
|
||||
pub(crate) use export::with_artboard_fills;
|
||||
pub(crate) use export::{EXPORT_PARAMS, export_crop, isolated};
|
||||
pub use export::{export_doc, export_source, unused_option_notes};
|
||||
pub use export::{export_doc, export_source, selection_doc, unused_option_notes};
|
||||
use load::err;
|
||||
pub(crate) use load::import_svg;
|
||||
pub(crate) use load::is_placeholder;
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Export Selection…",
|
||||
["File"],
|
||||
None,
|
||||
"{path?, format?: png|jpg|webp|svg|svgz|pdf|ai|gif|png8|tiff|bmp|tga|psd|txt|dxf|eps|emf|wmf|hpgl|vectorcraft|template (default: from the extension, else png), ppi?: number (raster formats: pixels per inch, stored in the file; wins over scale; without either, the document's raster effects resolution), scale?: number (pixels per point), …the format's options} the selected objects (template layers left out), each in the layers and groups that hold it, whose clipping paths, opacity masks, effects, opacity and blend modes apply as on the canvas (an object of a blend, envelope, repeat or intertwine on its own; txt: the selected type alone), cropped to the bounds the renderer measures for that art (a clip group by its clip) → {path, format, bytes, warnings, bounds} (no path → {dataBase64, format, bytes, warnings, bounds}); warnings say what the format approximated or left out, as for document.export. A key no format takes, and an unknown key inside a PDF or EPS option's object, are refused; a key only another format takes is not used, and warnings name it. Under automation read roots, svg and svgz are refused while the document links a JavaScript file outside them, as document.export refuses them",
|
||||
"{path?, format?: png|jpg|webp|svg|svgz|pdf|ai|gif|png8|tiff|bmp|tga|psd|txt|dxf|eps|emf|wmf|hpgl|vectorcraft|template (default: from the extension, else png), ppi?: number (raster formats: pixels per inch, stored in the file; wins over scale; without either, the document's raster effects resolution), scale?: number (pixels per point), …the format's options} the selected objects (template layers left out), each in the layers and groups that hold it, whose clipping paths, opacity masks, effects, opacity and blend modes apply as on the canvas (an object of a blend, envelope, repeat or intertwine on its own; txt: the selected type alone), cropped to the bounds the renderer measures for that art (a clip group by its clip) → {path, format, bytes, warnings, bounds} (no path → {dataBase64, format, bytes, warnings, bounds}); warnings say what the format approximated or left out, as for document.export. A key no format takes, and an unknown key inside a PDF or EPS option's object, are refused; a key only another format takes is not used, and warnings name it. Under automation read roots, svg and svgz are refused while the document links a JavaScript file outside them, as document.export refuses them. In the app, with no params or {}, the save panel picks the file and its format's options dialog follows (Save PDF, SVG Options, the raster options, EPS Options, DXF Options, Text Export Options or an exporter plug-in's parameters; other formats are written at once), whose OK writes the selection: the call returns null while the dialog is open, and OK returns what file.exportAs returns, without format or bounds (Text Export Options: {path})",
|
||||
has_selection,
|
||||
export::export_selection
|
||||
),
|
||||
|
||||
@@ -296,8 +296,14 @@ fn source<'a>(s: &'a mut Session, f: &Format, p: &Value, cmd: &str) -> Result<(C
|
||||
/// ([`isolated`]), with one artboard: their crop. With `text`, the objects alone on one layer, so a
|
||||
/// text export holds the type of the selected objects and not that of a clipping path around them.
|
||||
fn selection(s: &mut Session, cmd: &str, text: bool) -> Result<(Document, vectorcraft_geom::Rect)> {
|
||||
let ids = selected_artwork(s)?;
|
||||
let doc = &s.doc()?.doc;
|
||||
selection_doc(s.doc()?, cmd, text)
|
||||
}
|
||||
|
||||
/// The document an Export Selection of `st` writes and its crop, as `document.exportSelection`
|
||||
/// writes it ([`selection`]); `cmd` names the command in the error when nothing selected draws.
|
||||
pub fn selection_doc(st: &DocState, cmd: &str, text: bool) -> Result<(Document, vectorcraft_geom::Rect)> {
|
||||
let ids = artwork_of(st);
|
||||
let doc = &st.doc;
|
||||
let (mut d, bounds) = isolated(doc, &ids, "Selection").ok_or_else(|| bad(cmd, "select something to export"))?;
|
||||
if text {
|
||||
let mut layer = Node::layer(d.alloc_id(), "Selection", vectorcraft_doc::LayerColor::Preset(0));
|
||||
@@ -313,12 +319,16 @@ fn selection(s: &mut Session, cmd: &str, text: bool) -> Result<(Document, vector
|
||||
/// layers are guides and are left out. With an object of a dynamic symbol instance's art selected,
|
||||
/// the instance is exported whole.
|
||||
pub(crate) fn selected_artwork(s: &Session) -> Result<Vec<NodeId>> {
|
||||
let st = s.doc()?;
|
||||
Ok(artwork_of(s.doc()?))
|
||||
}
|
||||
|
||||
/// [`selected_artwork`] of document `st`.
|
||||
fn artwork_of(st: &DocState) -> Vec<NodeId> {
|
||||
let ids = edit::roots_of(&st.doc, st.selection.in_paint_order(&st.doc));
|
||||
let is_template = |id| st.doc.node(id).is_some_and(|l| matches!(l.kind, NodeKind::Layer { template: true, .. }));
|
||||
// On a template layer or sublayer, at any depth.
|
||||
let on_template = |id| st.doc.ancestry(id).is_some_and(|a| a.into_iter().any(is_template));
|
||||
Ok(ids.into_iter().filter(|id| !on_template(*id)).collect())
|
||||
ids.into_iter().filter(|id| !on_template(*id)).collect()
|
||||
}
|
||||
|
||||
/// The crop of an export of objects `ids` of `doc` alone ([`isolated`]): the bounds the renderer
|
||||
|
||||
@@ -1330,7 +1330,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"file.exportAs",
|
||||
"Export As…",
|
||||
"",
|
||||
"{format?, useArtboards?, range?} opens Export As (then the format's options); with {path, …document.export options} writes the file(s) → {path, warnings, files?}; under automation read roots, an SVG or SVGZ is refused while the document links a JavaScript file outside them, as document.export refuses it",
|
||||
"{format?, useArtboards?, range?} opens Export As (then the format's options); with {path, …document.export options} writes the file(s) → {path, warnings, files?}; exportSelection: true writes the selection alone, cropped to it, as document.exportSelection writes it (artboard options are not used); under automation read roots, an SVG or SVGZ is refused while the document links a JavaScript file outside them, as document.export refuses it",
|
||||
),
|
||||
(
|
||||
"ui.fileInfoDialog",
|
||||
|
||||
@@ -153,7 +153,7 @@ pub use color_picker::open as open_color_picker;
|
||||
pub use document_setup::open as open_document_setup;
|
||||
pub use effect::open as open_effect_dialog;
|
||||
pub(crate) use effect::{FILM_GRAIN_FIELDS, PAINT_DAUB_BRUSH_TYPE_LABELS, SHADES, shown_choices};
|
||||
pub use export_as::open as open_export_as;
|
||||
pub use export_as::{export_selection, open as open_export_as};
|
||||
pub(crate) use export_for_screens::drop_unread_screen_settings;
|
||||
pub use export_for_screens::open as open_export_for_screens;
|
||||
pub(crate) use export_for_screens::{
|
||||
@@ -945,6 +945,8 @@ mod tests_accessibility;
|
||||
#[cfg(test)]
|
||||
mod tests_export;
|
||||
#[cfg(test)]
|
||||
mod tests_export_selection;
|
||||
#[cfg(test)]
|
||||
mod tests_import_pdf;
|
||||
#[cfg(test)]
|
||||
mod tests_package;
|
||||
|
||||
@@ -158,8 +158,11 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
// Hidden for Export Selection, which writes the selected art alone.
|
||||
let selection = app.session.active().is_some_and(|st| !st.selection.is_empty());
|
||||
if widgets::check(ui, tl!("Export Selected Art Only"), d.bool("selectedOnly") && selection, selection) {
|
||||
if !d.bool(super::export_as::SELECTION_FIELD)
|
||||
&& widgets::check(ui, tl!("Export Selected Art Only"), d.bool("selectedOnly") && selection, selection)
|
||||
{
|
||||
d.fields.insert("selectedOnly".into(), json!(!d.bool("selectedOnly")));
|
||||
}
|
||||
check(ui, d, "alterPaths", tl!("Alter Paths for Appearance"));
|
||||
@@ -227,5 +230,5 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
app.ui.dxf_options = Value::Object(remembered.into_iter().flatten().collect());
|
||||
app.ui.dialog = None;
|
||||
let path = d.fields.get("path").and_then(Value::as_str).map(str::to_string);
|
||||
io::export(app, Some("dxf"), path, ¶ms)
|
||||
io::export(app, Some("dxf"), path, &super::export_as::exported(d, params))
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
|
||||
};
|
||||
|
||||
/// Fields that are not remembered between exports (they belong to one export).
|
||||
const PER_EXPORT: [&str; 7] = ["format", "path", "useArtboards", "range", "artboard", "artboards", "selectedOnly"];
|
||||
const PER_EXPORT: [&str; 8] = ["format", "path", "useArtboards", "range", "artboard", "artboards", "selectedOnly", super::export_as::SELECTION_FIELD];
|
||||
|
||||
/// The options a first EPS Options dialog starts from (the export's defaults).
|
||||
fn defaults() -> Map<String, Value> {
|
||||
@@ -133,5 +133,5 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
Value::Object(d.fields.iter().filter(|(k, _)| !PER_EXPORT.contains(&k.as_str())).map(|(k, v)| (k.clone(), v.clone())).collect());
|
||||
app.ui.dialog = None;
|
||||
let path = d.fields.get("path").and_then(Value::as_str).map(str::to_string);
|
||||
io::export(app, Some("eps"), path, ¶ms)
|
||||
io::export(app, Some("eps"), path, &super::export_as::exported(d, params))
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
//! picks the file, then the format's options dialog follows (PNG/JPEG/WebP/PNG-8/GIF Options, SVG
|
||||
//! Options, Text Export Options, DXF Options, EPS Options, and Save PDF),
|
||||
//! else the file is written. DWG is listed, greyed out, with what to use instead. The formats of
|
||||
//! installed exporter plug-ins come after the built-in ones: they write the whole document, and one
|
||||
//! with parameters asks for them before writing (`plugin::open_export`).
|
||||
//! installed exporter plug-ins come after the built-in ones: they write the whole document (from
|
||||
//! Export Selection, the selection), and one with parameters asks for them before writing
|
||||
//! (`plugin::open_export`). Export Selection ([`export_selection`]) takes the same route after its
|
||||
//! save panel, and every format then writes the selection alone, cropped to it.
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
@@ -144,12 +146,52 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
app.ui.dialog = None;
|
||||
let path = io::export_path(app, None, f)?;
|
||||
let range = if all { "all".to_string() } else { range };
|
||||
let r = options_or_write(app, f, plugin, path, use_artboards, all, range);
|
||||
let r = options_or_write(app, f, plugin, path, Pages { use_artboards, all, range, selection: false });
|
||||
// The options follow the save panel: the Enter that closed it isn't theirs (as in Save As).
|
||||
super::opened_by_panel(app);
|
||||
r
|
||||
}
|
||||
|
||||
/// The options dialogs' field that is true when Export Selection opened them: OK then writes the
|
||||
/// selection alone ([`exported`]). Not remembered with the dialog's options.
|
||||
pub(super) const SELECTION_FIELD: &str = "__selection";
|
||||
|
||||
/// `params` of an options dialog `d` for [`io::export`]: with [`io::EXPORT_SELECTION`] when
|
||||
/// Export Selection opened `d`.
|
||||
pub(super) fn exported(d: &Dialog, mut params: Value) -> Value {
|
||||
if d.bool(SELECTION_FIELD)
|
||||
&& let Some(o) = params.as_object_mut()
|
||||
{
|
||||
o.insert(io::EXPORT_SELECTION.into(), json!(true));
|
||||
}
|
||||
params
|
||||
}
|
||||
|
||||
/// File › Export Selection (`document.exportSelection` without params): the save panel picks the
|
||||
/// file and its format, then the format's options dialog follows as in Export As, else the file is
|
||||
/// written. Each route writes the selected objects alone, cropped to them. Nothing selected to
|
||||
/// export stops before the save panel.
|
||||
pub fn export_selection(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
let st = app.session.active().ok_or("no document")?;
|
||||
fileio::selection_doc(st, "document.exportSelection", false).map_err(|e| e.to_string())?;
|
||||
let path = io::selection_path(app)?;
|
||||
let f = fileio::writable_format(None, Some(&path))?;
|
||||
let plugin = vectorcraft_plugins::export::by_format(f.id);
|
||||
let pages = Pages { use_artboards: false, all: true, range: String::new(), selection: true };
|
||||
let r = options_or_write(app, f, plugin, path, pages);
|
||||
super::opened_by_panel(app);
|
||||
r
|
||||
}
|
||||
|
||||
/// What an export writes: with `use_artboards` the artboards (`all`, else the 1-based `range`),
|
||||
/// else the bounds of the visible art, or with `selection` the selection alone.
|
||||
struct Pages {
|
||||
use_artboards: bool,
|
||||
all: bool,
|
||||
range: String,
|
||||
selection: bool,
|
||||
}
|
||||
|
||||
/// After the save panel picked `path`: the options dialog of format `f` (or of its exporter
|
||||
/// plug-in), else the file written.
|
||||
fn options_or_write(
|
||||
@@ -157,11 +199,10 @@ fn options_or_write(
|
||||
f: &'static Format,
|
||||
plugin: Option<std::sync::Arc<vectorcraft_plugins::Plugin>>,
|
||||
path: String,
|
||||
use_artboards: bool,
|
||||
all: bool,
|
||||
range: String,
|
||||
pages: Pages,
|
||||
) -> Result<Value, String> {
|
||||
match f.id {
|
||||
let Pages { use_artboards, all, range, selection } = pages;
|
||||
let r = match f.id {
|
||||
"svg" | "svgz" => {
|
||||
svg_options::open(app, svg_options::Mode::Export, Some(&path));
|
||||
if let Some(o) = app.ui.dialog.as_mut() {
|
||||
@@ -169,24 +210,33 @@ fn options_or_write(
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
// Save PDF writes the artboards as pages, or one page of the art's bounds.
|
||||
// Save PDF writes the artboards as pages, or one page of the art's bounds or the selection.
|
||||
"pdf" if use_artboards => save_pdf::open(app, &json!({ "path": path, "range": (!all).then_some(range) })),
|
||||
"pdf" => save_pdf::open(app, &json!({ "path": path, "useArtboards": false })),
|
||||
"txt" => {
|
||||
text_export::open(app, &path);
|
||||
if let Some(o) = app.ui.dialog.as_mut().filter(|_| selection) {
|
||||
o.fields.insert("selectionOnly".into(), json!(true));
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
_ => {
|
||||
let mut params = json!({ "format": f.id, "path": path });
|
||||
// A format that holds the whole document (a template) has no Use Artboards: the
|
||||
// export is not given the option, which its warnings would name as not used.
|
||||
if plugin.is_some() || f.options.iter().any(|o| o.name == "useArtboards") {
|
||||
if !selection && (plugin.is_some() || f.options.iter().any(|o| o.name == "useArtboards")) {
|
||||
params["useArtboards"] = json!(use_artboards);
|
||||
if use_artboards {
|
||||
params["range"] = json!(range);
|
||||
}
|
||||
}
|
||||
if let Some(plugin) = plugin.filter(|p| !p.manifest().params.is_empty()) {
|
||||
let plugin = plugin.filter(|p| !p.manifest().params.is_empty());
|
||||
let dialog = plugin.is_none() && (f.raster || matches!(f.id, "dxf" | "eps"));
|
||||
if selection {
|
||||
// An options dialog keeps the mark as a field of its own, which OK hands on.
|
||||
params[if dialog { SELECTION_FIELD } else { io::EXPORT_SELECTION }] = json!(true);
|
||||
}
|
||||
if let Some(plugin) = plugin {
|
||||
plugin::open_export(app, &plugin, params);
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
@@ -202,7 +252,12 @@ fn options_or_write(
|
||||
eps_options::open(app, ¶ms);
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
io::export(app, Some(f.id), Some(path), ¶ms)
|
||||
return io::export(app, Some(f.id), Some(path), ¶ms);
|
||||
}
|
||||
};
|
||||
// Export Selection's mark on the options dialog that opened.
|
||||
if let Some(o) = app.ui.dialog.as_mut().filter(|_| selection) {
|
||||
o.fields.insert(SELECTION_FIELD.into(), json!(true));
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
@@ -53,7 +53,11 @@ fn format_of(d: &Dialog) -> Option<&'static Format> {
|
||||
/// Open the options of raster format `f` for an export whose params (format, path, artboard
|
||||
/// choice) are `params`.
|
||||
pub fn open(app: &mut VectorcraftApp, f: &Format, mut params: Value) {
|
||||
let size = app.session.active().and_then(|st| export_size(&st.doc, ¶ms));
|
||||
let selection = params[super::export_as::SELECTION_FIELD] == true;
|
||||
let size = app.session.active().and_then(|st| match selection {
|
||||
true => vectorcraft_engine::cmd::fileio::selection_doc(st, "document.exportSelection", false).ok().map(|(_, r)| (r.width(), r.height())),
|
||||
false => export_size(&st.doc, ¶ms),
|
||||
});
|
||||
if let Some(o) = params.as_object_mut() {
|
||||
o.insert("ppi".into(), json!(72));
|
||||
o.extend(defaults(app, f));
|
||||
@@ -340,5 +344,5 @@ pub(super) fn choice(ui: &mut egui::Ui, d: &mut Dialog, key: &str, ids: &[&str],
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let (format, path) = (d.str("format"), d.str("path"));
|
||||
app.ui.dialog = None;
|
||||
io::export(app, Some(&format), Some(path), &form::params(d))
|
||||
io::export(app, Some(&format), Some(path), &super::export_as::exported(d, form::params(d)))
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
//! `ui.dialog.set`. OK runs `document.exportPdf` through [`io::export_pdf`]; the Summary is
|
||||
//! `document.pdfSettings`. Save Preset… names the settings as a preset (`pdf.preset.save`).
|
||||
//! Opened from Export As with Use Artboards off (`__artBounds`), the dialog has no Artboards
|
||||
//! section, and OK writes one page of the visible art's bounds through [`io::export`].
|
||||
//! section, and OK writes one page of the visible art's bounds through [`io::export`]; opened
|
||||
//! from Export Selection (`__selection` too), the page is the selection's crop.
|
||||
//!
|
||||
//! The same dialog edits a preset (kind `pdfPreset`, from Edit → PDF Presets): a `name` and
|
||||
//! `description` instead of the preset and artboards, and OK saves the preset.
|
||||
@@ -154,7 +155,7 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
}
|
||||
let r = match SaveMode::of(&d.str("__save")) {
|
||||
Some(mode) => io::save_pdf(app, mode, params(d)),
|
||||
None if d.bool(ART_BOUNDS) => export_art(app, params(d)),
|
||||
None if d.bool(ART_BOUNDS) => export_art(app, d),
|
||||
None => io::export_pdf(app, params(d)),
|
||||
};
|
||||
// A question the write asked (replace the file the document was opened from?) stays open.
|
||||
@@ -164,12 +165,14 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
r
|
||||
}
|
||||
|
||||
/// A page of the art's bounds (Export As with Use Artboards off): written as Export As writes
|
||||
/// other formats ([`io::export`]), with the dialog's options and a saved preset written out.
|
||||
fn export_art(app: &mut VectorcraftApp, mut p: Value) -> Result<Value, String> {
|
||||
/// A page of the art's bounds (Export As with Use Artboards off), or of the selection's crop
|
||||
/// (Export Selection): written as Export As writes other formats ([`io::export`]), with the
|
||||
/// dialog's options and a saved preset written out.
|
||||
fn export_art(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let mut p = params(d);
|
||||
let path = p.as_object_mut().and_then(|o| o.remove("path")).and_then(|v| v.as_str().map(str::to_string));
|
||||
let p = pdf::expand_preset(&app.session, "file.exportAs", &p).map_err(|e| e.to_string())?.into_owned();
|
||||
io::export(app, Some("pdf"), path, &p)
|
||||
io::export(app, Some("pdf"), path, &super::export_as::exported(d, p))
|
||||
}
|
||||
|
||||
/// The settings as `pdf.preset.save` params (without the artboards and path).
|
||||
|
||||
@@ -129,15 +129,23 @@ fn options(d: &Dialog) -> Value {
|
||||
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let opts = options(d);
|
||||
if Mode::of(d) == Mode::Export && d.bool("useArtboards") {
|
||||
let selection = d.bool(super::export_as::SELECTION_FIELD);
|
||||
if Mode::of(d) == Mode::Export && d.bool("useArtboards") && !selection {
|
||||
let n = app.session.active().map_or(0, |st| st.doc.artboards.len());
|
||||
let pick: vectorcraft_engine::cmd::fileio::ArtboardPick = serde_json::from_value(opts.clone()).map_err(|e| e.to_string())?;
|
||||
pick.resolve(n)?;
|
||||
}
|
||||
// Remembered for next time, without the document's artboard range.
|
||||
// Remembered for next time, without the document's artboard range. Export Selection shows no
|
||||
// Use Artboards, so the one remembered before stays.
|
||||
let mut last = opts.clone();
|
||||
if let Some(o) = last.as_object_mut() {
|
||||
o.remove("range");
|
||||
if selection {
|
||||
match app.ui.svg_options.get("useArtboards") {
|
||||
Some(v) => o.insert("useArtboards".into(), v.clone()),
|
||||
None => o.remove("useArtboards"),
|
||||
};
|
||||
}
|
||||
}
|
||||
app.ui.svg_options = last;
|
||||
app.ui.dialog = None;
|
||||
@@ -145,6 +153,7 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
if let Some(p) = d.fields.get("path").filter(|p| p.is_string()) {
|
||||
params["path"] = p.clone();
|
||||
}
|
||||
let params = super::export_as::exported(d, params);
|
||||
let id = match Mode::of(d) {
|
||||
Mode::Export => "file.export.svg",
|
||||
Mode::Save => "file.saveAs",
|
||||
@@ -177,8 +186,11 @@ fn check(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str, enabled: boo
|
||||
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
option_fields(ui, d, false);
|
||||
// Export Selection writes the selection's crop, without artboards to choose.
|
||||
if Mode::of(d) == Mode::Export {
|
||||
artboards(app, ui, d);
|
||||
if !d.bool(super::export_as::SELECTION_FIELD) {
|
||||
artboards(app, ui, d);
|
||||
}
|
||||
} else {
|
||||
check(ui, d, "preserveEditing", tl!("Preserve Editing Capabilities"), true);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
//! Export Selection (`document.exportSelection` without params): the picked format's options
|
||||
//! dialog follows the save panel, as in Export As, and writes the selection alone.
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_plugins::wat::{self as tpl, Echo};
|
||||
|
||||
use super::tests_export::{Written, app_saving, frame, kind, names, set};
|
||||
use super::*;
|
||||
|
||||
/// The last file written, as text.
|
||||
fn last_text(written: &Written) -> String {
|
||||
String::from_utf8_lossy(&written.borrow().last().unwrap().1).into_owned()
|
||||
}
|
||||
|
||||
/// The width and height in a PNG's header.
|
||||
fn png_size(png: &[u8]) -> (u32, u32) {
|
||||
let n = |at: usize| u32::from_be_bytes([png[at], png[at + 1], png[at + 2], png[at + 3]]);
|
||||
(n(16), n(20))
|
||||
}
|
||||
|
||||
/// Pick `path` in the next save panels.
|
||||
fn picking(app: &mut VectorcraftApp, path: &str) {
|
||||
let picked = path.to_string();
|
||||
app.services.pick_save = Some(Box::new(move |_| Some(picked.clone())));
|
||||
}
|
||||
|
||||
/// How many paths a native document's JSON (or the input an exporter plug-in is given) holds.
|
||||
fn paths_in(bytes: &[u8]) -> usize {
|
||||
String::from_utf8_lossy(vectorcraft_format::json_part(bytes)).matches("\"anchors\"").count()
|
||||
}
|
||||
|
||||
/// A PDF continues in Save PDF, without its artboard choice, and its options apply: the page is
|
||||
/// the selected 10 m rectangle's crop (not the art's bounds, which take in the ellipse), with a
|
||||
/// `UserUnit` once "Write pages longer than 200 in at full size in points" is turned off.
|
||||
#[test]
|
||||
fn export_selection_to_pdf_continues_in_save_pdf() {
|
||||
let (mut app, written) = app_saving(2, |_| "/out/sel.pdf".into());
|
||||
app.run("shape.rectangle", json!({"x": 0, "y": 100, "width": 28_346.46, "height": 50})).unwrap();
|
||||
app.run("document.exportSelection", json!({})).unwrap();
|
||||
assert_eq!(kind(&app), Some("savePdf"), "the PDF options follow");
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!(d.str("path"), "/out/sel.pdf");
|
||||
assert!(d.bool("__artBounds") && d.bool(export_as::SELECTION_FIELD));
|
||||
assert!(written.borrow().is_empty(), "nothing is written before OK");
|
||||
set(&mut app, "longPagesInPoints", json!(false));
|
||||
frame(&mut app);
|
||||
assert_eq!(confirm(&mut app).unwrap()["path"], "/out/sel.pdf");
|
||||
assert!(app.ui.dialog.is_none());
|
||||
let bytes = written.borrow()[0].1.clone();
|
||||
assert!(String::from_utf8_lossy(&bytes).contains("/UserUnit"), "the dialog's option applies");
|
||||
let pdf = vectorcraft_pdf::import(&bytes).unwrap();
|
||||
assert_eq!(pdf.artboards.len(), 1);
|
||||
let page = pdf.artboards[0].rect;
|
||||
assert!((28_346.0..28_350.0).contains(&page.width()), "{}", page.width());
|
||||
assert_eq!(page.height().round(), 50.0, "the selection's crop");
|
||||
}
|
||||
|
||||
/// Every format with an options dialog shows it after the save panel, and OK writes the
|
||||
/// selection alone, cropped to the selected 10×5 pt rectangle: the PNG at 72 ppi, the SVG's view
|
||||
/// box, the EPS bounding box and the DXF's extents. The mark is not kept with the options
|
||||
/// remembered for the next export.
|
||||
#[test]
|
||||
fn export_selection_continues_in_each_format_s_options() {
|
||||
let (mut app, written) = app_saving(2, |_| String::new());
|
||||
app.run("shape.rectangle", json!({"x": 40, "y": 30, "width": 10, "height": 5})).unwrap();
|
||||
for (ext, options) in [("svg", "svgOptions"), ("png", "pngOptions"), ("eps", "epsOptions"), ("dxf", "dxfOptions")] {
|
||||
let path = format!("/out/sel.{ext}");
|
||||
picking(&mut app, &path);
|
||||
app.run("document.exportSelection", json!({})).unwrap();
|
||||
assert_eq!(kind(&app), Some(options), "{ext}");
|
||||
assert!(app.ui.dialog.as_ref().unwrap().bool(export_as::SELECTION_FIELD), "{ext}");
|
||||
if ext == "png" {
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["__size"], json!([10.0, 5.0]), "the selection's size");
|
||||
}
|
||||
frame(&mut app);
|
||||
confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none(), "{ext}");
|
||||
assert_eq!(names(&written).last(), Some(&path), "{ext}");
|
||||
let text = last_text(&written);
|
||||
match ext {
|
||||
"png" => assert_eq!(png_size(&written.borrow().last().unwrap().1), (10, 5), "10×5 pt at 72 ppi"),
|
||||
"svg" => assert!(text.contains(r#"viewBox="0 0 10 5""#), "{text}"),
|
||||
"eps" => assert!(text.contains("%%BoundingBox: 0 0 10 5"), "{text}"),
|
||||
_ => {
|
||||
let at = text.find("$EXTMAX").unwrap_or_else(|| panic!("no extents: {text}"));
|
||||
let max: Vec<f64> = text[at..].lines().skip(1).collect::<Vec<_>>().chunks(2).take(2).map(|c| c[1].trim().parse().unwrap()).collect();
|
||||
// In millimeters, DXF Options' default unit: 10×5 pt.
|
||||
assert!((max[0] - 10.0 * 25.4 / 72.0).abs() < 1e-3 && (max[1] - 5.0 * 25.4 / 72.0).abs() < 1e-3, "the selection's extents: {max:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(names(&written).len(), 4, "one file each");
|
||||
assert!(app.ui.eps_options.get(export_as::SELECTION_FIELD).is_none(), "EPS Options does not remember the mark");
|
||||
assert!(app.ui.svg_options.get(export_as::SELECTION_FIELD).is_none());
|
||||
}
|
||||
|
||||
/// Text Export Options opens with Selection Only on, and OK writes the selected type alone.
|
||||
#[test]
|
||||
fn export_selection_to_text_writes_the_selected_type() {
|
||||
let (mut app, written) = app_saving(1, |_| "/out/sel.txt".into());
|
||||
app.run("text.create", json!({"x": 10, "y": 10, "text": "Left out"})).unwrap();
|
||||
app.run("text.create", json!({"x": 10, "y": 30, "text": "Selected"})).unwrap();
|
||||
app.run("document.exportSelection", json!({})).unwrap();
|
||||
assert_eq!(kind(&app), Some("txtOptions"));
|
||||
assert!(app.ui.dialog.as_ref().unwrap().bool("selectionOnly"));
|
||||
frame(&mut app);
|
||||
confirm(&mut app).unwrap();
|
||||
assert_eq!(names(&written), ["/out/sel.txt"]);
|
||||
assert_eq!(last_text(&written), "Selected\n");
|
||||
}
|
||||
|
||||
/// SVG Options opened by Export Selection hides Use Artboards, and its OK leaves the Use
|
||||
/// Artboards setting that the next Export As SVG starts from as it was.
|
||||
#[test]
|
||||
fn export_selection_to_svg_keeps_the_remembered_use_artboards() {
|
||||
let (mut app, _) = app_saving(2, |_| "/out/sel.svg".into());
|
||||
app.run("file.export.svg", json!({})).unwrap();
|
||||
set(&mut app, "useArtboards", json!(true));
|
||||
confirm(&mut app).unwrap();
|
||||
assert_eq!(app.ui.svg_options["useArtboards"], true);
|
||||
app.run("shape.rectangle", json!({"x": 40, "y": 30, "width": 10, "height": 5})).unwrap();
|
||||
app.run("document.exportSelection", json!({})).unwrap();
|
||||
assert_eq!(kind(&app), Some("svgOptions"));
|
||||
confirm(&mut app).unwrap();
|
||||
assert_eq!(app.ui.svg_options["useArtboards"], true, "{}", app.ui.svg_options);
|
||||
}
|
||||
|
||||
/// A format without an options dialog (a native document) and an exporter plug-in, with or
|
||||
/// without parameters, are given the selection alone: fewer paths than the whole document.
|
||||
#[test]
|
||||
fn export_selection_without_a_dialog_and_to_plug_ins_writes_the_selection() {
|
||||
let (mut app, written) = app_saving(1, |_| String::new());
|
||||
app.run("shape.rectangle", json!({"x": 40, "y": 30, "width": 10, "height": 5})).unwrap();
|
||||
app.run("file.exportAs", json!({"path": "/out/all.vectorcraft"})).unwrap();
|
||||
let all = paths_in(&written.borrow().last().unwrap().1);
|
||||
picking(&mut app, "/out/sel.vectorcraft");
|
||||
app.run("document.exportSelection", json!({})).unwrap();
|
||||
assert!(app.ui.dialog.is_none(), "written at once");
|
||||
assert_eq!(names(&written).last().map(String::as_str), Some("/out/sel.vectorcraft"));
|
||||
let selected = paths_in(&written.borrow().last().unwrap().1);
|
||||
assert!(selected >= 1 && selected < all, "{selected} of {all} paths");
|
||||
|
||||
for (id, ext, params) in
|
||||
[("test.ui.selwhole", "tsw", "{}"), ("test.ui.selparams", "tsp", r#"{"density":{"type":"number","min":0,"max":10,"default":4}}"#)]
|
||||
{
|
||||
let wasm = wat::parse_str(tpl::echo_exporter(&tpl::exporter(id, &format!(r#"["{ext}"]"#), params), Echo::Input)).unwrap();
|
||||
app.run("plugin.install", json!({"dataBase64": vectorcraft_format::base64_encode(&wasm)})).unwrap();
|
||||
picking(&mut app, &format!("/out/sel.{ext}"));
|
||||
app.run("document.exportSelection", json!({})).unwrap();
|
||||
if params != "{}" {
|
||||
assert_eq!(kind(&app), Some(plugin::EXPORT_KIND), "{id} asks for its parameters");
|
||||
frame(&mut app);
|
||||
confirm(&mut app).unwrap();
|
||||
}
|
||||
assert!(app.ui.dialog.is_none(), "{id}");
|
||||
assert_eq!(names(&written).last(), Some(&format!("/out/sel.{ext}")));
|
||||
let given = paths_in(&written.borrow().last().unwrap().1);
|
||||
app.run("plugin.remove", json!({"id": id})).unwrap();
|
||||
assert_eq!(given, selected, "{id} is given the selection");
|
||||
}
|
||||
}
|
||||
|
||||
/// The question before replacing the file the document was opened from runs the export again as
|
||||
/// an Export Selection: the PNG written after OK is the selection's.
|
||||
#[test]
|
||||
fn export_selection_after_the_question_before_losing_writes_the_selection() {
|
||||
let (mut app, written) = app_saving(1, |_| "/out/sel.png".into());
|
||||
app.run("shape.rectangle", json!({"x": 40, "y": 30, "width": 10, "height": 5})).unwrap();
|
||||
let st = app.session.doc_mut().unwrap();
|
||||
st.imported_from = Some("/out/sel.png".into());
|
||||
st.import_losses = vec!["hidden type".into()];
|
||||
app.run("document.exportSelection", json!({})).unwrap();
|
||||
assert_eq!(kind(&app), Some("pngOptions"));
|
||||
frame(&mut app);
|
||||
assert_eq!(confirm(&mut app).unwrap()["pending"], super::confirm::KIND);
|
||||
assert!(written.borrow().is_empty(), "nothing is written before OK");
|
||||
confirm(&mut app).unwrap();
|
||||
assert_eq!(names(&written), ["/out/sel.png"]);
|
||||
assert_eq!(png_size(&written.borrow()[0].1), (10, 5), "the selection's crop");
|
||||
}
|
||||
|
||||
/// `file.exportAs {path, exportSelection: true}` writes the selection without a dialog; a value
|
||||
/// that is not true or false is refused.
|
||||
#[test]
|
||||
fn export_as_with_export_selection_writes_the_selection() {
|
||||
let (mut app, written) = app_saving(1, |_| String::new());
|
||||
app.run("shape.rectangle", json!({"x": 40, "y": 30, "width": 10, "height": 5})).unwrap();
|
||||
app.run("file.exportAs", json!({"path": "/out/sel.png", "exportSelection": true, "useArtboards": true})).unwrap();
|
||||
assert_eq!(png_size(&written.borrow()[0].1), (10, 5));
|
||||
let e = app.run("file.exportAs", json!({"path": "/out/sel.png", "exportSelection": "yes"})).unwrap_err();
|
||||
assert!(e.contains("exportSelection must be true or false"), "{e}");
|
||||
assert_eq!(names(&written).len(), 1);
|
||||
}
|
||||
|
||||
/// Appearance of Black › Printing / Exporting set to rich black draws a CMYK black as RGB black in
|
||||
/// a PNG from Export As and from Export Selection, as `document.export` draws it.
|
||||
#[test]
|
||||
fn rich_black_reaches_files_from_the_export_dialogs() {
|
||||
let (mut app, written) = app_saving(1, |_| "/out/sel.png".into());
|
||||
app.run("file.new", json!({"width": 40, "height": 30, "colorMode": "cmyk"})).unwrap();
|
||||
app.run("paint.setStroke", json!({"none": true})).unwrap();
|
||||
app.run("paint.setFill", json!({"color": {"c": 0, "m": 0, "y": 0, "k": 1}})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 40, "height": 30})).unwrap();
|
||||
let px = |w: &Written| {
|
||||
let w = w.borrow();
|
||||
image::load_from_memory(&w.last().unwrap().1).unwrap().to_rgba8().get_pixel(10, 10).0
|
||||
};
|
||||
app.run("file.exportAs", json!({"path": "/out/all.png"})).unwrap();
|
||||
let accurate = px(&written);
|
||||
assert!(accurate[0] > 10, "accurate: dark grey, {accurate:?}");
|
||||
app.session.prefs.black_output = "rich".into();
|
||||
app.run("file.exportAs", json!({"path": "/out/all.png"})).unwrap();
|
||||
assert_eq!(px(&written), [0, 0, 0, 255], "Export As");
|
||||
app.run("document.exportSelection", json!({})).unwrap();
|
||||
frame(&mut app);
|
||||
confirm(&mut app).unwrap();
|
||||
assert_eq!(names(&written).last().map(String::as_str), Some("/out/sel.png"));
|
||||
assert_eq!(px(&written), [0, 0, 0, 255], "Export Selection");
|
||||
}
|
||||
|
||||
/// With nothing selected, Export Selection stops before the save panel.
|
||||
#[test]
|
||||
fn nothing_selected_stops_before_the_save_panel() {
|
||||
let (mut app, written) = app_saving(1, |_| "/out/sel.pdf".into());
|
||||
app.run("select.none", json!({})).unwrap();
|
||||
let asked = Rc::new(Cell::new(false));
|
||||
let a = asked.clone();
|
||||
app.services.pick_save = Some(Box::new(move |_| {
|
||||
a.set(true);
|
||||
Some("/out/sel.pdf".into())
|
||||
}));
|
||||
let e = app.run("document.exportSelection", json!({})).unwrap_err();
|
||||
assert!(e.contains("select something to export"), "{e}");
|
||||
assert!(!asked.get() && app.ui.dialog.is_none() && written.borrow().is_empty());
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Export As → Text: the Text Export Options (encoding, line endings, selection only) for a
|
||||
//! picked `.txt` path. Fields are `document.export` params (`format`, `path`…).
|
||||
//! Export As → Text and Export Selection to `.txt`: the Text Export Options (encoding, line
|
||||
//! endings, selection only) for a picked `.txt` path. Fields are `document.export` params
|
||||
//! (`format`, `path`…).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -39,14 +40,25 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
ui.end_row();
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
let selected = app.session.active().is_some_and(|st| !st.selection.objects.is_empty());
|
||||
// Export Selection keeps Selection Only on.
|
||||
let selected = app.session.active().is_some_and(|st| !st.selection.objects.is_empty()) && !d.bool(super::export_as::SELECTION_FIELD);
|
||||
ui.add_enabled_ui(selected, |ui| form::check(ui, d, "selectionOnly", tl!("Selection Only")));
|
||||
false
|
||||
}
|
||||
|
||||
/// Write the text file (the command runs first, so a bad option keeps the dialog open).
|
||||
/// Write the text file (the command runs first, so a bad option keeps the dialog open). Opened by
|
||||
/// Export Selection, it writes `document.exportSelection`'s text.
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let path = io::save_command_output(app, "document.export", "txt", form::params(d))?;
|
||||
let mut params = form::params(d);
|
||||
let id = if d.bool(super::export_as::SELECTION_FIELD) {
|
||||
if let Some(o) = params.as_object_mut() {
|
||||
o.remove("selectionOnly");
|
||||
}
|
||||
"document.exportSelection"
|
||||
} else {
|
||||
"document.export"
|
||||
};
|
||||
let path = io::save_command_output(app, id, "txt", params)?;
|
||||
app.ui.dialog = None;
|
||||
Ok(json!({ "path": path }))
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ mod tests_read_only_save;
|
||||
|
||||
pub use export::{EXPORT_CANCELLED, cancel_exports, export, export_for_screens, export_pdf, hide_exports, reveal, save_command_output};
|
||||
pub(crate) use export::{
|
||||
export_files, exported_with_notes, file_url, open_in_app, open_original, reveal_path, run_to_file, save_command_output_named,
|
||||
EXPORT_SELECTION, export_files, exported_with_notes, file_url, open_in_app, open_original, reveal_path, run_to_file, save_command_output_named,
|
||||
selection_path,
|
||||
};
|
||||
pub(crate) use open::report_installed;
|
||||
pub(crate) use open::{EXPORT_PRESETS, import_presets, open_with_on, opened, show_opened};
|
||||
@@ -430,7 +431,9 @@ mod tests {
|
||||
app.session.execute("file.new", &json!({"width": 60, "height": 40})).unwrap();
|
||||
app.session.execute("shape.rectangle", &json!({"x": 5, "y": 5, "width": 20, "height": 10})).unwrap();
|
||||
app.services.pick_save = Some(Box::new(|_: &FilePick| Some("/tmp/sel.svg".into())));
|
||||
assert_eq!(app.run("document.exportSelection", json!({})).unwrap()["path"], "/tmp/sel.svg");
|
||||
app.run("document.exportSelection", json!({})).unwrap();
|
||||
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some("svgOptions"), "the picked format's options follow");
|
||||
assert_eq!(crate::dialogs::confirm(&mut app).unwrap()["path"], "/tmp/sel.svg");
|
||||
app.run("document.exportSelection", json!({"path": "/tmp/sel.png"})).unwrap();
|
||||
let w = written.borrow();
|
||||
assert!(String::from_utf8_lossy(&w[0].1).contains("<svg"), "SVG from the picked name");
|
||||
|
||||
@@ -8,6 +8,33 @@ use super::{EngineError, Progress, Value, VectorcraftApp, Writer, background, ca
|
||||
/// The status once an export was cancelled.
|
||||
pub const EXPORT_CANCELLED: &str = "Export cancelled";
|
||||
|
||||
/// The param that makes an [`export`] write the selection alone, cropped to it, as
|
||||
/// `document.exportSelection` writes it (`file.exportAs {path, exportSelection: true}`): set by
|
||||
/// the options dialogs that Export Selection opens.
|
||||
pub(crate) const EXPORT_SELECTION: &str = "exportSelection";
|
||||
|
||||
/// `params` without [`EXPORT_SELECTION`], and whether it asked for the selection. A selection
|
||||
/// export writes one page, the selection's crop, so the artboard choices are left out too.
|
||||
fn take_selection(params: &Value) -> Result<(bool, Value), String> {
|
||||
let mut p = params.clone();
|
||||
let Some(o) = p.as_object_mut() else { return Ok((false, p)) };
|
||||
match o.remove(EXPORT_SELECTION) {
|
||||
None | Some(Value::Null | Value::Bool(false)) => return Ok((false, p)),
|
||||
Some(Value::Bool(true)) => {}
|
||||
Some(v) => return Err(format!("{EXPORT_SELECTION} must be true or false, not {v}")),
|
||||
}
|
||||
let strip = |o: &mut serde_json::Map<String, Value>| {
|
||||
for k in fileio::ARTBOARD_PARAMS.iter().chain(&["useArtboards", "selectedOnly"]) {
|
||||
o.remove(*k);
|
||||
}
|
||||
};
|
||||
strip(o);
|
||||
if let Some(svg) = o.get_mut("svg").and_then(Value::as_object_mut) {
|
||||
strip(svg);
|
||||
}
|
||||
Ok((true, p))
|
||||
}
|
||||
|
||||
/// File → Cancel Export (the progress dialog's Cancel): every export running in the background
|
||||
/// stops and writes nothing more → `{cancelled: n}`.
|
||||
pub fn cancel_exports(app: &mut VectorcraftApp) -> Value {
|
||||
@@ -29,11 +56,16 @@ pub fn hide_exports(app: &mut VectorcraftApp) -> Value {
|
||||
/// options…): every file it writes (one per artboard, linked images) goes next to `path`. The
|
||||
/// document keeps its path. → `{path, warnings, files?}` (`files`: every file when there are
|
||||
/// several). When automation asked for the export, `warnings` also name each option the written
|
||||
/// format doesn't read, as `document.export`'s do.
|
||||
/// format doesn't read, as `document.export`'s do. With [`EXPORT_SELECTION`] the file holds the
|
||||
/// selection alone, cropped to it, as `document.exportSelection` writes it. With Appearance of
|
||||
/// Black › Printing / Exporting set to rich black, RGB files draw a CMYK black as RGB black, as in
|
||||
/// `document.export`.
|
||||
pub fn export(app: &mut VectorcraftApp, format: Option<&str>, path: Option<String>, params: &Value) -> Result<Value, String> {
|
||||
// Options given as anything but an object are none: the path and format still reach the
|
||||
// encoder (an exporter plug-in is told the file it writes).
|
||||
let params = &if params.is_object() { params.clone() } else { json!({}) };
|
||||
let (selection, params) = take_selection(params)?;
|
||||
let params = ¶ms;
|
||||
let f = fileio::writable_format(format, path.as_deref())?;
|
||||
// A Shape Builder merge still being computed for a click is exported with the document.
|
||||
let r = app.session.finish_tool_edit();
|
||||
@@ -42,9 +74,13 @@ pub fn export(app: &mut VectorcraftApp, format: Option<&str>, path: Option<Strin
|
||||
remember_active_artboard(app);
|
||||
let st = app.session.active().ok_or("no document")?;
|
||||
fileio::check_linked_scripts("file.exportAs", f.id, st).map_err(|e| e.to_string())?;
|
||||
let doc = match fileio::export_doc(st, f, params).map_err(|e| e.to_string())? {
|
||||
std::borrow::Cow::Borrowed(_) => st.doc.clone(),
|
||||
std::borrow::Cow::Owned(d) => std::sync::Arc::new(d),
|
||||
let doc = if selection {
|
||||
std::sync::Arc::new(fileio::selection_doc(st, "document.exportSelection", f.id == "txt").map_err(|e| e.to_string())?.0)
|
||||
} else {
|
||||
match fileio::export_doc(st, f, params).map_err(|e| e.to_string())? {
|
||||
std::borrow::Cow::Borrowed(_) => st.doc.clone(),
|
||||
std::borrow::Cow::Owned(d) => std::sync::Arc::new(d),
|
||||
}
|
||||
};
|
||||
// A PDF shows its title in the viewer's title bar only when it is the document's own, not the
|
||||
// file name it was titled after, as `document.export` writes it.
|
||||
@@ -55,6 +91,9 @@ pub fn export(app: &mut VectorcraftApp, format: Option<&str>, path: Option<Strin
|
||||
let mut again = params.clone();
|
||||
if let Some(o) = again.as_object_mut() {
|
||||
o.insert("format".into(), json!(f.id));
|
||||
if selection {
|
||||
o.insert(EXPORT_SELECTION.into(), json!(true));
|
||||
}
|
||||
}
|
||||
if let Some(r) = ask_before_losing(app, &path, "file.exportAs", &again) {
|
||||
return Ok(r);
|
||||
@@ -66,7 +105,8 @@ pub fn export(app: &mut VectorcraftApp, format: Option<&str>, path: Option<Strin
|
||||
};
|
||||
// An export that automation asked for, also through a save panel it opened, names the options
|
||||
// the file doesn't read, as `document.export` does. A person's export gets no such notes.
|
||||
let unused = if app.automation_asked() { fileio::unused_option_notes("document.export", f, params) } else { Vec::new() };
|
||||
let cmd = if selection { "document.exportSelection" } else { "document.export" };
|
||||
let unused = if app.automation_asked() { fileio::unused_option_notes(cmd, f, params) } else { Vec::new() };
|
||||
let noted = unused.clone();
|
||||
// Background Export: encoded and written from this snapshot while editing goes on. The
|
||||
// encoder is told the file it writes (an exporter plug-in gets its name and extension).
|
||||
@@ -75,9 +115,13 @@ pub fn export(app: &mut VectorcraftApp, format: Option<&str>, path: Option<Strin
|
||||
// automation asked for the export, also through a save panel it opened (checked now: a
|
||||
// background export ends after the request).
|
||||
let view = params.get("viewAfterSaving").and_then(Value::as_bool).unwrap_or(false) && !app.automation_asked();
|
||||
let rich_black = app.session.prefs.black_output == "rich";
|
||||
let (mut params, target) = (params.clone(), path.clone());
|
||||
if let Some(o) = params.as_object_mut() {
|
||||
o.insert("path".into(), json!(path));
|
||||
if rich_black {
|
||||
o.insert("richBlack".into(), json!(true));
|
||||
}
|
||||
if f.id == "pdf" {
|
||||
o.insert("_displayDocTitle".into(), json!(own_title));
|
||||
}
|
||||
@@ -190,6 +234,12 @@ fn take_output(v: &mut Value) -> Result<Vec<u8>, String> {
|
||||
bytes.ok_or_else(|| "no data".into())
|
||||
}
|
||||
|
||||
/// The file an Export Selection writes, picked in a save panel (the web: the suggested name): a
|
||||
/// typed extension of any format is kept, and names the format.
|
||||
pub(crate) fn selection_path(app: &mut VectorcraftApp) -> Result<String, String> {
|
||||
target_path_keeping(app, None, "png", |_| true)
|
||||
}
|
||||
|
||||
/// Run an engine command that returns `{dataBase64}` without its `path` and write the bytes to
|
||||
/// that path (else a picked or suggested name) → (path, the command's result without the data).
|
||||
/// The command runs before a path is asked for, so bad params never open a save dialog, except
|
||||
|
||||
@@ -171,6 +171,8 @@ fn run_ui_part1(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<
|
||||
r
|
||||
})
|
||||
}
|
||||
// Without params the save panel picks the file, then its format's options dialog follows.
|
||||
"document.exportSelection" if p.as_object().is_none_or(|o| o.is_empty()) => crate::dialogs::export_selection(app),
|
||||
"document.exportSelection" => {
|
||||
io::save_command_output(app, id, "png", if p.is_object() { p.clone() } else { json!({}) }).map(|p| json!({"path": p}))
|
||||
}
|
||||
|
||||
@@ -1331,7 +1331,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"file.exportAs",
|
||||
"Export As…",
|
||||
"",
|
||||
"{format?, useArtboards?, range?} opens Export As (then the format's options); with {path, …document.export options} writes the file(s) → {path, warnings, files?}; under automation read roots, an SVG or SVGZ is refused while the document links a JavaScript file outside them, as document.export refuses it",
|
||||
"{format?, useArtboards?, range?} opens Export As (then the format's options); with {path, …document.export options} writes the file(s) → {path, warnings, files?}; exportSelection: true writes the selection alone, cropped to it, as document.exportSelection writes it (artboard options are not used); under automation read roots, an SVG or SVGZ is refused while the document links a JavaScript file outside them, as document.export refuses it",
|
||||
),
|
||||
(
|
||||
"ui.fileInfoDialog",
|
||||
|
||||
@@ -378,6 +378,12 @@ fn every_write_through_a_save_panel_asks_before_replacing() {
|
||||
let d = app.ui.dialog.clone().expect(cmd);
|
||||
assert_eq!((d.kind.as_str(), d.str("path")), (dialogs::confirm::REPLACE, path.to_string()), "{cmd}");
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
// Export Selection without params continues in the format's options, whose OK writes it.
|
||||
if cmd == "document.exportSelection" {
|
||||
assert_eq!(app.ui.dialog.as_ref().map(|d| (d.kind.as_str(), d.str("path"))), Some(("pngOptions", path.to_string())), "{cmd}");
|
||||
assert!(w.borrow().is_empty(), "{cmd}: nothing is written before the options' OK");
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
}
|
||||
assert_eq!(last(&w), path, "{cmd}");
|
||||
}
|
||||
// Export As: the question's OK picks the file again, and PNG Options follows.
|
||||
@@ -480,5 +486,8 @@ fn an_export_keeps_a_typed_extension_of_the_format_it_writes() {
|
||||
let (mut app, w) = saving_app("/tmp/sel.pdf");
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
app.run("document.exportSelection", json!({})).unwrap();
|
||||
// Save PDF follows the save panel, and its OK writes the file.
|
||||
assert_eq!(app.ui.dialog.as_ref().map(|d| (d.kind.as_str(), d.str("path"))), Some(("savePdf", "/tmp/sel.pdf".to_string())));
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
assert_eq!(last(&w), "/tmp/sel.pdf", "Export Selection takes its format from the typed name");
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ the two can still win that race.
|
||||
| Background Save / Export | `app.save`, `app.export {path}`, `file.save`, `file.saveAs`, `file.saveCopy`, `file.exportForScreens {folder, …}` | with the `backgroundSave` / `backgroundExport` preferences on (the default) the desktop app writes the file from a snapshot on a worker thread: the call returns `{path, background: true, status}` at once (`file.exportForScreens`: `{background: true, status}`, and the files are written into the folder from a snapshot taken at the call; an `app.export` given an option the format doesn't read: also `warnings`, naming the option), `ui.inspect` lists the jobs still running in `background` (labels such as `"Saving a.vectorcraft"`), and the document counts as saved (as of the snapshot) once its file is written. Quit and Close wait for them. A job that fails reports the error in the status bar only (`ui.inspect` → `ui.status`), a path outside `--automation-write-root` included. With the preferences off (`prefs.set`), saves return once written. `app.save {path?, wait: true}` and `app.export {path, wait: true}` wait for the jobs already running, write the file before they reply whatever the preferences are set to, and return its result or the error. 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 preferences. A `wait` that is not `true`, `false` or `null` is refused with "`wait` must be true or false", and nothing is written |
|
||||
| `engine.execute` `file.exportForScreens` | `{}` / `{…document.exportForScreens params}` | without params opens `exportForScreens` on the settings the document last exported with (`document.exportSettings`). While the Artboard tool is in use, the artboards it has selected (its `selected` option, the rows the Artboards panel highlights) are checked instead, and `select` is `range`, or `all` when every artboard is selected. Fields: `tab` (`artboards` \| `assets` \| `layers` \| `selection`; OK reads Export Layer or Export Selection on those), `layers` ([layer id…] checked), `selectionMode` (`one` \| `each`), `name` (the file name pattern of `document.exportForScreens`), `counterStart`, `folders` (`none` \| `rows` \| `format` \| `item` \| `itemFormat`), `exportPreset` (the preset applied last), `__savePreset` (the name typed to save one), `select` (`all` \| `range` \| `full`), `range` ("1-3, 5"), `boards` (a bool per artboard; checking them sets `select` and `range`), `includeBleed`, `folder` (desktop; default the Desktop, else home), `openLocation`, `subfolders`, `prefix`, `preset` (`""` \| `mobile` \| `density`), `formats` (rows `{format, scale: "2x" \| "100w" \| "100h" \| "72ppi", suffix, quality?, folder?}`), `settings` (`{png \| png8 \| jpg \| webp \| gif \| svg \| pdf: {…options}}`, the gear's Format Settings; `__settings` names the format shown). `ui.dialog.confirm` exports (a bad range or no folder keeps it open); on the web OK reads "Download" and the files download (several as one `.zip`). With params it runs `document.exportForScreens`: no `folder` returns the files (or with `zip: true` one zip) as `dataBase64`; with `openLocation` the first file is shown in the file manager. When `warnings` is not empty, an export into a folder or a download shows the first in the status bar as Save As shows a save's notes |
|
||||
| Export for Screens' Assets tab, Asset Export panel | `ui.dialog.set` `tab: "assets"`, `assets: [asset id…]`; `window.panel {panel: "assetExport"}` | on the Assets tab of `exportForScreens` the `assets` field lists the checked asset ids (all of them when it opens); `ui.dialog.confirm` runs `document.exportForScreens {assets}` (nothing checked keeps it open) and OK reads "Export Asset". The Asset Export panel acts through `assets.*` and shares the format rows with the dialog (`assets.settings.set`). The dialog has the format presets menu (☰). In the dialog, `exportPreset` names the preset applied last (`ui.dialog.set` of it applies the preset it names, as picking it does; a name that is not a preset's is an error and leaves the dialog as it was), `__exportPresets` lists the presets, and `__savePreset` holds the name being typed after Save Preset…. The panel menu applies presets through `assets.settings.set {exportPreset}` and holds Save As Preset…, Save Changes, Delete Preset, Import Preset… and Export Preset…. Import and Export use picked files. |
|
||||
| `engine.execute` `file.exportSelection` | `{multiple?}` | File › Export Selection…: collects the selected objects as one asset (`assets.add`; one per object with `multiple: true`) and opens `exportForScreens` on its Assets tab with just them checked → `{assets}`. `document.exportSelection` still exports the selection in one go, without the dialog |
|
||||
| `engine.execute` `file.exportSelection` | `{multiple?}` | File › Export Selection…: collects the selected objects as one asset (`assets.add`; one per object with `multiple: true`) and opens `exportForScreens` on its Assets tab with just them checked → `{assets}`. `document.exportSelection` with params still exports the selection in one go, without the dialog; with no params or `{}` the save panel picks the file and its format's options dialog follows, as after Export As, and the call returns `null` while the dialog is open (OK returns what `file.exportAs` returns, without `format` or `bounds`). `file.exportAs {path, exportSelection: true, …options}` writes the selection without a dialog |
|
||||
| CSS Properties panel | `window.panel {panel: "cssProperties"}`; `engine.execute` `css.copy` / `css.exportFile` | the panel shows `css.selection` of the selection (after Generate CSS, while nothing is selected, `css.generate`); its menu holds the options the commands take (`units`, `position`, `dimensions`, `unnamed`, `rasterize`). `css.copy {scope?, …options}` copies the CSS to the clipboard → `{css, rules}`; `css.exportFile {path?, scope?, …options}` writes `css.export` to `path` (else a picked file; the web downloads it) with the pictures of rasterized art next to it → `{path, rules, images, warnings}`, and the status bar shows `warnings` as Export As shows an export's notes |
|
||||
|
||||
| `ui.dialog.*` on `importPdf` | `{field: "page" \| "allPages" \| "range" \| "cropTo" \| "password", value}` | opening (`app.open`, `file.open`) a PDF, `.ai` or `.ait` with a password, or one with several pages unless it is a `.ai` or `.ait` with its editing data or carries its VectorCraft document (those open whole), or placing (`file.place` without `page`) any of them with several pages or a password, opens the Import PDF dialog (`mode`: `open` or `place`; `pageCount`). While `__locked` is true, set `password` and `ui.dialog.confirm` to unlock it (a one-page file then opens or is placed). Then set `allPages: false` and `range` ("2-3, 5") to open some pages, or `page` to place one, and `cropTo` (`bounding`, `art`, `crop`, `trim`, `bleed`, `media`); `ui.dialog.confirm` opens or places them (placing keeps the other `file.place` params, such as `link`, `template` and `at`, in `__place`). `document.open {pages, cropTo, password}` and `file.place {page, crop, password}` do the same without the dialog |
|
||||
|
||||
+8
-1
@@ -1063,7 +1063,14 @@ apps (`editingData`), and always comes back with the warning `Preserve editing w
|
||||
bounds would replace the document's artboards when reopened`; with `pdfCompatible: false` it is refused, whatever
|
||||
`editingData` says. A legacy version 7 `.ai` (`aiVersion: "7"`) of the art's bounds is written as before.
|
||||
Export As PDF continues in the Save PDF dialog with Use Artboards on or off; with it off, the dialog has no
|
||||
Artboards section and the page is the bounds of the visible art.
|
||||
Artboards section and the page is the bounds of the visible art. `document.exportSelection` run in the app with no
|
||||
params or `{}` (a shortcut, or an automation call) opens the save panel, and the picked format's options dialog
|
||||
follows as after Export As: Save PDF without its Artboards section, SVG Options without its artboards, the raster
|
||||
options, EPS Options, DXF Options without Export Selected Art Only, Text Export Options with Selection Only on, or an
|
||||
exporter plug-in's parameters. Other formats are written at once. OK writes the selection alone, cropped to it, as
|
||||
`document.exportSelection` writes it, with the chosen options. While a dialog is open the call returns `null`, and
|
||||
OK returns what `file.exportAs` returns, which has no `format` or `bounds` (Text Export Options: `{path}`). To
|
||||
write without a dialog, pass `path` or any option. `file.exportAs {path, exportSelection: true, …options}` writes the selection the same way.
|
||||
|
||||
Raster exports default to the document's raster effects resolution. An explicit `scale` sets pixels per
|
||||
point (72 times that value in ppi); `options.ppi` wins over either scale option. CLI `run --export` and
|
||||
|
||||
@@ -1684,6 +1684,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
In a `.ai` opened from its editing data, Bas Relief in an art style takes its Detail, Smoothness and Light direction from the style's settings block, and an object other than type with it opens as itself with the live effect. A settings block that VectorCraft can't read (malformed, another effect's, a value out of range or a key it doesn't know) keeps the object's drawn look (part of #637; [raster effects reference](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-filter-effects/raster-effects-reference.html), [Photoshop File Formats Specification](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/), [scripting the filter gallery](https://community.adobe.com/questions-712/scripting-support-for-the-filter-gallery-1064646); `crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`).
|
||||
PDF export and `.ai` saves write a page with a side longer than 14,400 pt with a `UserUnit` (PDF 1.6), its longer side divided by 14,400 and rounded up to the next thousandth, so that no side is longer than 14,400 units (ISO 32000-1, Annex C.2); a PDF 1.5 file with such a page is written as PDF 1.6 with PDF 1.5's encryption, PDF/X files and PDF 1.3 and 1.4 files write it in points (in PDF 1.3 and 1.4, a page past 32,767 pt that needs a real number past 32,767 is refused with an error naming it), a page whose short side is 3 pt or longer but would be under 3 units at that `UserUnit` gets a smaller one that keeps the short side at 3 units, and the export's warnings give each such page's size and how it is written (#6382). The Save PDF dialog's "Write pages longer than 200 in at full size in points" (`longPagesInPoints`, on by default; `.ai` saves leave it off) writes such pages in points without a `UserUnit` at every PDF version, in files without a PDF/X or PDF/A standard, so readers that ignore `UserUnit` show them at full size.
|
||||
Export As PDF with Use Artboards off also opens the Save PDF dialog, without its Artboards section, and writes one page of the visible art's bounds with the dialog's options (`document.exportPdf {useArtboards: false}`; `crates/ui-egui/src/dialogs/tests_export.rs`, `crates/engine/src/cmd/fileio/tests_pdf.rs`).
|
||||
`document.exportSelection` run without params (a shortcut) takes the same route after its save panel: a picked PDF opens the Save PDF dialog without its Artboards section, and SVG, the raster formats, EPS, DXF, text and exporter plug-ins with parameters open their options, whose OK writes the selection alone, cropped to it, with the chosen options (`crates/ui-egui/src/dialogs/tests_export_selection.rs`).
|
||||
A `.ai` export with Use Artboards off writes one page of the visible art's bounds without the native document or the editing copy for other apps, with a warning, so the file does not reopen with one artboard in place of the document's; without PDF content it is refused (`crates/engine/src/cmd/fileio/tests_pdfedit.rs`).
|
||||
PDF export warns when a PDF/X file has no bleed on a side where the document's bleed has one, and writes the file as asked.
|
||||
In a PDF without a standard, the warning for a page longer than 14,400 pt written with a `UserUnit` names "Write pages longer than 200 in at full size in points" (`crates/engine/src/cmd/fileio/tests_pdf.rs`).
|
||||
|
||||
Reference in New Issue
Block a user