mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 23:31:23 +00:00
Asset Export: Import Preset and Export Preset read and write preset files
Squashed from #5032 onto main.
This commit is contained in:
@@ -7,7 +7,7 @@ use serde_json::{Map, Value, json};
|
||||
use vectorcraft_doc::{ExportAsset, NodeId};
|
||||
|
||||
use super::edit::{roots_of, selected_roots};
|
||||
use super::fileio::{SCREEN_SETTINGS_KEYS, check_screen_settings, export_crop, export_screens, store_screen_settings};
|
||||
use super::fileio::{SCREEN_SETTINGS_KEYS, check_screen_settings, export_crop, export_screens, store_screen_settings, with_screen_export_preset};
|
||||
use super::{CommandSpec, EngineError, Result, Session, bad, bool_or, checked_ids_param, cmd, has_doc, ok, str_param};
|
||||
|
||||
/// The longest asset name kept (characters).
|
||||
@@ -48,7 +48,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Export Assets",
|
||||
[],
|
||||
None,
|
||||
"{assets?: [asset id…] (default all), folder?, zip?, openLocation?, formats?, preset?, settings?, prefix?, subfolders?, antiAlias?} each asset's art alone, cropped to it, in every format row: document.exportForScreens {assets} at the document's export settings (document.exportSettings) under the ones given → what document.exportForScreens returns (no folder: the files, or one zip, as dataBase64). Leaves the remembered settings as they are (assets.settings.set changes them)",
|
||||
"{assets?: [asset id…] (default all), folder?, zip?, openLocation?, formats?, preset?, settings?, prefix?, subfolders?, antiAlias?} each asset's art alone, cropped to it, in every format row: document.exportForScreens {assets} at the document's export settings (document.exportSettings) under the ones given; with subfolders false every file goes into the folder itself, mobile and density rows too → what document.exportForScreens returns (no folder: the files, or one zip, as dataBase64). Leaves the remembered settings as they are (assets.settings.set changes them)",
|
||||
has_doc,
|
||||
export
|
||||
),
|
||||
@@ -57,7 +57,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Asset Export Settings",
|
||||
[],
|
||||
None,
|
||||
"{formats?, preset?: mobile|density|\"\" (none), settings?, prefix?, subfolders?, antiAlias?} change the export settings the Asset Export panel and Export for Screens share (document.exportSettings; the values of document.exportForScreens; null removes one): formats replace a preset, a preset replaces the formats → {settings}. Not an undo step",
|
||||
"{exportPreset?: (a preset of document.exportPresets.list: its formats or preset, its format settings over the saved ones, its prefix (none when it has none), its folders (or subfolders: rows or none) with subfolders on for rows, and antiAlias; the other keys go over it), formats?, preset?: mobile|density|\"\" (none), settings?, prefix?, subfolders?, antiAlias?} change the export settings the Asset Export panel and Export for Screens share (document.exportSettings; the values of document.exportForScreens; null removes one): formats replace a preset, a preset replaces the formats → {settings}. Not an undo step",
|
||||
has_doc,
|
||||
settings_set
|
||||
),
|
||||
@@ -223,6 +223,10 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(o) = p.as_object() {
|
||||
overlay(&mut q, o);
|
||||
}
|
||||
// Create Sub-folders off puts every file in the folder itself, mobile and density rows too.
|
||||
if q.get("subfolders").and_then(Value::as_bool) == Some(false) && p.get("folders").is_none() {
|
||||
q.insert("folders".into(), json!("none"));
|
||||
}
|
||||
if !q.contains_key("assets") {
|
||||
let all: Vec<u64> = s.doc()?.doc.assets.iter().map(|a| a.id).collect();
|
||||
if all.is_empty() {
|
||||
@@ -236,11 +240,22 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn settings_set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "assets.settings.set";
|
||||
let o = p.as_object().ok_or_else(|| bad(C, "give the settings to change, e.g. {\"formats\": [{\"format\": \"png\", \"scale\": \"2x\"}]}"))?;
|
||||
if let Some(k) = o.keys().find(|k| !SCREEN_SETTINGS_KEYS.contains(&k.as_str())) {
|
||||
if let Some(k) = o.keys().find(|k| !SCREEN_SETTINGS_KEYS.contains(&k.as_str()) && k.as_str() != "exportPreset") {
|
||||
return Err(bad(C, format!("`{k}` isn't an export setting: {}", SCREEN_SETTINGS_KEYS.join(", "))));
|
||||
}
|
||||
let mut settings = s.doc()?.doc.export_settings.clone();
|
||||
overlay(&mut settings, o);
|
||||
let mut given = o.clone();
|
||||
if let Some(name) = given.remove("exportPreset") {
|
||||
let name = name.as_str().ok_or_else(|| bad(C, "`exportPreset` is the name of a preset of document.exportPresets.list"))?;
|
||||
with_screen_export_preset(s, C, &mut settings, name)?;
|
||||
}
|
||||
// Create Sub-folders is Export for Screens' folder per row: the dialog opens on the choice.
|
||||
match given.get("subfolders").and_then(Value::as_bool) {
|
||||
Some(true) => _ = settings.insert("folders".into(), json!("rows")),
|
||||
Some(false) if settings.get("folders").and_then(Value::as_str) == Some("rows") => _ = settings.insert("folders".into(), json!("none")),
|
||||
_ => {}
|
||||
}
|
||||
overlay(&mut settings, &given);
|
||||
check_screen_settings(s, &Value::Object(settings.clone())).map_err(|e| of_cmd(C, e))?;
|
||||
store_screen_settings(s, settings)?;
|
||||
Ok(json!({ "settings": shared_settings(s)? }))
|
||||
|
||||
@@ -83,12 +83,14 @@ pub use save::{
|
||||
templates_folder,
|
||||
};
|
||||
pub use screens::{
|
||||
PRESET_KEYS as SCREEN_EXPORT_PRESET_KEYS, PRESETS as SCREEN_PRESETS, ScreenExport, ScreenExportPreset, ScreenSize,
|
||||
builtin_presets as screen_export_builtin_presets, first_file as screen_first_file, preset_rows as screen_preset_rows,
|
||||
PRESET_FILE_EXTS as SCREEN_EXPORT_PRESET_EXTS, PRESET_FILE_FORMAT as SCREEN_EXPORT_PRESET_FORMAT, PRESET_KEYS as SCREEN_EXPORT_PRESET_KEYS,
|
||||
PRESETS as SCREEN_PRESETS, SHARED_KEYS as SCREEN_SETTINGS_KEYS, ScreenExport, ScreenExportPreset, ScreenSize,
|
||||
builtin_presets as screen_export_builtin_presets, first_file as screen_first_file, preset_list as screen_export_preset_list,
|
||||
preset_rows as screen_preset_rows,
|
||||
};
|
||||
pub(crate) use screens::{
|
||||
SHARED_KEYS as SCREEN_SETTINGS_KEYS, check_settings as check_screen_settings, export as export_screens, safe_file_name,
|
||||
store_settings as store_screen_settings,
|
||||
check_settings as check_screen_settings, export as export_screens, safe_file_name, store_settings as store_screen_settings,
|
||||
with_export_preset as with_screen_export_preset,
|
||||
};
|
||||
pub use svg::options_map as svg_options;
|
||||
/// Atomic file writes (a temporary file renamed over the target: a failed write never damages the
|
||||
@@ -122,7 +124,7 @@ pub use commands::specs;
|
||||
pub use export_names::artboard_file_names;
|
||||
pub(crate) use export_names::{default_name, merge, unique_file_names, with_black_output, write_or_return};
|
||||
use export_names::{distinct_names_by, write_encoded};
|
||||
use formats::{ARTBOARD, ARTBOARDS, RANGE, USE_ARTBOARDS, writable};
|
||||
use formats::{ARTBOARD, ARTBOARDS, RANGE, USE_ARTBOARDS, import_name, writable};
|
||||
pub use formats::{
|
||||
DOC_EXTS, DOCX_EXTS, FORMATS, Format, FormatOption, OPEN_EXTS, PLACE_EXTS, RTF_EXTS, TEXT_EXTS, UNASSOCIATED_EXTS, export_extensions, extension,
|
||||
file_stem, format, format_for_name, has_own_title, library_open_filters, open_filters, place_filters, plugin_formats, titled_for_export,
|
||||
|
||||
@@ -85,6 +85,24 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
always,
|
||||
screens::presets_delete
|
||||
),
|
||||
cmd!(
|
||||
query "document.exportPresets.import",
|
||||
"Import Export Presets",
|
||||
[],
|
||||
None,
|
||||
"{path? | data?: file text | dataBase64?, replace?: false (replace saved presets of the same names; else the imported ones get a number)} add the presets of a .vcexportpresets file (as document.exportPresets.export writes) to the saved ones; a built-in name gets a number; a taken name that ends in a number gets the next free one (Web 2 comes in as Web 3); settings that document.exportPresets.save refuses, more than 1000 presets, or a file larger than 4194304 bytes fail the whole file → {imported: [names]}",
|
||||
always,
|
||||
screens::presets_import
|
||||
),
|
||||
cmd!(
|
||||
query "document.exportPresets.export",
|
||||
"Export Export Presets",
|
||||
[],
|
||||
None,
|
||||
"{names?: [preset names, built-in ones too] (default: every saved preset), path?} write the presets as a .vcexportpresets file (JSON); more than 1000 presets or a file larger than 4194304 bytes is an error → {path, count}; without path → {data: the file's text, count}",
|
||||
always,
|
||||
screens::presets_export
|
||||
),
|
||||
cmd!(
|
||||
query "command.batch",
|
||||
"Batch",
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use super::{Result, Session, TEMPLATE_FORMATS, UNSUPPORTED, Unsupported, Value, bad, dxf, eps, json, metafile, native, pdf, svg, text, unsupported};
|
||||
use super::super::unique_name;
|
||||
use super::{
|
||||
Result, Session, TEMPLATE_FORMATS, UNSUPPORTED, Unsupported, Value, bad, dxf, eps, json, metafile, native, pdf, screens, svg, text, unsupported,
|
||||
};
|
||||
|
||||
/// One option a format's encoder reads from the export params.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -625,8 +628,8 @@ fn format_filters() -> impl Iterator<Item = (&'static str, &'static [&'static st
|
||||
}
|
||||
|
||||
/// Open-dialog filters: "All readable files" first, then one per readable format, then swatch
|
||||
/// libraries (which open in the library panel), Libraries panel libraries, flattener, PDF and print
|
||||
/// presets (imported), scripts (checked without running them) and plug-ins (installed).
|
||||
/// libraries (which open in the library panel), Libraries panel libraries, flattener, PDF, print
|
||||
/// and export presets (imported), scripts (checked without running them) and plug-ins (installed).
|
||||
pub fn open_filters() -> impl Iterator<Item = (&'static str, &'static [&'static str])> {
|
||||
std::iter::once(("All readable files", OPEN_EXTS))
|
||||
.chain(format_filters())
|
||||
@@ -635,6 +638,7 @@ pub fn open_filters() -> impl Iterator<Item = (&'static str, &'static [&'static
|
||||
.chain(std::iter::once(("Flattener presets", crate::cmd::flatten::PRESET_EXTS)))
|
||||
.chain(std::iter::once(("PDF presets", crate::cmd::pdfcmds::PRESET_EXTS)))
|
||||
.chain(std::iter::once(("Print presets", crate::cmd::printpresets::PRESET_EXTS)))
|
||||
.chain(std::iter::once(("Export presets", screens::PRESET_FILE_EXTS)))
|
||||
.chain(std::iter::once(("Shortcut sets", crate::cmd::shortcutset::EXTS)))
|
||||
.chain(std::iter::once(("Scripts", crate::cmd::scripts::EXTS)))
|
||||
.chain(std::iter::once(("Plug-ins", crate::cmd::plugin::EXTS)))
|
||||
@@ -714,6 +718,25 @@ pub fn file_stem(name: &str) -> String {
|
||||
std::path::Path::new(name).file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_else(|| name.to_string())
|
||||
}
|
||||
|
||||
/// The name an imported preset `base` takes: `base` if `taken` doesn't claim it, else for a name
|
||||
/// that ends in a space and a number ("Logo Package 2") the first free name with a higher number
|
||||
/// ("Logo Package 3"), and for any other name the first free "base 2", "base 3"… ([`unique_name`]).
|
||||
pub(crate) fn import_name(base: &str, taken: impl Fn(&str) -> bool) -> String {
|
||||
if !taken(base) {
|
||||
return base.to_string();
|
||||
}
|
||||
let numbered = base
|
||||
.rsplit_once(' ')
|
||||
.filter(|(stem, n)| !stem.trim().is_empty() && !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()))
|
||||
.and_then(|(stem, n)| Some((stem, n.parse::<u64>().ok()?)));
|
||||
match numbered {
|
||||
Some((stem, n)) => {
|
||||
(n.saturating_add(1)..=u64::MAX).map(|i| format!("{stem} {i}")).find(|c| !taken(c)).unwrap_or_else(|| unique_name(base, &taken))
|
||||
}
|
||||
None => unique_name(base, taken),
|
||||
}
|
||||
}
|
||||
|
||||
/// What a `format` export of `doc`, whose file is `path`, writes when it isn't `doc` itself: a
|
||||
/// document that only has the Untitled-N name New gave it is titled after its file, as its tab is
|
||||
/// (the title PDF, PNG and SVG metadata carry). A title of its own stays, and native files keep
|
||||
|
||||
@@ -16,7 +16,7 @@ use serde_json::{Map, Value, json};
|
||||
use vectorcraft_doc::Document;
|
||||
use vectorcraft_geom::Rect;
|
||||
|
||||
use super::super::{NodeId, Result, Session, bad, str_param};
|
||||
use super::super::{EngineError, NodeId, Result, Session, bad, bool_or, str_param};
|
||||
use super::encode::single_artboard;
|
||||
use super::export::{export_crop, isolated, selected_artwork, without_artboards};
|
||||
use super::naming::{self, NameParts};
|
||||
@@ -277,7 +277,7 @@ impl Row {
|
||||
|
||||
/// The settings the Asset Export panel shares with the dialog: what is exported, not which art
|
||||
/// or where to.
|
||||
pub(crate) const SHARED_KEYS: [&str; 6] = ["formats", "preset", "settings", "prefix", "subfolders", "antiAlias"];
|
||||
pub const SHARED_KEYS: [&str; 6] = ["formats", "preset", "settings", "prefix", "subfolders", "antiAlias"];
|
||||
|
||||
/// What `document.exportForScreens` reads besides the formats.
|
||||
#[derive(Default, Deserialize)]
|
||||
@@ -667,24 +667,45 @@ fn with_preset(s: &Session, p: &Value) -> Result<Value> {
|
||||
Ok(Value::Object(merged))
|
||||
}
|
||||
|
||||
pub(super) fn presets_list(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
/// The rows of `document.exportPresets.list`: the built-in presets, then `saved`, each
|
||||
/// `{name, builtIn, settings}`.
|
||||
pub fn preset_list(saved: &[ScreenExportPreset]) -> Value {
|
||||
let row = |p: &ScreenExportPreset, built_in: bool| json!({"name": p.name, "builtIn": built_in, "settings": p.settings});
|
||||
let rows: Vec<Value> =
|
||||
builtin_presets().iter().map(|p| row(p, true)).chain(s.prefs.screen_export_presets.iter().map(|p| row(p, false))).collect();
|
||||
Ok(json!({ "presets": rows }))
|
||||
Value::Array(builtin_presets().iter().map(|p| row(p, true)).chain(saved.iter().map(|p| row(p, false))).collect())
|
||||
}
|
||||
|
||||
pub(super) fn presets_list(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
Ok(json!({ "presets": preset_list(&s.prefs.screen_export_presets) }))
|
||||
}
|
||||
|
||||
/// The [`PRESET_KEYS`] of `given`, refused as an error of command `cmd` when an export would
|
||||
/// refuse them.
|
||||
fn preset_settings(s: &Session, cmd: &str, given: &Map<String, Value>) -> Result<Map<String, Value>> {
|
||||
let settings: Map<String, Value> = given.iter().filter(|(k, _)| PRESET_KEYS.contains(&k.as_str())).map(|(k, v)| (k.clone(), v.clone())).collect();
|
||||
// Refuse what an export would refuse, now rather than when the preset is used.
|
||||
check_settings(s, &Value::Object(settings.clone())).map_err(|e| bad(cmd, e.to_string()))?;
|
||||
Folders::of(&ScreenParams::of(&Value::Object(settings.clone()))?).map_err(|e| bad(cmd, e.to_string()))?;
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
/// The longest export preset name kept (characters), as for asset names (`assets::MAX_NAME`).
|
||||
const MAX_PRESET_NAME: usize = 255;
|
||||
|
||||
/// `name` trimmed and at most [`MAX_PRESET_NAME`] characters long (`None` when empty), as
|
||||
/// `assets::clean_name` keeps an asset name.
|
||||
fn preset_name(name: &str) -> Option<String> {
|
||||
let n: String = name.trim().chars().take(MAX_PRESET_NAME).collect();
|
||||
(!n.is_empty()).then_some(n)
|
||||
}
|
||||
|
||||
pub(super) fn presets_save(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const SAVE: &str = "document.exportPresets.save";
|
||||
let name = str_param(p, "name").map(str::trim).filter(|n| !n.is_empty()).ok_or_else(|| bad(SAVE, "give the preset a name"))?.to_string();
|
||||
let name = str_param(p, "name").and_then(preset_name).ok_or_else(|| bad(SAVE, "give the preset a name"))?;
|
||||
if builtin_presets().iter().any(|b| b.name == name) {
|
||||
return Err(bad(SAVE, format!("`{name}` is a built-in preset: save under another name")));
|
||||
}
|
||||
let given = p.get("settings").and_then(Value::as_object).ok_or_else(|| bad(SAVE, "settings is an object of document.exportForScreens params"))?;
|
||||
let settings: Map<String, Value> = given.iter().filter(|(k, _)| PRESET_KEYS.contains(&k.as_str())).map(|(k, v)| (k.clone(), v.clone())).collect();
|
||||
// Refuse what an export would refuse, now rather than when the preset is used.
|
||||
check_settings(s, &Value::Object(settings.clone())).map_err(|e| bad(SAVE, e.to_string()))?;
|
||||
Folders::of(&ScreenParams::of(&Value::Object(settings.clone()))?).map_err(|e| bad(SAVE, e.to_string()))?;
|
||||
let settings = preset_settings(s, SAVE, given)?;
|
||||
let preset = ScreenExportPreset { name: name.clone(), settings };
|
||||
let created = match s.prefs.screen_export_presets.iter_mut().find(|q| q.name == name) {
|
||||
Some(slot) => {
|
||||
@@ -713,6 +734,178 @@ pub(super) fn presets_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "deleted": name }))
|
||||
}
|
||||
|
||||
/// What `document.exportPresets.export` writes and `document.exportPresets.import` reads.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct PresetFile {
|
||||
format: String,
|
||||
#[serde(default)]
|
||||
presets: Vec<ScreenExportPreset>,
|
||||
}
|
||||
|
||||
/// The `format` of an export presets file, also its extension.
|
||||
pub const PRESET_FILE_FORMAT: &str = "vcexportpresets";
|
||||
|
||||
/// The extensions of export presets files (opening one imports it).
|
||||
pub const PRESET_FILE_EXTS: &[&str] = &[PRESET_FILE_FORMAT];
|
||||
|
||||
/// The most presets one file imports.
|
||||
const MAX_IMPORT: usize = 1000;
|
||||
|
||||
/// The most bytes in an export presets file.
|
||||
const MAX_PRESET_FILE_BYTES: usize = 4 << 20;
|
||||
|
||||
pub(super) fn presets_export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const EXPORT: &str = "document.exportPresets.export";
|
||||
// A file holds no more presets than an import reads.
|
||||
let too_many = || bad(EXPORT, format!("more than {MAX_IMPORT} presets"));
|
||||
let presets = match p.get("names") {
|
||||
Some(Value::Array(names)) if names.len() > MAX_IMPORT => return Err(too_many()),
|
||||
Some(Value::Array(names)) => {
|
||||
let find = |n: &Value| {
|
||||
let n = n.as_str().ok_or_else(|| bad(EXPORT, "`names` lists preset names"))?;
|
||||
find_preset(s, n).ok_or_else(|| bad(EXPORT, format!("no export preset `{n}` (see document.exportPresets.list)")))
|
||||
};
|
||||
names.iter().map(find).collect::<Result<Vec<_>>>()?
|
||||
}
|
||||
None | Some(Value::Null) => s.prefs.screen_export_presets.clone(),
|
||||
Some(_) => return Err(bad(EXPORT, "`names` is a list of preset names")),
|
||||
};
|
||||
if presets.is_empty() {
|
||||
return Err(bad(EXPORT, "no saved export presets to export (name built-in ones in `names`)"));
|
||||
}
|
||||
if presets.len() > MAX_IMPORT {
|
||||
return Err(too_many());
|
||||
}
|
||||
let count = presets.len();
|
||||
let text = serde_json::to_string_pretty(&PresetFile { format: PRESET_FILE_FORMAT.into(), presets }).map_err(|e| bad(EXPORT, e.to_string()))?;
|
||||
if text.len() > MAX_PRESET_FILE_BYTES {
|
||||
return Err(bad(EXPORT, format!("export presets file is larger than {MAX_PRESET_FILE_BYTES} bytes")));
|
||||
}
|
||||
match str_param(p, "path") {
|
||||
Some(path) => {
|
||||
write_file(path, text.as_bytes())?;
|
||||
Ok(json!({"path": path, "count": count}))
|
||||
}
|
||||
None => Ok(json!({"data": text, "count": count})),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn presets_import(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const IMPORT: &str = "document.exportPresets.import";
|
||||
let bytes = match (str_param(p, "path"), str_param(p, "data"), str_param(p, "dataBase64")) {
|
||||
(Some(path), ..) => super::read_file_head(path, MAX_PRESET_FILE_BYTES + 1)?,
|
||||
(None, Some(text), _) => {
|
||||
if text.len() > MAX_PRESET_FILE_BYTES {
|
||||
return Err(bad(IMPORT, format!("export presets file is larger than {MAX_PRESET_FILE_BYTES} bytes")));
|
||||
}
|
||||
text.as_bytes().to_vec()
|
||||
}
|
||||
(None, None, Some(b64)) => {
|
||||
if b64.bytes().filter(|b| !b.is_ascii_whitespace()).count() > MAX_PRESET_FILE_BYTES.div_ceil(3) * 4 {
|
||||
return Err(bad(IMPORT, format!("export presets file is larger than {MAX_PRESET_FILE_BYTES} bytes")));
|
||||
}
|
||||
vectorcraft_format::base64_decode(b64).ok_or_else(|| bad(IMPORT, "bad dataBase64"))?
|
||||
}
|
||||
_ => return Err(bad(IMPORT, "give `path`, `data` or `dataBase64`")),
|
||||
};
|
||||
if bytes.len() > MAX_PRESET_FILE_BYTES {
|
||||
return Err(bad(IMPORT, format!("export presets file is larger than {MAX_PRESET_FILE_BYTES} bytes")));
|
||||
}
|
||||
let file: PresetFile = serde_json::from_slice(&bytes).map_err(|e| bad(IMPORT, format!("not an export presets file: {e}")))?;
|
||||
if file.format != PRESET_FILE_FORMAT {
|
||||
return Err(bad(IMPORT, format!("not an export presets file (format `{}`)", file.format)));
|
||||
}
|
||||
if file.presets.len() > MAX_IMPORT {
|
||||
return Err(bad(IMPORT, format!("more than {MAX_IMPORT} presets")));
|
||||
}
|
||||
// Settings a hand-edited file may carry are refused as document.exportPresets.save refuses
|
||||
// them, before any preset is added.
|
||||
let mut presets = vec![];
|
||||
for q in file.presets {
|
||||
let settings = preset_settings(s, IMPORT, &q.settings).map_err(|e| match e {
|
||||
EngineError::BadParams { msg, .. } => bad(IMPORT, format!("`{}`: {msg}", q.name)),
|
||||
e => e,
|
||||
})?;
|
||||
let name = preset_name(&q.name).unwrap_or_else(|| "Export Preset".to_string());
|
||||
presets.push((name, settings));
|
||||
}
|
||||
let replace = bool_or(p, "replace", false);
|
||||
let saved = &mut s.prefs.screen_export_presets;
|
||||
let saved_before = saved.len();
|
||||
// A built-in name, or a saved one that stays, gets a number.
|
||||
let mut taken: std::collections::HashSet<String> =
|
||||
builtin_presets().into_iter().map(|b| b.name).chain(saved.iter().map(|q| q.name.clone())).collect();
|
||||
let mut imported = vec![];
|
||||
for (base, settings) in presets {
|
||||
if replace && let Some(slot) = saved.iter_mut().take(saved_before).find(|q| q.name == base) {
|
||||
slot.settings = settings;
|
||||
imported.push(base);
|
||||
continue;
|
||||
}
|
||||
let mut name = super::import_name(&base, |n| taken.contains(n));
|
||||
if name.chars().count() > MAX_PRESET_NAME {
|
||||
// A number after a long name stays within the limit: the name is cut to leave room for
|
||||
// a space and the longest number.
|
||||
let short: String = base.chars().take(MAX_PRESET_NAME - 21).collect();
|
||||
let short = short.trim_end();
|
||||
name = super::import_name(short, |n| n == short || taken.contains(n));
|
||||
}
|
||||
taken.insert(name.clone());
|
||||
saved.push(ScreenExportPreset { name: name.clone(), settings });
|
||||
imported.push(name);
|
||||
}
|
||||
Ok(json!({ "imported": imported }))
|
||||
}
|
||||
|
||||
/// `settings` (a document's export settings) with export preset `name` applied as the Asset
|
||||
/// Export panel applies one: its formats (or mobile/density `preset`), its format settings over
|
||||
/// the ones there, its prefix (none when it has none), its folders (`folders`, or `subfolders`)
|
||||
/// with Create Sub-folders on for a folder per row, and its anti-aliasing.
|
||||
pub(crate) fn with_export_preset(s: &Session, cmd: &str, settings: &mut Map<String, Value>, name: &str) -> Result<()> {
|
||||
let preset = find_preset(s, name).ok_or_else(|| bad(cmd, format!("no export preset `{name}` (see document.exportPresets.list)")))?.settings;
|
||||
if let Some(formats) = preset.get("formats") {
|
||||
settings.insert("formats".into(), formats.clone());
|
||||
settings.remove("preset");
|
||||
} else if let Some(p) = preset.get("preset") {
|
||||
settings.insert("preset".into(), p.clone());
|
||||
settings.remove("formats");
|
||||
}
|
||||
if let Some(given) = preset.get("settings").and_then(Value::as_object) {
|
||||
let all = settings.entry("settings").or_insert_with(|| json!({}));
|
||||
if !all.is_object() {
|
||||
*all = json!({});
|
||||
}
|
||||
if let Some(all) = all.as_object_mut() {
|
||||
for (id, o) in given {
|
||||
match (all.get_mut(id).and_then(Value::as_object_mut), o.as_object()) {
|
||||
(Some(mine), Some(o)) => mine.extend(o.iter().map(|(k, v)| (k.clone(), v.clone()))),
|
||||
_ => _ = all.insert(id.clone(), o.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match preset.get("prefix") {
|
||||
Some(prefix) => _ = settings.insert("prefix".into(), prefix.clone()),
|
||||
None => _ = settings.remove("prefix"),
|
||||
}
|
||||
// Its `folders` (Export for Screens shows it; empty is none, as an export reads it), else its
|
||||
// `subfolders` (a folder per row or none). Create Sub-folders is on for a folder per row, as
|
||||
// the dialog sets it.
|
||||
let folders = preset
|
||||
.get("folders")
|
||||
.and_then(Value::as_str)
|
||||
.map(|f| if f.is_empty() { "none" } else { f })
|
||||
.or_else(|| preset.get("subfolders").and_then(Value::as_bool).map(|b| if b { "rows" } else { "none" }));
|
||||
if let Some(f) = folders {
|
||||
settings.insert("folders".into(), json!(f));
|
||||
settings.insert("subfolders".into(), json!(f == "rows"));
|
||||
}
|
||||
if let Some(aa) = preset.get("antiAlias") {
|
||||
settings.insert("antiAlias".into(), aa.clone());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Every chosen artboard (asset, layer, selected object) in every format row, one file each (a PDF
|
||||
/// holds its artboard alone), or with `fullDocument` one file per row (a PDF of every artboard,
|
||||
/// else the bounds of all art). Items that share a name get `-2`, `-3`… instead of overwriting, and
|
||||
|
||||
@@ -377,9 +377,10 @@ fn open_exts_cover_every_readable_format() {
|
||||
assert_eq!(filters[0], ("All readable files", OPEN_EXTS));
|
||||
assert_eq!(
|
||||
filters.len(),
|
||||
9 + FORMATS.iter().filter(|f| f.read).count(),
|
||||
"and swatch libraries, libraries, flattener, PDF and print presets, shortcut sets, scripts, plug-ins"
|
||||
10 + FORMATS.iter().filter(|f| f.read).count(),
|
||||
"and swatch libraries, libraries, flattener, PDF, print and export presets, shortcut sets, scripts, plug-ins"
|
||||
);
|
||||
assert!(filters.contains(&("Export presets", SCREEN_EXPORT_PRESET_EXTS)), "File › Open imports export presets");
|
||||
assert!(filters.contains(&("Scripts", crate::cmd::scripts::EXTS)), "File › Open checks scripts");
|
||||
assert_eq!(filters.last(), Some(&("Plug-ins", crate::cmd::plugin::EXTS)), "File › Open installs plug-ins");
|
||||
}
|
||||
|
||||
@@ -222,3 +222,231 @@ fn export_presets_save_use_and_delete() {
|
||||
assert!(s.execute(bad.0, &bad.1).is_err(), "{bad:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Export presets go to a file and come back from one: a name in use gets a number unless
|
||||
/// `replace`, a built-in name is never replaced, and a file in another format or with a setting an
|
||||
/// export refuses adds nothing.
|
||||
#[test]
|
||||
fn export_presets_export_and_import() {
|
||||
let mut s = session(2);
|
||||
assert!(s.execute("document.exportPresets.export", &json!({})).is_err(), "nothing saved to export");
|
||||
let web = json!({"name": "ACME-<name>", "folders": "item", "formats": [{"format": "jpg", "quality": 80}]});
|
||||
s.execute("document.exportPresets.save", &json!({"name": "Web JPG", "settings": web})).unwrap();
|
||||
let out = s.execute("document.exportPresets.export", &json!({})).unwrap();
|
||||
assert_eq!(out["count"], 1);
|
||||
let data = out["data"].as_str().unwrap().to_string();
|
||||
assert_eq!(serde_json::from_str::<Value>(&data).unwrap()["format"], "vcexportpresets");
|
||||
// Into a session with no saved presets: the preset as it was saved, and it exports.
|
||||
let mut other = session(2);
|
||||
let r = other.execute("document.exportPresets.import", &json!({ "data": data })).unwrap();
|
||||
assert_eq!(r["imported"], json!(["Web JPG"]));
|
||||
assert_eq!(other.prefs.screen_export_presets, s.prefs.screen_export_presets);
|
||||
assert_eq!(names(&export(&mut other, json!({"exportPreset": "Web JPG", "range": "2"}))), ["Artboard-2/ACME-Artboard 2.jpg"]);
|
||||
// Again: a second copy with a number; with replace, the saved one changes.
|
||||
let r = other.execute("document.exportPresets.import", &json!({ "data": data })).unwrap();
|
||||
assert_eq!(r["imported"], json!(["Web JPG 2"]));
|
||||
s.execute("document.exportPresets.save", &json!({"name": "Web JPG", "settings": {"formats": [{"format": "png"}]}})).unwrap();
|
||||
let changed = s.execute("document.exportPresets.export", &json!({"names": ["Web JPG"]})).unwrap()["data"].clone();
|
||||
let r = other.execute("document.exportPresets.import", &json!({"data": changed, "replace": true})).unwrap();
|
||||
assert_eq!(r["imported"], json!(["Web JPG"]));
|
||||
let saved: Vec<(&str, &Value)> =
|
||||
other.prefs.screen_export_presets.iter().map(|p| (p.name.as_str(), &p.settings["formats"][0]["format"])).collect();
|
||||
assert_eq!(saved, [("Web JPG", &json!("png")), ("Web JPG 2", &json!("jpg"))]);
|
||||
// A built-in preset exports by name and comes back under a new name; the built-in one stays.
|
||||
let path = std::env::temp_dir().join(format!("vc-export-presets-{}.vcexportpresets", std::process::id()));
|
||||
let path = path.to_string_lossy().to_string();
|
||||
let out = s.execute("document.exportPresets.export", &json!({"names": ["Logo Package"], "path": path})).unwrap();
|
||||
assert_eq!((out["path"].as_str(), out["count"].as_u64()), (Some(path.as_str()), Some(1)));
|
||||
let r = other.execute("document.exportPresets.import", &json!({"path": path, "replace": true})).unwrap();
|
||||
assert_eq!(r["imported"], json!(["Logo Package 2"]));
|
||||
let builtin = screens::builtin_presets();
|
||||
assert_eq!(other.prefs.screen_export_presets.last().map(|p| &p.settings), builtin.first().map(|p| &p.settings));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
// Refused files add nothing.
|
||||
let before = other.prefs.screen_export_presets.clone();
|
||||
let preset = |settings: Value| {
|
||||
json!({"format": "vcexportpresets", "presets": [{"name": "Fine", "settings": {"formats": [{"format": "png"}]}}, {"name": "Bad", "settings": settings}]}).to_string()
|
||||
};
|
||||
for bad in [
|
||||
json!({"data": "{\"format\": \"vcflattener\", \"presets\": []}"}),
|
||||
json!({"data": "not json"}),
|
||||
json!({"data": preset(json!({"formats": [{"format": "txt"}]}))}),
|
||||
json!({"data": preset(json!({"folders": "sideways"}))}),
|
||||
json!({"dataBase64": "%%"}),
|
||||
json!({}),
|
||||
] {
|
||||
assert!(other.execute("document.exportPresets.import", &bad).is_err(), "{bad}");
|
||||
}
|
||||
assert_eq!(other.prefs.screen_export_presets, before);
|
||||
assert!(s.execute("document.exportPresets.export", &json!({"names": ["Never saved"]})).is_err());
|
||||
// `names` that is not a list of names is an error, not every saved preset.
|
||||
for bad in [json!({"names": "Web JPG"}), json!({"names": [3]}), json!({"names": {"Web JPG": true}})] {
|
||||
assert!(s.execute("document.exportPresets.export", &bad).is_err(), "{bad}");
|
||||
}
|
||||
// One file imports at most 1,000 presets; names repeated in a file are numbered in turn.
|
||||
let many = |n: usize| {
|
||||
let one = json!({"name": "Same", "settings": {"formats": [{"format": "png"}]}});
|
||||
json!({"format": "vcexportpresets", "presets": vec![one; n]}).to_string()
|
||||
};
|
||||
assert!(other.execute("document.exportPresets.import", &json!({"data": many(1001)})).is_err(), "more than 1,000");
|
||||
assert_eq!(other.prefs.screen_export_presets, before);
|
||||
let r = other.execute("document.exportPresets.import", &json!({"data": many(1000)})).unwrap();
|
||||
let imported = r["imported"].as_array().unwrap();
|
||||
assert_eq!((imported.len(), imported.first(), imported.last()), (1000, Some(&json!("Same")), Some(&json!("Same 1000"))));
|
||||
// An export writes no file that an import refuses: at most 1,000 presets.
|
||||
assert!(other.prefs.screen_export_presets.len() > 1000);
|
||||
assert!(other.execute("document.exportPresets.export", &json!({})).is_err(), "every saved preset");
|
||||
let names: Vec<String> = other.prefs.screen_export_presets.iter().take(1001).map(|p| p.name.clone()).collect();
|
||||
assert!(other.execute("document.exportPresets.export", &json!({ "names": names })).is_err(), "1,001 names");
|
||||
let r = other.execute("document.exportPresets.export", &json!({ "names": names[..1000] })).unwrap();
|
||||
assert_eq!(r["count"], 1000);
|
||||
}
|
||||
|
||||
/// An imported file's unknown keys are dropped, a scale that is not a positive number refuses
|
||||
/// the file, and a prefix with folder separators names files inside the export folder.
|
||||
#[test]
|
||||
fn export_presets_import_drops_unknown_keys_and_checks_values() {
|
||||
let file = |settings: Value| {
|
||||
json!({"format": "vcexportpresets", "comment": "extra", "presets": [{"name": "P", "extra": 1, "settings": settings}]}).to_string()
|
||||
};
|
||||
let mut s = session(2);
|
||||
let r = s.execute("document.exportPresets.import", &json!({"data": file(json!({"formats": [{"format": "svg"}], "unknown": true}))})).unwrap();
|
||||
assert_eq!(r["imported"], json!(["P"]));
|
||||
let saved = &s.prefs.screen_export_presets[0].settings;
|
||||
assert_eq!(saved.keys().collect::<Vec<_>>(), ["formats"], "{saved:?}");
|
||||
for scale in [json!("0x"), json!("-2x"), json!("1e9x"), json!("2y"), json!("nanx")] {
|
||||
let bad = file(json!({"formats": [{"format": "png", "scale": scale}]}));
|
||||
assert!(s.execute("document.exportPresets.import", &json!({ "data": bad })).is_err(), "{scale}");
|
||||
}
|
||||
assert_eq!(s.prefs.screen_export_presets.len(), 1);
|
||||
let climb = file(json!({"formats": [{"format": "svg"}], "prefix": "../..\\up/"}));
|
||||
let r = s.execute("document.exportPresets.import", &json!({ "data": climb })).unwrap();
|
||||
assert_eq!(r["imported"], json!(["P 2"]));
|
||||
assert_eq!(names(&export(&mut s, json!({"exportPreset": "P 2", "range": "2"}))), ["..-..-up-Artboard-2.svg"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_presets_import_replace_numbers_duplicates_from_one_file() {
|
||||
let mut source = session(1);
|
||||
source.execute("document.exportPresets.save", &json!({"name": "A", "settings": {"formats": [{"format": "png"}]}})).unwrap();
|
||||
let data = source.execute("document.exportPresets.export", &json!({"names": ["A", "A"]})).unwrap()["data"].clone();
|
||||
|
||||
let mut empty = session(1);
|
||||
let reply = empty.execute("document.exportPresets.import", &json!({"data": data, "replace": true})).unwrap();
|
||||
assert_eq!(reply["imported"], json!(["A", "A 2"]));
|
||||
assert_eq!(empty.prefs.screen_export_presets.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["A", "A 2"]);
|
||||
|
||||
let mut existing = session(1);
|
||||
existing.execute("document.exportPresets.save", &json!({"name": "A", "settings": {"formats": [{"format": "jpg"}]}})).unwrap();
|
||||
let reply = existing.execute("document.exportPresets.import", &json!({"data": data, "replace": true})).unwrap();
|
||||
assert_eq!(reply["imported"], json!(["A", "A"]));
|
||||
assert_eq!(existing.prefs.screen_export_presets.len(), 1);
|
||||
assert_eq!(existing.prefs.screen_export_presets[0].settings["formats"][0]["format"], "png");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_presets_import_refuses_oversize_input_from_every_source() {
|
||||
let mut s = session(1);
|
||||
let good = s.execute("document.exportPresets.export", &json!({"names": ["Logo Package"]})).unwrap()["data"].as_str().unwrap().to_string();
|
||||
let oversized = format!("{good}{}", " ".repeat((4 << 20) + 1));
|
||||
let path = std::env::temp_dir().join(format!("vc-oversize-export-presets-{}.vcexportpresets", std::process::id()));
|
||||
std::fs::write(&path, &oversized).unwrap();
|
||||
for params in [
|
||||
json!({"path": path.to_string_lossy()}),
|
||||
json!({"data": oversized}),
|
||||
json!({"dataBase64": vectorcraft_format::base64_encode(oversized.as_bytes())}),
|
||||
] {
|
||||
let error = s.execute("document.exportPresets.import", ¶ms).unwrap_err().to_string();
|
||||
assert!(error.contains("larger than 4194304 bytes"), "{error}");
|
||||
assert!(s.prefs.screen_export_presets.is_empty());
|
||||
}
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_presets_import_accepts_wrapped_base64_near_size_limit() {
|
||||
const LIMIT: usize = 4 << 20;
|
||||
let empty = json!({
|
||||
"format": "vcexportpresets",
|
||||
"presets": [{"name": "Wrapped", "settings": {"formats": [{"format": "png"}], "prefix": ""}}],
|
||||
});
|
||||
let prefix = "x".repeat(LIMIT - 1 - empty.to_string().len());
|
||||
let file = json!({
|
||||
"format": "vcexportpresets",
|
||||
"presets": [{"name": "Wrapped", "settings": {"formats": [{"format": "png"}], "prefix": prefix}}],
|
||||
})
|
||||
.to_string();
|
||||
assert_eq!(file.len(), LIMIT - 1);
|
||||
let encoded = vectorcraft_format::base64_encode(file.as_bytes());
|
||||
let wrapped = encoded.as_bytes().chunks(76).map(|line| std::str::from_utf8(line).unwrap()).collect::<Vec<_>>().join("\n");
|
||||
assert!(wrapped.len() > LIMIT.div_ceil(3) * 4);
|
||||
assert_eq!(vectorcraft_format::base64_decode(&wrapped).as_deref(), Some(file.as_bytes()));
|
||||
|
||||
let mut s = session(1);
|
||||
let reply = s.execute("document.exportPresets.import", &json!({"dataBase64": wrapped})).unwrap();
|
||||
assert_eq!(reply["imported"], json!(["Wrapped"]));
|
||||
assert_eq!(s.prefs.screen_export_presets[0].settings["prefix"], prefix);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_presets_export_refuses_oversize_output() {
|
||||
let mut s = session(1);
|
||||
let prefix = "x".repeat((4 << 20) + 1);
|
||||
s.execute("document.exportPresets.save", &json!({"name": "Large", "settings": {"prefix": prefix}})).unwrap();
|
||||
|
||||
let error = s.execute("document.exportPresets.export", &json!({})).unwrap_err().to_string();
|
||||
assert!(error.contains("larger than 4194304 bytes"), "{error}");
|
||||
|
||||
let path = std::env::temp_dir().join(format!("vc-oversize-preset-output-{}.vcexportpresets", std::process::id()));
|
||||
assert!(!path.exists());
|
||||
let error = s.execute("document.exportPresets.export", &json!({"path": path.to_string_lossy()})).unwrap_err().to_string();
|
||||
assert!(error.contains("larger than 4194304 bytes"), "{error}");
|
||||
assert!(!path.exists());
|
||||
}
|
||||
|
||||
/// An imported preset whose name is taken and ends in a number gets the next free number, not a
|
||||
/// second one; any other taken name gets the first free number.
|
||||
#[test]
|
||||
fn an_imported_numbered_name_counts_on() {
|
||||
let taken = ["Logo Package", "Logo Package 2", "Logo Package 4", "Web", "Web 2", "2", "Max 18446744073709551615"];
|
||||
let name = |base: &str| import_name(base, |n| taken.contains(&n));
|
||||
assert_eq!(name("Print"), "Print");
|
||||
assert_eq!(name("Logo Package 3"), "Logo Package 3");
|
||||
assert_eq!(name("Logo Package"), "Logo Package 3");
|
||||
assert_eq!(name("Logo Package 2"), "Logo Package 3");
|
||||
assert_eq!(name("Logo Package 4"), "Logo Package 5");
|
||||
assert_eq!(name("Web"), "Web 3");
|
||||
// A number alone, or one past the largest, is not counted on.
|
||||
assert_eq!(name("2"), "2 2");
|
||||
assert_eq!(name("Max 18446744073709551615"), "Max 18446744073709551615 2");
|
||||
// Through the command: a built-in preset imported twice is Logo Package 2, then Logo Package 3.
|
||||
let mut s = session(1);
|
||||
let data = s.execute("document.exportPresets.export", &json!({"names": ["Logo Package"]})).unwrap()["data"].clone();
|
||||
assert_eq!(s.execute("document.exportPresets.import", &json!({ "data": data })).unwrap()["imported"], json!(["Logo Package 2"]));
|
||||
let data = s.execute("document.exportPresets.export", &json!({"names": ["Logo Package 2"]})).unwrap()["data"].clone();
|
||||
assert_eq!(s.execute("document.exportPresets.import", &json!({ "data": data })).unwrap()["imported"], json!(["Logo Package 3"]));
|
||||
}
|
||||
|
||||
/// A preset name, imported or saved, keeps at most 255 characters, as an asset name does.
|
||||
#[test]
|
||||
fn export_preset_names_are_capped() {
|
||||
let mut s = session(1);
|
||||
let long = "é".repeat(300);
|
||||
let capped = "é".repeat(255);
|
||||
let file = json!({"format": "vcexportpresets", "presets": [{"name": long, "settings": {"formats": [{"format": "png"}]}}]}).to_string();
|
||||
let r = s.execute("document.exportPresets.import", &json!({ "data": file })).unwrap();
|
||||
assert_eq!(r["imported"], json!([capped]));
|
||||
let r = s.execute("document.exportPresets.save", &json!({"name": format!(" {}", "w".repeat(400)), "settings": {}})).unwrap();
|
||||
assert_eq!(r["name"], "w".repeat(255));
|
||||
let names: Vec<usize> = s.prefs.screen_export_presets.iter().map(|p| p.name.chars().count()).collect();
|
||||
assert_eq!(names, [255, 255]);
|
||||
// The same long name imported again gets a number within the limit, and saving under the name
|
||||
// it got changes that preset alone.
|
||||
let r = s.execute("document.exportPresets.import", &json!({ "data": file })).unwrap();
|
||||
let again = r["imported"][0].as_str().unwrap().to_string();
|
||||
assert!(again.chars().count() <= 255 && again != capped && again.ends_with(" 2"), "{again}");
|
||||
s.execute("document.exportPresets.save", &json!({"name": again, "settings": {"formats": [{"format": "svg"}]}})).unwrap();
|
||||
let formats: Vec<Option<&str>> =
|
||||
s.prefs.screen_export_presets.iter().map(|p| p.settings.get("formats").and_then(|f| f[0]["format"].as_str())).collect();
|
||||
assert_eq!(formats, [Some("png"), None, Some("svg")]);
|
||||
}
|
||||
|
||||
@@ -268,6 +268,110 @@ fn the_shared_settings_drive_the_export_and_arent_an_undo_step() {
|
||||
assert_eq!(run(&mut s, "document.exportSettings", json!({}))["settings"]["formats"][0]["format"], "jpg");
|
||||
}
|
||||
|
||||
/// An export preset sets the shared settings as the Asset Export panel applies one: its rows (or
|
||||
/// mobile/density), its format settings over the saved ones, its prefix and its sub-folders.
|
||||
#[test]
|
||||
fn an_export_preset_sets_the_shared_settings() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 10.0, 10.0, 20.0, 20.0);
|
||||
run(&mut s, "assets.add", json!({"ids": [a]}));
|
||||
run(
|
||||
&mut s,
|
||||
"assets.settings.set",
|
||||
json!({"preset": "mobile", "prefix": "old_", "settings": {"png": {"background": "white"}, "jpg": {"quality": 30}}}),
|
||||
);
|
||||
let pack =
|
||||
json!({"formats": [{"format": "png"}, {"format": "svg"}], "folders": "rows", "settings": {"png": {"antiAlias": "type"}}, "name": "x-<name>"});
|
||||
run(&mut s, "document.exportPresets.save", json!({"name": "Pack", "settings": pack}));
|
||||
run(&mut s, "assets.settings.set", json!({"exportPreset": "Pack"}));
|
||||
let settings = run(&mut s, "document.exportSettings", json!({}))["settings"].clone();
|
||||
assert!(settings.get("preset").is_none() && settings.get("prefix").is_none(), "{settings}");
|
||||
assert_eq!(settings["formats"], pack["formats"]);
|
||||
assert_eq!(settings["subfolders"], true);
|
||||
assert_eq!(settings["settings"], json!({"png": {"background": "white", "antiAlias": "type"}, "jpg": {"quality": 30}}));
|
||||
let out = files(&run(&mut s, "assets.export", json!({})));
|
||||
assert_eq!(out.iter().map(|f| f.0.as_str()).collect::<Vec<_>>(), ["1x/Asset-1.png", "SVG/Asset-1.svg"]);
|
||||
// A built-in preset; the call's own keys go over it.
|
||||
run(&mut s, "assets.settings.set", json!({"exportPreset": "Print-Ready PDF", "prefix": "p_"}));
|
||||
let settings = run(&mut s, "document.exportSettings", json!({}))["settings"].clone();
|
||||
assert_eq!((settings["formats"][0]["format"].as_str(), settings["prefix"].as_str()), (Some("pdf"), Some("p_")));
|
||||
for bad in [json!({"exportPreset": "Never saved"}), json!({"exportPreset": 3})] {
|
||||
assert!(s.execute("assets.settings.set", &bad).is_err(), "{bad}");
|
||||
}
|
||||
assert_eq!(run(&mut s, "document.exportSettings", json!({}))["settings"]["prefix"], "p_");
|
||||
}
|
||||
|
||||
/// An export preset applied to the shared settings keeps its folders, with Create Sub-folders on
|
||||
/// for a folder per row; with Create Sub-folders off every file goes into the folder itself,
|
||||
/// mobile and density rows too.
|
||||
#[test]
|
||||
fn create_sub_folders_off_puts_every_asset_file_in_the_folder() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 10.0, 10.0, 20.0, 20.0);
|
||||
run(&mut s, "assets.add", json!({"ids": [a]}));
|
||||
let saved = [("None", json!({"folders": "none"})), ("Items", json!({"folders": "itemFormat"})), ("Panel", json!({"subfolders": true}))];
|
||||
for (name, mut settings) in saved {
|
||||
settings["formats"] = json!([{"format": "svg"}]);
|
||||
run(&mut s, "document.exportPresets.save", json!({"name": name, "settings": settings}));
|
||||
}
|
||||
// (preset, the folders and Create Sub-folders it leaves).
|
||||
let want = [("Logo Package", "format", false), ("None", "none", false), ("Items", "itemFormat", false), ("Panel", "rows", true)];
|
||||
for (preset, folders, sub) in want {
|
||||
run(&mut s, "assets.settings.set", json!({"exportPreset": preset}));
|
||||
let settings = run(&mut s, "document.exportSettings", json!({}))["settings"].clone();
|
||||
assert_eq!((settings["folders"].as_str(), settings["subfolders"].as_bool()), (Some(folders), Some(sub)), "{preset}: {settings}");
|
||||
}
|
||||
let paths = |s: &mut Session, p: Value| -> Vec<String> { files(&run(s, "assets.export", p)).into_iter().map(|f| f.0).collect() };
|
||||
assert_eq!(paths(&mut s, json!({})), ["SVG/Asset-1.svg"]);
|
||||
assert_eq!(paths(&mut s, json!({"subfolders": false})), ["Asset-1.svg"]);
|
||||
run(&mut s, "assets.settings.set", json!({"exportPreset": "Logo Package"}));
|
||||
let out = paths(&mut s, json!({}));
|
||||
assert_eq!(out.len(), 5, "{out:?}");
|
||||
assert!(out.iter().all(|f| !f.contains('/')), "{out:?}");
|
||||
// Create Sub-folders set in the panel is the dialog's folder per row, on and off.
|
||||
let folders = |s: &mut Session| run(s, "document.exportSettings", json!({}))["settings"]["folders"].clone();
|
||||
run(&mut s, "assets.settings.set", json!({"subfolders": true}));
|
||||
assert_eq!(folders(&mut s), "rows");
|
||||
run(&mut s, "assets.settings.set", json!({"subfolders": false}));
|
||||
assert_eq!(folders(&mut s), "none");
|
||||
run(&mut s, "assets.settings.set", json!({"exportPreset": "Items", "subfolders": false}));
|
||||
assert_eq!(folders(&mut s), "itemFormat");
|
||||
// A mobile preset saved with no sub-folders exports its rows into the folder itself.
|
||||
run(&mut s, "document.exportPresets.save", json!({"name": "Flat Mobile", "settings": {"preset": "mobile", "folders": "none"}}));
|
||||
run(&mut s, "assets.settings.set", json!({"exportPreset": "Flat Mobile"}));
|
||||
let out = paths(&mut s, json!({}));
|
||||
assert_eq!(out.len(), 3, "{out:?}");
|
||||
assert!(out.iter().all(|f| !f.contains('/')), "{out:?}");
|
||||
// Create Sub-folders turned off keeps mobile rows in the folder, set or given to the export.
|
||||
run(&mut s, "assets.settings.set", json!({"exportPreset": "Flat Mobile", "subfolders": false}));
|
||||
assert!(paths(&mut s, json!({})).iter().all(|f| !f.contains('/')));
|
||||
run(&mut s, "assets.settings.set", json!({"subfolders": true}));
|
||||
assert_eq!(paths(&mut s, json!({})).iter().filter(|f| f.starts_with("3x/")).count(), 1);
|
||||
assert!(paths(&mut s, json!({"subfolders": false})).iter().all(|f| !f.contains('/')));
|
||||
run(&mut s, "assets.settings.set", json!({"subfolders": false}));
|
||||
assert!(paths(&mut s, json!({})).iter().all(|f| !f.contains('/')));
|
||||
}
|
||||
|
||||
/// An export preset whose `folders` is empty puts every file in the folder itself, whatever its
|
||||
/// `subfolders`: exported with `document.exportForScreens` and applied to the shared settings.
|
||||
#[test]
|
||||
fn an_export_preset_with_empty_folders_exports_into_the_folder() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 10.0, 10.0, 20.0, 20.0);
|
||||
let ids = run(&mut s, "assets.add", json!({"ids": [a]}))["assets"].clone();
|
||||
let settings = json!({"formats": [{"format": "svg"}, {"format": "png", "scale": "2x"}], "folders": "", "subfolders": true});
|
||||
run(&mut s, "document.exportPresets.save", json!({"name": "Flat", "settings": settings}));
|
||||
let names = |r: &Value| -> Vec<String> { files(r).into_iter().map(|f| f.0).collect() };
|
||||
let direct = names(&run(&mut s, "document.exportForScreens", json!({"assets": ids, "exportPreset": "Flat"})));
|
||||
assert_eq!(direct.len(), 2, "{direct:?}");
|
||||
assert!(direct.iter().all(|f| !f.contains('/')), "{direct:?}");
|
||||
run(&mut s, "assets.settings.set", json!({"exportPreset": "Flat"}));
|
||||
let shared = run(&mut s, "document.exportSettings", json!({}))["settings"].clone();
|
||||
assert_eq!((shared["folders"].as_str(), shared["subfolders"].as_bool()), (Some("none"), Some(false)), "{shared}");
|
||||
let panel = names(&run(&mut s, "assets.export", json!({})));
|
||||
assert_eq!(panel, direct);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_for_screens_writes_chosen_assets_and_keeps_its_rules() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Untrusted files never crash the app: garbage, truncated, mutated and hostile SVG, PDF and DXF input,
|
||||
//! mutated raster images (Photoshop documents among them) placed with File → Place, and swatch (`.vcswatches`, `.gpl`, `.ase`), graphic style (`.vcstyles`) and flattener preset
|
||||
//! (`.vcflattener`) libraries, shortcut sets other apps save as text (`.kys`), and native files (compressed, damaged, saved for older versions),
|
||||
//! mutated raster images (Photoshop documents among them) placed with File → Place, and swatch (`.vcswatches`, `.gpl`, `.ase`), graphic style (`.vcstyles`), flattener preset
|
||||
//! (`.vcflattener`) and export preset (`.vcexportpresets`) libraries, shortcut sets other apps save as text (`.kys`), and native files (compressed, damaged, saved for older versions),
|
||||
//! must load as an error or as a document that then renders and exports, without a panic; nor may
|
||||
//! bitmaps, PDF and text pasted from other apps, nor EMF and WMF pictures (damaged files, records
|
||||
//! of every kind with random contents) opened, placed or pasted, nor EPS and PostScript files
|
||||
@@ -1228,6 +1228,16 @@ fn flattener_presets(what: &str, data: &str) -> Result<(), TestCaseError> {
|
||||
})
|
||||
}
|
||||
|
||||
fn export_presets(what: &str, data: &str) -> Result<(), TestCaseError> {
|
||||
survive_library(what, "document.exportPresets.import", data, |s, r| {
|
||||
if let Some(name) = r["imported"].get(0) {
|
||||
let _ = no_panic(s.execute("assets.settings.set", &json!({"exportPreset": name})));
|
||||
let _ = no_panic(s.execute("document.exportPresets.export", &json!({"names": [name]})));
|
||||
let _ = no_panic(s.execute("document.exportForScreens", &json!({"exportPreset": name, "range": "1"})));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Swatch exchange (`.ase`) bytes loaded as a swatch library, then added to the rich document.
|
||||
fn swatch_exchange(what: &str, bytes: &[u8]) -> Result<(), TestCaseError> {
|
||||
survive(what, || {
|
||||
@@ -1279,11 +1289,12 @@ proptest! {
|
||||
#![proptest_config(config())]
|
||||
|
||||
#[test]
|
||||
fn library_garbage_never_panics(s in ".{0,300}", head in prop::sample::select(vec!["", "GIMP Palette\n","{\"format\": \"vcswatches\", ", "{\"format\": \"vcstyles\", ", "{\"format\": \"vcflattener\", ", "{\"name\": \"L\", "])) {
|
||||
fn library_garbage_never_panics(s in ".{0,300}", head in prop::sample::select(vec!["", "GIMP Palette\n","{\"format\": \"vcswatches\", ", "{\"format\": \"vcstyles\", ", "{\"format\": \"vcflattener\", ", "{\"format\": \"vcexportpresets\", ", "{\"name\": \"L\", "])) {
|
||||
let data = format!("{head}{s}");
|
||||
swatches("swatch library garbage", &data)?;
|
||||
styles("style library garbage", &data)?;
|
||||
flattener_presets("flattener preset garbage", &data)?;
|
||||
export_presets("export preset garbage", &data)?;
|
||||
libraries("library garbage", &data)?;
|
||||
}
|
||||
|
||||
@@ -1305,6 +1316,12 @@ proptest! {
|
||||
flattener_presets("mutated flattener presets", &mutate_text(&text, cut, &edits))?;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutated_export_presets_never_panic(cut in 0usize..5_000, edits in prop::collection::vec(arb_edit(), 0..10)) {
|
||||
let text = saved("document.exportPresets.export", json!({"names": ["Logo Package", "Print-Ready PDF"]}));
|
||||
export_presets("mutated export presets", &mutate_text(&text, cut, &edits))?;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutated_libraries_never_panic(keep in 0.0f64..1.2, edits in prop::collection::vec(arb_edit(), 0..10)) {
|
||||
// Cut anywhere in the file (whole most of the time), the edits anywhere in it.
|
||||
|
||||
@@ -150,6 +150,7 @@ impl VectorcraftApp {
|
||||
ime_caret_rect: None,
|
||||
ime_discard: false,
|
||||
prefs_changed: false,
|
||||
asset_export_presets: Value::Null,
|
||||
touch_screen: false,
|
||||
scrub_group: false,
|
||||
scrub_scope: None,
|
||||
|
||||
@@ -335,10 +335,20 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlReques
|
||||
let names = crate::dialogs::field_names(d).join(", ");
|
||||
return err(format!("the `{}` dialog has no field `{f}`; its fields: {names}", d.kind));
|
||||
}
|
||||
// In order: a field that sets others (an export preset) goes before the fields after it.
|
||||
// A field that fails leaves the dialog as it was before the request.
|
||||
let before = app.ui.dialog.clone();
|
||||
for (f, value) in fields {
|
||||
crate::dialogs::set_field(d, f, value);
|
||||
let old = app.ui.dialog.as_ref().and_then(|d| d.fields.get(&f).cloned());
|
||||
if let Some(d) = app.ui.dialog.as_mut() {
|
||||
crate::dialogs::set_field(d, f.clone(), value);
|
||||
}
|
||||
if let Err(e) = crate::dialogs::field_set(app, &f, old.as_ref()) {
|
||||
app.ui.dialog = before;
|
||||
return err(e);
|
||||
}
|
||||
}
|
||||
ok(serde_json::to_value(&*d).unwrap_or_default())
|
||||
ok(app.ui.dialog.as_ref().and_then(|d| serde_json::to_value(d).ok()).unwrap_or_default())
|
||||
}
|
||||
None => err("no dialog open"),
|
||||
},
|
||||
|
||||
@@ -135,8 +135,9 @@ pub(crate) use effect::{FILM_GRAIN_FIELDS, PAINT_DAUB_BRUSH_TYPE_LABELS, SHADES,
|
||||
pub use export_as::open as open_export_as;
|
||||
pub use export_for_screens::open as open_export_for_screens;
|
||||
pub(crate) use export_for_screens::{
|
||||
KIND as EXPORT_FOR_SCREENS, formats as screen_formats, open_assets as open_export_for_screens_assets, saved_options as screen_saved_options,
|
||||
saved_rows as screen_saved_rows,
|
||||
KIND as EXPORT_FOR_SCREENS, PresetPick as ScreenPresetPick, export_preset as screen_export_preset, formats as screen_formats,
|
||||
import_presets as screen_import_presets, open_assets as open_export_for_screens_assets, preset_list as screen_preset_list,
|
||||
saved_options as screen_saved_options, saved_rows as screen_saved_rows,
|
||||
};
|
||||
pub(crate) use form::{humanize, number_list_text, read_number_list, unit_suffix};
|
||||
pub(crate) use new_document::preset_name;
|
||||
@@ -487,6 +488,24 @@ pub(crate) fn parent(d: &Dialog) -> Option<Dialog> {
|
||||
d.fields.get(PARENT).and_then(|v| serde::Deserialize::deserialize(v).ok())
|
||||
}
|
||||
|
||||
/// `ui.dialog.set` changed `field` of the open dialog from `old`: a choice that sets other fields
|
||||
/// sets them as choosing it in the dialog does (Export for Screens' `exportPreset`), and a change
|
||||
/// to Export for Screens' sub-folder controls ends an applied preset's item folders, as it does in
|
||||
/// the dialog.
|
||||
pub(crate) fn field_set(app: &mut VectorcraftApp, field: &str, old: Option<&Value>) -> Result<(), String> {
|
||||
let Some(d) = app.ui.dialog.as_mut().filter(|d| d.kind == export_for_screens::KIND) else { return Ok(()) };
|
||||
match field {
|
||||
"exportPreset" => export_for_screens::preset_set(app),
|
||||
"subfolders" | "subfolderBy" => {
|
||||
if d.fields.get(field) != old {
|
||||
d.fields.remove("__itemFolders");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the open dialog as Cancel does, rolling back a live preview and showing the dialog it was
|
||||
/// opened over (`ui.dialog.cancel`).
|
||||
pub fn cancel(app: &mut VectorcraftApp) {
|
||||
|
||||
@@ -170,6 +170,7 @@ pub fn open(app: &mut VectorcraftApp) {
|
||||
"openLocation": flag("openLocation", true),
|
||||
"subfolders": subfolders,
|
||||
"subfolderBy": if folders == Some("format") { "format" } else { "scale" },
|
||||
"__itemFolders": folders.filter(|f| matches!(*f, "item" | "itemFormat")).unwrap_or_default(),
|
||||
"pdfs": if get("pdfs").and_then(Value::as_str) == Some("single") { "single" } else { "multiple" },
|
||||
"prefix": get("prefix").and_then(Value::as_str).unwrap_or_default(),
|
||||
"preset": preset.unwrap_or_default(),
|
||||
@@ -262,7 +263,7 @@ pub(super) fn export_presets(app: &mut VectorcraftApp) -> Value {
|
||||
|
||||
/// The `document.exportForScreens` params the dialog describes (the web: no folder, one ZIP for
|
||||
/// several files).
|
||||
fn params(app: &VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
pub(super) fn params(app: &VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let (mut p, files) = settings_params(app, d)?;
|
||||
if io::is_web(app) {
|
||||
p.insert("zip".into(), json!(files > 1));
|
||||
@@ -327,11 +328,7 @@ fn selection_params(app: &VectorcraftApp, d: &Dialog) -> Result<(Map<String, Val
|
||||
let files = if single { (rows.len() - pdf_rows) * pieces + pdf_rows } else { rows.len() * pieces };
|
||||
p.insert("includeBleed".into(), json!(d.bool("includeBleed")));
|
||||
p.insert("includeArtboardFill".into(), json!(d.bool("includeArtboardFill")));
|
||||
let folders = match (d.bool("subfolders"), d.str("subfolderBy").as_str()) {
|
||||
(false, _) => "none",
|
||||
(true, "format") => "format",
|
||||
(true, _) => "rows",
|
||||
};
|
||||
let folders = folders(d);
|
||||
// Create Sub-folders, as the Asset Export panel (which shares these settings) names it.
|
||||
p.insert("subfolders".into(), json!(folders == "rows"));
|
||||
p.insert("folders".into(), json!(folders));
|
||||
@@ -342,6 +339,21 @@ fn selection_params(app: &VectorcraftApp, d: &Dialog) -> Result<(Map<String, Val
|
||||
Ok((p, files, pieces))
|
||||
}
|
||||
|
||||
/// The `folders` the dialog's sub-folder controls give: `none`, `format`, or a folder per scale
|
||||
/// (`rows`), or the `item` or `itemFormat` of an applied preset (`__itemFolders`) while the
|
||||
/// controls have not changed.
|
||||
fn folders(d: &Dialog) -> &'static str {
|
||||
match (d.bool("subfolders"), d.str("subfolderBy").as_str()) {
|
||||
(false, _) => "none",
|
||||
(true, "format") => "format",
|
||||
(true, _) => match d.str("__itemFolders").as_str() {
|
||||
"item" => "item",
|
||||
"itemFormat" => "itemFormat",
|
||||
_ => "rows",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Export; the dialog stays open when the export fails.
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let p = params(app, d)?;
|
||||
@@ -627,6 +639,7 @@ fn destination(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
});
|
||||
form::check(ui, d, "openLocation", tl!("Open Location after Export"));
|
||||
}
|
||||
let before = (d.bool("subfolders"), d.str("subfolderBy"));
|
||||
form::check(ui, d, "subfolders", tl!("Create Sub-folders"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(22.0);
|
||||
@@ -639,6 +652,10 @@ fn destination(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
d.fields.insert("subfolderBy".into(), json!("format"));
|
||||
}
|
||||
});
|
||||
// A change to the sub-folder controls replaces an applied preset's item folders.
|
||||
if (d.bool("subfolders"), d.str("subfolderBy")) != before {
|
||||
d.fields.remove("__itemFolders");
|
||||
}
|
||||
}
|
||||
|
||||
/// Export PDFs As: one file of every chosen artboard, or a file each; only for artboards (not
|
||||
@@ -662,20 +679,40 @@ fn pdf_mode(ui: &mut egui::Ui, d: &mut Dialog, artboards: bool) {
|
||||
});
|
||||
}
|
||||
|
||||
/// What the format presets menu (or the Asset Export panel's menu) asked for.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum PresetPick {
|
||||
/// Use this preset (`{name, builtIn, settings}`).
|
||||
Apply(Value),
|
||||
/// Save the settings as the preset of this name (Save, after a name was typed).
|
||||
Save(String),
|
||||
/// Delete this saved preset.
|
||||
Delete(String),
|
||||
/// Import Preset…: add the presets of a file.
|
||||
Import,
|
||||
/// Export Preset…: write this preset to a file.
|
||||
Export(String),
|
||||
}
|
||||
|
||||
/// The format presets (the ☰ by iOS and Android): apply a built-in or saved one (its rows, their
|
||||
/// settings, the prefix and the sub-folders), save the dialog's rows as one, or delete the saved
|
||||
/// one applied last.
|
||||
fn preset_menu(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let list: Vec<Value> = d.fields.get("__exportPresets").and_then(Value::as_array).cloned().unwrap_or_default();
|
||||
// Presets saved, deleted or imported elsewhere (a command, a file opened) show at once.
|
||||
let mut list = d.fields.remove("__exportPresets").unwrap_or_default();
|
||||
preset_list(&mut list, &app.session.prefs.screen_export_presets);
|
||||
let rows: Vec<Value> = list.as_array().cloned().unwrap_or_default();
|
||||
d.fields.insert("__exportPresets".into(), list);
|
||||
let current = d.str("exportPreset");
|
||||
let saved = list.iter().any(|p| p["name"] == current && p["builtIn"] == false);
|
||||
let saved = rows.iter().any(|p| p["name"] == current && p["builtIn"] == false);
|
||||
let mut pick = None;
|
||||
let resp = widgets::icon_button(ui, "menu", tl!("Format Presets"), false, 24.0);
|
||||
let clicked = resp.clicked();
|
||||
let _ = widgets::popover(&resp, clicked, |ui| {
|
||||
for p in &list {
|
||||
for p in &rows {
|
||||
let name = p["name"].as_str().unwrap_or_default();
|
||||
if widgets::menu_item_name(ui, name, true, name == current) {
|
||||
apply_preset(d, p);
|
||||
pick = Some(PresetPick::Apply(p.clone()));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
@@ -684,12 +721,14 @@ fn preset_menu(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
d.fields.insert("__savePreset".into(), json!(if saved { current.clone() } else { String::new() }));
|
||||
ui.close();
|
||||
}
|
||||
if widgets::menu_item(ui, tl!("Delete Preset"), saved, false) && app.run("document.exportPresets.delete", json!({"name": current})).is_ok() {
|
||||
d.fields.insert("exportPreset".into(), json!(""));
|
||||
d.fields.insert("__exportPresets".into(), export_presets(app));
|
||||
if widgets::menu_item(ui, tl!("Delete Preset"), saved, false) {
|
||||
pick = Some(PresetPick::Delete(current.clone()));
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
if let Some(pick) = pick {
|
||||
preset_pick(app, d, pick);
|
||||
}
|
||||
}
|
||||
|
||||
/// The row that names a format preset being saved (Save Preset… in the ☰ menu).
|
||||
@@ -697,39 +736,148 @@ fn save_preset_row(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog)
|
||||
if !d.fields.contains_key("__savePreset") {
|
||||
return;
|
||||
}
|
||||
let mut pick = None;
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("Name:"));
|
||||
form::text(ui, d, "__savePreset", 150.0);
|
||||
let name = d.str("__savePreset").trim().to_string();
|
||||
if ui.add_enabled(!name.is_empty(), egui::Button::new(tl!("Save"))).clicked() {
|
||||
let settings = json!({
|
||||
"formats": d.fields.get("formats").cloned().unwrap_or(json!([])),
|
||||
"settings": d.fields.get("settings").cloned().unwrap_or(json!({})),
|
||||
"prefix": d.str("prefix"),
|
||||
"folders": if !d.bool("subfolders") { "none" } else if d.str("subfolderBy") == "format" { "format" } else { "rows" },
|
||||
});
|
||||
match app.run("document.exportPresets.save", json!({"name": name, "settings": settings})) {
|
||||
Ok(_) => {
|
||||
d.fields.remove("__savePreset");
|
||||
d.fields.insert("exportPreset".into(), json!(name));
|
||||
d.fields.insert("__exportPresets".into(), export_presets(app));
|
||||
}
|
||||
Err(e) => app.ui.status = e,
|
||||
}
|
||||
pick = Some(PresetPick::Save(name));
|
||||
}
|
||||
if ui.small_button(tl!("Cancel")).clicked() {
|
||||
d.fields.remove("__savePreset");
|
||||
}
|
||||
});
|
||||
if let Some(pick) = pick {
|
||||
preset_pick(app, d, pick);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the dialog to format preset `preset` (`{name, settings}`): its rows, their settings, the
|
||||
/// prefix and the sub-folders; what it doesn't name keeps the dialog's value.
|
||||
/// Import Preset… (the Asset Export panel's): the presets of a picked file are added to the saved
|
||||
/// ones → the names added
|
||||
/// (none on the web, where the picked file arrives later and imports through `io::open_bytes`).
|
||||
pub(crate) fn import_presets(app: &mut VectorcraftApp) -> Result<Vec<String>, String> {
|
||||
if let Some(f) = app.services.open_async.as_mut() {
|
||||
f();
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let pick = crate::FilePick { filters: vec![crate::FilePick::filter("Export presets", fileio::SCREEN_EXPORT_PRESET_EXTS)], ..Default::default() };
|
||||
let path = crate::picks::open(app, &pick).ok_or("cancelled")?;
|
||||
let r = io::import_presets(app, "document.exportPresets.import", io::EXPORT_PRESETS, json!({ "path": path }))?;
|
||||
Ok(r["imported"].as_array().into_iter().flatten().filter_map(Value::as_str).map(str::to_string).collect())
|
||||
}
|
||||
|
||||
/// Export Preset… (the Asset Export panel's): preset `name` written to a picked file named after
|
||||
/// it (the web downloads it) → where it went.
|
||||
pub(crate) fn export_preset(app: &mut VectorcraftApp, name: &str) -> Result<String, String> {
|
||||
io::save_command_output_named(app, "document.exportPresets.export", &preset_file_name(name), json!({ "names": [name] }))
|
||||
}
|
||||
|
||||
/// The file name Export Preset… suggests for preset `name`: the name made safe as a file name, as
|
||||
/// Export Library… makes a library's (`library::file_stem_for`), with the `.vcexportpresets`
|
||||
/// extension.
|
||||
pub(crate) fn preset_file_name(name: &str) -> String {
|
||||
format!("{}.{}", vectorcraft_engine::cmd::library::file_stem_for(name), fileio::SCREEN_EXPORT_PRESET_FORMAT)
|
||||
}
|
||||
|
||||
/// `list` (the rows of `document.exportPresets.list`) made the rows for the saved presets `saved`:
|
||||
/// built again only when it does not list them as they are, so that a frame that changes nothing
|
||||
/// builds nothing → whether it was built again.
|
||||
pub(crate) fn preset_list(list: &mut Value, saved: &[fileio::ScreenExportPreset]) -> bool {
|
||||
// The built-in rows never change; the saved ones are compared in place.
|
||||
let current = list.as_array().is_some_and(|rows| {
|
||||
let mut mine = rows.iter().filter(|p| p["builtIn"] == false);
|
||||
rows.iter().any(|p| p["builtIn"] == true)
|
||||
&& saved.iter().all(|q| mine.next().is_some_and(|row| row["name"] == q.name.as_str() && row["settings"].as_object() == Some(&q.settings)))
|
||||
&& mine.next().is_none()
|
||||
});
|
||||
if !current {
|
||||
*list = fileio::screen_export_preset_list(saved);
|
||||
}
|
||||
!current
|
||||
}
|
||||
|
||||
/// `ui.dialog.set` of `exportPreset` in the open dialog: the preset it names is applied as a pick
|
||||
/// in the ☰ menu applies it, and `""` is none. A name that is not a preset's is an error, and
|
||||
/// `ui.dialog.set` then leaves the dialog as it was.
|
||||
pub(super) fn preset_set(app: &mut VectorcraftApp) -> Result<(), String> {
|
||||
let Some(mut d) = app.ui.dialog.take() else { return Ok(()) };
|
||||
let list = fileio::screen_export_preset_list(&app.session.prefs.screen_export_presets);
|
||||
let name = d.str("exportPreset");
|
||||
let preset = list.as_array().and_then(|l| l.iter().find(|p| p["name"] == name.as_str())).cloned();
|
||||
let r = match preset {
|
||||
Some(preset) => {
|
||||
preset_pick(app, &mut d, PresetPick::Apply(preset));
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
d.fields.insert("exportPreset".into(), json!(""));
|
||||
d.fields.insert("__exportPresets".into(), list);
|
||||
if name.is_empty() { Ok(()) } else { Err(format!("no export preset `{name}` (see document.exportPresets.list)")) }
|
||||
}
|
||||
};
|
||||
app.ui.dialog = Some(d);
|
||||
r
|
||||
}
|
||||
|
||||
/// Act on what the format presets menu of dialog `d` asked for. Save keeps the applied preset's
|
||||
/// keys that the dialog does not show (a file name pattern, Full Document, a folder).
|
||||
pub(super) fn preset_pick(app: &mut VectorcraftApp, d: &mut Dialog, pick: PresetPick) {
|
||||
let done = match pick {
|
||||
PresetPick::Apply(preset) => {
|
||||
apply_preset(d, &preset);
|
||||
Ok(())
|
||||
}
|
||||
// The name as saved (trimmed and capped).
|
||||
PresetPick::Save(name) => {
|
||||
let mut settings = fileio::screen_export_preset_list(&app.session.prefs.screen_export_presets)
|
||||
.as_array()
|
||||
.and_then(|rows| rows.iter().find(|p| p["name"] == d.str("exportPreset")))
|
||||
.and_then(|p| p["settings"].as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
for key in ["formats", "preset", "settings", "prefix", "folders", "subfolders", "includeBleed", "includeArtboardFill"] {
|
||||
settings.remove(key);
|
||||
}
|
||||
settings.insert("formats".into(), d.fields.get("formats").cloned().unwrap_or(json!([])));
|
||||
settings.insert("settings".into(), d.fields.get("settings").cloned().unwrap_or(json!({})));
|
||||
settings.insert("prefix".into(), json!(d.str("prefix")));
|
||||
settings.insert("folders".into(), json!(folders(d)));
|
||||
for key in ["includeBleed", "includeArtboardFill"] {
|
||||
settings.insert(key.into(), json!(d.bool(key)));
|
||||
}
|
||||
app.run("document.exportPresets.save", json!({"name": name, "settings": settings})).map(|r| {
|
||||
d.fields.remove("__savePreset");
|
||||
d.fields.insert("exportPreset".into(), r["name"].clone());
|
||||
})
|
||||
}
|
||||
PresetPick::Delete(name) => app.run("document.exportPresets.delete", json!({ "name": name })).map(|_| {
|
||||
d.fields.insert("exportPreset".into(), json!(""));
|
||||
}),
|
||||
// The dialog's menu has no Import Preset… or Export Preset….
|
||||
PresetPick::Import | PresetPick::Export(_) => Ok(()),
|
||||
};
|
||||
if let Err(e) = done
|
||||
&& e != "cancelled"
|
||||
{
|
||||
app.ui.status = e;
|
||||
}
|
||||
let list = d.fields.entry("__exportPresets".to_string()).or_insert(Value::Null);
|
||||
preset_list(list, &app.session.prefs.screen_export_presets);
|
||||
}
|
||||
|
||||
/// Set the dialog to format preset `preset` (`{name, settings}`): its rows (or the mobile or
|
||||
/// density rows it names), their settings, the prefix and the sub-folders; what it doesn't name
|
||||
/// keeps the dialog's value.
|
||||
pub(super) fn apply_preset(d: &mut Dialog, preset: &Value) {
|
||||
let st = &preset["settings"];
|
||||
if let Some(rows) = st["formats"].as_array() {
|
||||
d.fields.insert("formats".into(), json!(rows.iter().filter(|r| r.is_object()).map(editable_row).collect::<Vec<_>>()));
|
||||
d.fields.insert("preset".into(), json!(""));
|
||||
} else if let Some((p, rows)) = st["preset"].as_str().and_then(|p| Some((p, fileio::screen_preset_rows(p)?))) {
|
||||
// A preset saved with the mobile or density rows (the Asset Export panel's).
|
||||
d.fields.insert("formats".into(), json!(rows));
|
||||
d.fields.insert("preset".into(), json!(p));
|
||||
}
|
||||
if let (Some(all), Some(given)) = (d.fields.get_mut("settings").and_then(Value::as_object_mut), st["settings"].as_object()) {
|
||||
for (id, o) in given {
|
||||
@@ -739,10 +887,15 @@ pub(super) fn apply_preset(d: &mut Dialog, preset: &Value) {
|
||||
}
|
||||
}
|
||||
}
|
||||
let folders = st["folders"].as_str().or_else(|| st["subfolders"].as_bool().map(|b| if b { "rows" } else { "none" }));
|
||||
// An empty `folders` is none, as an export reads it.
|
||||
let folders = st["folders"]
|
||||
.as_str()
|
||||
.map(|f| if f.is_empty() { "none" } else { f })
|
||||
.or_else(|| st["subfolders"].as_bool().map(|b| if b { "rows" } else { "none" }));
|
||||
if let Some(f) = folders {
|
||||
d.fields.insert("subfolders".into(), json!(f != "none"));
|
||||
d.fields.insert("subfolderBy".into(), json!(if f == "format" { "format" } else { "scale" }));
|
||||
d.fields.insert("__itemFolders".into(), json!(if matches!(f, "item" | "itemFormat") { f } else { "" }));
|
||||
}
|
||||
if let Some(prefix) = st["prefix"].as_str() {
|
||||
d.fields.insert("prefix".into(), json!(prefix));
|
||||
@@ -793,6 +946,7 @@ pub(crate) fn formats(ui: &mut egui::Ui, d: &mut Dialog, mut menu: Option<Format
|
||||
rows = preset_rows;
|
||||
d.fields.insert("subfolders".into(), json!(true));
|
||||
d.fields.insert("subfolderBy".into(), json!("scale"));
|
||||
d.fields.remove("__itemFolders");
|
||||
d.fields.insert("formats".into(), json!(rows));
|
||||
d.fields.insert("preset".into(), json!(id));
|
||||
}
|
||||
|
||||
@@ -906,3 +906,300 @@ fn sub_folders_and_single_file_pdfs() {
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Save Preset… on an applied preset saves the dialog's rows, settings, prefix and sub-folders and
|
||||
/// keeps the preset's keys the dialog does not show (a file name pattern, other export settings).
|
||||
#[test]
|
||||
fn saving_an_applied_preset_keeps_the_keys_the_dialog_does_not_show() {
|
||||
use export_for_screens::PresetPick;
|
||||
let (mut app, _) = app(2, false);
|
||||
app.run(
|
||||
"document.exportPresets.save",
|
||||
json!({
|
||||
"name": "Repeat",
|
||||
"settings": {"formats": [{"format": "png"}], "name": "<name>-###", "counterPer": "file", "antiAlias": "none", "prefix": "a-"}
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
open(&mut app);
|
||||
let preset = export_for_screens::export_presets(&mut app).as_array().unwrap().iter().find(|p| p["name"] == "Repeat").cloned().unwrap();
|
||||
export_for_screens::apply_preset(app.ui.dialog.as_mut().unwrap(), &preset);
|
||||
set(&mut app, "prefix", json!("b-"));
|
||||
set(&mut app, "subfolders", json!(true));
|
||||
set(&mut app, "subfolderBy", json!("format"));
|
||||
let mut d = app.ui.dialog.take().unwrap();
|
||||
export_for_screens::preset_pick(&mut app, &mut d, PresetPick::Save("Repeat".into()));
|
||||
app.ui.dialog = Some(d);
|
||||
|
||||
let saved = app.session.prefs.screen_export_presets.iter().find(|p| p.name == "Repeat").unwrap_or_else(|| panic!("{}", app.ui.status));
|
||||
let s = &saved.settings;
|
||||
assert_eq!((s.get("counterPer"), s.get("antiAlias"), s.get("name")), (Some(&json!("file")), Some(&json!("none")), Some(&json!("<name>-###"))));
|
||||
assert_eq!((s.get("prefix"), s.get("folders")), (Some(&json!("b-")), Some(&json!("format"))));
|
||||
assert_eq!(field(&app, "exportPreset"), "Repeat");
|
||||
}
|
||||
|
||||
/// Save Preset… on an applied preset saves the dialog's Include Bleed and Include Artboard Fill.
|
||||
#[test]
|
||||
fn saving_an_applied_preset_keeps_its_bleed_and_fill() {
|
||||
use export_for_screens::PresetPick;
|
||||
let (mut app, _) = app(2, false);
|
||||
let settings = json!({"formats": [{"format": "png"}], "includeBleed": true, "includeArtboardFill": false});
|
||||
app.run("document.exportPresets.save", json!({"name": "Bleed", "settings": settings})).unwrap();
|
||||
open(&mut app);
|
||||
let preset = export_for_screens::export_presets(&mut app).as_array().unwrap().iter().find(|p| p["name"] == "Bleed").cloned().unwrap();
|
||||
export_for_screens::apply_preset(app.ui.dialog.as_mut().unwrap(), &preset);
|
||||
let mut d = app.ui.dialog.take().unwrap();
|
||||
export_for_screens::preset_pick(&mut app, &mut d, PresetPick::Save("Bleed".into()));
|
||||
app.ui.dialog = Some(d);
|
||||
|
||||
let saved = app.session.prefs.screen_export_presets.iter().find(|p| p.name == "Bleed").unwrap_or_else(|| panic!("{}", app.ui.status));
|
||||
let s = &saved.settings;
|
||||
assert_eq!((s.get("includeBleed"), s.get("includeArtboardFill")), (Some(&json!(true)), Some(&json!(false))));
|
||||
}
|
||||
|
||||
/// A preset that puts each item in its own folder (`item` or `itemFormat`, which the dialog has no
|
||||
/// control for) keeps that folder mode when it is exported or saved again from the dialog, until
|
||||
/// the sub-folder controls change (in the dialog or by `ui.dialog.set`).
|
||||
#[test]
|
||||
fn an_applied_preset_keeps_its_item_folders() {
|
||||
use crate::control::{ControlRequest, Outcome};
|
||||
use export_for_screens::PresetPick;
|
||||
for mode in ["item", "itemFormat"] {
|
||||
let (mut app, _) = app(2, false);
|
||||
app.run("document.exportPresets.save", json!({"name": "Items", "settings": {"formats": [{"format": "png"}], "folders": mode}})).unwrap();
|
||||
open(&mut app);
|
||||
let preset = export_for_screens::export_presets(&mut app).as_array().unwrap().iter().find(|p| p["name"] == "Items").cloned().unwrap();
|
||||
export_for_screens::apply_preset(app.ui.dialog.as_mut().unwrap(), &preset);
|
||||
set(&mut app, "folder", json!("out"));
|
||||
let p = export_for_screens::params(&app, app.ui.dialog.as_ref().unwrap()).unwrap();
|
||||
assert_eq!(p["folders"], json!(mode), "exported");
|
||||
let mut d = app.ui.dialog.take().unwrap();
|
||||
export_for_screens::preset_pick(&mut app, &mut d, PresetPick::Save("Items".into()));
|
||||
app.ui.dialog = Some(d);
|
||||
let saved = app.session.prefs.screen_export_presets.iter().find(|p| p.name == "Items").unwrap_or_else(|| panic!("{}", app.ui.status));
|
||||
assert_eq!(saved.settings.get("folders"), Some(&json!(mode)), "saved");
|
||||
|
||||
// `ui.dialog.set` of the sub-folder controls, as a change in the dialog, ends the item folders.
|
||||
for (on, folders) in [(false, "none"), (true, "rows")] {
|
||||
let (req, _rx) = ControlRequest::new("ui.dialog.set", json!({"field": "subfolders", "value": on}));
|
||||
let Outcome::Done(r) = crate::control::handle(&mut app, &egui::Context::default(), &req) else { panic!("not done") };
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
let p = export_for_screens::params(&app, app.ui.dialog.as_ref().unwrap()).unwrap();
|
||||
assert_eq!(p["folders"], json!(folders), "subfolders {on}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A preset whose `folders` is empty turns Create Sub-folders off whatever its `subfolders`, as
|
||||
/// the export command puts every file in the folder itself; without `folders` it uses its
|
||||
/// `subfolders`, and without either it keeps the dialog's sub-folders.
|
||||
#[test]
|
||||
fn a_preset_with_empty_folders_exports_into_the_folder() {
|
||||
let (mut app, _) = app(2, false);
|
||||
open(&mut app);
|
||||
let apply = |app: &mut VectorcraftApp, settings: Value| {
|
||||
export_for_screens::apply_preset(app.ui.dialog.as_mut().unwrap(), &json!({"name": "Empty", "settings": settings}));
|
||||
};
|
||||
for settings in [json!({"folders": ""}), json!({"folders": "", "subfolders": true})] {
|
||||
set(&mut app, "subfolders", json!(true));
|
||||
apply(&mut app, settings.clone());
|
||||
assert_eq!(field(&app, "subfolders"), json!(false), "{settings}");
|
||||
}
|
||||
apply(&mut app, json!({"subfolders": true}));
|
||||
assert_eq!(field(&app, "subfolders"), json!(true));
|
||||
set(&mut app, "subfolderBy", json!("format"));
|
||||
apply(&mut app, json!({}));
|
||||
assert_eq!((field(&app, "subfolders"), field(&app, "subfolderBy")), (json!(true), json!("format")));
|
||||
}
|
||||
|
||||
/// The Asset Export panel menu's Export Preset… writes a preset to a file and its Import Preset… adds
|
||||
/// the presets of a file, which the open dialog's dropdown then lists, as it does after opening
|
||||
/// the file (the web's Import).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn export_preset_and_import_preset_go_through_files() {
|
||||
let dir = temp_folder("preset-files");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let file = dir.join("pack.vcexportpresets").to_string_lossy().to_string();
|
||||
let (saved_to, picked) = (file.clone(), file.clone());
|
||||
let suggested = Rc::new(RefCell::new(vec![]));
|
||||
let shown = suggested.clone();
|
||||
let services = Services {
|
||||
pick_save: Some(Box::new(move |pick: &crate::FilePick| {
|
||||
shown.borrow_mut().push(pick.name.clone());
|
||||
Some(saved_to.clone())
|
||||
})),
|
||||
pick_open: Some(Box::new(move |_: &crate::FilePick| Some(picked.clone()))),
|
||||
write: Some(Box::new(|p: &str, b: &[u8]| std::fs::write(p, b).map_err(|e| e.to_string()))),
|
||||
..Default::default()
|
||||
};
|
||||
let mut app = VectorcraftApp::new(Session::new(), services);
|
||||
app.run("file.new", json!({"width": 60, "height": 40})).unwrap();
|
||||
app.run("document.exportPresets.save", json!({"name": "Pack", "settings": {"formats": [{"format": "svg"}], "folders": "format"}})).unwrap();
|
||||
open(&mut app);
|
||||
let names = |app: &VectorcraftApp| -> Vec<String> {
|
||||
field(app, "__exportPresets").as_array().unwrap().iter().filter_map(|p| p["name"].as_str().map(str::to_string)).collect()
|
||||
};
|
||||
assert_eq!(export_for_screens::export_preset(&mut app, "Pack").unwrap(), file);
|
||||
// The save panel suggests the preset's name, not the document's.
|
||||
assert_eq!(*suggested.borrow(), ["Pack.vcexportpresets"]);
|
||||
app.run("document.exportPresets.delete", json!({"name": "Pack"})).unwrap();
|
||||
frame(&mut app);
|
||||
assert_eq!(names(&app), ["Logo Package", "Print-Ready PDF"]);
|
||||
assert_eq!(export_for_screens::import_presets(&mut app).unwrap(), ["Pack"]);
|
||||
assert_eq!(app.ui.status, "Imported export presets: Pack");
|
||||
frame(&mut app);
|
||||
assert_eq!(names(&app), ["Logo Package", "Print-Ready PDF", "Pack"]);
|
||||
let pack = field(&app, "__exportPresets")[2].clone();
|
||||
export_for_screens::apply_preset(app.ui.dialog.as_mut().unwrap(), &pack);
|
||||
assert_eq!((field(&app, "formats")[0]["format"].clone(), field(&app, "subfolderBy")), (json!("svg"), json!("format")));
|
||||
// The file opened again: a second copy, numbered, in the open dialog's dropdown.
|
||||
let bytes = std::fs::read(&file).unwrap();
|
||||
crate::io::open_bytes(&mut app, &file, &bytes, Some(file.clone())).unwrap();
|
||||
frame(&mut app);
|
||||
assert_eq!(kind(&app), Some(export_for_screens::KIND));
|
||||
assert_eq!(names(&app), ["Logo Package", "Print-Ready PDF", "Pack", "Pack 2"]);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Export Preset… suggests the preset's name as a file name, as Export Library… does a library's.
|
||||
#[test]
|
||||
fn an_exported_preset_file_is_named_after_the_preset() {
|
||||
let name = export_for_screens::preset_file_name;
|
||||
assert_eq!(name("Logo Package 2"), "Logo Package 2.vcexportpresets");
|
||||
assert_eq!(name("Print-Ready PDF"), "Print-Ready PDF.vcexportpresets");
|
||||
assert_eq!(name("Web/App: v1.2"), "Web-App- v1-2.vcexportpresets");
|
||||
assert_eq!(name("CON"), "CON-.vcexportpresets");
|
||||
}
|
||||
|
||||
/// A preset saved with the mobile or density rows (as the Asset Export panel saves one) sets the
|
||||
/// dialog's rows to them.
|
||||
#[test]
|
||||
fn a_mobile_preset_sets_the_rows() {
|
||||
let (mut app, _) = app(1, false);
|
||||
app.run("document.exportPresets.save", json!({"name": "Phones", "settings": {"preset": "mobile", "subfolders": true}})).unwrap();
|
||||
open(&mut app);
|
||||
let phones = export_for_screens::export_presets(&mut app).as_array().unwrap().iter().find(|p| p["name"] == "Phones").cloned().unwrap();
|
||||
set(&mut app, "subfolders", json!(false));
|
||||
set(&mut app, "subfolderBy", json!("format"));
|
||||
export_for_screens::apply_preset(app.ui.dialog.as_mut().unwrap(), &phones);
|
||||
assert_eq!(field(&app, "formats"), json!(fileio::screen_preset_rows("mobile").unwrap()));
|
||||
assert_eq!((field(&app, "preset"), field(&app, "subfolders"), field(&app, "subfolderBy")), (json!("mobile"), json!(true), json!("scale")));
|
||||
}
|
||||
|
||||
/// `ui.dialog.set` of `exportPreset` applies the preset it names, as a pick in the ☰ menu does, so
|
||||
/// the dialog holds the settings of the preset it names; a name that is no preset's is an error and
|
||||
/// leaves the dialog as it was.
|
||||
#[test]
|
||||
fn setting_the_export_preset_field_applies_the_preset() {
|
||||
use crate::control::{ControlRequest, Outcome};
|
||||
let (mut app, _) = app(1, false);
|
||||
let ctx = egui::Context::default();
|
||||
open(&mut app);
|
||||
let set_preset = |app: &mut VectorcraftApp, name: &str| {
|
||||
let (req, _rx) = ControlRequest::new("ui.dialog.set", json!({"field": "exportPreset", "value": name}));
|
||||
let Outcome::Done(r) = crate::control::handle(app, &ctx, &req) else { panic!("not done") };
|
||||
r
|
||||
};
|
||||
let r = set_preset(&mut app, "Logo Package");
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
let rows: Vec<Value> = field(&app, "formats").as_array().unwrap().iter().map(|r| r["format"].clone()).collect();
|
||||
assert_eq!(rows, [json!("svg"), json!("png"), json!("png"), json!("pdf"), json!("pdf")]);
|
||||
assert_eq!(field(&app, "subfolderBy"), json!("format"));
|
||||
assert_eq!(r["result"]["fields"]["subfolderBy"], "format", "{r}");
|
||||
let r = set_preset(&mut app, "Never saved");
|
||||
assert_eq!(r["ok"], false, "{r}");
|
||||
assert_eq!(field(&app, "exportPreset"), "Logo Package");
|
||||
assert_eq!(field(&app, "subfolderBy"), "format");
|
||||
// A request that fails on a later field changes none of its fields.
|
||||
let fields = json!({"fields": [{"field": "subfolderBy", "value": "scale"}, {"field": "exportPreset", "value": "Never saved"}]});
|
||||
let (req, _rx) = ControlRequest::new("ui.dialog.set", fields);
|
||||
let Outcome::Done(r) = crate::control::handle(&mut app, &ctx, &req) else { panic!("not done") };
|
||||
assert_eq!(r["ok"], false, "{r}");
|
||||
assert_eq!((field(&app, "exportPreset"), field(&app, "subfolderBy")), (json!("Logo Package"), json!("format")));
|
||||
// In one request the fields apply in order: a field after the preset goes over it.
|
||||
let fields = json!({"fields": [{"field": "exportPreset", "value": "Logo Package"}, {"field": "subfolderBy", "value": "scale"}]});
|
||||
let (req, _rx) = ControlRequest::new("ui.dialog.set", fields);
|
||||
let Outcome::Done(r) = crate::control::handle(&mut app, &ctx, &req) else { panic!("not done") };
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
assert_eq!((field(&app, "exportPreset"), field(&app, "subfolderBy")), (json!("Logo Package"), json!("scale")));
|
||||
}
|
||||
|
||||
/// The Asset Export panel menu's Import Preset… and Export Preset… show their file dialogs off the UI
|
||||
/// thread (as sheets on macOS) and read or write the file when the dialog answers.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn preset_files_ask_off_the_ui_thread() {
|
||||
use crate::panels::asset_export;
|
||||
use crate::picks::PickRequest;
|
||||
use export_for_screens::PresetPick;
|
||||
let mut services = Services { dialogs_are_sheets: true, ..Default::default() };
|
||||
let (shown, written) = crate::tests_picks::off_the_ui_thread(&mut services);
|
||||
let mut app = VectorcraftApp::new(Session::new(), services);
|
||||
app.run("file.new", json!({"width": 60, "height": 40})).unwrap();
|
||||
app.run("document.exportPresets.save", json!({"name": "Pack", "settings": {"formats": [{"format": "svg"}]}})).unwrap();
|
||||
let dir = temp_folder("preset-sheets");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let file = dir.join("pack.vcexportpresets");
|
||||
std::fs::write(&file, app.run("document.exportPresets.export", json!({"names": ["Pack"]})).unwrap()["data"].as_str().unwrap()).unwrap();
|
||||
let file = file.to_string_lossy().to_string();
|
||||
let names = |app: &VectorcraftApp| app.session.prefs.screen_export_presets.iter().map(|p| p.name.clone()).collect::<Vec<_>>();
|
||||
let poll = |app: &mut VectorcraftApp| crate::picks::poll(app, &egui::Context::default());
|
||||
let ctx = egui::Context::default();
|
||||
let doc = app.session.active().unwrap().uid;
|
||||
asset_export::preset(&mut app, &ctx, doc, PresetPick::Export("Pack".into()));
|
||||
assert!(matches!(shown.requests().as_slice(), [PickRequest::Save(p)] if p.name == "Pack.vcexportpresets"), "{:?}", shown.requests());
|
||||
assert!(written.borrow().is_empty() && app.ui.status.is_empty(), "{}", app.ui.status);
|
||||
shown.answer(&["/presets/pack.vcexportpresets"]);
|
||||
poll(&mut app);
|
||||
assert_eq!(*written.borrow(), ["/presets/pack.vcexportpresets"], "{}", app.ui.status);
|
||||
asset_export::preset(&mut app, &ctx, doc, PresetPick::Import);
|
||||
assert!(matches!(shown.requests().as_slice(), [PickRequest::Open(_)]), "{:?}", shown.requests());
|
||||
assert_eq!(names(&app), ["Pack"]);
|
||||
shown.answer(&[&file]);
|
||||
poll(&mut app);
|
||||
assert_eq!(names(&app), ["Pack", "Pack 2"], "{}", app.ui.status);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// The dialog's Export Preset row has no Import Preset… or Export Preset…: those picks change
|
||||
/// nothing there.
|
||||
#[test]
|
||||
fn the_dialog_leaves_preset_files_to_the_panel() {
|
||||
use export_for_screens::PresetPick;
|
||||
let (mut app, _) = app(1, false);
|
||||
open(&mut app);
|
||||
let before = serde_json::to_value(&app.ui.dialog).unwrap();
|
||||
for pick in [PresetPick::Import, PresetPick::Export("Logo Package".into())] {
|
||||
let mut d = app.ui.dialog.take().unwrap();
|
||||
export_for_screens::preset_pick(&mut app, &mut d, pick);
|
||||
app.ui.dialog = Some(d);
|
||||
}
|
||||
assert_eq!(serde_json::to_value(&app.ui.dialog).unwrap(), before);
|
||||
assert_eq!(app.ui.status, "");
|
||||
}
|
||||
|
||||
/// The dialog keeps its list of export presets from frame to frame, and builds it again when the
|
||||
/// saved presets change.
|
||||
#[test]
|
||||
fn the_dialog_builds_the_preset_list_again_only_when_the_presets_change() {
|
||||
let (mut app, _) = app(1, false);
|
||||
open(&mut app);
|
||||
frame(&mut app);
|
||||
let mark = |app: &mut VectorcraftApp| app.ui.dialog.as_mut().unwrap().fields.get_mut("__exportPresets").unwrap()[0]["mark"] = json!(true);
|
||||
mark(&mut app);
|
||||
frame(&mut app);
|
||||
frame(&mut app);
|
||||
assert_eq!(field(&app, "__exportPresets")[0]["mark"], true, "the list was not built again");
|
||||
app.run("document.exportPresets.save", json!({"name": "Mine", "settings": {"formats": [{"format": "svg"}]}})).unwrap();
|
||||
frame(&mut app);
|
||||
let list = field(&app, "__exportPresets");
|
||||
assert!(list[0].get("mark").is_none() && list.as_array().unwrap().iter().any(|p| p["name"] == "Mine"), "{list}");
|
||||
// A saved preset changed under the same name is a change too.
|
||||
mark(&mut app);
|
||||
app.run("document.exportPresets.save", json!({"name": "Mine", "settings": {"formats": [{"format": "pdf"}]}})).unwrap();
|
||||
frame(&mut app);
|
||||
let list = field(&app, "__exportPresets");
|
||||
let mine = list.as_array().unwrap().iter().find(|p| p["name"] == "Mine").unwrap();
|
||||
assert_eq!(mine["settings"]["formats"][0]["format"], "pdf", "{list}");
|
||||
}
|
||||
|
||||
@@ -1016,6 +1016,10 @@ font-technology Variable Променливи
|
||||
Delete Export Preset Изтриване на набор за експортиране
|
||||
Export Presets Набори за експортиране
|
||||
Save Export Preset Запазване на набор за експортиране
|
||||
Export Export Presets Експортиране на набори за експортиране
|
||||
Export Preset… Експортиране на набор…
|
||||
Import Export Presets Импортиране на набори за експортиране
|
||||
Import Preset… Импортиране на набор…
|
||||
Export Layer Експортиране на слой
|
||||
Export Selection Експортиране на избраното
|
||||
All in One Folder Всичко в една папка
|
||||
@@ -2723,11 +2727,13 @@ digits Malayalam Малаялам
|
||||
Save a Copy Запазване на копие
|
||||
Save a Copy… Запазване на копие…
|
||||
Save As Запазване като
|
||||
Save As Preset… Запази като предварителна настройка…
|
||||
Save as PDF… Запазване като PDF…
|
||||
Save as preset: Запази като предварителна настройка:
|
||||
Save as Template Запази като шаблон
|
||||
Save as Template… Запази като шаблон…
|
||||
Save As… Запази като…
|
||||
Save Changes Запази промените
|
||||
Save changes to {name} Запазване на промените в {name}
|
||||
Save color group to Swatch panel Запазване на цветовата група в панела „Образци“
|
||||
Save Colors as Swatches Запази цветовете като образци
|
||||
@@ -4452,6 +4458,13 @@ axis Both И двете
|
||||
@msg this isn't a flat-colour logo ({_1}% of its solid areas match a few flat colours), so Flat Logo mode would turn its gradients into patches: use Color mode instead това не е лого с плътни цветове ({_1}% от плътните му зони съвпадат с малко на брой плътни цветове), затова режимът „Плоско лого“ ще превърне градиентите му в петна: използвайте режима „Цвят“
|
||||
@msg `{what}` closes nothing `{what}` не затваря нищо
|
||||
@msg `{what}` closes something it didn't open `{what}` затваря нещо, което не е отворило
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` е името на настройка от document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) няма настройка за експортиране `{n}` (вижте document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) няма запазени настройки за експортиране (посочете вградените в `names`)
|
||||
@msg not an export presets file (format `{_1}`) не е файл с настройки за експортиране (формат `{_1}`)
|
||||
@msg not an export presets file: {e} не е файл с настройки за експортиране: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
@msg `globalAdjust` must be an object `globalAdjust` трябва да е обект
|
||||
@msg a path has too many points пътят има твърде много точки
|
||||
@msg it ends inside a group or compound path той завършва вътре в група или съставен път
|
||||
@msg it ends inside a layer той завършва вътре в слой
|
||||
@@ -5236,6 +5249,7 @@ file-filter Flattener presets Предварителни настройки за
|
||||
file-filter PDF presets Предварителни настройки за PDF
|
||||
file-filter PDF-compatible .ai Съвместими с PDF .ai файлове
|
||||
file-filter Print presets Предварителни настройки за печат
|
||||
file-filter Export presets Набори за експортиране
|
||||
file-filter SVG Compressed Компресиран SVG
|
||||
file-filter Swatch libraries Библиотеки с образци
|
||||
file-filter Templates Шаблони
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -43,6 +43,8 @@
|
||||
Close All Zatvori sve
|
||||
Save Sačuvaj
|
||||
Save As… Sačuvaj kao…
|
||||
Save As Preset… Sačuvaj kao predložak…
|
||||
Save Changes Sačuvaj promjene
|
||||
Save a Copy… Sačuvaj kopiju…
|
||||
Save as Template… Sačuvaj kao predložak…
|
||||
Save as PDF… Sačuvaj kao PDF…
|
||||
@@ -2928,6 +2930,10 @@ digits Malayalam Malajalam
|
||||
Delete Export Preset Izbriši predložak izvoza
|
||||
Export Presets Izvezi predloške
|
||||
Save Export Preset Sačuvaj predložak izvoza
|
||||
Export Export Presets Izvezi predloške izvoza
|
||||
Export Preset… Izvezi predložak…
|
||||
Import Export Presets Uvezi predloške izvoza
|
||||
Import Preset… Uvezi predložak…
|
||||
Export Layer Izvezi sloj
|
||||
Export Selection Izvezi odabir
|
||||
All in One Folder Sve u jednom folderu
|
||||
@@ -4255,6 +4261,7 @@ imageTrace Outline Obris
|
||||
@msg `freeform.points` must be an array `freeform.points` mora biti niz
|
||||
@msg `freeform.lines` must be an array `freeform.lines` mora biti niz
|
||||
@msg `gradient` must be an object `gradient` mora biti objekat
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` je naziv predloška iz document.exportPresets.list
|
||||
@msg `groupColors` must be an array `groupColors` mora biti niz
|
||||
@msg `library` is a library id or name (see {list}) `library` je ID ili naziv biblioteke (vidi {list})
|
||||
@msg `mode` applies to freeform gradients `mode` se primjenjuje na gradijente slobodnog oblika
|
||||
@@ -4264,6 +4271,7 @@ imageTrace Outline Obris
|
||||
@msg `stops` must be an array `stops` mora biti niz
|
||||
@msg `svg` must be an object of SVG options (see document.formats) `svg` mora biti objekat SVG opcija (vidi document.formats)
|
||||
@msg `{_1}` is no save command `{_1}` nije naredba čuvanja
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
@msg `{area}` is no recovery area name `{area}` nije naziv područja za oporavak
|
||||
@msg `{color}` is not a colour (#rrggbb or a preset name) `{color}` nije boja (#rrggbb ili naziv predloška)
|
||||
@msg `{id}` has no envelope dialog `{id}` nema dijalog omotnice
|
||||
@@ -4498,12 +4506,14 @@ imageTrace Outline Obris
|
||||
@msg no plug-in {id} is installed nije instaliran dodatak {id}
|
||||
@msg no point {i} (the gradient has {_1}) nema tačke {i} (gradijent ima {_1})
|
||||
@msg no preset editor nema uređivača predložaka
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) nema predloška izvoza `{n}` (vidi document.exportPresets.list)
|
||||
@msg no previous transform nema prethodne transformacije
|
||||
@msg no print preset named `{base}` nema predloška štampanja pod nazivom `{base}`
|
||||
@msg no print preset named `{name}` nema predloška štampanja pod nazivom `{name}`
|
||||
@msg no print service nema usluge štampanja
|
||||
@msg no revert command nema naredbe za vraćanje
|
||||
@msg no saved PDF preset named `{name}` (built-in presets are read-only: start a new one from them) nema sačuvanog PDF predloška pod nazivom `{name}` (ugrađeni predlošci su samo za čitanje: započnite novi od njih)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) nema sačuvanih predložaka izvoza za izvoz (ugrađene navedite u `names`)
|
||||
@msg no saved preset named `{name}` nema sačuvane unaprijed postavljene vrijednosti `{name}`
|
||||
@msg no saved preset named `{name}` to rename nema sačuvane unaprijed postavljene vrijednosti `{name}` za preimenovanje
|
||||
@msg no saved presets nema sačuvanih predložaka
|
||||
@@ -4559,6 +4569,8 @@ imageTrace Outline Obris
|
||||
@msg not a stored central record nije sačuvani centralni zapis
|
||||
@msg not a valid plug-in module: {0} nije važeći modul dodatka: {0}
|
||||
@msg not an EMF or WMF file nije EMF ni WMF datoteka
|
||||
@msg not an export presets file (format `{_1}`) nije datoteka predložaka izvoza (format `{_1}`)
|
||||
@msg not an export presets file: {e} nije datoteka predložaka izvoza: {e}
|
||||
@msg not found nije pronađeno
|
||||
@msg not in pattern editing mode nije u načinu uređivanja uzorka
|
||||
@msg finish editing the pattern first prvo završite uređivanje uzorka
|
||||
@@ -5264,6 +5276,7 @@ file-filter Swatch libraries Biblioteke uzoraka boja
|
||||
file-filter Flattener presets Predlošci spljoštavanja
|
||||
file-filter PDF presets PDF predlošci
|
||||
file-filter Print presets Predlošci štampanja
|
||||
file-filter Export presets Predlošci izvoza
|
||||
file-filter SVG Compressed Komprimirani SVG
|
||||
file-filter PDF-compatible .ai .ai kompatibilan s PDF-om
|
||||
file-filter VectorCraft Template VectorCraft predložak
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3495,6 +3495,7 @@ digits Malayalam Malaiàlam
|
||||
Save a Copy… Desa una còpia…
|
||||
Save and Close Desa i tanca
|
||||
Save As Desa com a
|
||||
Save As Preset… Desa com a preajust…
|
||||
Save as New Preset… Desa com a preajust nou…
|
||||
Save as PDF… Desa com a PDF…
|
||||
Save as preset: Desa com a preajust:
|
||||
@@ -3504,6 +3505,7 @@ digits Malayalam Malaiàlam
|
||||
Save Brush Library Desa la biblioteca de pinzells
|
||||
Save Brush Library… Desa la biblioteca de pinzells…
|
||||
Save Brushes… Desa els pinzells…
|
||||
Save Changes Desa els canvis
|
||||
Save changes to {name} Desa els canvis a {name}
|
||||
Save color group to Swatch panel Desa el grup de colors al panell de mostres
|
||||
Save Colors as Swatches Desa els colors com a mostres
|
||||
@@ -3511,6 +3513,10 @@ digits Malayalam Malaiàlam
|
||||
Save Document Info… Desa la informació del document…
|
||||
Save each artboard to a separate file Desa cada tauler en un fitxer separat
|
||||
Save Export Preset Desa el preajust d’exportació
|
||||
Export Export Presets Exporta els preajustos d’exportació
|
||||
Export Preset… Exporta el preajust…
|
||||
Import Export Presets Importa preajustos d’exportació
|
||||
Import Preset… Importa un preajust…
|
||||
Save for Office Documents Desa per a documents d’Office
|
||||
Save for Office Documents… Desa per a documents d’Office…
|
||||
Save for Web Desa per a web
|
||||
@@ -4835,6 +4841,12 @@ digits Malayalam Malaiàlam
|
||||
@msg `{to}` is not an available name `{to}` no és un nom disponible
|
||||
@msg `{what}` closes nothing `{what}` no tanca res
|
||||
@msg `{what}` closes something it didn't open `{what}` tanca una cosa que no havia obert
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` és el nom d’un preajust de document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) no hi ha cap preajust d’exportació `{n}` (vegeu document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) no hi ha preajustos d’exportació desats per exportar (anomeneu els integrats a `names`)
|
||||
@msg not an export presets file (format `{_1}`) no és un fitxer de preajustos d’exportació (format `{_1}`)
|
||||
@msg not an export presets file: {e} no és un fitxer de preajustos d’exportació: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
@msg a mask that covers several layers comes in as a clipping group in each layer it covers una màscara que cobreix diverses capes entra com un grup de retall a cada capa que cobreix
|
||||
@msg a graph's records don't end where they should: its art was read as it is els registres d’un gràfic no acaben on haurien: la seva il·lustració s’ha llegit tal com és
|
||||
@msg `{w}` takes no operands `{w}` no admet operands
|
||||
@@ -5715,6 +5727,7 @@ file-filter Flattener presets Preajustos de l’aplanador
|
||||
file-filter PDF presets Preajustos de PDF
|
||||
file-filter PDF-compatible .ai .ai compatible amb PDF
|
||||
file-filter Print presets Preajustos d’impressió
|
||||
file-filter Export presets Preajustos d’exportació
|
||||
file-filter SVG Compressed SVG comprimit
|
||||
file-filter Swatch libraries Biblioteques de mostres
|
||||
file-filter Templates Plantilles
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2357,6 +2357,8 @@ digits Malayalam Malajálamské
|
||||
Preserve Overprints and Spot Colors Zachovat přetisky a přímé barvy
|
||||
Rasters Rastry
|
||||
Replaces the saved preset of that name. Nahradí uložené přednastavení se stejným názvem.
|
||||
Save As Preset… Uložit jako předvolbu…
|
||||
Save Changes Uložit změny
|
||||
Save Preset… Uložit přednastavení…
|
||||
Vectors Vektory
|
||||
A saved copy of the selected preset Uložená kopie vybraného přednastavení
|
||||
@@ -2957,6 +2959,16 @@ digits Malayalam Malajálamské
|
||||
Delete Export Preset Odstranit přednastavení exportu
|
||||
Export Presets Přednastavení exportu
|
||||
Save Export Preset Uložit přednastavení exportu
|
||||
Export Export Presets Exportovat přednastavení exportu
|
||||
Export Preset… Exportovat přednastavení…
|
||||
Import Export Presets Importovat přednastavení exportu
|
||||
Import Preset… Importovat přednastavení…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` je název přednastavení z document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) přednastavení exportu `{n}` neexistuje (viz document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) žádná uložená přednastavení exportu k exportu (vestavěná uveďte v `names`)
|
||||
@msg not an export presets file (format `{_1}`) nejde o soubor přednastavení exportu (formát `{_1}`)
|
||||
@msg not an export presets file: {e} nejde o soubor přednastavení exportu: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
Export Layer Exportovat vrstvu
|
||||
Export Selection Exportovat výběr
|
||||
All in One Folder Vše v jedné složce
|
||||
@@ -5334,6 +5346,7 @@ file-filter Swatch libraries Knihovny vzorníků
|
||||
file-filter Flattener presets Přednastavení sloučení průhlednosti
|
||||
file-filter PDF presets Přednastavení PDF
|
||||
file-filter Print presets Přednastavení tisku
|
||||
file-filter Export presets Přednastavení exportu
|
||||
file-filter SVG Compressed Komprimované SVG
|
||||
file-filter PDF-compatible .ai .ai kompatibilní s PDF
|
||||
file-filter VectorCraft Template Šablona VectorCraft
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2155,6 +2155,8 @@ digits Malayalam Malayalam
|
||||
Preserve Overprints and Spot Colors Bevar overtryk og spotfarver
|
||||
Rasters Rasterbilleder
|
||||
Replaces the saved preset of that name. Erstatter den gemte forudindstilling med det navn.
|
||||
Save As Preset… Gem som forudindstilling…
|
||||
Save Changes Gem ændringer
|
||||
Save Preset… Gem forudindstilling…
|
||||
Vectors Vektorer
|
||||
A saved copy of the selected preset En gemt kopi af den valgte forudindstilling
|
||||
@@ -4543,6 +4545,13 @@ imageTrace Outline Kontur
|
||||
@msg this isn't a flat-colour logo ({_1}% of its solid areas match a few flat colours), so Flat Logo mode would turn its gradients into patches: use Color mode instead dette er ikke et logo med flade farver ({_1}% af de ensfarvede områder matcher nogle få flade farver), så tilstanden Fladt logo ville omdanne dets gradienter til pletter: brug tilstanden Farve i stedet
|
||||
@msg `{what}` closes nothing `{what}` lukker intet
|
||||
@msg `{what}` closes something it didn't open `{what}` lukker noget, den ikke åbnede
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` er navnet på en forudindstilling fra document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) ingen eksportforudindstilling `{n}` (se document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) ingen gemte eksportforudindstillinger at eksportere (angiv de indbyggede i `names`)
|
||||
@msg not an export presets file (format `{_1}`) ikke en fil med eksportforudindstillinger (format `{_1}`)
|
||||
@msg not an export presets file: {e} ikke en fil med eksportforudindstillinger: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
@msg `globalAdjust` must be an object `globalAdjust` skal være et objekt
|
||||
@msg a path has too many points en sti har for mange punkter
|
||||
@msg it ends inside a group or compound path den slutter inde i en gruppe eller en sammensat kurve
|
||||
@msg it ends inside a layer den slutter inde i et lag
|
||||
@@ -4920,6 +4929,7 @@ file-filter Swatch libraries Farveprøvebiblioteker
|
||||
file-filter Flattener presets Flattener-forudindstillinger
|
||||
file-filter PDF presets PDF-forudindstillinger
|
||||
file-filter Print presets Printforudindstillinger
|
||||
file-filter Export presets Eksportforudindstillinger
|
||||
file-filter SVG Compressed Komprimeret SVG
|
||||
file-filter PDF-compatible .ai PDF-kompatibel .ai
|
||||
file-filter VectorCraft Template VectorCraft-skabelon
|
||||
@@ -5276,6 +5286,10 @@ swatch Bright Violet Kraftig violet
|
||||
Delete Export Preset Slet eksportforudindstilling
|
||||
Export Presets Eksportforudindstillinger
|
||||
Save Export Preset Gem eksportforudindstilling
|
||||
Export Export Presets Eksportér eksportforudindstillinger
|
||||
Export Preset… Eksportér forudindstilling…
|
||||
Import Export Presets Importér eksportforudindstillinger
|
||||
Import Preset… Importér forudindstilling…
|
||||
Export Layer Eksportér lag
|
||||
Export Selection Eksportér markering
|
||||
All in One Folder Alt i én mappe
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1010,6 +1010,16 @@ font-technology Variable Variabel
|
||||
Delete Export Preset Exportvorgabe löschen
|
||||
Export Presets Exportvorgaben
|
||||
Save Export Preset Exportvorgabe speichern
|
||||
Export Export Presets Exportvorgaben exportieren
|
||||
Export Preset… Vorgabe exportieren…
|
||||
Import Export Presets Exportvorgaben importieren
|
||||
Import Preset… Vorgabe importieren…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` ist der Name einer Vorgabe aus document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) keine Exportvorgabe `{n}` (siehe document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) keine gespeicherten Exportvorgaben zum Exportieren (integrierte in `names` angeben)
|
||||
@msg not an export presets file (format `{_1}`) keine Exportvorgabendatei (Format `{_1}`)
|
||||
@msg not an export presets file: {e} keine Exportvorgabendatei: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
Export Layer Ebene exportieren
|
||||
Export Selection Auswahl exportieren
|
||||
All in One Folder Alles in einem Ordner
|
||||
@@ -1182,6 +1192,7 @@ file-filter Swatch libraries Farbfeldbibliotheken
|
||||
file-filter Flattener presets Reduzierungsvorgaben
|
||||
file-filter PDF presets PDF-Vorgaben
|
||||
file-filter Print presets Druckvorgaben
|
||||
file-filter Export presets Exportvorgaben
|
||||
file-filter SVG Compressed SVG komprimiert
|
||||
file-filter PDF-compatible .ai PDF-kompatible .ai
|
||||
file-filter VectorCraft Template VectorCraft-Vorlage
|
||||
@@ -2744,11 +2755,13 @@ digits Malayalam Malayalam
|
||||
Save a Copy Kopie speichern
|
||||
Save a Copy… Kopie speichern…
|
||||
Save As Speichern unter
|
||||
Save As Preset… Als Vorgabe speichern…
|
||||
Save as PDF… Als PDF speichern…
|
||||
Save as preset: Als Vorgabe speichern:
|
||||
Save as Template Als Vorlage speichern
|
||||
Save as Template… Als Vorlage speichern…
|
||||
Save As… Speichern unter…
|
||||
Save Changes Änderungen speichern
|
||||
Save changes to {name} Änderungen an {name} speichern
|
||||
Save color group to Swatch panel Farbgruppe im Bedienfeld „Farbfelder“ speichern
|
||||
Save Colors as Swatches Farben als Farbfelder speichern
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -983,6 +983,16 @@ font-technology Variable Μεταβλητές
|
||||
Delete Export Preset Διαγραφή προκαθορισμένου εξαγωγής
|
||||
Export Presets Προκαθορισμένα εξαγωγής
|
||||
Save Export Preset Αποθήκευση προκαθορισμένου εξαγωγής
|
||||
Export Export Presets Εξαγωγή προκαθορισμένων εξαγωγής
|
||||
Export Preset… Εξαγωγή προκαθορισμένου…
|
||||
Import Export Presets Εισαγωγή προκαθορισμένων εξαγωγής
|
||||
Import Preset… Εισαγωγή προκαθορισμένου…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list Το `exportPreset` είναι το όνομα ενός προκαθορισμένου από το document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) δεν υπάρχει προκαθορισμένο εξαγωγής `{n}` (δείτε document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) δεν υπάρχουν αποθηκευμένα προκαθορισμένα εξαγωγής για εξαγωγή (δώστε τα ενσωματωμένα στο `names`)
|
||||
@msg not an export presets file (format `{_1}`) δεν είναι αρχείο προκαθορισμένων εξαγωγής (μορφή `{_1}`)
|
||||
@msg not an export presets file: {e} δεν είναι αρχείο προκαθορισμένων εξαγωγής: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
Export Layer Εξαγωγή επιπέδου
|
||||
Export Selection Εξαγωγή επιλογής
|
||||
All in One Folder Όλα σε έναν φάκελο
|
||||
@@ -1853,6 +1863,7 @@ font-technology Variable Μεταβλητές
|
||||
Save a Copy Αποθήκευση αντιγράφου
|
||||
Save a Copy… Αποθήκευση αντιγράφου…
|
||||
Save As Αποθήκευση ως
|
||||
Save As Preset… Αποθήκευση ως προεπιλογή…
|
||||
Save as PDF… Αποθήκευση ως PDF…
|
||||
Save As… Αποθήκευση ως…
|
||||
Save for Web (Legacy)… Αποθήκευση για ιστό (παλαιού τύπου)…
|
||||
@@ -2489,6 +2500,7 @@ file-filter Flattener presets Προεπιλογές ισοπέδωσης
|
||||
file-filter PDF presets Προεπιλογές PDF
|
||||
file-filter PDF-compatible .ai .ai συμβατό με PDF
|
||||
file-filter Print presets Προεπιλογές εκτύπωσης
|
||||
file-filter Export presets Προκαθορισμένα εξαγωγής
|
||||
file-filter SVG Compressed Συμπιεσμένο SVG
|
||||
file-filter Swatch libraries Βιβλιοθήκες δειγμάτων χρωμάτων
|
||||
file-filter Templates Πρότυπα
|
||||
@@ -3313,6 +3325,7 @@ digits Malayalam Μαλαγιάλαμ
|
||||
Save as preset: Αποθήκευση ως προεπιλογή:
|
||||
Save as Template Αποθήκευση ως πρότυπο
|
||||
Save as Template… Αποθήκευση ως πρότυπο…
|
||||
Save Changes Αποθήκευση αλλαγών
|
||||
Save changes to {name} Αποθήκευση αλλαγών στο {name}
|
||||
Save color group to Swatch panel Αποθήκευση ομάδας χρωμάτων στον πίνακα Δείγματα
|
||||
Save Colors as Swatches Αποθήκευση χρωμάτων ως δειγμάτων
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 163 and column 5.
|
@@ -1039,6 +1039,16 @@ font-technology Variable Variables
|
||||
Delete Export Preset Eliminar ajuste de exportación
|
||||
Export Presets Ajustes de exportación
|
||||
Save Export Preset Guardar ajuste de exportación
|
||||
Export Export Presets Exportar ajustes de exportación
|
||||
Export Preset… Exportar ajuste…
|
||||
Import Export Presets Importar ajustes de exportación
|
||||
Import Preset… Importar ajuste…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` es el nombre de un ajuste de document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) no hay ningún ajuste de exportación `{n}` (consulta document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) no hay ajustes de exportación guardados para exportar (indica los integrados en `names`)
|
||||
@msg not an export presets file (format `{_1}`) no es un archivo de ajustes de exportación (formato `{_1}`)
|
||||
@msg not an export presets file: {e} no es un archivo de ajustes de exportación: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
Export Layer Exportar capa
|
||||
Export Selection Exportar selección
|
||||
All in One Folder Todo en una carpeta
|
||||
@@ -2787,11 +2797,13 @@ digits Malayalam Malayálam
|
||||
Save a Copy Guardar una copia
|
||||
Save a Copy… Guardar una copia…
|
||||
Save As Guardar como
|
||||
Save As Preset… Guardar como ajuste preestablecido…
|
||||
Save as PDF… Guardar como PDF…
|
||||
Save as preset: Guardar como preajuste:
|
||||
Save as Template Guardar como plantilla
|
||||
Save as Template… Guardar como plantilla…
|
||||
Save As… Guardar como…
|
||||
Save Changes Guardar cambios
|
||||
Save changes to {name} Guardar cambios en {name}
|
||||
Save color group to Swatch panel Guardar grupo de colores en el panel Muestras
|
||||
Save Colors as Swatches Guardar colores como muestras
|
||||
@@ -5202,6 +5214,7 @@ file-filter Swatch libraries Bibliotecas de muestras
|
||||
file-filter Flattener presets Preajustes del acoplador
|
||||
file-filter PDF presets Preajustes de PDF
|
||||
file-filter Print presets Preajustes de impresión
|
||||
file-filter Export presets Ajustes de exportación
|
||||
file-filter SVG Compressed SVG comprimido
|
||||
file-filter PDF-compatible .ai .ai compatible con PDF
|
||||
file-filter VectorCraft Template Plantilla de VectorCraft
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2312,6 +2312,8 @@ digits Malayalam Malajalam
|
||||
Preserve Overprints and Spot Colors Säilytä päällepainatukset ja spottivärit
|
||||
Rasters Rasterit
|
||||
Replaces the saved preset of that name. Korvaa samannimisen tallennetun esiasetuksen.
|
||||
Save As Preset… Tallenna esiasetuksena…
|
||||
Save Changes Tallenna muutokset
|
||||
Save Preset… Tallenna esiasetus…
|
||||
Vectors Vektorit
|
||||
A saved copy of the selected preset Tallennettu kopio valitusta esiasetuksesta
|
||||
@@ -2901,6 +2903,16 @@ digits Malayalam Malajalam
|
||||
Delete Export Preset Poista vientiesiasetus
|
||||
Export Presets Vientiesiasetukset
|
||||
Save Export Preset Tallenna vientiesiasetus
|
||||
Export Export Presets Vie vientiesiasetukset
|
||||
Export Preset… Vie esiasetus…
|
||||
Import Export Presets Tuo vientiesiasetukset
|
||||
Import Preset… Tuo esiasetus…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` on luettelon document.exportPresets.list esiasetuksen nimi
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) ei vientiesiasetusta `{n}` (katso document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) ei tallennettuja vientiesiasetuksia vietäväksi (anna sisäänrakennetut parametrissa `names`)
|
||||
@msg not an export presets file (format `{_1}`) ei vientiesiasetustiedosto (muoto `{_1}`)
|
||||
@msg not an export presets file: {e} ei vientiesiasetustiedosto: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
Export Layer Vie taso
|
||||
Export Selection Vie valinta
|
||||
All in One Folder Kaikki yhteen kansioon
|
||||
@@ -5220,6 +5232,7 @@ file-filter Swatch libraries Värimallikirjastot
|
||||
file-filter Flattener presets Litistyksen esiasetukset
|
||||
file-filter PDF presets PDF-esiasetukset
|
||||
file-filter Print presets Tulostuksen esiasetukset
|
||||
file-filter Export presets Vientiesiasetukset
|
||||
file-filter SVG Compressed Pakattu SVG
|
||||
file-filter PDF-compatible .ai PDF-yhteensopiva .ai
|
||||
file-filter VectorCraft Template VectorCraft-malli
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1034,6 +1034,16 @@ font-technology Variable Variables
|
||||
Delete Export Preset Supprimer le paramètre prédéfini d’export
|
||||
Export Presets Paramètres prédéfinis d’export
|
||||
Save Export Preset Enregistrer le paramètre prédéfini d’export
|
||||
Export Export Presets Exporter les paramètres prédéfinis d’export
|
||||
Export Preset… Exporter le paramètre prédéfini…
|
||||
Import Export Presets Importer des paramètres prédéfinis d’export
|
||||
Import Preset… Importer un paramètre prédéfini…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` est le nom d’un paramètre prédéfini de document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) aucun paramètre prédéfini d’export `{n}` (voir document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) aucun paramètre prédéfini d’export enregistré à exporter (indiquez les paramètres intégrés dans `names`)
|
||||
@msg not an export presets file (format `{_1}`) ce n’est pas un fichier de paramètres prédéfinis d’export (format `{_1}`)
|
||||
@msg not an export presets file: {e} ce n’est pas un fichier de paramètres prédéfinis d’export : {e}
|
||||
@msg `{_1}`: {msg} `{_1}` : {msg}
|
||||
Export Layer Exporter le calque
|
||||
Export Selection Exporter la sélection
|
||||
All in One Folder Tout dans un dossier
|
||||
@@ -2779,11 +2789,13 @@ digits Malayalam Malayalam
|
||||
Save a Copy Enregistrer une copie
|
||||
Save a Copy… Enregistrer une copie…
|
||||
Save As Enregistrer sous
|
||||
Save As Preset… Enregistrer comme paramètre prédéfini…
|
||||
Save as PDF… Enregistrer au format PDF…
|
||||
Save as preset: Enregistrer comme paramètre prédéfini :
|
||||
Save as Template Enregistrer comme modèle
|
||||
Save as Template… Enregistrer comme modèle…
|
||||
Save As… Enregistrer sous…
|
||||
Save Changes Enregistrer les modifications
|
||||
Save changes to {name} Enregistrer les modifications de {name}
|
||||
Save color group to Swatch panel Enregistrer le groupe de couleurs dans le panneau Nuancier
|
||||
Save Colors as Swatches Enregistrer les couleurs comme nuances
|
||||
@@ -5391,6 +5403,7 @@ file-filter Flattener presets Paramètres prédéfinis d’aplatissement
|
||||
file-filter PDF presets Paramètres prédéfinis PDF
|
||||
file-filter PDF-compatible .ai .ai compatible PDF
|
||||
file-filter Print presets Paramètres prédéfinis d’impression
|
||||
file-filter Export presets Paramètres prédéfinis d’export
|
||||
file-filter SVG Compressed SVG compressé
|
||||
file-filter Swatch libraries Bibliothèques de nuances
|
||||
file-filter Templates Modèles
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1315,6 +1315,16 @@ font-technology Variable Változó
|
||||
Delete Export Preset Exportálási előbeállítás törlése
|
||||
Export Presets Exportálási előbeállítások
|
||||
Save Export Preset Exportálási előbeállítás mentése
|
||||
Export Export Presets Exportálási előbeállítások exportálása
|
||||
Export Preset… Előbeállítás exportálása…
|
||||
Import Export Presets Exportálási előbeállítások importálása
|
||||
Import Preset… Előbeállítás importálása…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset`: a document.exportPresets.list egyik előbeállításának neve kell
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) nincs ilyen exportálási előbeállítás: `{n}` (lásd: document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) nincs exportálható mentett exportálási előbeállítás (a beépítetteket a `names` paraméterben add meg)
|
||||
@msg not an export presets file (format `{_1}`) nem exportálási előbeállítás-fájl (formátum: `{_1}`)
|
||||
@msg not an export presets file: {e} nem exportálási előbeállítás-fájl: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
Export Layer Réteg exportálása
|
||||
Export Selection Kijelölés exportálása
|
||||
All in One Folder Mind egy mappába
|
||||
@@ -1500,6 +1510,7 @@ file-filter Swatch libraries Színminta-könyvtárak
|
||||
file-filter Flattener presets Lapítási előbeállítások
|
||||
file-filter PDF presets PDF-előbeállítások
|
||||
file-filter Print presets Nyomtatási előbeállítások
|
||||
file-filter Export presets Exportálási előbeállítások
|
||||
file-filter SVG Compressed Tömörített SVG
|
||||
file-filter PDF-compatible .ai PDF-kompatibilis .ai
|
||||
file-filter VectorCraft Template VectorCraft-sablon
|
||||
@@ -3363,11 +3374,13 @@ mark-style Japanese Japán
|
||||
Save a Copy Másolat mentése
|
||||
Save a Copy… Másolat mentése…
|
||||
Save As Mentés másként
|
||||
Save As Preset… Mentés készletként…
|
||||
Save as PDF… Mentés PDF-ként…
|
||||
Save as preset: Mentés előbeállításként:
|
||||
Save as Template Mentés sablonként
|
||||
Save as Template… Mentés sablonként…
|
||||
Save As… Mentés másként…
|
||||
Save Changes Módosítások mentése
|
||||
Save changes to {name} Módosítások mentése: {name}
|
||||
Save color group to Swatch panel Színcsoport mentése a Színminták panelre
|
||||
Save Colors as Swatches Színek mentése színmintaként
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2155,6 +2155,8 @@ digits Malayalam Malayalam
|
||||
Preserve Overprints and Spot Colors Pertahankan overprint dan warna spot
|
||||
Rasters Raster
|
||||
Replaces the saved preset of that name. Mengganti preset tersimpan dengan nama tersebut.
|
||||
Save As Preset… Simpan sebagai Preset…
|
||||
Save Changes Simpan Perubahan
|
||||
Save Preset… Simpan preset…
|
||||
Vectors Vektor
|
||||
A saved copy of the selected preset Salinan tersimpan dari preset terpilih
|
||||
@@ -4543,6 +4545,13 @@ imageTrace Outline Kontur
|
||||
@msg this isn't a flat-colour logo ({_1}% of its solid areas match a few flat colours), so Flat Logo mode would turn its gradients into patches: use Color mode instead ini bukan logo dengan warna rata ({_1}% area solidnya cocok dengan beberapa warna rata), jadi mode Logo Rata akan mengubah gradiennya menjadi patch: gunakan mode Warna sebagai gantinya
|
||||
@msg `{what}` closes nothing `{what}` tidak menutup apa pun
|
||||
@msg `{what}` closes something it didn't open `{what}` menutup sesuatu yang tidak dibukanya
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` adalah nama preset dari document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) tidak ada preset ekspor `{n}` (lihat document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) tidak ada preset ekspor tersimpan untuk diekspor (sebutkan yang bawaan di `names`)
|
||||
@msg not an export presets file (format `{_1}`) bukan file preset ekspor (format `{_1}`)
|
||||
@msg not an export presets file: {e} bukan file preset ekspor: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
@msg `globalAdjust` must be an object `globalAdjust` harus berupa objek
|
||||
@msg a path has too many points sebuah jalur memiliki terlalu banyak titik
|
||||
@msg it ends inside a group or compound path berakhir di dalam grup atau jalur majemuk
|
||||
@msg it ends inside a layer berakhir di dalam lapisan
|
||||
@@ -4920,6 +4929,7 @@ file-filter Swatch libraries Pustaka swatch
|
||||
file-filter Flattener presets Preset perata
|
||||
file-filter PDF presets Preset PDF
|
||||
file-filter Print presets Preset cetak
|
||||
file-filter Export presets Preset ekspor
|
||||
file-filter SVG Compressed SVG terkompresi
|
||||
file-filter PDF-compatible .ai .ai kompatibel PDF
|
||||
file-filter VectorCraft Template Templat VectorCraft
|
||||
@@ -5276,6 +5286,10 @@ swatch Bright Violet Violet cerah
|
||||
Delete Export Preset Hapus preset ekspor
|
||||
Export Presets Daftar preset ekspor
|
||||
Save Export Preset Simpan preset ekspor
|
||||
Export Export Presets Ekspor preset ekspor
|
||||
Export Preset… Ekspor preset…
|
||||
Import Export Presets Impor preset ekspor
|
||||
Import Preset… Impor preset…
|
||||
Export Layer Ekspor lapisan
|
||||
Export Selection Ekspor seleksi
|
||||
All in One Folder Semua dalam satu folder
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2206 and column 5.
|
@@ -1040,6 +1040,16 @@ font-technology Variable Variabili
|
||||
Delete Export Preset Elimina predefinito di esportazione
|
||||
Export Presets Predefiniti di esportazione
|
||||
Save Export Preset Salva predefinito di esportazione
|
||||
Export Export Presets Esporta predefiniti di esportazione
|
||||
Export Preset… Esporta predefinito…
|
||||
Import Export Presets Importa predefiniti di esportazione
|
||||
Import Preset… Importa predefinito…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` è il nome di un predefinito di document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) nessun predefinito di esportazione `{n}` (vedi document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) nessun predefinito di esportazione salvato da esportare (indica quelli integrati in `names`)
|
||||
@msg not an export presets file (format `{_1}`) non è un file di predefiniti di esportazione (formato `{_1}`)
|
||||
@msg not an export presets file: {e} non è un file di predefiniti di esportazione: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
Export Layer Esporta livello
|
||||
Export Selection Esporta selezione
|
||||
All in One Folder Tutto in una cartella
|
||||
@@ -1212,6 +1222,7 @@ file-filter Swatch libraries Librerie campioni
|
||||
file-filter Flattener presets Preset di appiattimento
|
||||
file-filter PDF presets Preset PDF
|
||||
file-filter Print presets Preset di stampa
|
||||
file-filter Export presets Predefiniti di esportazione
|
||||
file-filter SVG Compressed SVG compresso
|
||||
file-filter PDF-compatible .ai .ai compatibile con PDF
|
||||
file-filter VectorCraft Template Modello VectorCraft
|
||||
@@ -2820,11 +2831,13 @@ digits Malayalam Malayalam
|
||||
Save a Copy Salva una copia
|
||||
Save a Copy… Salva una copia…
|
||||
Save As Salva con nome
|
||||
Save As Preset… Salva come predefinito…
|
||||
Save as PDF… Salva come PDF…
|
||||
Save as preset: Salva come preset:
|
||||
Save as Template Salva come modello
|
||||
Save as Template… Salva come modello…
|
||||
Save As… Salva con nome…
|
||||
Save Changes Salva modifiche
|
||||
Save changes to {name} Salva le modifiche a {name}
|
||||
Save color group to Swatch panel Salva il gruppo colori nel pannello Campioni
|
||||
Save Colors as Swatches Salva colori come campioni
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1527,6 +1527,16 @@ digits Malayalam マラヤーラム数字
|
||||
Delete Export Preset 書き出しプリセットを削除
|
||||
Export Presets 書き出しプリセット
|
||||
Save Export Preset 書き出しプリセットを保存
|
||||
Export Export Presets 書き出しプリセットを書き出し
|
||||
Export Preset… プリセットを書き出し…
|
||||
Import Export Presets 書き出しプリセットを読み込み
|
||||
Import Preset… プリセットを読み込み…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` には document.exportPresets.list にあるプリセットの名前を指定してください
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) 書き出しプリセット `{n}` が見つかりません (document.exportPresets.list を参照)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) 書き出す保存済みの書き出しプリセットがありません (組み込みのプリセットは `names` で指定してください)
|
||||
@msg not an export presets file (format `{_1}`) 書き出しプリセットファイルではありません (形式: `{_1}`)
|
||||
@msg not an export presets file: {e} 書き出しプリセットファイルではありません: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
Export Layer レイヤーを書き出し
|
||||
Export Selection 選択範囲を書き出し
|
||||
All in One Folder すべて 1 つのフォルダーに
|
||||
@@ -2774,6 +2784,7 @@ mark-style Japanese 日本式
|
||||
Saturate 彩度を調整
|
||||
Saturation 彩度
|
||||
Save As 別名で保存
|
||||
Save As Preset… プリセットとして保存…
|
||||
Save Colors as Swatches カラーをスウォッチとして保存
|
||||
Save Document ドキュメントを保存
|
||||
Save Document Info… ドキュメント情報を保存…
|
||||
@@ -2797,6 +2808,7 @@ mark-style Japanese 日本式
|
||||
Save a Copy コピーを保存
|
||||
Save as Template テンプレートとして保存
|
||||
Save as preset: プリセットとして保存:
|
||||
Save Changes 変更を保存
|
||||
Save changes to {name} {name} への変更を保存
|
||||
Save color group to Swatch panel カラーグループをスウォッチパネルに保存
|
||||
Save each artboard to a separate file アートボードごとに別のファイルに保存
|
||||
@@ -3498,6 +3510,7 @@ file-filter Swatch libraries スウォッチライブラリ
|
||||
file-filter Flattener presets 分割・統合プリセット
|
||||
file-filter PDF presets PDF プリセット
|
||||
file-filter Print presets プリントプリセット
|
||||
file-filter Export presets 書き出しプリセット
|
||||
file-filter SVG Compressed SVG 圧縮
|
||||
file-filter PDF-compatible .ai PDF 互換の .ai
|
||||
file-filter VectorCraft Template VectorCraft テンプレート
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2054,6 +2054,8 @@ digits Malayalam მალაიალამური
|
||||
Preserve Overprints and Spot Colors ზედდაბეჭდვისა და სპოტ-ფერების შენარჩუნება
|
||||
Rasters რასტრები
|
||||
Replaces the saved preset of that name. ანაცვლებს ამავე სახელის შენახულ პრესეტს.
|
||||
Save As Preset… შენახვა წინასწარ პარამეტრად…
|
||||
Save Changes ცვლილებების შენახვა
|
||||
Save Preset… პრესეტის შენახვა…
|
||||
Vectors ვექტორები
|
||||
A saved copy of the selected preset მონიშნული პრესეტის შენახული ასლი
|
||||
@@ -4788,6 +4790,7 @@ file-filter Swatch libraries ფერის ნიმუშების ბი
|
||||
file-filter Flattener presets გაბრტყელების პრესეტები
|
||||
file-filter PDF presets PDF-ის პრესეტები
|
||||
file-filter Print presets ბეჭდვის პრესეტები
|
||||
file-filter Export presets ექსპორტის პრესეტები
|
||||
file-filter SVG Compressed შეკუმშული SVG
|
||||
file-filter PDF-compatible .ai PDF-თან თავსებადი .ai
|
||||
file-filter VectorCraft Template VectorCraft-ის შაბლონი
|
||||
@@ -5172,6 +5175,16 @@ imageTrace Outline კონტური
|
||||
Delete Export Preset ექსპორტის პრესეტის წაშლა
|
||||
Export Presets ექსპორტის პრესეტები
|
||||
Save Export Preset ექსპორტის პრესეტის შენახვა
|
||||
Export Export Presets ექსპორტის პრესეტების ექსპორტირება
|
||||
Export Preset… პრესეტის ექსპორტირება…
|
||||
Import Export Presets ექსპორტის პრესეტების იმპორტირება
|
||||
Import Preset… პრესეტის იმპორტირება…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` არის პრესეტის სახელი სიიდან document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) ექსპორტის პრესეტი `{n}` არ არის (იხ. document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) შენახული ექსპორტის პრესეტები ექსპორტისთვის არ არის (ჩაშენებულები მიუთითეთ `names`-ში)
|
||||
@msg not an export presets file (format `{_1}`) ეს არ არის ექსპორტის პრესეტების ფაილი (ფორმატი `{_1}`)
|
||||
@msg not an export presets file: {e} ეს არ არის ექსპორტის პრესეტების ფაილი: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
Export Layer შრის ექსპორტირება
|
||||
Export Selection მონიშნულის ექსპორტირება
|
||||
All in One Folder ყველა ერთ საქაღალდეში
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1517,6 +1517,16 @@ digits Malayalam 말라얄람어
|
||||
Delete Export Preset 내보내기 사전 설정 삭제
|
||||
Export Presets 내보내기 사전 설정
|
||||
Save Export Preset 내보내기 사전 설정 저장
|
||||
Export Export Presets 내보내기 사전 설정 내보내기
|
||||
Export Preset… 사전 설정 내보내기…
|
||||
Import Export Presets 내보내기 사전 설정 가져오기
|
||||
Import Preset… 사전 설정 가져오기…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset`은(는) document.exportPresets.list에 있는 사전 설정의 이름입니다
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) 내보내기 사전 설정 `{n}`이(가) 없습니다(document.exportPresets.list 참조)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) 내보낼 저장된 내보내기 사전 설정이 없습니다(기본 제공 사전 설정은 `names`에 지정하세요)
|
||||
@msg not an export presets file (format `{_1}`) 내보내기 사전 설정 파일이 아닙니다(형식 `{_1}`)
|
||||
@msg not an export presets file: {e} 내보내기 사전 설정 파일이 아닙니다: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
Export Layer 레이어 내보내기
|
||||
Export Selection 선택 영역 내보내기
|
||||
All in One Folder 모두 폴더 하나에
|
||||
@@ -2763,6 +2773,7 @@ mark-style Japanese 일본식
|
||||
Saturate 채도 높이기
|
||||
Saturation 채도
|
||||
Save As 다른 이름으로 저장
|
||||
Save As Preset… 사전 설정으로 저장…
|
||||
Save Colors as Swatches 색상을 견본으로 저장
|
||||
Save Document 문서 저장
|
||||
Save Document Info… 문서 정보 저장…
|
||||
@@ -2786,6 +2797,7 @@ mark-style Japanese 일본식
|
||||
Save a Copy 사본 저장
|
||||
Save as Template 템플릿으로 저장
|
||||
Save as preset: 사전 설정으로 저장:
|
||||
Save Changes 변경 사항 저장
|
||||
Save changes to {name} {name}의 변경 내용 저장
|
||||
Save color group to Swatch panel 색상 그룹을 견본 패널에 저장
|
||||
Save each artboard to a separate file 아트보드별로 별도 파일에 저장
|
||||
@@ -3486,6 +3498,7 @@ file-filter Swatch libraries 견본 라이브러리
|
||||
file-filter Flattener presets 병합 프리셋
|
||||
file-filter PDF presets PDF 프리셋
|
||||
file-filter Print presets 인쇄 프리셋
|
||||
file-filter Export presets 내보내기 사전 설정
|
||||
file-filter SVG Compressed 압축된 SVG
|
||||
file-filter PDF-compatible .ai PDF 호환 .ai
|
||||
file-filter VectorCraft Template VectorCraft 템플릿
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2169,6 +2169,8 @@ digits Malayalam Malayalam
|
||||
Preserve Overprints and Spot Colors Overprints en spotkleuren behouden
|
||||
Rasters Rasters
|
||||
Replaces the saved preset of that name. Vervangt de opgeslagen voorinstelling met die naam.
|
||||
Save As Preset… Opslaan als voorinstelling…
|
||||
Save Changes Wijzigingen opslaan
|
||||
Save Preset… Voorinstelling opslaan…
|
||||
Vectors Vectoren
|
||||
A saved copy of the selected preset Een opgeslagen kopie van de geselecteerde voorinstelling
|
||||
@@ -4555,6 +4557,13 @@ imageTrace Outline Contour
|
||||
@msg this isn't a flat-colour logo ({_1}% of its solid areas match a few flat colours), so Flat Logo mode would turn its gradients into patches: use Color mode instead dit is geen logo met effen kleuren ({_1}% van de effen vlakken komt overeen met enkele effen kleuren), dus de modus Effen logo zou de verlopen in vlakken veranderen: gebruik in plaats daarvan de modus Kleur
|
||||
@msg `{what}` closes nothing `{what}` sluit niets
|
||||
@msg `{what}` closes something it didn't open `{what}` sluit iets dat het niet heeft geopend
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` is de naam van een voorinstelling uit document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) geen exportvoorinstelling `{n}` (zie document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) geen opgeslagen exportvoorinstellingen om te exporteren (noem ingebouwde in `names`)
|
||||
@msg not an export presets file (format `{_1}`) geen bestand met exportvoorinstellingen (indeling `{_1}`)
|
||||
@msg not an export presets file: {e} geen bestand met exportvoorinstellingen: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
@msg `globalAdjust` must be an object `globalAdjust` moet een object zijn
|
||||
@msg a path has too many points een pad heeft te veel punten
|
||||
@msg it ends inside a group or compound path het eindigt binnen een groep of samengesteld pad
|
||||
@msg it ends inside a layer het eindigt binnen een laag
|
||||
@@ -4932,6 +4941,7 @@ file-filter Swatch libraries Stalenbibliotheken
|
||||
file-filter Flattener presets Afvlakvoorinstellingen
|
||||
file-filter PDF presets PDF-voorinstellingen
|
||||
file-filter Print presets Afdrukvoorinstellingen
|
||||
file-filter Export presets Exportvoorinstellingen
|
||||
file-filter SVG Compressed Gecomprimeerde SVG
|
||||
file-filter PDF-compatible .ai PDF-compatibel .ai
|
||||
file-filter VectorCraft Template VectorCraft-sjabloon
|
||||
@@ -5284,6 +5294,10 @@ swatch Bright Violet Felviolet
|
||||
Delete Export Preset Exportvoorinstelling verwijderen
|
||||
Export Presets Exportvoorinstellingen
|
||||
Save Export Preset Exportvoorinstelling opslaan
|
||||
Export Export Presets Exportvoorinstellingen exporteren
|
||||
Export Preset… Voorinstelling exporteren…
|
||||
Import Export Presets Exportvoorinstellingen importeren
|
||||
Import Preset… Voorinstelling importeren…
|
||||
Export Layer Laag exporteren
|
||||
Export Selection Selectie exporteren
|
||||
All in One Folder Alles in één map
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2273,6 +2273,8 @@ digits Malayalam Malajalam
|
||||
Preserve Overprints and Spot Colors Zachowaj nadruki i kolory dodatkowe
|
||||
Rasters Rastry
|
||||
Replaces the saved preset of that name. Zastępuje zapisane ustawienie o tej nazwie.
|
||||
Save As Preset… Zapisz jako ustawienie predefiniowane…
|
||||
Save Changes Zapisz zmiany
|
||||
Save Preset… Zapisz ustawienie…
|
||||
Vectors Wektory
|
||||
A saved copy of the selected preset Zapisana kopia zaznaczonego ustawienia
|
||||
@@ -2841,6 +2843,16 @@ digits Malayalam Malajalam
|
||||
Delete Export Preset Usuń ustawienie eksportu
|
||||
Export Presets Ustawienia eksportu
|
||||
Save Export Preset Zapisz ustawienie eksportu
|
||||
Export Export Presets Eksportuj ustawienia eksportu
|
||||
Export Preset… Eksportuj ustawienie…
|
||||
Import Export Presets Importuj ustawienia eksportu
|
||||
Import Preset… Importuj ustawienie…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` to nazwa ustawienia z document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) brak ustawienia eksportu `{n}` (zob. document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) brak zapisanych ustawień eksportu do wyeksportowania (wbudowane podaj w `names`)
|
||||
@msg not an export presets file (format `{_1}`) to nie jest plik ustawień eksportu (format `{_1}`)
|
||||
@msg not an export presets file: {e} to nie jest plik ustawień eksportu: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
Export Layer Eksportuj warstwę
|
||||
Export Selection Eksportuj zaznaczenie
|
||||
All in One Folder Wszystko w jednym folderze
|
||||
@@ -5166,6 +5178,7 @@ file-filter Swatch libraries Biblioteki próbek
|
||||
file-filter Flattener presets Ustawienia spłaszczania
|
||||
file-filter PDF presets Ustawienia PDF
|
||||
file-filter Print presets Ustawienia drukowania
|
||||
file-filter Export presets Ustawienia eksportu
|
||||
file-filter SVG Compressed SVG skompresowany
|
||||
file-filter PDF-compatible .ai .ai zgodny z PDF
|
||||
file-filter VectorCraft Template Szablon VectorCraft
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -928,6 +928,16 @@
|
||||
Delete Export Preset Excluir predefinição de exportação
|
||||
Export Presets Predefinições de exportação
|
||||
Save Export Preset Salvar predefinição de exportação
|
||||
Export Export Presets Exportar predefinições de exportação
|
||||
Export Preset… Exportar predefinição…
|
||||
Import Export Presets Importar predefinições de exportação
|
||||
Import Preset… Importar predefinição…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` é o nome de uma predefinição de document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) nenhuma predefinição de exportação `{n}` (consulte document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) nenhuma predefinição de exportação salva para exportar (informe as integradas em `names`)
|
||||
@msg not an export presets file (format `{_1}`) não é um arquivo de predefinições de exportação (formato `{_1}`)
|
||||
@msg not an export presets file: {e} não é um arquivo de predefinições de exportação: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
Export Layer Exportar camada
|
||||
Export Selection Exportar seleção
|
||||
All in One Folder Tudo em uma pasta
|
||||
@@ -2338,6 +2348,7 @@ digits Malayalam Malaiala
|
||||
Saturation Saturação
|
||||
Save Salvar
|
||||
Save As Salvar Como
|
||||
Save As Preset… Salvar como predefinição…
|
||||
Save As… Salvar Como…
|
||||
Save Colors as Swatches Salvar Cores como Amostras
|
||||
Save Document Salvar Documento
|
||||
@@ -2369,6 +2380,7 @@ digits Malayalam Malaiala
|
||||
Save as Template Salvar como Modelo
|
||||
Save as Template… Salvar como Modelo…
|
||||
Save as preset: Salvar como predefinido:
|
||||
Save Changes Salvar alterações
|
||||
Save changes to {name} Salvar alterações em {name}
|
||||
Save color group to Swatch panel Salvar grupo de cores no painel Amostras
|
||||
Save each artboard to a separate file Salvar cada mesa de trabalho em um arquivo separado
|
||||
@@ -5205,6 +5217,7 @@ file-filter Swatch libraries Bibliotecas de amostras
|
||||
file-filter Flattener presets Predefinidos de achatamento
|
||||
file-filter PDF presets Predefinidos de PDF
|
||||
file-filter Print presets Predefinidos de impressão
|
||||
file-filter Export presets Predefinições de exportação
|
||||
file-filter SVG Compressed SVG Compactado
|
||||
file-filter PDF-compatible .ai .ai compatível com PDF
|
||||
file-filter VectorCraft Template Modelo do VectorCraft
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1037,6 +1037,16 @@ font-technology Variable Вариативные
|
||||
Delete Export Preset Удалить набор настроек экспорта
|
||||
Export Presets Наборы настроек экспорта
|
||||
Save Export Preset Сохранить набор настроек экспорта
|
||||
Export Export Presets Экспортировать наборы настроек экспорта
|
||||
Export Preset… Экспортировать набор…
|
||||
Import Export Presets Импортировать наборы настроек экспорта
|
||||
Import Preset… Импортировать набор…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` — имя набора из document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) нет набора настроек экспорта `{n}` (см. document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) нет сохранённых наборов настроек экспорта для экспорта (встроенные укажите в `names`)
|
||||
@msg not an export presets file (format `{_1}`) не является файлом наборов настроек экспорта (формат `{_1}`)
|
||||
@msg not an export presets file: {e} не является файлом наборов настроек экспорта: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
Export Layer Экспортировать слой
|
||||
Export Selection Экспортировать выделение
|
||||
All in One Folder Всё в одну папку
|
||||
@@ -1209,6 +1219,7 @@ file-filter Swatch libraries Библиотеки образцов
|
||||
file-filter Flattener presets Наборы объединения
|
||||
file-filter PDF presets Наборы PDF
|
||||
file-filter Print presets Наборы печати
|
||||
file-filter Export presets Наборы настроек экспорта
|
||||
file-filter SVG Compressed Сжатый SVG
|
||||
file-filter PDF-compatible .ai .ai, совместимый с PDF
|
||||
file-filter VectorCraft Template Шаблон VectorCraft
|
||||
@@ -2815,11 +2826,13 @@ digits Malayalam Малаялам
|
||||
Save a Copy Сохранить копию
|
||||
Save a Copy… Сохранить копию…
|
||||
Save As Сохранить как
|
||||
Save As Preset… Сохранить как набор настроек…
|
||||
Save as PDF… Сохранить как PDF…
|
||||
Save as preset: Сохранить как набор:
|
||||
Save as Template Сохранить как шаблон
|
||||
Save as Template… Сохранить как шаблон…
|
||||
Save As… Сохранить как…
|
||||
Save Changes Сохранить изменения
|
||||
Save changes to {name} Сохранить изменения в {name}
|
||||
Save color group to Swatch panel Сохранить цветовую группу на панели «Образцы»
|
||||
Save Colors as Swatches Сохранить цвета как образцы
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1018,6 +1018,16 @@ font-technology Variable Variabla
|
||||
Delete Export Preset Ta bort exportförinställning
|
||||
Export Presets Exportförinställningar
|
||||
Save Export Preset Spara exportförinställning
|
||||
Export Export Presets Exportera exportförinställningar
|
||||
Export Preset… Exportera förinställning…
|
||||
Import Export Presets Importera exportförinställningar
|
||||
Import Preset… Importera förinställning…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` är namnet på en förinställning i document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) ingen exportförinställning `{n}` (se document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) inga sparade exportförinställningar att exportera (ange inbyggda i `names`)
|
||||
@msg not an export presets file (format `{_1}`) inte en fil med exportförinställningar (format `{_1}`)
|
||||
@msg not an export presets file: {e} inte en fil med exportförinställningar: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
Export Layer Exportera lager
|
||||
Export Selection Exportera markering
|
||||
All in One Folder Allt i en mapp
|
||||
@@ -2723,11 +2733,13 @@ digits Malayalam Malayalam
|
||||
Save a Copy Spara en kopia
|
||||
Save a Copy… Spara en kopia…
|
||||
Save As Spara som
|
||||
Save As Preset… Spara som förinställning…
|
||||
Save as PDF… Spara som PDF…
|
||||
Save as preset: Spara som förinställning:
|
||||
Save as Template Spara som mall
|
||||
Save as Template… Spara som mall…
|
||||
Save As… Spara som…
|
||||
Save Changes Spara ändringar
|
||||
Save changes to {name} Spara ändringar i {name}
|
||||
Save color group to Swatch panel Spara färggruppen i panelen Färgrutor
|
||||
Save Colors as Swatches Spara färger som färgrutor
|
||||
@@ -5295,6 +5307,7 @@ file-filter Flattener presets Förinställningar för plattning
|
||||
file-filter PDF presets PDF-förinställningar
|
||||
file-filter PDF-compatible .ai PDF-kompatibel .ai
|
||||
file-filter Print presets Utskriftsförinställningar
|
||||
file-filter Export presets Exportförinställningar
|
||||
file-filter SVG Compressed SVG komprimerad
|
||||
file-filter Swatch libraries Färgrutebibliotek
|
||||
file-filter Templates Mallar
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -2169,6 +2169,7 @@ const RENDER_SITE_ROWS: &[(&str, &str)] = &[
|
||||
("file-filter", "Flattener presets"),
|
||||
("file-filter", "PDF presets"),
|
||||
("file-filter", "Print presets"),
|
||||
("file-filter", "Export presets"),
|
||||
("file-filter", "SVG Compressed"),
|
||||
("file-filter", "PDF-compatible .ai"),
|
||||
("file-filter", "VectorCraft Template"),
|
||||
|
||||
@@ -1010,6 +1010,16 @@ font-technology Variable Değişken
|
||||
Delete Export Preset Dışa Aktarma Ön Ayarını Sil
|
||||
Export Presets Ön Ayarları Dışa Aktarma
|
||||
Save Export Preset Dışa Aktarma Ön Ayarını Kaydet
|
||||
Export Export Presets Dışa Aktarma Ön Ayarlarını Dışa Aktar
|
||||
Export Preset… Ön Ayarı Dışa Aktar…
|
||||
Import Export Presets Dışa Aktarma Ön Ayarlarını İçe Aktar
|
||||
Import Preset… Ön Ayarı İçe Aktar…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset`, document.exportPresets.list içindeki bir ön ayarın adıdır
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) `{n}` adında dışa aktarma ön ayarı yok (bkz. document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) dışa aktarılacak kayıtlı dışa aktarma ön ayarı yok (yerleşik olanları `names` içinde belirtin)
|
||||
@msg not an export presets file (format `{_1}`) dışa aktarma ön ayarları dosyası değil (`{_1}` biçimi)
|
||||
@msg not an export presets file: {e} dışa aktarma ön ayarları dosyası değil: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
Export Layer Katmanı Dışa Aktar
|
||||
Export Selection Seçimi Dışa Aktar
|
||||
All in One Folder Hepsi Bir Klasörde
|
||||
@@ -2591,11 +2601,13 @@ digits Malayalam Malayalam
|
||||
Save a Copy Bir Kopyasını Kaydet
|
||||
Save a Copy… Bir Kopyasını Kaydet…
|
||||
Save As Farklı Kaydet
|
||||
Save As Preset… Ön ayar olarak kaydet…
|
||||
Save as PDF… PDF olarak kaydet…
|
||||
Save as preset: Ön ayar olarak kaydet:
|
||||
Save as Template Şablon olarak kaydet
|
||||
Save as Template… Şablon Olarak Kaydet…
|
||||
Save As… Farklı Kaydet…
|
||||
Save Changes Değişiklikleri kaydet
|
||||
Save changes to {name} Değişiklikleri {name}'ye kaydet
|
||||
Save color group to Swatch panel Renk grubunu Swatch paneline kaydedin
|
||||
Save Colors as Swatches Renkleri Renk Örnekleri Olarak Kaydet
|
||||
@@ -5089,6 +5101,7 @@ effect Regular Düzenli
|
||||
effect Vertical Dikey
|
||||
file-filter All placeable files Yerleştirilebilir tüm dosyalar
|
||||
file-filter All readable files Okunabilir tüm dosyalar
|
||||
file-filter Export presets Dışa aktarma önayarları
|
||||
file-filter Flattener presets Düzleştirici önayarları
|
||||
file-filter PDF presets PDF önayarları
|
||||
file-filter PDF-compatible .ai PDF ile uyumlu .ai
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -1034,6 +1034,16 @@ font-technology Variable Варіативні
|
||||
Delete Export Preset Видалити набір параметрів експорту
|
||||
Export Presets Набори параметрів експорту
|
||||
Save Export Preset Зберегти набір параметрів експорту
|
||||
Export Export Presets Експортувати набори параметрів експорту
|
||||
Export Preset… Експортувати набір…
|
||||
Import Export Presets Імпортувати набори параметрів експорту
|
||||
Import Preset… Імпортувати набір…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` — це ім’я набору з document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) немає набору параметрів експорту `{n}` (див. document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) немає збережених наборів параметрів експорту для експорту (вбудовані вкажіть у `names`)
|
||||
@msg not an export presets file (format `{_1}`) не є файлом наборів параметрів експорту (формат `{_1}`)
|
||||
@msg not an export presets file: {e} не є файлом наборів параметрів експорту: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
Export Layer Експортувати шар
|
||||
Export Selection Експортувати виділення
|
||||
All in One Folder Усе в одну теку
|
||||
@@ -1206,6 +1216,7 @@ file-filter Swatch libraries Бібліотеки зразків
|
||||
file-filter Flattener presets Набори зведення
|
||||
file-filter PDF presets Набори PDF
|
||||
file-filter Print presets Набори друку
|
||||
file-filter Export presets Набори параметрів експорту
|
||||
file-filter SVG Compressed Стиснутий SVG
|
||||
file-filter PDF-compatible .ai .ai, сумісний з PDF
|
||||
file-filter VectorCraft Template Шаблон VectorCraft
|
||||
@@ -2812,11 +2823,13 @@ digits Malayalam Малаялам
|
||||
Save a Copy Зберегти копію
|
||||
Save a Copy… Зберегти копію…
|
||||
Save As Зберегти як
|
||||
Save As Preset… Зберегти як стиль…
|
||||
Save as PDF… Зберегти як PDF…
|
||||
Save as preset: Зберегти як набір:
|
||||
Save as Template Зберегти як шаблон
|
||||
Save as Template… Зберегти як шаблон…
|
||||
Save As… Зберегти як…
|
||||
Save Changes Зберегти зміни
|
||||
Save changes to {name} Зберегти зміни в {name}
|
||||
Save color group to Swatch panel Зберегти групу кольорів на панелі «Зразки»
|
||||
Save Colors as Swatches Зберегти кольори як зразки
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 177 and column 5.
|
@@ -1192,6 +1192,16 @@ font-technology Variable Biến thiên
|
||||
Delete Export Preset Xóa cài đặt sẵn xuất
|
||||
Export Presets Cài đặt sẵn xuất
|
||||
Save Export Preset Lưu cài đặt sẵn xuất
|
||||
Export Export Presets Xuất cài đặt sẵn xuất
|
||||
Export Preset… Xuất cài đặt sẵn…
|
||||
Import Export Presets Nhập cài đặt sẵn xuất
|
||||
Import Preset… Nhập cài đặt sẵn…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` là tên của một cài đặt sẵn trong document.exportPresets.list
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) không có cài đặt sẵn xuất `{n}` (xem document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) không có cài đặt sẵn xuất đã lưu nào để xuất (ghi các cài đặt tích hợp sẵn trong `names`)
|
||||
@msg not an export presets file (format `{_1}`) không phải tệp cài đặt sẵn xuất (định dạng `{_1}`)
|
||||
@msg not an export presets file: {e} không phải tệp cài đặt sẵn xuất: {e}
|
||||
@msg `{_1}`: {msg} `{_1}`: {msg}
|
||||
Export Layer Xuất lớp
|
||||
Export Selection Xuất vùng chọn
|
||||
All in One Folder Tất cả trong một thư mục
|
||||
@@ -1403,6 +1413,7 @@ file-filter Swatch libraries Thư viện mẫu màu
|
||||
file-filter Flattener presets Thiết lập sẵn làm phẳng
|
||||
file-filter PDF presets Thiết lập sẵn PDF
|
||||
file-filter Print presets Thiết lập sẵn in
|
||||
file-filter Export presets Cài đặt sẵn xuất
|
||||
file-filter SVG Compressed SVG nén
|
||||
file-filter PDF-compatible .ai .ai tương thích PDF
|
||||
file-filter VectorCraft Template Mẫu VectorCraft
|
||||
@@ -3119,11 +3130,13 @@ mark-style Japanese Kiểu Nhật
|
||||
Save a Copy Lưu bản sao
|
||||
Save a Copy… Lưu bản sao…
|
||||
Save As Lưu thành
|
||||
Save As Preset… Lưu dưới dạng cài đặt sẵn…
|
||||
Save as PDF… Lưu thành PDF…
|
||||
Save as preset: Lưu thành thiết lập sẵn:
|
||||
Save as Template Lưu thành mẫu
|
||||
Save as Template… Lưu thành mẫu…
|
||||
Save As… Lưu thành…
|
||||
Save Changes Lưu thay đổi
|
||||
Save changes to {name} Lưu thay đổi vào {name}
|
||||
Save color group to Swatch panel Lưu nhóm màu vào bảng mẫu màu
|
||||
Save Colors as Swatches Lưu màu thành mẫu màu
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 199 and column 5.
|
@@ -980,6 +980,16 @@
|
||||
Delete Export Preset 删除导出预设
|
||||
Export Presets 导出预设
|
||||
Save Export Preset 存储导出预设
|
||||
Export Export Presets 导出导出预设
|
||||
Export Preset… 导出预设…
|
||||
Import Export Presets 导入导出预设
|
||||
Import Preset… 导入预设…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` 是 document.exportPresets.list 中某个预设的名称
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) 没有导出预设 `{n}` (参见 document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) 没有可导出的已保存导出预设 (内置预设请在 `names` 中指定)
|
||||
@msg not an export presets file (format `{_1}`) 不是导出预设文件 (格式为 `{_1}`)
|
||||
@msg not an export presets file: {e} 不是导出预设文件:{e}
|
||||
@msg `{_1}`: {msg} `{_1}`:{msg}
|
||||
Export Layer 导出图层
|
||||
Export Selection 导出选区
|
||||
All in One Folder 全部放在一个文件夹
|
||||
@@ -1150,6 +1160,7 @@ file-filter Swatch libraries 色板库
|
||||
file-filter Flattener presets 拼合预设
|
||||
file-filter PDF presets PDF 预设
|
||||
file-filter Print presets 打印预设
|
||||
file-filter Export presets 导出预设
|
||||
file-filter SVG Compressed 压缩 SVG
|
||||
file-filter PDF-compatible .ai PDF 兼容 .ai
|
||||
file-filter VectorCraft Template VectorCraft 模板
|
||||
@@ -2685,11 +2696,13 @@ digits Malayalam 马拉雅拉姆数字
|
||||
Save a Copy 存储副本
|
||||
Save a Copy… 保存副本…
|
||||
Save As 存储为
|
||||
Save As Preset… 另存为预设…
|
||||
Save as PDF… 存为 PDF…
|
||||
Save as preset: 存储为预设:
|
||||
Save as Template 另存模板
|
||||
Save as Template… 存为模板…
|
||||
Save As… 另存为…
|
||||
Save Changes 保存更改
|
||||
Save changes to {name} 存储对 {name} 的更改
|
||||
Save color group to Swatch panel 将颜色组存储至色板面板
|
||||
Save Colors as Swatches 将颜色存储为色板
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1004,6 +1004,16 @@ font-technology Variable 可變
|
||||
Delete Export Preset 刪除轉存預設集
|
||||
Export Presets 轉存預設集
|
||||
Save Export Preset 儲存轉存預設集
|
||||
Export Export Presets 匯出轉存預設集
|
||||
Export Preset… 匯出預設集…
|
||||
Import Export Presets 匯入轉存預設集
|
||||
Import Preset… 匯入預設集…
|
||||
@msg `exportPreset` is the name of a preset of document.exportPresets.list `exportPreset` 是 document.exportPresets.list 中某個預設集的名稱
|
||||
@msg no export preset `{n}` (see document.exportPresets.list) 沒有轉存預設集 `{n}` (請參閱 document.exportPresets.list)
|
||||
@msg no saved export presets to export (name built-in ones in `names`) 沒有可轉存的已儲存轉存預設集 (內建預設集請在 `names` 中指定)
|
||||
@msg not an export presets file (format `{_1}`) 不是轉存預設集檔案 (格式為 `{_1}`)
|
||||
@msg not an export presets file: {e} 不是轉存預設集檔案:{e}
|
||||
@msg `{_1}`: {msg} `{_1}`:{msg}
|
||||
Export Layer 轉存圖層
|
||||
Export Selection 轉存選取範圍
|
||||
All in One Folder 全部放在一個資料夾
|
||||
@@ -1176,6 +1186,7 @@ file-filter Swatch libraries 色票資料庫
|
||||
file-filter Flattener presets 平面化預設集
|
||||
file-filter PDF presets PDF 預設集
|
||||
file-filter Print presets 列印預設集
|
||||
file-filter Export presets 轉存預設集
|
||||
file-filter SVG Compressed 壓縮 SVG
|
||||
file-filter PDF-compatible .ai PDF 相容 .ai
|
||||
file-filter VectorCraft Template VectorCraft 範本
|
||||
@@ -3666,11 +3677,13 @@ digits Malayalam 馬來亞拉姆數字
|
||||
Save a Copy 儲存拷貝
|
||||
Save a Copy… 儲存拷貝…
|
||||
Save As 另存新檔
|
||||
Save As Preset… 儲存為預設集…
|
||||
Save as PDF… 儲存為 PDF…
|
||||
Save as preset: 儲存為預設集:
|
||||
Save as Template 另存範本
|
||||
Save as Template… 另存範本…
|
||||
Save As… 另存新檔…
|
||||
Save Changes 儲存變更
|
||||
Save changes to {name} 儲存對 {name} 的變更
|
||||
Save color group to Swatch panel 將顏色群組儲存至色票面板
|
||||
Save Colors as Swatches 將顏色儲存為色票
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -19,7 +19,7 @@ mod 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, reveal_path, run_to_file, save_command_output_named};
|
||||
pub(crate) use open::opened;
|
||||
pub(crate) use open::{EXPORT_PRESETS, import_presets, opened};
|
||||
pub use open::{
|
||||
MAX_RECENT_FILES, OpensAs, ask_revert, install_module, install_plugin, new_from_template, note_recent, open_bytes, open_dialog, open_document,
|
||||
open_path, open_recent_reporting, open_reporting, opens_as, place_bytes, recent_files, report_open_error,
|
||||
@@ -262,4 +262,27 @@ mod tests {
|
||||
assert_eq!(&png[16..20], 60u32.to_be_bytes(), "30 pt at scale 2");
|
||||
assert!(written.borrow().is_empty(), "no file and no save dialog");
|
||||
}
|
||||
|
||||
/// A presets file, a Libraries panel file or a graphic style library that is opened or dropped
|
||||
/// goes to its importer as the file's bytes: a byte that is not UTF-8 is refused instead of
|
||||
/// read as a replacement character.
|
||||
#[test]
|
||||
fn opened_preset_and_library_files_keep_their_bytes() {
|
||||
let (mut app, _) = app();
|
||||
app.run("file.new", json!({"width": 60, "height": 40})).unwrap();
|
||||
app.run("document.exportPresets.save", json!({"name": "Pack", "settings": {"formats": [{"format": "svg"}]}})).unwrap();
|
||||
let presets = app.run("document.exportPresets.export", json!({"names": ["Pack"]})).unwrap()["data"].as_str().unwrap().to_string();
|
||||
let lib = app.run("library.create", json!({"name": "Brand"})).unwrap()["id"].as_str().unwrap().to_string();
|
||||
let library = app.run("library.export", json!({ "library": lib })).unwrap()["data"].as_str().unwrap().to_string();
|
||||
let styles = app.run("graphicStyle.saveLibrary", json!({"name": "Kit"})).unwrap()["data"].as_str().unwrap().to_string();
|
||||
for (file, text, name) in [("pack.vcexportpresets", presets, "Pack"), ("brand.vclibrary", library, "Brand"), ("kit.vcstyles", styles, "Kit")]
|
||||
{
|
||||
let mut bytes = text.as_bytes().to_vec();
|
||||
bytes.insert(text.find(name).unwrap() + 2, 0xFF);
|
||||
assert!(open_bytes(&mut app, file, &bytes, None).is_err(), "{file}: {}", app.ui.status);
|
||||
// The file as it was still opens.
|
||||
open_bytes(&mut app, file, text.as_bytes(), None).unwrap();
|
||||
}
|
||||
assert_eq!(app.session.prefs.screen_export_presets.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["Pack", "Pack 2"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ const TEMPLATE_EXTS: &[&str] = &["vctemplate", "ait", "vectorcraft", "drawcraft"
|
||||
|
||||
/// Open bytes of any readable format as a new document (templates open untitled); swatch and
|
||||
/// graphic style library files open in the library panel, Libraries panel files are added to it,
|
||||
/// flattener, PDF, print and perspective grid presets files are imported, a shortcut set another
|
||||
/// flattener, PDF, print, perspective grid and export presets files are imported, a shortcut set another
|
||||
/// app saved (`.kys`) opens Import Shortcut Set, and a script written for the reference app
|
||||
/// (`.jsx`, `.jsxinc`, `.jsxbin`) shows its check (Check Script…). → `document.open`'s result for
|
||||
/// a document opened now (its `warnings` say what didn't come in as it was), else null.
|
||||
@@ -28,21 +28,18 @@ pub fn open_bytes(app: &mut VectorcraftApp, name: &str, bytes: &[u8], path: Opti
|
||||
return dialogs::script_check::command(app, &p).map(|_| Value::Null);
|
||||
}
|
||||
if let Some(OpensAs::Presets { import, what }) = opens {
|
||||
let r = app.run(import, serde_json::json!({"data": String::from_utf8_lossy(bytes)}))?;
|
||||
let names: Vec<&str> = r["imported"].as_array().into_iter().flatten().filter_map(Value::as_str).collect();
|
||||
app.status(format!("Imported {what}: {}", names.join(", ")));
|
||||
import_presets(app, import, what, serde_json::json!({"dataBase64": vectorcraft_format::base64_encode(bytes)}))?;
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
if opens == Some(OpensAs::Library) {
|
||||
return crate::panels::libraries::import(app, json!({"data": String::from_utf8_lossy(bytes)})).map(|_| Value::Null);
|
||||
return crate::panels::libraries::import(app, json!({"dataBase64": vectorcraft_format::base64_encode(bytes)})).map(|_| Value::Null);
|
||||
}
|
||||
let swatches = opens == Some(OpensAs::SwatchLibrary);
|
||||
if swatches || opens == Some(OpensAs::StyleLibrary) {
|
||||
let p = match path {
|
||||
Some(path) => serde_json::json!({ "path": path }),
|
||||
// `.ase` swatch libraries are binary.
|
||||
None if swatches => serde_json::json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(bytes)}),
|
||||
None => serde_json::json!({"name": name, "data": String::from_utf8_lossy(bytes)}),
|
||||
// The file's bytes as they are (`.ase` swatch libraries are binary).
|
||||
None => serde_json::json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(bytes)}),
|
||||
};
|
||||
let load = if swatches { crate::panels::swatches::load_library } else { crate::panels::graphic_styles::load_library };
|
||||
return load(app, p).map(|_| Value::Null);
|
||||
@@ -67,7 +64,7 @@ pub enum OpensAs {
|
||||
ShortcutSet,
|
||||
/// A script written for the reference app: checked without running it (Check Script…).
|
||||
Script,
|
||||
/// Flattener, PDF, print or perspective grid presets: imported with command `import`, and
|
||||
/// Flattener, PDF, print, perspective grid or export presets: imported with command `import`, and
|
||||
/// `what` names them in the status bar.
|
||||
Presets { import: &'static str, what: &'static str },
|
||||
/// A Libraries panel file: added to the Libraries panel as a new library.
|
||||
@@ -78,6 +75,18 @@ pub enum OpensAs {
|
||||
StyleLibrary,
|
||||
}
|
||||
|
||||
/// What the status bar calls the presets of an export presets file.
|
||||
pub(crate) const EXPORT_PRESETS: &str = "export presets";
|
||||
|
||||
/// Run presets command `import` with `p` (the file) and say in the status bar which presets
|
||||
/// (`what`) it added → its result.
|
||||
pub(crate) fn import_presets(app: &mut VectorcraftApp, import: &str, what: &str, p: Value) -> Result<Value, String> {
|
||||
let r = app.run(import, p)?;
|
||||
let names: Vec<&str> = r["imported"].as_array().into_iter().flatten().filter_map(Value::as_str).collect();
|
||||
app.status(format!("Imported {what}: {}", names.join(", ")));
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// The kind of file named `name` (by its extension) that File → Open installs, imports, checks,
|
||||
/// shows in the library panel or the Libraries panel, or reviews in Import Shortcut Set
|
||||
/// ([`open_bytes`]), else `None`. A drop opens these files the same way, wherever they are dropped
|
||||
@@ -89,6 +98,7 @@ pub fn opens_as(name: &str) -> Option<OpensAs> {
|
||||
(vectorcraft_engine::cmd::pdfcmds::PRESET_EXTS, "pdf.preset.import", "PDF presets"),
|
||||
(vectorcraft_engine::cmd::printpresets::PRESET_EXTS, "print.presets.import", "print presets"),
|
||||
(vectorcraft_engine::cmd::perspgrid::PRESET_EXTS, "perspective.presets.import", "perspective grid presets"),
|
||||
(fileio::SCREEN_EXPORT_PRESET_EXTS, "document.exportPresets.import", EXPORT_PRESETS),
|
||||
];
|
||||
if vectorcraft_engine::cmd::plugin::EXTS.contains(&ext.as_str()) {
|
||||
Some(OpensAs::Plugin)
|
||||
|
||||
@@ -128,6 +128,7 @@ const FILE_TYPES: &[(&str, &[&str])] = &[
|
||||
("Perspective grid presets", vectorcraft_engine::cmd::perspgrid::PRESET_EXTS),
|
||||
("Print presets", vectorcraft_engine::cmd::printpresets::PRESET_EXTS),
|
||||
("Action sets", &[crate::panels::actions::ACTIONS_EXT]),
|
||||
("Export presets", fileio::SCREEN_EXPORT_PRESET_EXTS),
|
||||
("CSS", &["css"]),
|
||||
("PostScript", &["ps"]),
|
||||
];
|
||||
|
||||
@@ -268,6 +268,9 @@ pub struct VectorcraftApp {
|
||||
pub(crate) ime_discard: bool,
|
||||
/// Preferences were confirmed (see [`Self::take_prefs_changed`]).
|
||||
pub(crate) prefs_changed: bool,
|
||||
/// The export presets the Asset Export panel lists (`document.exportPresets.list`'s rows), built
|
||||
/// again when the saved presets change ([`dialogs::screen_preset_list`]).
|
||||
pub(crate) asset_export_presets: Value,
|
||||
/// Whether the last frame's input reported a touch screen (Preferences › Devices › Touch Workspace).
|
||||
pub touch_screen: bool,
|
||||
/// A numeric field is being scrubbed: the document's edits meanwhile are one undo step
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! names. Art dragged in off the canvas becomes assets, one per object (with Alt held, one asset
|
||||
//! of them all), and so does the selection with the + button. Clicking selects assets (Shift or
|
||||
//! Cmd adds), double-clicking one renames it and the bin removes the selected ones. Below are the
|
||||
//! export settings, the format rows Export for Screens shows too (one set per document), and
|
||||
//! export settings: the format rows shared with Export for Screens (one set per document), and
|
||||
//! Export, which writes the selected assets (else all of them) into a picked folder (on the
|
||||
//! desktop with Background Export on, on the background thread); the web downloads them. The
|
||||
//! screen button opens Export for Screens on its Assets tab.
|
||||
@@ -17,9 +17,13 @@ use egui::{Color32, Rect, Sense, Stroke, StrokeKind, Ui, vec2};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{ExportAsset, Node, NodeId};
|
||||
use vectorcraft_engine::DocState;
|
||||
use vectorcraft_engine::cmd::fileio::{SCREEN_SETTINGS_KEYS, screen_export_preset_list};
|
||||
|
||||
use super::{pstate, set_pstate};
|
||||
use crate::dialogs::{EXPORT_FOR_SCREENS, open_export_for_screens_assets, screen_formats, screen_saved_options, screen_saved_rows};
|
||||
use crate::dialogs::{
|
||||
EXPORT_FOR_SCREENS, ScreenPresetPick, open_export_for_screens_assets, screen_export_preset, screen_formats, screen_import_presets,
|
||||
screen_preset_list, screen_saved_options, screen_saved_rows,
|
||||
};
|
||||
use crate::state::Dialog;
|
||||
use crate::theme::Tokens;
|
||||
use crate::widgets::{self, menu_item};
|
||||
@@ -30,6 +34,10 @@ pub const ID: &str = "assetExport";
|
||||
const TILE: egui::Vec2 = vec2(70.0, 84.0);
|
||||
const THUMB: f32 = 58.0;
|
||||
const SELECTED: &str = "asset-export-selected";
|
||||
/// The export preset applied last: (the document's uid, its name; "" for none).
|
||||
const PRESET: &str = "asset-export-preset";
|
||||
/// The name being typed to save a preset (none when not saving).
|
||||
const SAVING: &str = "asset-export-save-preset";
|
||||
|
||||
/// The assets selected in the panel (the panel's own state) that are still in the document.
|
||||
fn selected(ctx: &egui::Context, st: &DocState) -> Vec<u64> {
|
||||
@@ -256,8 +264,7 @@ enum Settings {
|
||||
Format(String),
|
||||
}
|
||||
|
||||
/// The export settings: Export for Screens' format rows on the document's shared settings.
|
||||
/// Returns the change asked for and how many files one asset makes.
|
||||
/// The document's shared export format rows. Returns the change asked for and how many files one asset makes.
|
||||
fn settings(ui: &mut Ui, st: &DocState) -> (Option<Settings>, usize) {
|
||||
let (rows, preset) = screen_saved_rows(&st.doc.export_settings);
|
||||
let preset = preset.unwrap_or_default().to_string();
|
||||
@@ -281,6 +288,7 @@ fn settings(ui: &mut Ui, st: &DocState) -> (Option<Settings>, usize) {
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let ctx = ui.ctx().clone();
|
||||
screen_preset_list(&mut app.asset_export_presets, &app.session.prefs.screen_export_presets);
|
||||
let Some(st) = app.session.active() else {
|
||||
widgets::dim_label(ui, tl!("No document"));
|
||||
return;
|
||||
@@ -308,6 +316,26 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
ui.add_space(6.0);
|
||||
widgets::section_header(ui, tl!("Export Settings"));
|
||||
let (change, files_each) = settings(ui, st);
|
||||
let doc = st.uid;
|
||||
let mut save_name = pstate::<Option<String>>(&ctx, SAVING);
|
||||
let mut save_pick = None;
|
||||
let mut cancel_save = false;
|
||||
if let Some(name) = &mut save_name {
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("Name:"));
|
||||
ui.add(egui::TextEdit::singleline(name).desired_width((ui.available_width() - 110.0).max(60.0)));
|
||||
if ui.add_enabled(!name.trim().is_empty(), egui::Button::new(tl!("Save"))).clicked() {
|
||||
save_pick = Some(ScreenPresetPick::Save(name.trim().to_string()));
|
||||
}
|
||||
if ui.small_button(tl!("Cancel")).clicked() {
|
||||
cancel_save = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
if cancel_save {
|
||||
save_name = None;
|
||||
}
|
||||
set_pstate(&ctx, SAVING, save_name);
|
||||
let all: Vec<u64> = st.doc.assets.iter().map(|a| a.id).collect();
|
||||
let has_art = !st.selection.is_empty();
|
||||
let alt = ui.input(|i| i.modifiers.alt);
|
||||
@@ -350,7 +378,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
collect(app, &ctx, Some(ids), single);
|
||||
}
|
||||
match change {
|
||||
Some(Settings::Set(p)) => _ = run(app, "assets.settings.set", p),
|
||||
Some(Settings::Set(p)) => {
|
||||
run(app, "assets.settings.set", p);
|
||||
}
|
||||
Some(Settings::Format(f)) => {
|
||||
open_export_for_screens_assets(app, Some(&sel_or_all(&sel, &all)));
|
||||
if let Some(d) = app.ui.dialog.as_mut() {
|
||||
@@ -359,6 +389,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
if let Some(pick) = save_pick {
|
||||
preset(app, &ctx, doc, pick);
|
||||
}
|
||||
if add {
|
||||
collect(app, &ctx, None, alt);
|
||||
}
|
||||
@@ -374,6 +407,105 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Act on a preset menu pick for document `doc` (uid): a preset applied to the shared settings,
|
||||
/// the shared settings saved as one, a saved one deleted, presets imported from a file or the
|
||||
/// current one written to a file.
|
||||
pub(crate) fn preset(app: &mut VectorcraftApp, ctx: &egui::Context, doc: u64, pick: ScreenPresetPick) {
|
||||
let done = match pick {
|
||||
ScreenPresetPick::Apply(p) => {
|
||||
let name = p["name"].as_str().unwrap_or_default().to_string();
|
||||
app.run("assets.settings.set", json!({ "exportPreset": name })).map(|_| set_pstate(ctx, PRESET, (doc, name)))
|
||||
}
|
||||
ScreenPresetPick::Save(name) => {
|
||||
let settings = preset_settings(app, ctx, doc);
|
||||
// The name as saved (trimmed and capped).
|
||||
app.run("document.exportPresets.save", json!({ "name": name, "settings": settings })).map(|r| {
|
||||
set_pstate(ctx, SAVING, None::<String>);
|
||||
set_pstate(ctx, PRESET, (doc, r["name"].as_str().unwrap_or_default().to_string()));
|
||||
})
|
||||
}
|
||||
ScreenPresetPick::Delete(name) => {
|
||||
app.run("document.exportPresets.delete", json!({ "name": name })).map(|_| set_pstate(ctx, PRESET, (doc, String::new())))
|
||||
}
|
||||
// A file dialog shown off the UI thread runs these again when it answers.
|
||||
ScreenPresetPick::Import => {
|
||||
crate::picks::button(app, |app| {
|
||||
if let Err(e) = screen_import_presets(app)
|
||||
&& e != "cancelled"
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
ScreenPresetPick::Export(name) => {
|
||||
crate::picks::button(app, move |app| {
|
||||
if let Err(e) = screen_export_preset(app, &name)
|
||||
&& e != "cancelled"
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
if let Err(e) = done
|
||||
&& e != "cancelled"
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// What Save Preset saves for document `doc` (uid): the preset the panel applied to this document,
|
||||
/// with the document's current shared settings over it, replacing its format choice; else the
|
||||
/// shared settings alone.
|
||||
fn preset_settings(app: &VectorcraftApp, ctx: &egui::Context, doc: u64) -> Value {
|
||||
let shared = shared_settings(app).as_object().cloned().unwrap_or_default();
|
||||
let (shown_doc, current): (u64, String) = pstate(ctx, PRESET);
|
||||
if shown_doc != doc || current.is_empty() {
|
||||
return Value::Object(shared);
|
||||
}
|
||||
let list = screen_export_preset_list(&app.session.prefs.screen_export_presets);
|
||||
let Some(applied) = list.as_array().and_then(|rows| rows.iter().find(|p| p["name"] == current)).and_then(|p| p["settings"].as_object()) else {
|
||||
return Value::Object(shared);
|
||||
};
|
||||
let Some(st) = app.session.active() else { return Value::Object(shared) };
|
||||
if st.uid != doc {
|
||||
return Value::Object(shared);
|
||||
}
|
||||
let mut settings = applied.clone();
|
||||
if shared.contains_key("preset") {
|
||||
settings.remove("formats");
|
||||
}
|
||||
if shared.contains_key("formats") {
|
||||
settings.remove("preset");
|
||||
}
|
||||
settings.extend(shared);
|
||||
Value::Object(settings)
|
||||
}
|
||||
|
||||
/// The settings the panel shows, as a preset keeps them: the shared settings and effective folder
|
||||
/// choice, with the row shown when the document has no rows or mobile/density preset of its own.
|
||||
fn shared_settings(app: &VectorcraftApp) -> Value {
|
||||
let Some(st) = app.session.active() else { return json!({}) };
|
||||
let saved = &st.doc.export_settings;
|
||||
let mut settings: serde_json::Map<String, Value> =
|
||||
SCREEN_SETTINGS_KEYS.iter().filter_map(|k| Some((k.to_string(), saved.get(*k).filter(|v| !v.is_null())?.clone()))).collect();
|
||||
let (rows, preset) = screen_saved_rows(saved);
|
||||
if let Some(folders) = saved.get("folders").filter(|v| !v.is_null()) {
|
||||
settings.insert("folders".into(), folders.clone());
|
||||
} else if preset.is_some() && settings.get("subfolders").and_then(Value::as_bool) == Some(false) {
|
||||
settings.insert("folders".into(), json!("none"));
|
||||
}
|
||||
if preset.is_none() {
|
||||
settings.remove("preset");
|
||||
if !settings.get("formats").and_then(Value::as_array).is_some_and(|a| a.iter().any(Value::is_object)) {
|
||||
settings.insert("formats".into(), json!(rows));
|
||||
}
|
||||
}
|
||||
Value::Object(settings)
|
||||
}
|
||||
|
||||
/// The selected assets, else all of them.
|
||||
fn sel_or_all(sel: &[u64], all: &[u64]) -> Vec<u64> {
|
||||
if sel.is_empty() { all.to_vec() } else { sel.to_vec() }
|
||||
@@ -382,7 +514,9 @@ fn sel_or_all(sel: &[u64], all: &[u64]) -> Vec<u64> {
|
||||
/// The panel (≡) menu.
|
||||
pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let ctx = ui.ctx().clone();
|
||||
screen_preset_list(&mut app.asset_export_presets, &app.session.prefs.screen_export_presets);
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let doc = st.uid;
|
||||
let (has_art, sel) = (!st.selection.is_empty(), selected(&ctx, st));
|
||||
let all: Vec<u64> = st.doc.assets.iter().map(|a| a.id).collect();
|
||||
if menu_item(ui, tl!("Add Selected Artwork as One Asset"), has_art, false) {
|
||||
@@ -403,4 +537,37 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if menu_item(ui, tl!("Export for Screens…"), true, false) {
|
||||
open_export_for_screens_assets(app, Some(&sel_or_all(&sel, &all)));
|
||||
}
|
||||
ui.separator();
|
||||
let (preset_doc, current): (u64, String) = pstate(&ctx, PRESET);
|
||||
let current = if preset_doc == doc { current } else { String::new() };
|
||||
let list = app.asset_export_presets.as_array().cloned().unwrap_or_default();
|
||||
let saved = list.iter().any(|p| p["name"] == current && p["builtIn"] == false);
|
||||
let known = list.iter().any(|p| p["name"] == current);
|
||||
let mut pick = None;
|
||||
for p in &list {
|
||||
if let Some(name) = p["name"].as_str()
|
||||
&& widgets::menu_item_name(ui, name, true, current == name)
|
||||
{
|
||||
pick = Some(ScreenPresetPick::Apply(p.clone()));
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
if menu_item(ui, tl!("Save As Preset…"), true, false) {
|
||||
set_pstate(&ctx, SAVING, Some(String::new()));
|
||||
}
|
||||
if menu_item(ui, tl!("Save Changes"), saved, false) {
|
||||
pick = Some(ScreenPresetPick::Save(current.clone()));
|
||||
}
|
||||
if menu_item(ui, tl!("Delete Preset"), saved, false) {
|
||||
pick = Some(ScreenPresetPick::Delete(current.clone()));
|
||||
}
|
||||
if menu_item(ui, tl!("Import Preset…"), true, false) {
|
||||
pick = Some(ScreenPresetPick::Import);
|
||||
}
|
||||
if menu_item(ui, tl!("Export Preset…"), known, false) {
|
||||
pick = Some(ScreenPresetPick::Export(current));
|
||||
}
|
||||
if let Some(pick) = pick {
|
||||
preset(app, &ctx, doc, pick);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,23 @@ fn frame(ctx: &egui::Context, app: &mut VectorcraftApp, events: Vec<egui::Event>
|
||||
v
|
||||
}
|
||||
|
||||
/// One frame of the panel menu, including its item positions.
|
||||
fn menu_frame(ctx: &egui::Context, app: &mut VectorcraftApp, events: Vec<egui::Event>) -> Vec<(String, Rect)> {
|
||||
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(236.0, 700.0))), events, ..Default::default() };
|
||||
let mut out = ctx.run_ui(raw, |ui| menu(app, ui));
|
||||
out.textures_delta.clear();
|
||||
fn texts(s: &egui::Shape, out: &mut Vec<(String, Rect)>) {
|
||||
match s {
|
||||
egui::Shape::Text(t) => out.push((t.galley.text().to_string(), t.visual_bounding_rect())),
|
||||
egui::Shape::Vec(v) => v.iter().for_each(|s| texts(s, out)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut labels = vec![];
|
||||
out.shapes.iter().for_each(|s| texts(&s.shape, &mut labels));
|
||||
labels
|
||||
}
|
||||
|
||||
fn click(at: Pos2) -> Vec<egui::Event> {
|
||||
let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
vec![egui::Event::PointerMoved(at), button(true), button(false)]
|
||||
@@ -218,6 +235,282 @@ fn the_rows_are_the_shared_export_settings() {
|
||||
assert_eq!(log[0].0, "Untitled-1.zip");
|
||||
}
|
||||
|
||||
/// The panel menu applies and saves presets, and lists presets imported while it is open.
|
||||
#[test]
|
||||
fn the_preset_menu_applies_saves_and_lists_presets() {
|
||||
let (mut app, _, [a, _]) = app(true);
|
||||
app.run("assets.add", json!({"ids": [a]})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
frame(&ctx, &mut app, vec![], false);
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
assert!(!has(&texts, "Export Preset:"));
|
||||
menu_frame(&ctx, &mut app, vec![]);
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Logo Package")));
|
||||
let settings = app.run("document.exportSettings", json!({})).unwrap()["settings"].clone();
|
||||
assert_eq!(settings["formats"].as_array().map(Vec::len), Some(5), "{settings}");
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
assert!(has(&texts, "✓ Logo Package"), "the menu checks the applied preset");
|
||||
// Save: the shared settings under a name of their own.
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Save As Preset…")));
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
assert!(has(&texts, "Name:"));
|
||||
super::set_pstate(&ctx, "asset-export-save-preset", Some("Mine".to_string()));
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
frame(&ctx, &mut app, click(at(&texts, "Save")), false);
|
||||
let mine = app.session.prefs.screen_export_presets.iter().find(|p| p.name == "Mine").expect("saved").clone();
|
||||
assert_eq!(mine.settings["formats"], settings["formats"]);
|
||||
// Create Sub-folders, off after a preset that sorts by format, is saved with it.
|
||||
assert_eq!((mine.settings.get("folders"), mine.settings.get("subfolders")), (Some(&json!("format")), Some(&json!(false))), "{:?}", mine.settings);
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
assert!(has(&texts, "✓ Mine"), "the menu checks the saved preset");
|
||||
// A preset file opened while the panel shows (the web's Import) is in its menu.
|
||||
let data = app.run("document.exportPresets.export", json!({"names": ["Mine"]})).unwrap()["data"].as_str().unwrap().to_string();
|
||||
crate::io::open_bytes(&mut app, "pack.vcexportpresets", data.as_bytes(), None).unwrap();
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
assert!(has(&texts, " Mine 2"), "{:?}", texts.iter().map(|t| &t.0).collect::<Vec<_>>());
|
||||
// A mobile preset with no sub-folders is saved again with no sub-folders.
|
||||
app.run("document.exportPresets.save", json!({"name": "Flat", "settings": {"preset": "mobile", "folders": "none"}})).unwrap();
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Flat")));
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Save As Preset…")));
|
||||
super::set_pstate(&ctx, "asset-export-save-preset", Some("Flat Again".to_string()));
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
frame(&ctx, &mut app, click(at(&texts, "Save")), false);
|
||||
let again = app.session.prefs.screen_export_presets.iter().find(|p| p.name == "Flat Again").expect("saved").clone();
|
||||
assert_eq!(
|
||||
(again.settings.get("preset"), again.settings.get("folders"), again.settings.get("subfolders")),
|
||||
(Some(&json!("mobile")), Some(&json!("none")), Some(&json!(false))),
|
||||
"{:?}",
|
||||
again.settings
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_panel_saves_mobile_preset_without_subfolders() {
|
||||
let (mut app, _, [a, _]) = app(true);
|
||||
app.run("assets.add", json!({"ids": [a]})).unwrap();
|
||||
let asset = app.run("assets.list", json!({})).unwrap()["assets"][0]["id"].as_u64().unwrap();
|
||||
app.run("assets.settings.set", json!({"preset": "mobile", "subfolders": false})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
menu_frame(&ctx, &mut app, vec![]);
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Save As Preset…")));
|
||||
super::set_pstate(&ctx, "asset-export-save-preset", Some("Flat Mobile".to_string()));
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
frame(&ctx, &mut app, click(at(&texts, "Save")), false);
|
||||
|
||||
let saved = app.session.prefs.screen_export_presets.iter().find(|p| p.name == "Flat Mobile").expect("saved");
|
||||
assert_eq!(saved.settings.get("folders"), Some(&json!("none")));
|
||||
assert_eq!(saved.settings.get("subfolders"), Some(&json!(false)));
|
||||
|
||||
let out = app.run("document.exportForScreens", json!({"exportPreset": "Flat Mobile", "assets": [asset]})).unwrap();
|
||||
let files = out["files"].as_array().unwrap();
|
||||
assert_eq!(files.len(), 3, "{out}");
|
||||
assert!(files.iter().all(|f| !f["name"].as_str().unwrap().contains('/')), "{out}");
|
||||
|
||||
app.run("assets.settings.set", json!({"exportPreset": "Flat Mobile"})).unwrap();
|
||||
let settings = app.run("document.exportSettings", json!({})).unwrap();
|
||||
assert_eq!(settings["settings"]["folders"], "none");
|
||||
assert_eq!(settings["settings"]["subfolders"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asset_export_preserves_applied_preset_naming_and_folders_when_saving() {
|
||||
let (mut app, _, [a, _]) = app(true);
|
||||
app.run("assets.add", json!({"ids": [a]})).unwrap();
|
||||
let asset = app.run("assets.list", json!({})).unwrap()["assets"][0]["id"].as_u64().unwrap();
|
||||
let original = json!({
|
||||
"formats": [{"format": "svg"}],
|
||||
"name": "<name>-###",
|
||||
"folders": "format",
|
||||
"counterStart": 7,
|
||||
"counterPer": "file",
|
||||
"includeBleed": true,
|
||||
});
|
||||
app.run("document.exportPresets.save", json!({"name": "Named Assets", "settings": original})).unwrap();
|
||||
let before = app.run("document.exportForScreens", json!({"exportPreset": "Named Assets", "assets": [asset]})).unwrap();
|
||||
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
menu_frame(&ctx, &mut app, vec![]);
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Named Assets")));
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
assert!(has(&texts, "✓ Named Assets"));
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Save Changes")));
|
||||
|
||||
let saved = app.session.prefs.screen_export_presets.iter().find(|p| p.name == "Named Assets").expect("saved");
|
||||
for key in ["name", "folders", "counterStart", "counterPer", "includeBleed"] {
|
||||
assert_eq!(saved.settings.get(key), original.get(key), "{key}: {:?}", saved.settings);
|
||||
}
|
||||
let file = app.run("document.exportPresets.export", json!({"names": ["Named Assets"]})).unwrap();
|
||||
let data: Value = serde_json::from_str(file["data"].as_str().unwrap()).unwrap();
|
||||
for key in ["name", "folders", "counterStart", "counterPer", "includeBleed"] {
|
||||
assert_eq!(data["presets"][0]["settings"].get(key), original.get(key), "{key}: {data}");
|
||||
}
|
||||
let after = app.run("document.exportForScreens", json!({"exportPreset": "Named Assets", "assets": [asset]})).unwrap();
|
||||
assert_eq!(after["files"][0]["name"], before["files"][0]["name"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asset_export_replaces_applied_format_choice_when_saving() {
|
||||
let (mut app, _, _) = app(true);
|
||||
app.run(
|
||||
"document.exportPresets.save",
|
||||
json!({
|
||||
"name": "Named Rows",
|
||||
"settings": {"formats": [{"format": "svg"}], "name": "<name>-###"}
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
menu_frame(&ctx, &mut app, vec![]);
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Named Rows")));
|
||||
app.run("assets.settings.set", json!({"preset": "mobile"})).unwrap();
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Save Changes")));
|
||||
|
||||
let saved = app.session.prefs.screen_export_presets.iter().find(|p| p.name == "Named Rows").expect("saved");
|
||||
assert_eq!(saved.settings.get("preset"), Some(&json!("mobile")));
|
||||
assert!(saved.settings.get("formats").is_none(), "{:?}", saved.settings);
|
||||
assert_eq!(saved.settings.get("name"), Some(&json!("<name>-###")));
|
||||
|
||||
app.run("assets.settings.set", json!({"exportPreset": "Named Rows"})).unwrap();
|
||||
app.run("file.exportForScreens", json!({})).unwrap();
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["formats"], json!(vectorcraft_engine::cmd::fileio::screen_preset_rows("mobile").unwrap()));
|
||||
crate::dialogs::cancel(&mut app);
|
||||
|
||||
app.run("document.exportPresets.save", json!({"name": "Mobile First", "settings": {"preset": "mobile", "name": "<name>-###"}})).unwrap();
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Mobile First")));
|
||||
app.run("assets.settings.set", json!({"formats": [{"format": "svg"}]})).unwrap();
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Save Changes")));
|
||||
let saved = app.session.prefs.screen_export_presets.iter().find(|p| p.name == "Mobile First").expect("saved");
|
||||
assert_eq!(saved.settings.get("formats"), Some(&json!([{"format": "svg"}])));
|
||||
assert!(saved.settings.get("preset").is_none(), "{:?}", saved.settings);
|
||||
assert_eq!(saved.settings.get("name"), Some(&json!("<name>-###")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asset_export_saves_changed_rows_with_applied_preset_fields() {
|
||||
let (mut app, _, _) = app(true);
|
||||
let original = json!({
|
||||
"formats": [{"format": "svg"}],
|
||||
"name": "<name>-###",
|
||||
"counterStart": 7,
|
||||
"counterPer": "file",
|
||||
"folder": "exports",
|
||||
"includeBleed": true,
|
||||
});
|
||||
app.run("document.exportPresets.save", json!({"name": "Named Rows", "settings": original})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
menu_frame(&ctx, &mut app, vec![]);
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Named Rows")));
|
||||
app.run("assets.settings.set", json!({"formats": [{"format": "png", "scale": "2x"}]})).unwrap();
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Save Changes")));
|
||||
|
||||
let saved = app.session.prefs.screen_export_presets.iter().find(|p| p.name == "Named Rows").expect("saved");
|
||||
assert_eq!(saved.settings["formats"], json!([{"format": "png", "scale": "2x"}]));
|
||||
for key in ["name", "counterStart", "counterPer", "folder", "includeBleed"] {
|
||||
assert_eq!(saved.settings.get(key), original.get(key), "{key}: {:?}", saved.settings);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asset_export_saves_changed_subfolders_from_applied_preset() {
|
||||
let (mut app, _, _) = app(true);
|
||||
app.run("document.exportPresets.save", json!({"name": "Grouped Mobile", "settings": {"preset": "mobile", "folders": "format"}})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
menu_frame(&ctx, &mut app, vec![]);
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Grouped Mobile")));
|
||||
app.run("assets.settings.set", json!({"subfolders": true})).unwrap();
|
||||
let current = app.run("document.exportSettings", json!({})).unwrap();
|
||||
assert_eq!(current["settings"]["folders"], "rows");
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Save Changes")));
|
||||
|
||||
let saved = app.session.prefs.screen_export_presets.iter().find(|p| p.name == "Grouped Mobile").expect("saved");
|
||||
assert_eq!(saved.settings.get("folders"), Some(&json!("rows")));
|
||||
assert_eq!(saved.settings.get("subfolders"), Some(&json!(true)));
|
||||
app.run("assets.settings.set", json!({"exportPreset": "Grouped Mobile"})).unwrap();
|
||||
let reapplied = app.run("document.exportSettings", json!({})).unwrap();
|
||||
assert_eq!(reapplied["settings"]["folders"], "rows");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asset_export_preserves_applied_mobile_preset_folder_choice() {
|
||||
let (mut app, _, _) = app(true);
|
||||
app.run("document.exportPresets.save", json!({"name": "Mobile by Format", "settings": {"preset": "mobile", "folders": "format"}})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
menu_frame(&ctx, &mut app, vec![]);
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Mobile by Format")));
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
assert!(has(&texts, "✓ Mobile by Format"));
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Save Changes")));
|
||||
let saved = app.session.prefs.screen_export_presets.iter().find(|p| p.name == "Mobile by Format").expect("saved");
|
||||
assert_eq!(saved.settings.get("folders"), Some(&json!("format")));
|
||||
assert_eq!(saved.settings.get("subfolders"), Some(&json!(false)));
|
||||
}
|
||||
|
||||
/// The panel keeps its list of export presets from frame to frame, and builds it again when the
|
||||
/// saved presets change.
|
||||
#[test]
|
||||
fn the_panel_builds_the_preset_list_again_only_when_the_presets_change() {
|
||||
let (mut app, _, _) = app(true);
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
frame(&ctx, &mut app, vec![], false);
|
||||
menu_frame(&ctx, &mut app, vec![]);
|
||||
app.asset_export_presets[0]["mark"] = json!(true);
|
||||
frame(&ctx, &mut app, vec![], false);
|
||||
menu_frame(&ctx, &mut app, vec![]);
|
||||
assert_eq!(app.asset_export_presets[0]["mark"], true, "the list was not built again");
|
||||
app.run("document.exportPresets.save", json!({"name": "Mine", "settings": {"formats": [{"format": "svg"}]}})).unwrap();
|
||||
menu_frame(&ctx, &mut app, vec![]);
|
||||
let list = &app.asset_export_presets;
|
||||
assert!(list[0].get("mark").is_none() && list.as_array().unwrap().iter().any(|p| p["name"] == "Mine"), "{list}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_preset_menu_saves_changes_and_opens_import_and_export() {
|
||||
let (mut app, log, _) = app(true);
|
||||
app.run("document.exportPresets.save", json!({"name": "Pack", "settings": {"formats": [{"format": "svg"}]}})).unwrap();
|
||||
let opened = Rc::new(RefCell::new(0));
|
||||
let opened_for_picker = opened.clone();
|
||||
app.services.open_async = Some(Box::new(move || *opened_for_picker.borrow_mut() += 1));
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
menu_frame(&ctx, &mut app, vec![]);
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Pack")));
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
assert!(has(&texts, "✓ Pack"));
|
||||
app.run("assets.settings.set", json!({"formats": [{"format": "png", "scale": "2x"}]})).unwrap();
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Save Changes")));
|
||||
let pack = app.session.prefs.screen_export_presets.iter().find(|p| p.name == "Pack").unwrap();
|
||||
assert_eq!(pack.settings["formats"][0]["format"], "png");
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Export Preset…")));
|
||||
assert_eq!(log.borrow()[0].0, "Pack.vcexportpresets");
|
||||
let texts = menu_frame(&ctx, &mut app, vec![]);
|
||||
menu_frame(&ctx, &mut app, click(at(&texts, " Import Preset…")));
|
||||
assert_eq!(*opened.borrow(), 1);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn export_writes_into_the_picked_folder() {
|
||||
|
||||
@@ -151,7 +151,8 @@ fn libraries_presets_and_plugins_open_wherever_they_are_dropped() {
|
||||
let rect = laid_out(&mut app, &ctx);
|
||||
let on_canvas = rect.center() + vec2(60.0, -40.0);
|
||||
let libraries = ["a.vcswatches", "a.gpl", "a.ase", "a.acb", "a.vcstyles", "a.vclibrary"];
|
||||
let presets_and_plugins = ["a.vcflattener", "a.vcpdfpresets", "a.vcprintpresets", "a.vcperspective", "a.wasm", "a.jsx", "a.jsxinc", "a.jsxbin"];
|
||||
let presets_and_plugins =
|
||||
["a.vcflattener", "a.vcpdfpresets", "a.vcprintpresets", "a.vcperspective", "a.vcexportpresets", "a.wasm", "a.jsx", "a.jsxinc", "a.jsxbin"];
|
||||
for name in libraries.into_iter().chain(presets_and_plugins) {
|
||||
assert_eq!(app.drop_target(name, None, false), DropTarget::Open, "{name}");
|
||||
assert_eq!(app.drop_target(name, Some(on_canvas), true), DropTarget::Open, "{name} on the canvas");
|
||||
|
||||
@@ -93,6 +93,13 @@ fn save_panels_show_the_file_type_they_write() {
|
||||
("print.presets.save", json!({"name": "Mine"}), "print.presets.export", "Print presets", printpresets::PRESET_EXTS),
|
||||
("flattener.presets.save", json!({"name": "Mine"}), "flattener.presets.export", "Flattener presets", flatten::PRESET_EXTS),
|
||||
("perspective.presets.save", json!({"name": "Mine"}), "perspective.presets.export", "Perspective grid presets", perspgrid::PRESET_EXTS),
|
||||
(
|
||||
"document.exportPresets.save",
|
||||
json!({"name": "Mine", "settings": {"formats": [{"format": "svg"}]}}),
|
||||
"document.exportPresets.export",
|
||||
"Export presets",
|
||||
vectorcraft_engine::cmd::fileio::SCREEN_EXPORT_PRESET_EXTS,
|
||||
),
|
||||
];
|
||||
for (save, params, export, label, exts) in presets {
|
||||
app.run(save, params).unwrap();
|
||||
|
||||
@@ -113,7 +113,7 @@ the two can still win that race.
|
||||
| `engine.execute` `ui.savePdfDialog` | `{path?, preset?, range?, …document.exportPdf options}` | opens the Save PDF dialog (`file.export.pdf` with no params opens it too). Its fields are the `document.exportPdf` options: set `compatibility` with `ui.dialog.set {field: "compatibility", value: "1.5"}`, a whole section with `{field: "compression", value: {…}}`, the artboards with `__allArtboards: false` and `range`, the visible section with `__section` (General, Compression, Marks and Bleeds, Output, Advanced, Security, Summary). `ui.dialog.confirm` writes `path` (else asks for one), returns `{path, bytes, warnings}` and opens the file when `viewAfterSaving` is set |
|
||||
| `file.save` / `file.saveAs` / `file.saveCopy` / `file.saveAsTemplate` (via `engine.execute`) | `{path?, format?, options?, svg?}` | Save writes the document's own file in its own format (a document opened from or saved as SVG/PDF saves as that again; the result's `warnings` say what the format loses). With `path` the file is written at once (Save As or a Copy to an SVG path without SVG options first opens SVG Options, below). Without one (never saved, a converted older file, Save As, a Copy, a Template) the app shows a save panel with one file type per save format (`vectorcraft`, `template`, `pdf`, `svg`, `svgz`, `ai`; for a Template, `template` and `ait`), then the format's options: SVG opens the `svgOptions` dialog (below; returns `{"pending": "svgOptions", "path"}`), PDF the `savePdf` dialog (its `__save` field names the save command); `ui.dialog.confirm` finishes the save. A name typed in the panel without the file type's extension gets it, and a name the panel returns with the extension of a format the panel does not list has it replaced (in a native save, `card.png` and `card.ait` are saved as `card.vectorcraft`, and `notes.v2`, whose extension names no format, is saved as `notes.v2.vectorcraft`); when a file of that name exists, which the panel did not ask about, the save (and every export or other write through a save panel) answers `cancelled` and opens a `replaceFile` dialog (fields `message`, `detail`, `path`): `ui.dialog.confirm` writes that file, `ui.dialog.cancel` writes nothing. On the web a `saveOptions` dialog (fields `path`, `format`) first names the file and picks the format. `file.formatOptions {format?}` reads the options a save would use. `file.revert` opens a `confirm` dialog (`{"pending": "confirm"}`); `ui.dialog.confirm` reloads the saved file in the same tab |
|
||||
| SVG Options (`engine.execute` of `file.export.svg`, `file.saveAs`, `file.saveCopy`) | `{}` / `{path?, svg?: {…}}` | `file.export.svg` without params, and Save As or Save a Copy to an `.svg`/`.svgz` path without SVG options, open the `svgOptions` dialog (`ui.dialog.set` its fields: `styling`, `outlineText`, `images`, `objectIds`, `decimals`, `minify`, `responsive`, `fewerTspans`, `metadata`, `preserveEditing`, `useArtboards`, `allArtboards`, `range`, `showCode`, `profile` (`svg11`/`tiny12`), `encoding` (`utf8`/`utf16`/`latin1`), `embedFonts`; `ui.dialog.confirm` exports or saves and remembers the choices). With `svg: {…}` (the `document.formats` SVG options) they run directly. `app.save {path?, svg?}` takes the same options; a document saved as SVG saves as SVG again with the same options |
|
||||
| `engine.execute` `file.place` | `{paths?}` / `{path \| name+dataBase64, …file.place params}` | with no file, picks files and opens the `place` dialog; with `paths`, opens it for them (fields `files`, `link`, `template`, `replace`; `__replace` says whether Replace applies, `__info` describes each file). `ui.dialog.confirm` places one file centered in the view (or in place of the selected object) and loads the place cursor with several (`file.place.queue`). In the desktop app the dialog reads its files off the UI thread: `__info` is empty until they are read, `ui.dialog.confirm` returns `{background: true, status}`, `ui.inspect` lists the files still being read in `placing`, and the file is placed (or the cursor loaded) once it is read. With a file, it runs `file.place` centered in the view, at once. Files dropped on the window with no document open open. On the web, files dropped on the canvas are placed at the drop point (linked; Shift embeds) and files dropped off it (the tab bar) open, as in Illustrator; desktop drops carry no pointer position, so documents (native, SVG, PDF, `.ai`, EPS, DXF, EMF/WMF) open as tabs of their own and pictures and text are placed in the middle of the view. Swatch libraries and color books (`.vcswatches`, `.gpl`, `.ase`, `.acb`), graphic style libraries (`.vcstyles`), Libraries panel files (`.vclibrary`), presets (`.vcflattener`, `.vcpdfpresets`, `.vcprintpresets`, `.vcperspective`), plug-ins (`.wasm`) and shortcut sets (`.kys`, in Import Shortcut Set) open wherever they are dropped, as File › Open opens them. With the cursor loaded (`ui.inspect` → `tool: "place"`, `toolOptions: {count, current, name, names}`), `ui.pointer` clicks and drags place files (in the desktop app each file is read off the UI thread and placed once it is read, and `ui.inspect` lists it in `placing` until then) and `ui.key` ←/→/↑/↓ and Escape cycle and discard them |
|
||||
| `engine.execute` `file.place` | `{paths?}` / `{path \| name+dataBase64, …file.place params}` | with no file, picks files and opens the `place` dialog; with `paths`, opens it for them (fields `files`, `link`, `template`, `replace`; `__replace` says whether Replace applies, `__info` describes each file). `ui.dialog.confirm` places one file centered in the view (or in place of the selected object) and loads the place cursor with several (`file.place.queue`). In the desktop app the dialog reads its files off the UI thread: `__info` is empty until they are read, `ui.dialog.confirm` returns `{background: true, status}`, `ui.inspect` lists the files still being read in `placing`, and the file is placed (or the cursor loaded) once it is read. With a file, it runs `file.place` centered in the view, at once. Files dropped on the window with no document open open. On the web, files dropped on the canvas are placed at the drop point (linked; Shift embeds) and files dropped off it (the tab bar) open, as in Illustrator; desktop drops carry no pointer position, so documents (native, SVG, PDF, `.ai`, EPS, DXF, EMF/WMF) open as tabs of their own and pictures and text are placed in the middle of the view. Swatch libraries and color books (`.vcswatches`, `.gpl`, `.ase`, `.acb`), graphic style libraries (`.vcstyles`), Libraries panel files (`.vclibrary`), presets (`.vcflattener`, `.vcpdfpresets`, `.vcprintpresets`, `.vcperspective`, `.vcexportpresets`), plug-ins (`.wasm`) and shortcut sets (`.kys`, in Import Shortcut Set) open wherever they are dropped, as File › Open opens them. With the cursor loaded (`ui.inspect` → `tool: "place"`, `toolOptions: {count, current, name, names}`), `ui.pointer` clicks and drags place files (in the desktop app each file is read off the UI thread and placed once it is read, and `ui.inspect` lists it in `placing` until then) and `ui.key` ←/→/↑/↓ and Escape cycle and discard them |
|
||||
| `app.export` raster options | `{path?, format: png \| jpg \| webp, ppi?, width?, height?, background?, antiAlias?, interlaced?, quality?}` | `ppi` sets the pixel size (72 = one pixel per point) and is stored in PNG and JPEG files (WebP files store no resolution); `width` or `height` sets the size of each file in pixels instead (`width` wins), and PNG and JPEG files store the resolution that results; `background` is `transparent`, `white`, `black` or `"#rrggbb"`; `antiAlias` is `none`, `art` or `type` (text snapped to pixels); `interlaced` writes an Adam7 PNG. The PNG Options dialog (`pngOptions`; `jpgOptions` / `webpOptions` for the other raster formats) has the same fields: fill them with `ui.dialog.set` and press OK with `ui.dialog.confirm` |
|
||||
| `app.export` JPEG options | `{path?, format: jpg, quality?: 0–100, colorModel?: rgb \| cmyk \| gray, method?: baseline \| optimized \| progressive, scans?: 3–5, embedIcc?, imageMap?: none \| client \| server}` | `cmyk` writes ink amounts in the working CMYK space (CMYK colours keep their inks; RGB ones are separated with the colour settings); `embedIcc` (default true) embeds sRGB, the working CMYK profile or a grey profile, generated by the CMS; `imageMap` also writes `<stem>.html` (client-side `<map>`) or `<stem>.map` (NCSA) beside the image for the objects with a URL and an Image Map shape (`attributes.set`), returned in `linked`. The `jpgOptions` dialog has these fields too (`quality` is shown 0–10; a CMYK document defaults to `cmyk`) |
|
||||
| `app.export` PNG-8 and GIF | `{path?, format: png8 \| gif, colors?: 2–256, reduction?: perceptual \| selective \| adaptive \| web \| blackWhite \| gray, dither?: none \| diffusion \| pattern \| noise, ditherAmount?: 0–100, transparency?, matte?, interlaced?, …raster options}` | a palette of at most `colors` entries (art with that many colours or fewer keeps them exactly); pixels under half opacity become one transparent entry unless `transparency` is false, partly transparent edges are blended over `matte` (`none` keeps their colour). `png8` writes an indexed `.png` (1–8 bits per pixel); Export for Screens rows take both. The `png8Options` / `gifOptions` dialogs have these fields |
|
||||
@@ -122,7 +122,7 @@ the two can still win that race.
|
||||
| Native file dialogs | any command with no `path` that needs one (`file.open`, `file.saveAs`, `file.place`, exports…) | on macOS (a sheet on the window) and Linux the dialog doesn't hold the window: the call returns `"cancelled"` at once, `ui.inspect` reports `fileDialog`, and when the person at the computer picks a file the command runs again on the document it was asked for. While it is open another dialog is refused: that call returns `"cancelled"` too, and the status bar (`ui.inspect` → `ui.status`) says `"A file dialog is open: choose there first"`; on macOS `app.quit`, `file.close` and `file.closeAll` return that message and the menu bar does nothing (#592, #867). On Windows the dialog is shown in line: the call returns once it closes. Pass `path` to skip the dialog |
|
||||
| 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), `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`) |
|
||||
| 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 |
|
||||
| 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 the first of `warnings` as Export As shows an export's notes |
|
||||
|
||||
|
||||
+14
-2
@@ -4259,7 +4259,18 @@ Batch exports (#1935):
|
||||
- **Formats:** rows may also be `eps`, `ai` and `vectorcraft` (a document of the item alone). A PDF row with
|
||||
`marks` (`{trim, registration, colorBars, pageInfo}`) and a suffix such as `-print` sits next to a plain PDF row.
|
||||
- **Presets:** `document.exportPresets.list` (built-in: Logo Package, Print-Ready PDF), `.save {name, settings}`
|
||||
(kept with the preferences) and `.delete {name}`; `exportPreset: "<name>"` uses one under the call's own params.
|
||||
(kept with the preferences; a saved name is replaced) and `.delete {name}`; `exportPreset: "<name>"` uses one under
|
||||
the call's own params. `.export {names?, path?}` writes presets (every saved one, or the named ones, built-in ones too)
|
||||
as a `.vcexportpresets` JSON file (without `path` it returns `data`; more than 1000 presets or a file larger than 4194304 bytes is an error), and `.import {path? | data? | dataBase64?,
|
||||
replace?}` adds a file's presets (a name in use gets a number unless `replace`; a built-in name always gets one; a
|
||||
name that already ends in a number gets the next free one, so `Web 2` comes in as `Web 3`; a file with settings
|
||||
`.save` refuses, with more than 1000 presets, or larger than 4194304 bytes adds none). A preset name, saved or imported, keeps its first 255
|
||||
characters. Opening a `.vcexportpresets` file imports it.
|
||||
The Asset Export panel menu lists presets and holds Save As Preset…, Save Changes, Delete Preset, Import Preset…
|
||||
and Export Preset…. `assets.settings.set {exportPreset}` applies a preset to the settings the panel shares: its rows or
|
||||
`preset`, its format settings, its prefix (none when it has none), its `folders` (Create Sub-folders is on for `rows`
|
||||
only) and anti-aliasing. With Create Sub-folders off, `assets.export` writes every file into the folder itself,
|
||||
mobile and density rows included.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"document.exportForScreens","params":{"range":"1-3","name":"COMPANY-Logo-<filename>-####-YYYYMMDD","folders":"format","formats":[{"format":"svg"},{"format":"png"},{"format":"pdf"},{"format":"pdf","suffix":"-print","marks":{"trim":true,"registration":true,"colorBars":true,"pageInfo":true}}]}}}
|
||||
@@ -4502,7 +4513,8 @@ moving the art moves the crop, and deleting it removes it from its asset (an ass
|
||||
`assets.export {assets?, folder?, zip?}` writes each asset's art alone, cropped to it and named after it, in every
|
||||
format row of the document's export settings, the ones Export for Screens remembers (`document.exportSettings`); any
|
||||
`formats`, `preset`, `settings`, `prefix` or `subfolders` given win for that export. `assets.settings.set` changes
|
||||
those shared settings (not an undo step). `document.exportForScreens {assets: [id…]}` does the same with its own params
|
||||
those shared settings (not an undo step); its `subfolders: true` sets Export for Screens' Folders to a folder per row,
|
||||
and `subfolders: false` sets a folder per row back to All in One Folder. `document.exportForScreens {assets: [id…]}` does the same with its own params
|
||||
(the dialog's Assets tab). An asset's crop, and the crop of `export` with `selection: true`, is the bounds the
|
||||
renderer measures for what the art draws, its effects included.
|
||||
|
||||
|
||||
@@ -559,6 +559,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Batch… over data sets names each file after the document and the data set (`action.batch`). It writes `/`, `\`, `:`, `<`, `>`, `"`, `|`, `?`, `*` and control characters in the data set's name as `-`, and adds `-2`, `-3`… to a name an earlier data set used.
|
||||
- Batch… offers File + Number, File + Data Set Name, and Data Set Name for a Data Sets source. The dialog defaults to File + Number; `action.batch` without `datasetFileNaming` keeps File + Data Set Name. An output already in a destination folder the batch may read and write gets the next free `-2`, `-3` suffix. [Batch options](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/batch-options.html).
|
||||
- Export for Screens: the dialog's Example line shows the path of the file the export writes first, sub-folder included, named as the export names it, for the first chosen artboard, asset, layer or object (`document.exportForScreens`).
|
||||
- Export presets: the Asset Export panel menu lists the export presets of Export for Screens, with Import Preset… and Export Preset…, which read and write `.vcexportpresets` files (`document.exportPresets.import`, `document.exportPresets.export`). An imported preset whose name is taken gets a number. In the panel, a preset applies to the shared export settings (`assets.settings.set {exportPreset}`), and Save Preset… saves them. With Create Sub-folders off, the panel's Export writes every file into the folder itself, mobile and density rows included.
|
||||
- Export for Screens is laid out as the reference app's dialog: on the left the Artboards and Assets tabs (thumbnails with their checkboxes, number and name, in a grid or a list), Clear Selection and Prefix; on the right Select (All, Range, Include Bleed, Include Artboard Fill, Full Document), Export to, Open Location after Export, Create Sub-folders by Scale or by Format (on by default), Export PDFs As (Single File writes the chosen artboards as the pages of one PDF named after the document, `pdfs: "single"`), and the format rows with the iOS and Android buttons, the format presets (☰) and Format Settings; under them, how many items are selected and how many files the export writes. File name patterns, the Layers and Selection sources and per-item folders stay available through `document.exportForScreens` (scripts and MCP). Tests: `dialogs::tests_screens` (layout, iOS/Android, Clear Selection, sub-folders, Single File) and `fileio::tests_screens::single_file_pdfs_hold_every_chosen_artboard`.
|
||||
- Save for Web: Preview in Browser writes each preview's page and images to a folder of its own inside `VectorCraft Save for Web` in the system's temporary folder. A later preview, from the same app or another one running, does not overwrite an earlier one. Each preview first removes the preview folders there that are an hour old or older and, on Linux, macOS and FreeBSD, belong to the current user; it leaves other files and folders alone. The folders a preview makes there are open to their owner only on those systems. When `VectorCraft Save for Web` is a link or is not a folder, or on those systems belongs to another user or can be written by other users, the preview goes to a new folder of its own in the temporary folder, which a later preview removes once it is an hour old (`file.saveForWeb.browser`).
|
||||
- Export for Screens, Save for Web (slice images, and files named after the document's title) and the Libraries panel name files with the characters Windows does not allow written as `-` and with a `-` after a Windows device name such as `CON`, on every platform (`document.exportForScreens`, `document.exportForWeb`, `library.create`).
|
||||
|
||||
Reference in New Issue
Block a user