mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 18:56:55 -06:00
Export for Screens: batch exporting with name patterns, folders and presets (#1935)
Issue #1935 asks for batch exports of many logo variations in many formats, each format in its own folder. This builds on Export for Screens, which already writes every chosen artboard in every format row. Names: a file name pattern (`name`) names every file alike. Tokens: <filename> (the document), <name> (the artboard, asset, layer or object), <index>, a run of # (a counter, zero-padded to the run's length, from counterStart; counterPer item or file), YYYYMMDD, YYYY-MM-DD, <date>, <time>, <YYYY>, <MM>, <DD>, <format>, <scale> and <suffix>. A trailing .ext is dropped. Words are not changed: only the date patterns and the bracketed tokens are. `date` sets the day (the default is today, UTC). Different files that a pattern names alike get -2, -3... Without a pattern, two rows with one name still write the first row only, as before. Folders: `folders` puts all files in one folder, a folder per row (as Create Sub-folders did), per extension, per item, or per item and then extension. What is exported: besides artboards, assets and the full document, `layers` (each layer's art alone, sublayers included) and `selection` (one file of the selection, or a file per object). Formats: rows can also be EPS, AI and VectorCraft (a document of the item alone, its one artboard). A PDF row with printer's marks (PDF Print in the dialog: trim, registration, colour bars, page information, suffix -print) sits next to a plain PDF row. Presets: document.exportPresets.list, .save and .delete keep batch export presets with the preferences, and `exportPreset` uses one under the call's own params. Built-in: Logo Package (SVG, PNG 1x and 2x, PDF and print PDF, a folder per format) and Print-Ready PDF. Dialog: Layers and Selection tabs with thumbnails; an Export Preset menu with Save and Delete; Folders; File Name with Insert for the tokens, Start at, Prefix, and an example of the first file's name. The OK button says Export Layer or Export Selection on those tabs. Tests: the pattern tokens, the request's example (SVG, PNG, PDF and print PDF for artboards 1-3, a folder per extension, the print PDF a bigger page), folders by item and both, the counter by item and by file, numbered duplicates, layers and the selection, EPS, AI and VectorCraft per artboard, presets (built-in, saved, used, replaced, deleted, kept with the preferences), bad params, and the dialog (every tab, the example name, a pattern export into folders, the Layers and Selection tabs, presets applied and saved). The three tests that refused EPS and VectorCraft rows now check those files. New labels are translated in all 19 catalogs.
This commit is contained in:
committed by
bflatastic
parent
24583effe0
commit
6fc79c33b7
@@ -80,7 +80,7 @@ fn clean_name(name: &str) -> Option<String> {
|
||||
|
||||
/// Ids of art under `id`: the id itself when it is not a layer, else every non-layer under it
|
||||
/// (sublayers unfolded). Layers alone have nothing to collect.
|
||||
fn art_under(doc: &vectorcraft_doc::Document, id: NodeId, out: &mut Vec<NodeId>) {
|
||||
pub(crate) fn art_under(doc: &vectorcraft_doc::Document, id: NodeId, out: &mut Vec<NodeId>) {
|
||||
let Some(n) = doc.node(id) else { return };
|
||||
if n.is_layer() {
|
||||
if let Some(kids) = n.children() {
|
||||
|
||||
@@ -167,13 +167,19 @@ fn source<'a>(s: &'a mut Session, f: &Format, p: &Value, cmd: &str) -> Result<Co
|
||||
/// The selected objects (not on template layers) alone on one layer, with one artboard: their
|
||||
/// visual bounds.
|
||||
fn selection(s: &mut Session, cmd: &str) -> Result<(Document, vectorcraft_geom::Rect)> {
|
||||
let ids = selected_artwork(s)?;
|
||||
isolated(&s.doc()?.doc, &ids, "Selection").ok_or_else(|| bad(cmd, "select something to export"))
|
||||
}
|
||||
|
||||
/// The selected objects (their top-most selected ancestors) that are artwork: those on template
|
||||
/// layers are guides and are left out.
|
||||
pub(crate) fn selected_artwork(s: &mut Session) -> Result<Vec<NodeId>> {
|
||||
let ids = edit::selected_roots(s)?;
|
||||
let st = s.doc()?;
|
||||
let is_template = |id| st.doc.node(id).is_some_and(|l| matches!(l.kind, NodeKind::Layer { template: true, .. }));
|
||||
// On a template layer or sublayer, at any depth.
|
||||
let on_template = |id| st.doc.ancestry(id).is_some_and(|a| a.into_iter().any(is_template));
|
||||
let ids: Vec<NodeId> = ids.into_iter().filter(|id| !on_template(*id)).collect();
|
||||
isolated(&st.doc, &ids, "Selection").ok_or_else(|| bad(cmd, "select something to export"))
|
||||
Ok(ids.into_iter().filter(|id| !on_template(*id)).collect())
|
||||
}
|
||||
|
||||
/// The crop of an export of objects `ids` of `doc` alone: the bounds the renderer measures for what
|
||||
|
||||
@@ -34,6 +34,7 @@ mod legacy_ai;
|
||||
mod load;
|
||||
pub(crate) use load::{MAX_RASTER_ALLOC, too_large_note};
|
||||
mod metafile;
|
||||
mod naming;
|
||||
mod native;
|
||||
pub mod pdf;
|
||||
mod pdfimport;
|
||||
@@ -63,6 +64,7 @@ pub use load::{
|
||||
read_for_open, texture_image,
|
||||
};
|
||||
pub use load::{losses_summary, overwrite_losses};
|
||||
pub use naming::{NameParts as FileNameParts, parse_date as parse_file_date, render as render_file_name};
|
||||
pub(crate) use native::preview_png;
|
||||
pub use native::with_compression_pref;
|
||||
pub use pdfimport::{LoadOptions, page_document};
|
||||
@@ -71,7 +73,10 @@ pub use save::{
|
||||
SAVE_FORMATS, SaveJob, SaveMode, SavePlan, StreamWrite, TEMPLATE_FORMATS, export_folder, save_filters, save_format, save_job, save_plan,
|
||||
save_with, stamp_save_dates, template_filters, templates_dialog_folder, templates_folder,
|
||||
};
|
||||
pub use screens::{PRESETS as SCREEN_PRESETS, ScreenSize, preset_rows as screen_preset_rows};
|
||||
pub use screens::{
|
||||
PRESET_KEYS as SCREEN_EXPORT_PRESET_KEYS, PRESETS as SCREEN_PRESETS, ScreenExportPreset, ScreenSize,
|
||||
builtin_presets as screen_export_builtin_presets, 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, store_settings as store_screen_settings,
|
||||
};
|
||||
@@ -136,10 +141,37 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Export for Screens",
|
||||
["File", "Export"],
|
||||
None,
|
||||
"{folder?, zip?: false (one store-only .zip of every file: no folder → {name, dataBase64, bytes, files: [name…]}; with a folder it is written there → {path, bytes, files}), artboards?: [index…] | range?: \"1-3\" (default all), assets?: [asset id…] (instead of artboards: each asset's art alone, cropped to its bounds and named after the asset; see assets.list), fullDocument?: false (one file per format instead: a PDF of every artboard, other formats the bounds of all visible art, named after the document), includeBleed?: false (artboards grown by the document's bleed), subfolders?: false (each row's files in a sub-folder: its folder, else its size for raster (1x, 2x, 100w…) or its format (SVG, PDF)), preset?: mobile (PNG 1x, 2x, 3x) | density (PNG 0.75x–4x in ldpi…xxxhdpi sub-folders) instead of formats (turns subfolders on), formats?: [{format: png|png8|jpg|webp|gif|tiff|bmp|tga|psd|svg|svgz|pdf, scale?: 1 | \"2x\" | \"100w\" | \"100h\" | \"72ppi\", width?: px | height?: px | ppi? (raster only; width, then height, then ppi win over scale), suffix?: (raster default: @2x, @100w, @100h, none at 1x; vector formats drop size suffixes), folder?: (its sub-folder), quality?: (jpg 0–100), …the format's options (antiAlias, background, preset (pdf)…)}], settings?: {png|png8|jpg|webp|gif|tiff|bmp|tga|psd|svg|svgz|pdf: {…options for every row of that format}} (rows' own options win), prefix?, antiAlias?: none|art|type (raster rows without their own), openLocation?: (remembered; the app shows the files after exporting)} one file per artboard and format (a PDF holds its artboard alone; artboards with the same name, in any case, get -2, -3…; an unnamed one is Artboard-N) → {files: [path…]}; no folder → {files: [{name, dataBase64}]} (names include sub-folders: 2x/Icon@2x.png). The params (but zip and assets) are remembered in the document (document.exportSettings; not an undo step)",
|
||||
"{folder?, zip?: false (one store-only .zip of every file: no folder → {name, dataBase64, bytes, files: [name…]}; with a folder it is written there → {path, bytes, files}), artboards?: [index…] | range?: \"1-3\" (default all), assets?: [asset id…] (instead of artboards: each asset's art alone, cropped to its bounds and named after the asset; see assets.list), layers?: [layer id…] (instead of artboards: each layer's art alone, sublayers included, named after the layer), selection?: one|each (instead of artboards: the selected art as one file, or a file per selected object, named after it), fullDocument?: false (one file per format instead: a PDF of every artboard, other formats the bounds of all visible art, named after the document), includeBleed?: false (artboards grown by the document's bleed), exportPreset?: (a preset of document.exportPresets.list: its settings, under these params), subfolders?: false (each row's files in a sub-folder: its folder, else its size for raster (1x, 2x, 100w…) or its format (SVG, PDF)), folders?: none|rows (as subfolders)|format (a folder per extension: png, svg, pdf…)|item (a folder per artboard, asset, layer or object)|itemFormat (item/extension) (default: rows with subfolders or a preset, else none), name?: (a file name pattern instead of the item's name, with <filename> for the document, <name> for the artboard, asset, layer or object, <index> from 1, #### for the counter zero-padded to the run of #, <format>, <scale>, <suffix> (else the row's suffix ends the name), YYYYMMDD, YYYY-MM-DD, YYYY, <date>, <time>, <YYYY>, <MM>, <DD>, <hh>, <mm> and <ss>; a trailing .ext is dropped, as in `COMPANY-Logo-<filename>-####-YYYYMMDD`; different files it names alike get -2, -3…), counterStart?: 1, counterPer?: item (every file of an item has its number)|file, date?: YYYY-MM-DD (the date the pattern uses; default today, UTC), preset?: mobile (PNG 1x, 2x, 3x) | density (PNG 0.75x–4x in ldpi…xxxhdpi sub-folders) instead of formats (turns subfolders on), formats?: [{format: png|png8|jpg|webp|gif|tiff|bmp|tga|psd|svg|svgz|pdf|eps|ai|vectorcraft, scale?: 1 | \"2x\" | \"100w\" | \"100h\" | \"72ppi\", width?: px | height?: px | ppi? (raster only; width, then height, then ppi win over scale), suffix?: (raster default: @2x, @100w, @100h, none at 1x; vector formats drop size suffixes), folder?: (its sub-folder), quality?: (jpg 0–100), …the format's options (antiAlias, background, preset (pdf)…)}], settings?: {png|png8|jpg|webp|gif|tiff|bmp|tga|psd|svg|svgz|pdf|eps|ai|vectorcraft: {…options for every row of that format}} (rows' own options win), prefix?, antiAlias?: none|art|type (raster rows without their own), openLocation?: (remembered; the app shows the files after exporting)} one file per artboard and format (a PDF holds its artboard alone; artboards with the same name, in any case, get -2, -3…; an unnamed one is Artboard-N) → {files: [path…]}; no folder → {files: [{name, dataBase64}]} (names include sub-folders: 2x/Icon@2x.png). eps, ai and vectorcraft rows write a document of the item alone (its one artboard). The params (but zip and assets) are remembered in the document (document.exportSettings; not an undo step)",
|
||||
has_doc,
|
||||
screens::export_for_screens
|
||||
),
|
||||
cmd!(
|
||||
query "document.exportPresets.list",
|
||||
"Export Presets",
|
||||
[],
|
||||
None,
|
||||
"{} → {presets: [{name, builtIn, settings}]} the built-in batch export presets (Logo Package, Print-Ready PDF), then the saved ones; any name works as document.exportForScreens' exportPreset",
|
||||
always,
|
||||
screens::presets_list
|
||||
),
|
||||
cmd!(
|
||||
query "document.exportPresets.save",
|
||||
"Save Export Preset",
|
||||
[],
|
||||
None,
|
||||
"{name, settings: {formats, settings, preset, name, prefix, counterStart, counterPer, folders, subfolders, includeBleed, antiAlias, folder, fullDocument} (as document.exportForScreens; other keys are left out)} save (or replace) a batch export preset, kept with the preferences; built-in names are refused → {name, created, settings}",
|
||||
always,
|
||||
screens::presets_save
|
||||
),
|
||||
cmd!(
|
||||
query "document.exportPresets.delete",
|
||||
"Delete Export Preset",
|
||||
[],
|
||||
None,
|
||||
"{name} delete a saved batch export preset (built-in ones stay) → {deleted}",
|
||||
always,
|
||||
screens::presets_delete
|
||||
),
|
||||
cmd!(
|
||||
query "command.batch",
|
||||
"Batch",
|
||||
@@ -1143,6 +1175,8 @@ mod tests_jpeg;
|
||||
#[cfg(test)]
|
||||
mod tests_palette;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_batch;
|
||||
#[cfg(test)]
|
||||
mod tests_screens;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
//! File-name templates for batch exports (`document.exportForScreens {name}`): the same pattern
|
||||
//! gives every exported file a consistent name, such as `COMPANY-Logo-<filename>-####-YYYYMMDD`.
|
||||
//!
|
||||
//! Tokens:
|
||||
//! - `<filename>` (or `<document>`): the document's name without its extension;
|
||||
//! - `<name>` (or `<artboard>`, `<item>`): the artboard's, asset's, layer's or selection's name;
|
||||
//! - `<index>`: the item's place in the export, from 1;
|
||||
//! - a run of `#`: the counter, zero-padded to the run's length (`####` → `0001`);
|
||||
//! - `<format>` (or `<ext>`): the file's extension; `<scale>`: the row's size (`2x`, `100w`);
|
||||
//! `<suffix>`: the row's suffix (otherwise added at the end);
|
||||
//! - the date and time: `YYYYMMDD`, `YYYY-MM-DD` and `YYYY` as written, and `<date>`
|
||||
//! (YYYYMMDD), `<time>` (hhmmss), `<YYYY>`, `<MM>`, `<DD>`, `<hh>`, `<mm>`, `<ss>`.
|
||||
//!
|
||||
//! A trailing `.ext` (or `.<ext>`, `.<format>`) is dropped: the extension comes from the format.
|
||||
//! Anything that could start a folder (`/`, `\`, `:`) becomes `-`.
|
||||
|
||||
/// What a template's tokens stand for, for one file.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct NameParts<'a> {
|
||||
pub filename: &'a str,
|
||||
pub name: &'a str,
|
||||
pub index: usize,
|
||||
pub counter: u64,
|
||||
pub format: &'a str,
|
||||
pub scale: &'a str,
|
||||
pub suffix: &'a str,
|
||||
/// `[year, month, day, hour, minute, second]`.
|
||||
pub when: [i64; 6],
|
||||
}
|
||||
|
||||
/// The bracketed tokens and what each stands for.
|
||||
fn token(name: &str, p: &NameParts<'_>) -> Option<String> {
|
||||
let [y, mo, d, h, mi, s] = p.when;
|
||||
Some(match name {
|
||||
"filename" | "document" => p.filename.to_string(),
|
||||
"name" | "artboard" | "item" => p.name.to_string(),
|
||||
"index" => p.index.to_string(),
|
||||
"format" | "ext" => p.format.to_string(),
|
||||
"scale" => p.scale.to_string(),
|
||||
"suffix" => p.suffix.to_string(),
|
||||
"date" => format!("{y:04}{mo:02}{d:02}"),
|
||||
"time" => format!("{h:02}{mi:02}{s:02}"),
|
||||
"YYYY" => format!("{y:04}"),
|
||||
"MM" => format!("{mo:02}"),
|
||||
"DD" => format!("{d:02}"),
|
||||
"hh" => format!("{h:02}"),
|
||||
"mm" => format!("{mi:02}"),
|
||||
"ss" => format!("{s:02}"),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Text with `/`, `\` and `:` turned into `-`, so it can't start a folder.
|
||||
fn clean(s: &str) -> String {
|
||||
s.chars().map(|c| if matches!(c, '/' | '\\' | ':') { '-' } else { c }).collect()
|
||||
}
|
||||
|
||||
/// Does `template` place the row's suffix itself (so it isn't added at the end)?
|
||||
pub fn has_suffix_token(template: &str) -> bool {
|
||||
template.contains("<suffix>")
|
||||
}
|
||||
|
||||
/// The file name (without extension) `template` gives for `p`.
|
||||
pub fn render(template: &str, p: &NameParts<'_>) -> String {
|
||||
let mut t = template.trim();
|
||||
for ext in [".ext", ".<ext>", ".<format>"] {
|
||||
if let Some(stripped) = t.strip_suffix(ext) {
|
||||
t = stripped;
|
||||
}
|
||||
}
|
||||
let [y, mo, d, ..] = p.when;
|
||||
let mut out = String::with_capacity(t.len() + 16);
|
||||
let mut rest = t;
|
||||
while let Some(c) = rest.chars().next() {
|
||||
// Bracketed tokens; an unknown one stays as written.
|
||||
if c == '<'
|
||||
&& let Some(end) = rest.find('>')
|
||||
&& let Some(value) = rest.get(1..end).and_then(|name| token(name, p))
|
||||
{
|
||||
out.push_str(&clean(&value));
|
||||
rest = rest.get(end + 1..).unwrap_or("");
|
||||
continue;
|
||||
}
|
||||
if c == '#' {
|
||||
let run = rest.chars().take_while(|c| *c == '#').count();
|
||||
out.push_str(&format!("{:0run$}", p.counter));
|
||||
rest = rest.get(run..).unwrap_or("");
|
||||
continue;
|
||||
}
|
||||
// The date spelled out, longest first.
|
||||
let date = [("YYYYMMDD", format!("{y:04}{mo:02}{d:02}")), ("YYYY-MM-DD", format!("{y:04}-{mo:02}-{d:02}")), ("YYYY", format!("{y:04}"))];
|
||||
if let Some((pat, value)) = date.iter().find(|(pat, _)| rest.starts_with(pat)) {
|
||||
out.push_str(value);
|
||||
rest = rest.get(pat.len()..).unwrap_or("");
|
||||
continue;
|
||||
}
|
||||
out.push(if matches!(c, '/' | '\\' | ':') { '-' } else { c });
|
||||
rest = rest.get(c.len_utf8()..).unwrap_or("");
|
||||
}
|
||||
out.trim().to_string()
|
||||
}
|
||||
|
||||
/// `YYYY-MM-DD` (or `YYYYMMDD`) as `[year, month, day, 0, 0, 0]`; `None` when it isn't a date.
|
||||
pub fn parse_date(s: &str) -> Option<[i64; 6]> {
|
||||
let digits: String = s.trim().chars().filter(|c| *c != '-').collect();
|
||||
if digits.len() != 8 || !digits.bytes().all(|b| b.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
let num = |a: usize, b: usize| digits.get(a..b).and_then(|t| t.parse::<i64>().ok());
|
||||
let (y, m, d) = (num(0, 4)?, num(4, 6)?, num(6, 8)?);
|
||||
((1..=12).contains(&m) && (1..=31).contains(&d)).then_some([y, m, d, 0, 0, 0])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parts() -> NameParts<'static> {
|
||||
NameParts { filename: "Brand", name: "Dark", index: 3, counter: 7, format: "png", scale: "2x", suffix: "@2x", when: [2026, 10, 16, 9, 5, 0] }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_example_from_the_request() {
|
||||
assert_eq!(render("COMPANY-Logo-<filename>-####-YYYYMMDD.ext", &parts()), "COMPANY-Logo-Brand-0007-20261016");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_token() {
|
||||
let p = parts();
|
||||
assert_eq!(render("<document>_<name>_<artboard>_<item>", &p), "Brand_Dark_Dark_Dark");
|
||||
assert_eq!(render("<index>-#-##-<format>-<ext>-<scale><suffix>", &p), "3-7-07-png-png-2x@2x");
|
||||
assert_eq!(render("<date>T<time> <YYYY>/<MM>/<DD> <hh>:<mm>:<ss>", &p), "20261016T090500 2026-10-16 09-05-00");
|
||||
assert_eq!(render("YYYY-MM-DD YYYY", &p), "2026-10-16 2026");
|
||||
// Words are left alone: only the date patterns and bracketed tokens are replaced.
|
||||
assert_eq!(render("SUMMER-DD-MM", &p), "SUMMER-DD-MM");
|
||||
assert_eq!(render("<unknown>-x", &p), "<unknown>-x");
|
||||
assert_eq!(render("logo.<format>", &p), "logo", "the extension comes from the format");
|
||||
assert!(has_suffix_token("a<suffix>") && !has_suffix_token("a"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_name_climbs_out_of_its_folder() {
|
||||
let p = NameParts { name: "../../etc", ..parts() };
|
||||
let n = render("a/b\\c:<name>", &p);
|
||||
assert!(!n.contains('/') && !n.contains('\\') && !n.contains(':'), "{n}");
|
||||
// A long counter is padded, a short run is not cut.
|
||||
assert_eq!(render("########", &NameParts { counter: 123, ..parts() }), "00000123");
|
||||
assert_eq!(render("#", &NameParts { counter: 12345, ..parts() }), "12345");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dates_parse() {
|
||||
assert_eq!(parse_date("2026-10-16"), Some([2026, 10, 16, 0, 0, 0]));
|
||||
assert_eq!(parse_date("20261016"), Some([2026, 10, 16, 0, 0, 0]));
|
||||
for bad in ["", "2026-13-01", "2026-1-1", "today", "2026-10-1x"] {
|
||||
assert_eq!(parse_date(bad), None, "{bad}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,25 @@
|
||||
//! File → Export → Export for Screens (`document.exportForScreens`): the chosen artboards (or the
|
||||
//! whole document, or the chosen assets of the Asset Export panel) in every format row at each
|
||||
//! row's size, into a folder and its sub-folders, or returned for download (each file, or one ZIP);
|
||||
//! the presets the dialog offers; and the settings the document remembers
|
||||
//! whole document, the chosen assets of the Asset Export panel, layers, or the selection) in every
|
||||
//! format row at each row's size, into a folder and its sub-folders, or returned for download (each
|
||||
//! file, or one ZIP); the presets the dialog offers; and the settings the document remembers
|
||||
//! (`document.exportSettings`), which the Asset Export panel shares ([`SHARED_KEYS`]).
|
||||
//!
|
||||
//! A batch export names every file from one pattern (`name`, see [`super::naming`]: the document's
|
||||
//! and the item's names, a counter, the date), and sorts the files into folders by row, by format,
|
||||
//! by item, or by item and format (`folders`).
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_doc::Document;
|
||||
use vectorcraft_geom::Rect;
|
||||
|
||||
use super::super::*;
|
||||
use super::encode::single_artboard;
|
||||
use super::export::{isolated, without_artboards};
|
||||
use super::export::{isolated, selected_artwork, without_artboards};
|
||||
use super::naming::{self, NameParts};
|
||||
use super::{
|
||||
ARTBOARD_PARAMS, ArtboardPick, Format, artboard_file_names, create_dir, encode, unique_file_names, with_black_output, writable, write_file,
|
||||
};
|
||||
@@ -161,6 +166,8 @@ struct Row {
|
||||
folder: String,
|
||||
/// The encoder options: the format's settings, then the row's own.
|
||||
options: Value,
|
||||
/// What makes two rows the same file: format, size, suffix and options.
|
||||
key: String,
|
||||
}
|
||||
|
||||
impl Row {
|
||||
@@ -169,8 +176,8 @@ impl Row {
|
||||
return Err(bad(C, "each format is an object {format, scale?, suffix?}"));
|
||||
}
|
||||
let format = writable(C, Some(str_param(row, "format").unwrap_or("png")), None)?;
|
||||
if matches!(format.id, "vectorcraft" | "template" | "ai" | "txt" | "dxf" | "eps" | "emf" | "wmf") {
|
||||
return Err(bad(C, "Export for Screens writes png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz or pdf"));
|
||||
if matches!(format.id, "template" | "txt" | "dxf" | "emf" | "wmf") {
|
||||
return Err(bad(C, "Export for Screens writes png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai or vectorcraft"));
|
||||
}
|
||||
// Vector formats have no pixel size: it doesn't apply and adds no @2x suffix (not even one
|
||||
// left over from a raster row switched to SVG or PDF).
|
||||
@@ -203,13 +210,21 @@ impl Row {
|
||||
let options = Value::Object(options);
|
||||
// A PDF preset (built-in or saved) becomes its settings.
|
||||
let options = if format.id == "pdf" { super::pdf::expand_preset(s, C, &options)?.into_owned() } else { options };
|
||||
Ok(Self { format, size, suffix, folder, options })
|
||||
// Two rows that would write the same bytes under the same name are one file.
|
||||
let key = format!("{}|{}|{suffix}|{options}", format.id, size.label());
|
||||
Ok(Self { format, size, suffix, folder, options, key })
|
||||
}
|
||||
|
||||
/// Formats that hold a whole document to edit (native, Illustrator, EPS): written from a
|
||||
/// document of the item alone, one artboard, rather than with an `artboard` option.
|
||||
fn whole_document(&self) -> bool {
|
||||
matches!(self.format.id, "vectorcraft" | "ai" | "eps")
|
||||
}
|
||||
|
||||
/// The encoder options for `region`, `artboard` of the document written.
|
||||
fn options_for(&self, region: Rect, artboard: Option<usize>) -> Result<Value> {
|
||||
let mut o = self.options.clone();
|
||||
if let Some(b) = artboard {
|
||||
if let Some(b) = artboard.filter(|_| !self.whole_document()) {
|
||||
o["artboard"] = json!(b);
|
||||
}
|
||||
if self.format.raster {
|
||||
@@ -247,6 +262,60 @@ struct ScreenParams {
|
||||
prefix: Option<String>,
|
||||
/// Asset ids (Asset Export) to write instead of artboards.
|
||||
assets: Option<Vec<u64>>,
|
||||
/// Layer ids to write instead of artboards: each layer's art alone.
|
||||
layers: Option<Vec<u64>>,
|
||||
/// The selection instead of artboards: `one` file of it all, or one for `each` object.
|
||||
selection: Option<String>,
|
||||
/// The file name pattern ([`naming`]); without it, `prefix` + name + suffix.
|
||||
name: Option<String>,
|
||||
/// The counter's first value (`####` in `name`).
|
||||
counter_start: Option<u64>,
|
||||
/// The counter counts `item`s (every file of an artboard has its number) or `file`s.
|
||||
counter_per: Option<String>,
|
||||
/// The date the pattern's date tokens use (`YYYY-MM-DD`); default today (UTC).
|
||||
date: Option<String>,
|
||||
/// Where files go: `none` (all in the folder), `rows` (the rows' sub-folders, as
|
||||
/// `subfolders`), `format` (a folder per extension), `item`, or `itemFormat`.
|
||||
folders: Option<String>,
|
||||
}
|
||||
|
||||
/// How exported files are sorted into folders.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Folders {
|
||||
None,
|
||||
Rows,
|
||||
Format,
|
||||
Item,
|
||||
ItemFormat,
|
||||
}
|
||||
|
||||
impl Folders {
|
||||
fn of(o: &ScreenParams) -> Result<Self> {
|
||||
Ok(match o.folders.as_deref() {
|
||||
None if o.subfolders || o.preset.is_some() => Self::Rows,
|
||||
None | Some("none" | "") => Self::None,
|
||||
Some("rows") => Self::Rows,
|
||||
Some("format") => Self::Format,
|
||||
Some("item") => Self::Item,
|
||||
Some("itemFormat") => Self::ItemFormat,
|
||||
Some(f) => return Err(bad(C, format!("folders `{f}`: none, rows, format, item or itemFormat"))),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// One thing a batch export writes a file of per row: an artboard, an asset, a layer, the
|
||||
/// selection or one selected object, or the whole document.
|
||||
struct Item {
|
||||
/// Its name as the user gave it (`<name>`).
|
||||
name: String,
|
||||
/// Its name made safe and unique among the items (the default file name, the item folder).
|
||||
stem: String,
|
||||
/// The document written: the whole document with `artboard`, or the item alone.
|
||||
doc: Arc<Document>,
|
||||
artboard: Option<usize>,
|
||||
region: Rect,
|
||||
/// Full Document: what a PDF row writes (every artboard of the document).
|
||||
every_artboard: Option<Arc<Document>>,
|
||||
}
|
||||
|
||||
impl ScreenParams {
|
||||
@@ -289,8 +358,9 @@ pub(crate) fn check_settings(s: &Session, p: &Value) -> Result<()> {
|
||||
/// The file names (and bytes) an export wrote, or the files it returns.
|
||||
#[derive(Default)]
|
||||
struct Output {
|
||||
/// Lower-case relative names already used (two identical rows write one file).
|
||||
taken: HashSet<String>,
|
||||
/// Lower-case relative names already used, with what wrote each (two identical rows write
|
||||
/// one file; different files that would share a name get `-2`, `-3`…).
|
||||
taken: std::collections::HashMap<String, String>,
|
||||
/// Sub-folders already made.
|
||||
made: HashSet<String>,
|
||||
/// `(relative name, bytes)` kept to return or zip (empty when written to a folder).
|
||||
@@ -300,9 +370,30 @@ struct Output {
|
||||
}
|
||||
|
||||
impl Output {
|
||||
/// Is `name` new (not written by an identical row already)?
|
||||
fn claim(&mut self, name: &str) -> bool {
|
||||
self.taken.insert(name.to_lowercase())
|
||||
/// The name the file `key` writes as `name` takes: `name`, else (with `number`) `name-2`,
|
||||
/// `name-3`… when another file has it; `None` when the same file (`key`) was written as `name`
|
||||
/// already, or, without `number`, when any file was (the first row wins, as Export for Screens
|
||||
/// always did when no name pattern is given).
|
||||
fn claim(&mut self, name: &str, key: &str, number: bool) -> Option<String> {
|
||||
let (base, ext) = name.rsplit_once('.').map_or((name, ""), |(b, e)| (b, e));
|
||||
for n in 1.. {
|
||||
let candidate = if n == 1 {
|
||||
name.to_string()
|
||||
} else if ext.is_empty() {
|
||||
format!("{base}-{n}")
|
||||
} else {
|
||||
format!("{base}-{n}.{ext}")
|
||||
};
|
||||
match self.taken.get(&candidate.to_lowercase()) {
|
||||
Some(k) if k == key || !number => return None,
|
||||
Some(_) => continue,
|
||||
None => {
|
||||
self.taken.insert(candidate.to_lowercase(), key.to_string());
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Write `bytes` as `name` (relative) under `folder`, or keep them.
|
||||
@@ -324,32 +415,166 @@ impl Output {
|
||||
}
|
||||
}
|
||||
|
||||
/// File → Export for Screens: [`export`], with the params remembered in the document
|
||||
/// (`document.exportSettings`).
|
||||
/// File → Export for Screens: [`export`] (over the settings of `exportPreset`, when given), with
|
||||
/// the params remembered in the document (`document.exportSettings`).
|
||||
pub(super) fn export_for_screens(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let r = export(s, p)?;
|
||||
remember(s, p);
|
||||
let p = with_preset(s, p)?;
|
||||
let r = export(s, &p)?;
|
||||
remember(s, &p);
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Every chosen artboard 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), or
|
||||
/// with `assets` each asset's art alone, cropped to it. Artboards (assets) that share a name get
|
||||
/// `-2`, `-3`… instead of overwriting.
|
||||
// ---------- export presets ----------
|
||||
|
||||
/// A saved batch export: what to write and how to name and sort it, to use again
|
||||
/// (`document.exportPresets.*`, `exportPreset`). Kept with the preferences.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ScreenExportPreset {
|
||||
pub name: String,
|
||||
/// The [`PRESET_KEYS`] of `document.exportForScreens` params.
|
||||
pub settings: Map<String, Value>,
|
||||
}
|
||||
|
||||
/// What a preset keeps: the formats, their settings, the naming and the folders; not which art
|
||||
/// (it changes with the document) nor where to (unless given).
|
||||
pub const PRESET_KEYS: [&str; 13] = [
|
||||
"formats",
|
||||
"settings",
|
||||
"preset",
|
||||
"name",
|
||||
"prefix",
|
||||
"counterStart",
|
||||
"counterPer",
|
||||
"folders",
|
||||
"subfolders",
|
||||
"includeBleed",
|
||||
"antiAlias",
|
||||
"folder",
|
||||
"fullDocument",
|
||||
];
|
||||
|
||||
/// The built-in export presets: (name, settings).
|
||||
pub fn builtin_presets() -> Vec<ScreenExportPreset> {
|
||||
let marks = json!({"trim": true, "registration": true, "colorBars": true, "pageInfo": true});
|
||||
let preset = |name: &str, settings: Value| ScreenExportPreset { name: name.into(), settings: settings.as_object().cloned().unwrap_or_default() };
|
||||
vec![
|
||||
preset(
|
||||
"Logo Package",
|
||||
json!({
|
||||
"name": "<filename>-<name>",
|
||||
"folders": "format",
|
||||
"formats": [
|
||||
{"format": "svg"},
|
||||
{"format": "png", "scale": "1x", "suffix": ""},
|
||||
{"format": "png", "scale": "2x", "suffix": "@2x"},
|
||||
{"format": "pdf"},
|
||||
{"format": "pdf", "suffix": "-print", "marks": marks},
|
||||
],
|
||||
}),
|
||||
),
|
||||
preset("Print-Ready PDF", json!({"formats": [{"format": "pdf", "suffix": "-print", "marks": marks, "bleed": {"useDocument": true}}]})),
|
||||
]
|
||||
}
|
||||
|
||||
/// The preset called `name`: a saved one, else a built-in one.
|
||||
fn find_preset(s: &Session, name: &str) -> Option<ScreenExportPreset> {
|
||||
s.prefs.screen_export_presets.iter().find(|p| p.name == name).cloned().or_else(|| builtin_presets().into_iter().find(|p| p.name == name))
|
||||
}
|
||||
|
||||
/// `p` over the settings of its `exportPreset`, when it names one (its own keys win).
|
||||
fn with_preset(s: &Session, p: &Value) -> Result<Value> {
|
||||
let Some(name) = str_param(p, "exportPreset") else { return Ok(p.clone()) };
|
||||
let preset = find_preset(s, name).ok_or_else(|| bad(C, format!("no export preset `{name}` (see document.exportPresets.list)")))?;
|
||||
let mut merged = preset.settings;
|
||||
if let Some(o) = p.as_object() {
|
||||
merged.extend(o.iter().filter(|(k, _)| k.as_str() != "exportPreset").map(|(k, v)| (k.clone(), v.clone())));
|
||||
}
|
||||
Ok(Value::Object(merged))
|
||||
}
|
||||
|
||||
pub(super) fn presets_list(s: &mut Session, _: &Value) -> Result<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 }))
|
||||
}
|
||||
|
||||
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();
|
||||
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 preset = ScreenExportPreset { name: name.clone(), settings };
|
||||
let created = match s.prefs.screen_export_presets.iter_mut().find(|q| q.name == name) {
|
||||
Some(slot) => {
|
||||
*slot = preset.clone();
|
||||
false
|
||||
}
|
||||
None => {
|
||||
s.prefs.screen_export_presets.push(preset.clone());
|
||||
true
|
||||
}
|
||||
};
|
||||
Ok(json!({ "name": name, "created": created, "settings": preset.settings }))
|
||||
}
|
||||
|
||||
pub(super) fn presets_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const DELETE: &str = "document.exportPresets.delete";
|
||||
let name = str_param(p, "name").ok_or_else(|| bad(DELETE, "missing `name`"))?;
|
||||
if builtin_presets().iter().any(|b| b.name == name) {
|
||||
return Err(bad(DELETE, format!("`{name}` is built in and stays")));
|
||||
}
|
||||
let before = s.prefs.screen_export_presets.len();
|
||||
s.prefs.screen_export_presets.retain(|q| q.name != name);
|
||||
if s.prefs.screen_export_presets.len() == before {
|
||||
return Err(bad(DELETE, format!("no saved export preset `{name}`")));
|
||||
}
|
||||
Ok(json!({ "deleted": name }))
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// so do different files the `name` pattern gives one name.
|
||||
pub(crate) fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let o = ScreenParams::of(p)?;
|
||||
let rows = rows_of(s, p, o.preset.as_deref())?;
|
||||
if o.full_document && o.assets.is_some() {
|
||||
return Err(bad(C, "give assets or fullDocument, not both"));
|
||||
let sources = [o.full_document, o.assets.is_some(), o.layers.is_some(), o.selection.is_some()].iter().filter(|b| **b).count();
|
||||
if sources > 1 {
|
||||
return Err(bad(C, "give one of assets, layers, selection or fullDocument"));
|
||||
}
|
||||
let subfolders = o.subfolders || o.preset.is_some();
|
||||
let folders = Folders::of(&o)?;
|
||||
// A prefix names files, not folders.
|
||||
let prefix = file_part(o.prefix.as_deref().unwrap_or(""));
|
||||
let folder = o.folder.as_deref().filter(|f| !f.is_empty());
|
||||
let when = match o.date.as_deref() {
|
||||
Some(d) => naming::parse_date(d).ok_or_else(|| bad(C, format!("date `{d}`: YYYY-MM-DD")))?,
|
||||
None => vectorcraft_doc::metadata::now_unix().map_or([1970, 1, 1, 0, 0, 0], vectorcraft_doc::metadata::civil),
|
||||
};
|
||||
let per_file = match o.counter_per.as_deref() {
|
||||
None | Some("item") => false,
|
||||
Some("file") => true,
|
||||
Some(c) => return Err(bad(C, format!("counterPer `{c}`: item or file"))),
|
||||
};
|
||||
let counter_start = o.counter_start.unwrap_or(1);
|
||||
let selection = match o.selection.as_deref() {
|
||||
None => None,
|
||||
Some("one") => Some(false),
|
||||
Some("each") => Some(true),
|
||||
Some(m) => return Err(bad(C, format!("selection `{m}`: one or each"))),
|
||||
};
|
||||
let selected = if selection.is_some() { selected_artwork(s)? } else { vec![] };
|
||||
if let Some(dir) = folder {
|
||||
create_dir(dir)?;
|
||||
}
|
||||
let doc = s.doc()?.doc.clone();
|
||||
let filename = safe_name(&super::file_stem(&doc.title)).unwrap_or_else(|| "Untitled".into());
|
||||
// Include Bleed: each artboard grown by the document's bleed.
|
||||
let doc = if o.include_bleed && doc.setup.has_bleed() {
|
||||
let mut d = (*doc).clone();
|
||||
@@ -360,79 +585,66 @@ pub(crate) fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
} else {
|
||||
doc
|
||||
};
|
||||
let name_of = |r: &Row, stem: &str| {
|
||||
let file = format!("{prefix}{stem}{}.{}", r.suffix, r.format.extensions[0]);
|
||||
if subfolders { format!("{}/{file}", r.folder) } else { file }
|
||||
};
|
||||
let crop = rows.iter().any(|r| r.format.id != "pdf");
|
||||
let items = items(&doc, &o, selection, &selected, &filename, crop)?;
|
||||
let mut out = Output::default();
|
||||
// Appearance of Black › Printing / Exporting for RGB raster rows.
|
||||
let write = |doc: &Document, format: &str, options: &Value| encode(doc, format, &with_black_output(s, options));
|
||||
if o.full_document {
|
||||
let stem = safe_name(&super::file_stem(&doc.title)).unwrap_or_else(|| "Untitled".into());
|
||||
// The bounds of all visible art, as the one artboard of the files that aren't PDFs.
|
||||
let mut whole: Option<Document> = None;
|
||||
// A whole-document copy of an artboard, for the formats that write documents to edit.
|
||||
let mut board_docs: std::collections::HashMap<usize, Arc<Document>> = std::collections::HashMap::new();
|
||||
let mut files = 0u64;
|
||||
for (index, item) in items.iter().enumerate() {
|
||||
for r in &rows {
|
||||
let name = name_of(r, &stem);
|
||||
if !out.claim(&name) {
|
||||
continue;
|
||||
}
|
||||
let bytes = if r.format.id == "pdf" {
|
||||
let counter = counter_start.saturating_add(if per_file { files } else { index as u64 });
|
||||
let ext = r.format.extensions.first().copied().unwrap_or(r.format.id);
|
||||
let stem = match o.name.as_deref().filter(|n| !n.trim().is_empty()) {
|
||||
Some(pattern) => {
|
||||
let parts = NameParts {
|
||||
filename: &filename,
|
||||
name: &item.name,
|
||||
index: index + 1,
|
||||
counter,
|
||||
format: ext,
|
||||
scale: &if r.format.raster { r.size.label() } else { String::new() },
|
||||
suffix: &r.suffix,
|
||||
when,
|
||||
};
|
||||
let named = naming::render(pattern, &parts);
|
||||
let named = if named.is_empty() { item.stem.clone() } else { named };
|
||||
if naming::has_suffix_token(pattern) { format!("{prefix}{named}") } else { format!("{prefix}{named}{}", r.suffix) }
|
||||
}
|
||||
None => format!("{prefix}{}{}", item.stem, r.suffix),
|
||||
};
|
||||
let file = format!("{stem}.{ext}");
|
||||
let sub = match folders {
|
||||
Folders::None => None,
|
||||
Folders::Rows => Some(r.folder.clone()),
|
||||
Folders::Format => Some(ext.to_string()),
|
||||
Folders::Item => Some(item.stem.clone()),
|
||||
Folders::ItemFormat => Some(format!("{}/{ext}", item.stem)),
|
||||
};
|
||||
let name = sub.map_or_else(|| file.clone(), |d| format!("{d}/{file}"));
|
||||
let Some(name) = out.claim(&name, &format!("{}|{}", item.stem, r.key), o.name.is_some()) else { continue };
|
||||
let bytes = if let (Some(all), "pdf") = (&item.every_artboard, r.format.id) {
|
||||
// One page per artboard.
|
||||
let mut options = r.options.clone();
|
||||
options["range"] = json!("all");
|
||||
write(&doc, "pdf", &options)?
|
||||
write(all, "pdf", &options)?
|
||||
} else if r.whole_document() && item.artboard.is_some() {
|
||||
let b = item.artboard.unwrap_or(0);
|
||||
let d = board_docs.entry(b).or_insert_with(|| Arc::new(single_artboard(&item.doc, item.region, &item.name))).clone();
|
||||
write(&d, r.format.id, &r.options_for(item.region, None)?)?
|
||||
} else {
|
||||
if whole.is_none() {
|
||||
let bounds =
|
||||
vectorcraft_render::encode::art_bounds(&doc).ok_or_else(|| bad(C, "nothing to export: the document has no visible art"))?;
|
||||
whole = Some(single_artboard(&doc, bounds, "Document"));
|
||||
}
|
||||
let Some(d) = whole.as_ref() else { continue };
|
||||
let region = d.artboards.first().map_or(Rect::ZERO, |a| a.rect);
|
||||
write(d, r.format.id, &r.options_for(region, Some(0))?)?
|
||||
write(&item.doc, r.format.id, &r.options_for(item.region, item.artboard)?)?
|
||||
};
|
||||
out.put(folder, o.zip, name, bytes)?;
|
||||
}
|
||||
} else if let Some(ids) = &o.assets {
|
||||
let assets =
|
||||
ids.iter().map(|id| doc.asset(*id).ok_or_else(|| bad(C, format!("no asset {id} (see assets.list)")))).collect::<Result<Vec<_>>>()?;
|
||||
if assets.is_empty() {
|
||||
return Err(bad(C, "no assets to export"));
|
||||
}
|
||||
for (a, stem) in assets.iter().zip(unique_file_names(assets.iter().map(|a| (a.name.as_str(), "Asset".to_string())))) {
|
||||
let (d, bounds) = isolated(&doc, &doc.paint_order(a.nodes.iter().copied()), &a.name)
|
||||
.ok_or_else(|| bad(C, format!("asset `{}` has no art to export", a.name)))?;
|
||||
for r in &rows {
|
||||
let name = name_of(r, &stem);
|
||||
if !out.claim(&name) {
|
||||
continue;
|
||||
}
|
||||
let bytes = write(&d, r.format.id, &r.options_for(bounds, Some(0))?)?;
|
||||
out.put(folder, o.zip, name, bytes)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let n = doc.artboards.len();
|
||||
let boards = o.boards.resolve(n).map_err(|e| bad(C, e))?.unwrap_or_else(|| (0..n).collect());
|
||||
if boards.is_empty() {
|
||||
return Err(bad(C, "the document has no artboard"));
|
||||
}
|
||||
for (b, stem) in boards.iter().copied().zip(artboard_file_names(&doc, &boards)) {
|
||||
let region = doc.artboards.get(b).map_or(Rect::ZERO, |a| a.rect);
|
||||
for r in &rows {
|
||||
let name = name_of(r, &stem);
|
||||
if !out.claim(&name) {
|
||||
continue; // the same file from two identical rows
|
||||
}
|
||||
let bytes = write(&doc, r.format.id, &r.options_for(region, Some(b))?)?;
|
||||
out.put(folder, o.zip, name, bytes)?;
|
||||
}
|
||||
files += 1;
|
||||
}
|
||||
}
|
||||
let names: Vec<&str> = out.kept.iter().map(|(n, _)| n.as_str()).collect();
|
||||
if o.zip {
|
||||
let zip = super::zip::store(&out.kept).map_err(|e| bad(C, e))?;
|
||||
let zip_name = format!("{prefix}{}.zip", safe_name(&super::file_stem(&doc.title)).unwrap_or_else(|| "Untitled".into()));
|
||||
let zip_name = format!("{prefix}{filename}.zip");
|
||||
return match folder {
|
||||
Some(dir) => {
|
||||
let path = format!("{dir}/{zip_name}");
|
||||
@@ -450,6 +662,98 @@ pub(crate) fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})
|
||||
}
|
||||
|
||||
/// What `o` exports a file of per row ([`Item`]): the whole document, the assets, the layers, the
|
||||
/// selection (one item, or one per object), else the chosen artboards.
|
||||
/// `crop`: some row needs the Full Document's art bounds (a format other than PDF).
|
||||
fn items(doc: &Arc<Document>, o: &ScreenParams, selection: Option<bool>, selected: &[NodeId], filename: &str, crop: bool) -> Result<Vec<Item>> {
|
||||
// The art of `ids` alone, as an item named `name`.
|
||||
let alone = |ids: &[NodeId], name: &str| -> Option<(Arc<Document>, Rect)> {
|
||||
isolated(doc, &doc.paint_order(ids.iter().copied()), name).map(|(d, r)| (Arc::new(d), r))
|
||||
};
|
||||
let named = |pieces: Vec<(String, Arc<Document>, Option<usize>, Rect)>, fallback: &str| -> Vec<Item> {
|
||||
let stems = unique_file_names(pieces.iter().map(|(n, ..)| (n.as_str(), fallback.to_string())));
|
||||
pieces
|
||||
.into_iter()
|
||||
.zip(stems)
|
||||
.map(|((name, doc, artboard, region), stem)| Item { name, stem, doc, artboard, region, every_artboard: None })
|
||||
.collect()
|
||||
};
|
||||
if o.full_document {
|
||||
// The bounds of all visible art, as the one artboard of the files that aren't PDFs (a PDF
|
||||
// row writes every artboard of the document itself).
|
||||
let (whole, region) = if crop {
|
||||
let bounds = vectorcraft_render::encode::art_bounds(doc).ok_or_else(|| bad(C, "nothing to export: the document has no visible art"))?;
|
||||
(Arc::new(single_artboard(doc, bounds, "Document")), bounds)
|
||||
} else {
|
||||
(doc.clone(), Rect::ZERO)
|
||||
};
|
||||
let name = filename.to_string();
|
||||
return Ok(vec![Item { name: name.clone(), stem: name, doc: whole, artboard: Some(0), region, every_artboard: Some(doc.clone()) }]);
|
||||
}
|
||||
if let Some(ids) = &o.assets {
|
||||
let mut pieces = vec![];
|
||||
for id in ids {
|
||||
let a = doc.asset(*id).ok_or_else(|| bad(C, format!("no asset {id} (see assets.list)")))?;
|
||||
let (d, r) = alone(&a.nodes, &a.name).ok_or_else(|| bad(C, format!("asset `{}` has no art to export", a.name)))?;
|
||||
pieces.push((a.name.clone(), d, Some(0), r));
|
||||
}
|
||||
if pieces.is_empty() {
|
||||
return Err(bad(C, "no assets to export"));
|
||||
}
|
||||
return Ok(named(pieces, "Asset"));
|
||||
}
|
||||
if let Some(ids) = &o.layers {
|
||||
let mut pieces = vec![];
|
||||
for id in ids {
|
||||
let layer = doc.node(NodeId(*id)).filter(|n| n.is_layer()).ok_or_else(|| bad(C, format!("no layer {id} (see layer.list)")))?;
|
||||
let mut art = vec![];
|
||||
crate::cmd::assets::art_under(doc, layer.id, &mut art);
|
||||
let name = layer.display_name();
|
||||
let (d, r) = alone(&art, &name).ok_or_else(|| bad(C, format!("layer `{name}` has no art to export")))?;
|
||||
pieces.push((name, d, Some(0), r));
|
||||
}
|
||||
if pieces.is_empty() {
|
||||
return Err(bad(C, "no layers to export"));
|
||||
}
|
||||
return Ok(named(pieces, "Layer"));
|
||||
}
|
||||
if let Some(each) = selection {
|
||||
if selected.is_empty() {
|
||||
return Err(bad(C, "select something to export"));
|
||||
}
|
||||
let mut pieces = vec![];
|
||||
if each {
|
||||
for id in selected {
|
||||
let name = doc.node(*id).map(|n| n.display_name()).filter(|n| !n.is_empty()).unwrap_or_else(|| "Selection".into());
|
||||
if let Some((d, r)) = alone(&[*id], &name) {
|
||||
pieces.push((name, d, Some(0), r));
|
||||
}
|
||||
}
|
||||
} else if let Some((d, r)) = alone(selected, "Selection") {
|
||||
pieces.push(("Selection".to_string(), d, Some(0), r));
|
||||
}
|
||||
if pieces.is_empty() {
|
||||
return Err(bad(C, "the selection has no art to export"));
|
||||
}
|
||||
return Ok(named(pieces, "Selection"));
|
||||
}
|
||||
let n = doc.artboards.len();
|
||||
let boards = o.boards.resolve(n).map_err(|e| bad(C, e))?.unwrap_or_else(|| (0..n).collect());
|
||||
if boards.is_empty() {
|
||||
return Err(bad(C, "the document has no artboard"));
|
||||
}
|
||||
let stems = artboard_file_names(doc, &boards);
|
||||
Ok(boards
|
||||
.into_iter()
|
||||
.zip(stems)
|
||||
.map(|(b, stem)| {
|
||||
let a = doc.artboards.get(b);
|
||||
let name = a.map(|a| a.name.clone()).filter(|n| !n.is_empty()).unwrap_or_else(|| stem.clone());
|
||||
Item { name, stem, doc: doc.clone(), artboard: Some(b), region: a.map_or(Rect::ZERO, |a| a.rect), every_artboard: None }
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Keep the params of a successful export in the document (without `zip`, a delivery detail, and
|
||||
/// `assets`, a choice of art), so the dialog reopens on them.
|
||||
fn remember(s: &mut Session, p: &Value) {
|
||||
|
||||
@@ -378,7 +378,9 @@ fn export_for_screens_one_page_per_pdf_unique_names() {
|
||||
let r = s.execute("document.exportForScreens", &json!({"range": "3", "formats": [{"format": "jpg", "scale": 0.5}]})).unwrap();
|
||||
assert_eq!(r["files"][0]["name"], "Artboard-3@0.5x.jpg");
|
||||
assert_eq!(image::load_from_memory(&b64(&r["files"][0])).unwrap().width(), 50);
|
||||
assert!(s.execute("document.exportForScreens", &json!({"formats": [{"format": "vectorcraft"}]})).is_err());
|
||||
// A native row: each artboard as a document of its own to edit.
|
||||
let r = s.execute("document.exportForScreens", &json!({"range": "1", "formats": [{"format": "vectorcraft"}]})).unwrap();
|
||||
assert!(r["files"][0]["name"].as_str().unwrap().ends_with(".vectorcraft"), "{r}");
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
//! Batch exports through Export for Screens (#1935): a file name pattern (the document's and the
|
||||
//! item's names, a counter, the date), folders by format, by item or both, layers and the selection
|
||||
//! as what is exported, and document formats (VectorCraft, Illustrator, EPS) as rows.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn b64(v: &Value) -> Vec<u8> {
|
||||
vectorcraft_format::base64_decode(v["dataBase64"].as_str().expect("dataBase64")).unwrap()
|
||||
}
|
||||
|
||||
/// A document of `artboards` 40×30 pt artboards side by side with a 20 pt square on the first.
|
||||
fn session(artboards: usize) -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 40, "height": 30, "artboards": artboards})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 5, "y": 5, "width": 20, "height": 20})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn export(s: &mut Session, p: Value) -> Value {
|
||||
s.execute("document.exportForScreens", &p).unwrap()
|
||||
}
|
||||
|
||||
fn files(r: &Value) -> Vec<(String, Vec<u8>)> {
|
||||
r["files"].as_array().unwrap().iter().map(|f| (f["name"].as_str().unwrap().to_string(), b64(f))).collect()
|
||||
}
|
||||
|
||||
fn names(r: &Value) -> Vec<String> {
|
||||
files(r).into_iter().map(|(n, _)| n).collect()
|
||||
}
|
||||
|
||||
/// The request in #1935: SVG, PNG, PDF and PDF with printer's marks for artboards 1–3, named
|
||||
/// `COMPANY-Logo-<filename>-####-YYYYMMDD`, sorted into a folder per extension.
|
||||
#[test]
|
||||
fn the_logo_package_of_the_request() {
|
||||
let mut s = session(4);
|
||||
let r = export(
|
||||
&mut s,
|
||||
json!({
|
||||
"range": "1-3",
|
||||
"name": "COMPANY-Logo-<filename>-####-YYYYMMDD.ext",
|
||||
"date": "2026-10-16",
|
||||
"folders": "format",
|
||||
"formats": [
|
||||
{"format": "svg"},
|
||||
{"format": "png", "scale": "2x", "suffix": ""},
|
||||
{"format": "pdf"},
|
||||
{"format": "pdf", "suffix": "-print", "marks": {"trim": true, "registration": true, "colorBars": true, "pageInfo": true}}
|
||||
]
|
||||
}),
|
||||
);
|
||||
let n = names(&r);
|
||||
assert_eq!(n.len(), 12, "{n:?}");
|
||||
for (i, number) in ["0001", "0002", "0003"].iter().enumerate() {
|
||||
let stem = format!("COMPANY-Logo-Untitled-1-{number}-20261016");
|
||||
for want in [format!("svg/{stem}.svg"), format!("png/{stem}.png"), format!("pdf/{stem}.pdf"), format!("pdf/{stem}-print.pdf")] {
|
||||
assert!(n.contains(&want), "artboard {}: {want} in {n:?}", i + 1);
|
||||
}
|
||||
}
|
||||
// The printed PDF has room for its marks: a bigger page than the plain one.
|
||||
let f = files(&r);
|
||||
let page = |name: &str| {
|
||||
let bytes = &f.iter().find(|(n, _)| n == name).unwrap().1;
|
||||
let d = vectorcraft_pdf::import(bytes).unwrap();
|
||||
let a = d.artboards.first().unwrap().rect;
|
||||
(a.width(), a.height())
|
||||
};
|
||||
let plain = page("pdf/COMPANY-Logo-Untitled-1-0001-20261016.pdf");
|
||||
let print = page("pdf/COMPANY-Logo-Untitled-1-0001-20261016-print.pdf");
|
||||
assert!(print.0 > plain.0 && print.1 > plain.1, "{plain:?} vs {print:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folders_by_item_and_by_item_and_format() {
|
||||
let mut s = session(2);
|
||||
let rows = json!([{"format": "png"}, {"format": "svg"}]);
|
||||
let by_item = names(&export(&mut s, json!({"folders": "item", "formats": rows})));
|
||||
assert_eq!(by_item, ["Artboard-1/Artboard-1.png", "Artboard-1/Artboard-1.svg", "Artboard-2/Artboard-2.png", "Artboard-2/Artboard-2.svg"]);
|
||||
let both = names(&export(&mut s, json!({"folders": "itemFormat", "formats": rows})));
|
||||
assert_eq!(both[0], "Artboard-1/png/Artboard-1.png");
|
||||
let flat = names(&export(&mut s, json!({"folders": "none", "subfolders": true, "formats": rows})));
|
||||
assert_eq!(flat[0], "Artboard-1.png", "folders wins over subfolders");
|
||||
// Without folders, subfolders keeps its meaning (a folder per row).
|
||||
let rows_mode = names(&export(&mut s, json!({"subfolders": true, "formats": rows})));
|
||||
assert_eq!(rows_mode[0], "1x/Artboard-1.png");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_counter_counts_items_or_files_from_any_start() {
|
||||
let mut s = session(2);
|
||||
let rows = json!([{"format": "png"}, {"format": "svg"}]);
|
||||
let items = names(&export(&mut s, json!({"name": "logo-###", "counterStart": 9, "formats": rows})));
|
||||
assert_eq!(items, ["logo-009.png", "logo-009.svg", "logo-010.png", "logo-010.svg"]);
|
||||
let per_file = names(&export(&mut s, json!({"name": "logo-#", "counterPer": "file", "formats": rows})));
|
||||
assert_eq!(per_file, ["logo-1.png", "logo-2.svg", "logo-3.png", "logo-4.svg"]);
|
||||
// A pattern that names different files alike numbers them instead of dropping one.
|
||||
let same = names(&export(&mut s, json!({"name": "logo", "formats": [{"format": "png"}]})));
|
||||
assert_eq!(same, ["logo.png", "logo-2.png"]);
|
||||
// The item's name, its place and the row's size and suffix.
|
||||
let parts = names(&export(
|
||||
&mut s,
|
||||
json!({"name": "<index>_<name>_<scale><suffix>_<format>", "range": "2", "formats": [{"format": "png", "scale": "2x"}]}),
|
||||
));
|
||||
assert_eq!(parts, ["1_Artboard 2_2x@2x_png.png"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layers_and_the_selection_are_exported_by_name() {
|
||||
let mut s = session(1);
|
||||
let first_layer = s.doc().unwrap().doc.layers[0].id.0;
|
||||
let top = s.execute("layer.new", &json!({"name": "Dark Mode"})).unwrap()["id"].as_u64().unwrap();
|
||||
let circle = s.execute("shape.ellipse", &json!({"x": 2, "y": 2, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap();
|
||||
let r = export(&mut s, json!({"layers": [first_layer, top], "formats": [{"format": "svg"}]}));
|
||||
assert_eq!(names(&r), ["Layer-1.svg", "Dark-Mode.svg"]);
|
||||
// The selection: one file of it all, or a file per object.
|
||||
let square = s.doc().unwrap().doc.layers[0].children().unwrap()[0].id.0;
|
||||
s.execute("select.set", &json!({"ids": [square, circle]})).unwrap();
|
||||
assert_eq!(names(&export(&mut s, json!({"selection": "one", "formats": [{"format": "png"}]}))), ["Selection.png"]);
|
||||
let each = names(&export(&mut s, json!({"selection": "each", "formats": [{"format": "png"}]})));
|
||||
assert_eq!(each.len(), 2, "{each:?}");
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
assert!(s.execute("document.exportForScreens", &json!({"selection": "one"})).is_err(), "nothing selected");
|
||||
}
|
||||
|
||||
/// Document formats write each artboard as a document of its own: one artboard, its art.
|
||||
#[test]
|
||||
fn document_formats_write_one_artboard_each() {
|
||||
let mut s = session(2);
|
||||
let r = export(&mut s, json!({"formats": [{"format": "vectorcraft"}, {"format": "ai"}, {"format": "eps"}]}));
|
||||
let n = names(&r);
|
||||
assert_eq!(n, ["Artboard-1.vectorcraft", "Artboard-1.ai", "Artboard-1.eps", "Artboard-2.vectorcraft", "Artboard-2.ai", "Artboard-2.eps"]);
|
||||
let native = &files(&r)[0].1;
|
||||
let mut t = Session::new();
|
||||
t.execute("document.open", &json!({"name": "a.vectorcraft", "dataBase64": vectorcraft_format::base64_encode(native)})).unwrap();
|
||||
assert_eq!(t.doc().unwrap().doc.artboards.len(), 1, "that artboard alone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_batch_params_are_refused() {
|
||||
let mut s = session(1);
|
||||
for bad in [
|
||||
json!({"folders": "byColour"}),
|
||||
json!({"counterPer": "page"}),
|
||||
json!({"date": "16/10/2026"}),
|
||||
json!({"selection": "some"}),
|
||||
json!({"selection": "one", "fullDocument": true}),
|
||||
json!({"layers": [99999]}),
|
||||
json!({"layers": [1], "assets": [1]}),
|
||||
] {
|
||||
assert!(s.execute("document.exportForScreens", &bad).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Export presets: the built-in ones, saving (only what a preset keeps), using one under other
|
||||
/// params, replacing, deleting, and keeping them with the preferences.
|
||||
#[test]
|
||||
fn export_presets_save_use_and_delete() {
|
||||
let mut s = session(2);
|
||||
let list = s.execute("document.exportPresets.list", &json!({})).unwrap();
|
||||
let built_in: Vec<&str> =
|
||||
list["presets"].as_array().unwrap().iter().filter(|p| p["builtIn"] == true).map(|p| p["name"].as_str().unwrap()).collect();
|
||||
assert_eq!(built_in, ["Logo Package", "Print-Ready PDF"]);
|
||||
// The Logo Package: SVG, PNG 1x and 2x, PDF and PDF with marks, a folder per format.
|
||||
let r = export(&mut s, json!({"exportPreset": "Logo Package", "range": "1"}));
|
||||
assert_eq!(
|
||||
names(&r),
|
||||
[
|
||||
"svg/Untitled-1-Artboard 1.svg",
|
||||
"png/Untitled-1-Artboard 1.png",
|
||||
"png/Untitled-1-Artboard 1@2x.png",
|
||||
"pdf/Untitled-1-Artboard 1.pdf",
|
||||
"pdf/Untitled-1-Artboard 1-print.pdf"
|
||||
]
|
||||
);
|
||||
|
||||
let settings = json!({"name": "ACME-<name>-##", "folders": "item", "formats": [{"format": "jpg", "quality": 80}], "range": "2", "assets": [1]});
|
||||
let saved = s.execute("document.exportPresets.save", &json!({"name": "Web JPG", "settings": settings})).unwrap();
|
||||
assert_eq!(saved["created"], true);
|
||||
assert!(saved["settings"].get("range").is_none() && saved["settings"].get("assets").is_none(), "which art is not kept: {saved}");
|
||||
let r = export(&mut s, json!({"exportPreset": "Web JPG", "range": "2"}));
|
||||
assert_eq!(names(&r), ["Artboard-2/ACME-Artboard 2-01.jpg"]);
|
||||
// The call's own params win over the preset's.
|
||||
let r = export(&mut s, json!({"exportPreset": "Web JPG", "range": "2", "folders": "none"}));
|
||||
assert_eq!(names(&r), ["ACME-Artboard 2-01.jpg"]);
|
||||
// Saving again replaces it; the preferences keep it.
|
||||
let again = s.execute("document.exportPresets.save", &json!({"name": "Web JPG", "settings": {"formats": [{"format": "png"}]}})).unwrap();
|
||||
assert_eq!(again["created"], false);
|
||||
let prefs = serde_json::to_value(&s.prefs).unwrap();
|
||||
assert_eq!(prefs["screenExportPresets"][0]["name"], "Web JPG");
|
||||
s.execute("document.exportPresets.delete", &json!({"name": "Web JPG"})).unwrap();
|
||||
assert!(s.execute("document.exportForScreens", &json!({"exportPreset": "Web JPG"})).is_err());
|
||||
// Built-in names stay; bad settings are refused when saved.
|
||||
for bad in [
|
||||
("document.exportPresets.save", json!({"name": "Logo Package", "settings": {}})),
|
||||
("document.exportPresets.save", json!({"name": "", "settings": {}})),
|
||||
("document.exportPresets.save", json!({"name": "X", "settings": {"formats": [{"format": "txt"}]}})),
|
||||
("document.exportPresets.save", json!({"name": "X", "settings": {"folders": "sideways"}})),
|
||||
("document.exportPresets.save", json!({"name": "X"})),
|
||||
("document.exportPresets.delete", json!({"name": "Logo Package"})),
|
||||
("document.exportPresets.delete", json!({"name": "Never saved"})),
|
||||
] {
|
||||
assert!(s.execute(bad.0, &bad.1).is_err(), "{bad:?}");
|
||||
}
|
||||
}
|
||||
@@ -171,7 +171,9 @@ fn bad_eps_options_are_refused_and_formats_list_eps() {
|
||||
let e = s.execute("document.exportEps", &p).unwrap_err().to_string();
|
||||
assert!(e.contains("EPS"), "{p}: {e}");
|
||||
}
|
||||
assert!(s.execute("document.exportForScreens", &json!({"formats": [{"format": "eps"}]})).is_err());
|
||||
// Export for Screens writes an EPS per artboard (a document of that artboard alone).
|
||||
let r = s.execute("document.exportForScreens", &json!({"formats": [{"format": "eps"}]})).unwrap();
|
||||
assert!(r["files"][0]["name"].as_str().unwrap().ends_with(".eps"), "{r}");
|
||||
let r = s.execute("document.formats", &json!({})).unwrap();
|
||||
let eps = r["formats"].as_array().unwrap().iter().find(|f| f["id"] == "eps").unwrap().clone();
|
||||
// Readable too since EPS import (tests_epsimport.rs).
|
||||
|
||||
@@ -904,6 +904,10 @@ pub struct Prefs {
|
||||
/// not a Preferences dialog field: resetting the preferences keeps it; `pdf.preset.*` edit it.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub pdf_presets: Vec<vectorcraft_pdf::PdfPreset>,
|
||||
/// File → Export for Screens: the user's batch export presets (`document.exportPresets.*`;
|
||||
/// the built-in ones aren't stored). A local library, not a Preferences dialog field.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub screen_export_presets: Vec<cmd::fileio::ScreenExportPreset>,
|
||||
// File Handling (continued)
|
||||
/// Where Save as Template and New from Template start ("" = `Documents/VectorCraft Templates`).
|
||||
pub templates_folder: String,
|
||||
@@ -1089,6 +1093,7 @@ impl Default for Prefs {
|
||||
new_doc_presets: vec![],
|
||||
recent_new_docs: vec![],
|
||||
pdf_presets: vec![],
|
||||
screen_export_presets: vec![],
|
||||
templates_folder: String::new(),
|
||||
append_converted: true,
|
||||
use_compression: false,
|
||||
|
||||
@@ -6,17 +6,28 @@
|
||||
//! document last exported with, except that while the Artboard tool is in use the artboards it has
|
||||
//! selected are the ones chosen; the format rows are the Asset Export panel's too ([`formats`]).
|
||||
//!
|
||||
//! Fields (`ui.dialog.set`): `tab` (artboards|assets), `select` (all|range|full), `range`,
|
||||
//! The Layers tab exports each checked layer's art alone, and the Selection tab the selected art as
|
||||
//! one file or a file per object. A file name pattern (`name`: `<filename>`, `<name>`, `####`,
|
||||
//! `YYYYMMDD`…, see `document.exportForScreens`) names every file alike, with a live example, and
|
||||
//! the files go into one folder, or a folder per row, format, item, or item and format
|
||||
//! (`folders`). Export presets (built-in and saved) set the formats, naming and folders at once.
|
||||
//!
|
||||
//! Fields (`ui.dialog.set`): `tab` (artboards|assets|layers|selection), `select` (all|range|full), `range`,
|
||||
//! `boards` ([bool] per artboard), `assets` ([asset id] checked on the Assets tab), `includeBleed`,
|
||||
//! `folder`, `openLocation`, `subfolders`, `prefix`, `preset` (""|mobile|density: where the rows
|
||||
//! came from), `formats` (rows of `document.exportForScreens`, `scale` as text), `settings`
|
||||
//! ({png|png8|jpg|webp|gif|tiff|svg|pdf: {…}}); `__settings` names the format whose Format Settings
|
||||
//! show ("" when closed).
|
||||
//! ({png|png8|jpg|webp|gif|tiff|svg|pdf: {…}}), `layers` ([layer id] checked on the Layers tab),
|
||||
//! `selectionMode` (one|each), `name` (the pattern), `counterStart`, `folders`
|
||||
//! (none|rows|format|item|itemFormat), `exportPreset` (the preset applied last, "" for none);
|
||||
//! `__settings` names the format whose Format Settings show ("" when closed), `__savePreset` is the
|
||||
//! name being typed to save a preset (absent when not saving).
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_engine::cmd::fileio::{self, ArtboardPick, SCREEN_PRESETS, ScreenSize, pdf};
|
||||
|
||||
use vectorcraft_doc::{ExportAsset, NodeId};
|
||||
|
||||
use super::{DialogSpec, form, png_options, svg_options};
|
||||
use crate::state::Dialog;
|
||||
use crate::theme::Tokens;
|
||||
@@ -27,10 +38,12 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
|
||||
body,
|
||||
confirm,
|
||||
ok: Some("Export Artboard"),
|
||||
ok_label: Some(|app| match (io::is_web(app), app.ui.dialog.as_ref().is_some_and(|d| d.str("tab") == "assets")) {
|
||||
ok_label: Some(|app| match (io::is_web(app), app.ui.dialog.as_ref().map(|d| d.str("tab")).as_deref()) {
|
||||
(true, _) => "Download",
|
||||
(false, true) => "Export Asset",
|
||||
(false, false) => "Export Artboard",
|
||||
(false, Some("assets")) => "Export Asset",
|
||||
(false, Some("layers")) => "Export Layer",
|
||||
(false, Some("selection")) => "Export Selection",
|
||||
(false, _) => "Export Artboard",
|
||||
}),
|
||||
min_width: 660.0,
|
||||
..DialogSpec::FORM
|
||||
@@ -39,20 +52,58 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
|
||||
/// `Dialog::kind` of this dialog.
|
||||
pub const KIND: &str = "exportForScreens";
|
||||
|
||||
/// The format choices of a row: (label, format, JPEG quality).
|
||||
const FORMATS: [(&str, &str, Option<u8>); 10] = [
|
||||
("PNG", "png", None),
|
||||
("PNG 8", "png8", None),
|
||||
("JPG 100", "jpg", Some(100)),
|
||||
("JPG 80", "jpg", Some(80)),
|
||||
("JPG 50", "jpg", Some(50)),
|
||||
("WebP", "webp", None),
|
||||
("GIF", "gif", None),
|
||||
("TIFF", "tiff", None),
|
||||
("SVG", "svg", None),
|
||||
("PDF", "pdf", None),
|
||||
/// The format choices of a row: (label, format, JPEG quality, with printer's marks).
|
||||
const FORMATS: [(&str, &str, Option<u8>, bool); 14] = [
|
||||
("PNG", "png", None, false),
|
||||
("PNG 8", "png8", None, false),
|
||||
("JPG 100", "jpg", Some(100), false),
|
||||
("JPG 80", "jpg", Some(80), false),
|
||||
("JPG 50", "jpg", Some(50), false),
|
||||
("WebP", "webp", None, false),
|
||||
("GIF", "gif", None, false),
|
||||
("TIFF", "tiff", None, false),
|
||||
("SVG", "svg", None, false),
|
||||
("PDF", "pdf", None, false),
|
||||
("PDF Print", "pdf", None, true),
|
||||
("EPS", "eps", None, false),
|
||||
("AI", "ai", None, false),
|
||||
("VectorCraft", "vectorcraft", None, false),
|
||||
];
|
||||
const FORMAT_LABELS: [&str; 10] = ["PNG", "PNG 8", "JPG 100", "JPG 80", "JPG 50", "WebP", "GIF", "TIFF", "SVG", "PDF"];
|
||||
const FORMAT_LABELS: [&str; 14] =
|
||||
["PNG", "PNG 8", "JPG 100", "JPG 80", "JPG 50", "WebP", "GIF", "TIFF", "SVG", "PDF", "PDF Print", "EPS", "AI", "VectorCraft"];
|
||||
|
||||
/// The printer's marks a PDF Print row writes: trim marks, registration targets, colour bars and
|
||||
/// page information.
|
||||
fn print_marks() -> Value {
|
||||
json!({"trim": true, "registration": true, "colorBars": true, "pageInfo": true})
|
||||
}
|
||||
|
||||
/// Where the files go: (`folders` value, label).
|
||||
const FOLDERS: [(&str, &str); 5] = [
|
||||
("none", "All in One Folder"),
|
||||
("rows", "By Size or Row"),
|
||||
("format", "By Format"),
|
||||
("item", "By Artboard or Item"),
|
||||
("itemFormat", "By Item, Then Format"),
|
||||
];
|
||||
const FOLDER_LABELS: [&str; 5] = ["All in One Folder", "By Size or Row", "By Format", "By Artboard or Item", "By Item, Then Format"];
|
||||
|
||||
/// The tokens Insert adds to the file name pattern: (label, token).
|
||||
const TOKENS: [(&str, &str); 8] = [
|
||||
("Document Name", "<filename>"),
|
||||
("Artboard or Item Name", "<name>"),
|
||||
("Number (####)", "####"),
|
||||
("Date (YYYYMMDD)", "YYYYMMDD"),
|
||||
("Date (YYYY-MM-DD)", "YYYY-MM-DD"),
|
||||
("Time (hhmmss)", "<time>"),
|
||||
("Format", "<format>"),
|
||||
("Scale", "<scale>"),
|
||||
];
|
||||
const TOKEN_LABELS: [&str; 8] =
|
||||
["Document Name", "Artboard or Item Name", "Number (####)", "Date (YYYYMMDD)", "Date (YYYY-MM-DD)", "Time (hhmmss)", "Format", "Scale"];
|
||||
|
||||
/// The tabs: (`tab` value, label).
|
||||
const TABS: [&str; 4] = ["artboards", "assets", "layers", "selection"];
|
||||
|
||||
/// The formats Format Settings has options for: (format, label).
|
||||
const SETTINGS: [(&str, &str); 8] =
|
||||
@@ -115,6 +166,12 @@ pub fn open(app: &mut VectorcraftApp) {
|
||||
settings.insert(id.into(), Value::Object(saved_options(app, &saved, id)));
|
||||
}
|
||||
let folder = get("folder").and_then(Value::as_str).filter(|f| !f.is_empty()).map(str::to_string).or_else(fileio::export_folder);
|
||||
// Where files go: the saved choice, else a folder per row as Create Sub-folders did.
|
||||
let folders = get("folders")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|f| FOLDERS.iter().any(|(id, _)| id == f))
|
||||
.map_or_else(|| if flag("subfolders", preset.is_some()) { "rows" } else { "none" }.to_string(), str::to_string);
|
||||
let layers: Vec<u64> = app.session.active().map(|st| st.doc.layers.iter().map(|l| l.id.0).collect()).unwrap_or_default();
|
||||
let fields = json!({
|
||||
"tab": "artboards",
|
||||
"select": select,
|
||||
@@ -131,6 +188,13 @@ pub fn open(app: &mut VectorcraftApp) {
|
||||
"settings": settings,
|
||||
"__settings": "",
|
||||
"__presets": pdf::presets(&app.session),
|
||||
"layers": layers,
|
||||
"selectionMode": get("selection").and_then(Value::as_str).filter(|m| *m == "each").unwrap_or("one"),
|
||||
"name": get("name").and_then(Value::as_str).unwrap_or_default(),
|
||||
"counterStart": get("counterStart").and_then(Value::as_u64).unwrap_or(1),
|
||||
"folders": folders,
|
||||
"exportPreset": "",
|
||||
"__exportPresets": export_presets(app),
|
||||
});
|
||||
app.ui.dialog = Some(Dialog::new(KIND, fields));
|
||||
}
|
||||
@@ -206,6 +270,29 @@ pub(super) fn range_text(boards: &[bool]) -> String {
|
||||
parts.join(", ")
|
||||
}
|
||||
|
||||
/// The export presets: `[{name, builtIn, settings}]` (built-in first).
|
||||
pub(super) fn export_presets(app: &mut VectorcraftApp) -> Value {
|
||||
app.run("document.exportPresets.list", json!({})).ok().and_then(|r| r.get("presets").cloned()).unwrap_or_else(|| json!([]))
|
||||
}
|
||||
|
||||
/// The art of layer `id` (sublayers unfolded), back to front: what its file holds.
|
||||
fn layer_art(doc: &vectorcraft_doc::Document, id: NodeId, out: &mut Vec<NodeId>) {
|
||||
let Some(n) = doc.node(id) else { return };
|
||||
if n.is_layer() {
|
||||
for c in n.children().into_iter().flatten() {
|
||||
layer_art(doc, c.id, out);
|
||||
}
|
||||
} else {
|
||||
out.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// The checked layers that are still in the document, in stacking order.
|
||||
fn checked_layers(app: &VectorcraftApp, d: &Dialog) -> Vec<u64> {
|
||||
let on: Vec<u64> = d.fields.get("layers").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).collect()).unwrap_or_default();
|
||||
app.session.active().map(|st| st.doc.layers.iter().map(|l| l.id.0).filter(|id| on.contains(id)).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
@@ -215,8 +302,10 @@ fn params(app: &VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
return Err("add a format to export".into());
|
||||
}
|
||||
let mut p = Map::new();
|
||||
// What each row writes a file of: the checked assets on the Assets tab, else the artboards.
|
||||
let pieces = if d.str("tab") == "assets" {
|
||||
// What each row writes a file of: the checked assets, layers, the selection, else the
|
||||
// artboards.
|
||||
let tab = d.str("tab");
|
||||
let pieces = if tab == "assets" {
|
||||
let checked = checked_assets(app, d);
|
||||
if checked.is_empty() {
|
||||
return Err("check an asset to export".into());
|
||||
@@ -224,6 +313,22 @@ fn params(app: &VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let count = checked.len();
|
||||
p.insert("assets".into(), json!(checked));
|
||||
count
|
||||
} else if tab == "layers" {
|
||||
let checked = checked_layers(app, d);
|
||||
if checked.is_empty() {
|
||||
return Err("check a layer to export".into());
|
||||
}
|
||||
let count = checked.len();
|
||||
p.insert("layers".into(), json!(checked));
|
||||
count
|
||||
} else if tab == "selection" {
|
||||
let selected = app.session.active().map_or(0, |st| st.selection.objects.len());
|
||||
if selected == 0 {
|
||||
return Err("select the art to export first".into());
|
||||
}
|
||||
let each = d.str("selectionMode") == "each";
|
||||
p.insert("selection".into(), json!(if each { "each" } else { "one" }));
|
||||
if each { selected } else { 1 }
|
||||
} else {
|
||||
match d.str("select").as_str() {
|
||||
"full" => {
|
||||
@@ -241,10 +346,25 @@ fn params(app: &VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
}
|
||||
};
|
||||
let files = pieces * rows.len();
|
||||
for k in ["includeBleed", "subfolders"] {
|
||||
p.insert(k.into(), json!(d.bool(k)));
|
||||
}
|
||||
p.insert("includeBleed".into(), json!(d.bool("includeBleed")));
|
||||
let folders = d.str("folders");
|
||||
let folders = match (FOLDERS.iter().any(|(id, _)| *id == folders), d.bool("subfolders")) {
|
||||
// Create Sub-folders (as the Asset Export panel and older scripts set it) is a folder per
|
||||
// row while Folders is still All in One Folder.
|
||||
(true, true) if folders == "none" => "rows".to_string(),
|
||||
(true, _) => folders,
|
||||
(false, sub) => if sub { "rows" } else { "none" }.to_string(),
|
||||
};
|
||||
// Create Sub-folders, as the Asset Export panel (which shares these settings) still names it.
|
||||
p.insert("subfolders".into(), json!(folders == "rows"));
|
||||
p.insert("folders".into(), json!(folders));
|
||||
p.insert("prefix".into(), json!(d.str("prefix")));
|
||||
let name = d.str("name");
|
||||
if !name.trim().is_empty() {
|
||||
p.insert("name".into(), json!(name.trim()));
|
||||
}
|
||||
let start = d.fields.get("counterStart").and_then(|v| v.as_u64().or_else(|| v.as_str().and_then(|t| t.trim().parse().ok()))).unwrap_or(1);
|
||||
p.insert("counterStart".into(), json!(start));
|
||||
p.insert("formats".into(), Value::Array(rows));
|
||||
p.insert("settings".into(), d.fields.get("settings").cloned().unwrap_or(json!({})));
|
||||
if io::is_web(app) {
|
||||
@@ -272,18 +392,19 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let tab = usize::from(d.str("tab") == "assets");
|
||||
if let Some(i) = widgets::tab_bar(ui, &[tl!("Artboards"), tl!("Assets")], tab) {
|
||||
d.fields.insert("tab".into(), json!(if i == 1 { "assets" } else { "artboards" }));
|
||||
let tab = TABS.iter().position(|k| *k == d.str("tab")).unwrap_or(0);
|
||||
if let Some(i) = widgets::tab_bar(ui, &[tl!("Artboards"), tl!("Assets"), tl!("Layers"), tl!("Selection")], tab) {
|
||||
d.fields.insert("tab".into(), json!(TABS.get(i).copied().unwrap_or("artboards")));
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal_top(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(270.0);
|
||||
if tab == 1 {
|
||||
assets(app, ui, d);
|
||||
} else {
|
||||
artboards(app, ui, d);
|
||||
match tab {
|
||||
1 => assets(app, ui, d),
|
||||
2 => layers(app, ui, d),
|
||||
3 => selection(app, ui, d),
|
||||
_ => artboards(app, ui, d),
|
||||
}
|
||||
});
|
||||
let (sep, _) = ui.allocate_exact_size(egui::vec2(17.0, 380.0), egui::Sense::hover());
|
||||
@@ -294,14 +415,13 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
if SETTINGS.iter().any(|(id, _)| *id == shown) {
|
||||
format_settings(ui, d, &shown);
|
||||
} else {
|
||||
presets(app, ui, d);
|
||||
ui.add_space(8.0);
|
||||
destination(app, ui, d);
|
||||
ui.add_space(10.0);
|
||||
formats(ui, d);
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("Prefix:"));
|
||||
form::text(ui, d, "prefix", 140.0);
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
naming(app, ui, d);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -449,7 +569,231 @@ fn destination(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
});
|
||||
form::check(ui, d, "openLocation", tl!("Open Location after Export"));
|
||||
}
|
||||
form::check(ui, d, "subfolders", tl!("Create Sub-folders"));
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("Folders:"));
|
||||
let current = d.str("folders");
|
||||
let at = FOLDERS.iter().position(|(id, _)| *id == current).unwrap_or(0);
|
||||
if let Some((id, _)) = widgets::dropdown(ui, "efs-folders", FOLDER_LABELS[at], &FOLDER_LABELS, 190.0).and_then(|i| FOLDERS.get(i)) {
|
||||
d.fields.insert("folders".into(), json!(id));
|
||||
d.fields.insert("subfolders".into(), json!(*id == "rows"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Export Preset: apply a built-in or saved preset (its formats, settings, naming and folders),
|
||||
/// save the dialog's settings as one (Save…, then a name), or delete a saved one.
|
||||
fn presets(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();
|
||||
let names: Vec<&str> = list.iter().filter_map(|p| p["name"].as_str()).collect();
|
||||
let current = d.str("exportPreset");
|
||||
let saved = list.iter().any(|p| p["name"] == current && p["builtIn"] == false);
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("Export Preset:"));
|
||||
let labels: Vec<&str> = std::iter::once("Custom").chain(names.iter().copied()).collect();
|
||||
let at = names.iter().position(|n| *n == current).map_or(0, |i| i + 1);
|
||||
if let Some(i) = widgets::dropdown(ui, "efs-export-preset", labels[at], &labels, 150.0)
|
||||
&& let Some(preset) = i.checked_sub(1).and_then(|i| list.get(i))
|
||||
{
|
||||
apply_preset(d, preset);
|
||||
}
|
||||
if widgets::icon_button(ui, "save", tl!("Save Preset…"), false, 24.0).clicked() {
|
||||
let name = if saved { current.clone() } else { String::new() };
|
||||
d.fields.insert("__savePreset".into(), json!(name));
|
||||
}
|
||||
ui.add_enabled_ui(saved, |ui| {
|
||||
if widgets::icon_button(ui, "trash-2", tl!("Delete Preset"), false, 24.0).clicked()
|
||||
&& 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 d.fields.contains_key("__savePreset") {
|
||||
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 = params(app, d).unwrap_or_else(|_| json!({"formats": d.fields.get("formats").cloned().unwrap_or(json!([]))}));
|
||||
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,
|
||||
}
|
||||
}
|
||||
if ui.small_button(tl!("Cancel")).clicked() {
|
||||
d.fields.remove("__savePreset");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the dialog to preset `preset` (`{name, settings}`): its formats, settings, naming and
|
||||
/// 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!(""));
|
||||
}
|
||||
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 {
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
let folders = st["folders"].as_str().or_else(|| st["subfolders"].as_bool().map(|b| if b { "rows" } else { "none" }));
|
||||
if let Some(f) = folders {
|
||||
d.fields.insert("folders".into(), json!(f));
|
||||
d.fields.insert("subfolders".into(), json!(f == "rows"));
|
||||
}
|
||||
for k in ["name", "prefix"] {
|
||||
d.fields.insert(k.into(), json!(st[k].as_str().unwrap_or_default()));
|
||||
}
|
||||
d.fields.insert("counterStart".into(), json!(st["counterStart"].as_u64().unwrap_or(1)));
|
||||
if let Some(b) = st["includeBleed"].as_bool() {
|
||||
d.fields.insert("includeBleed".into(), json!(b));
|
||||
}
|
||||
if let Some(f) = st["folder"].as_str().filter(|f| !f.is_empty()) {
|
||||
d.fields.insert("folder".into(), json!(f));
|
||||
}
|
||||
d.fields.insert("exportPreset".into(), json!(preset["name"]));
|
||||
}
|
||||
|
||||
/// File Name: the pattern (with Insert for its tokens), the counter's start, the prefix and an
|
||||
/// example of the first file's name.
|
||||
fn naming(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("File Name:"));
|
||||
// A pattern typed by hand is no longer the preset's.
|
||||
if form::text(ui, d, "name", 170.0) {
|
||||
d.fields.insert("exportPreset".into(), json!(""));
|
||||
}
|
||||
if let Some((_, token)) = widgets::dropdown(ui, "efs-name-token", tl!("Insert"), &TOKEN_LABELS, 90.0).and_then(|i| TOKENS.get(i)) {
|
||||
let name = d.str("name");
|
||||
d.fields.insert("name".into(), json!(format!("{name}{token}")));
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("Prefix:"));
|
||||
form::text(ui, d, "prefix", 110.0);
|
||||
widgets::dim_label(ui, tl!("Start at:"));
|
||||
let start = d.fields.get("counterStart").and_then(Value::as_u64).unwrap_or(1) as f64;
|
||||
if let Some(v) = widgets::range_field(ui, "efs-counter-start", start, 0.0..=999_999.0, "", 0, 64.0) {
|
||||
d.fields.insert("counterStart".into(), json!(v.round().max(0.0) as u64));
|
||||
}
|
||||
});
|
||||
let example = example_name(app, d);
|
||||
widgets::dim_label(ui, &crate::i18n::fmt(tl!("Example: {name}"), &[("name", &example)]));
|
||||
}
|
||||
|
||||
/// The first file the export would write, as the folders and pattern name it.
|
||||
pub(super) fn example_name(app: &VectorcraftApp, d: &Dialog) -> String {
|
||||
let Some(st) = app.session.active() else { return String::new() };
|
||||
let row = d.fields.get("formats").and_then(Value::as_array).and_then(|r| r.first()).cloned().unwrap_or(json!({}));
|
||||
let format = row["format"].as_str().and_then(fileio::format);
|
||||
let ext = format.and_then(|f| f.extensions.first().copied()).unwrap_or("png");
|
||||
let raster = format.is_none_or(|f| f.raster);
|
||||
let scale = if raster { row["scale"].as_str().unwrap_or("1x").to_string() } else { String::new() };
|
||||
let suffix = row["suffix"].as_str().unwrap_or_default().to_string();
|
||||
let item = match d.str("tab").as_str() {
|
||||
"assets" => checked_assets(app, d).first().and_then(|id| st.doc.asset(*id)).map(|a| a.name.clone()),
|
||||
"layers" => checked_layers(app, d).first().and_then(|id| st.doc.node(NodeId(*id))).map(|n| n.display_name()),
|
||||
"selection" => Some("Selection".to_string()),
|
||||
_ => st.doc.artboards.first().map(|a| a.name.clone()),
|
||||
}
|
||||
.unwrap_or_else(|| "Artboard 1".into());
|
||||
let filename = fileio::file_stem(&st.doc.title);
|
||||
let when = vectorcraft_doc::metadata::now_unix().map_or([1970, 1, 1, 0, 0, 0], vectorcraft_doc::metadata::civil);
|
||||
let start = d.fields.get("counterStart").and_then(Value::as_u64).unwrap_or(1);
|
||||
let pattern = d.str("name");
|
||||
let stem = if pattern.trim().is_empty() {
|
||||
format!("{item}{suffix}")
|
||||
} else {
|
||||
let parts =
|
||||
fileio::FileNameParts { filename: &filename, name: &item, index: 1, counter: start, format: ext, scale: &scale, suffix: &suffix, when };
|
||||
let named = fileio::render_file_name(&pattern, &parts);
|
||||
if pattern.contains("<suffix>") { named } else { format!("{named}{suffix}") }
|
||||
};
|
||||
let file = format!("{}{stem}.{ext}", d.str("prefix"));
|
||||
match d.str("folders").as_str() {
|
||||
"rows" => format!("{}/{file}", if raster { scale } else { ext.to_uppercase() }),
|
||||
"format" => format!("{ext}/{file}"),
|
||||
"item" => format!("{item}/{file}"),
|
||||
"itemFormat" => format!("{item}/{ext}/{file}"),
|
||||
_ => file,
|
||||
}
|
||||
}
|
||||
|
||||
/// The Layers tab: each top-level layer's thumbnail and checkbox (a file of its art alone),
|
||||
/// Select All and Clear.
|
||||
fn layers(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let mut on = checked_layers(app, d);
|
||||
let before = on.clone();
|
||||
ui.horizontal(|ui| {
|
||||
if ui.small_button(tl!("Select All")).clicked() {
|
||||
on = st.doc.layers.iter().map(|l| l.id.0).collect();
|
||||
}
|
||||
if ui.small_button(tl!("Clear")).clicked() {
|
||||
on.clear();
|
||||
}
|
||||
});
|
||||
egui::ScrollArea::vertical().max_height(330.0).show(ui, |ui| {
|
||||
for l in &st.doc.layers {
|
||||
ui.horizontal(|ui| {
|
||||
let (r, _) = ui.allocate_exact_size(egui::vec2(46.0, 46.0), egui::Sense::hover());
|
||||
let mut art = vec![];
|
||||
layer_art(&st.doc, l.id, &mut art);
|
||||
// An id of its own for the thumbnail cache, away from the assets' ids.
|
||||
let a = ExportAsset { id: u64::MAX - l.id.0, name: l.display_name(), nodes: art };
|
||||
crate::panels::asset_export::paint_thumb(ui, st, &a, r);
|
||||
let checked = on.contains(&l.id.0);
|
||||
if crate::widgets::check_name(ui, &l.display_name(), checked) {
|
||||
on.retain(|id| *id != l.id.0);
|
||||
if !checked {
|
||||
on.push(l.id.0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if on != before {
|
||||
d.fields.insert("layers".into(), json!(on));
|
||||
}
|
||||
}
|
||||
|
||||
/// The Selection tab: what is selected (a thumbnail and the count), as one file or a file per
|
||||
/// object.
|
||||
fn selection(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let ids = st.selection.objects.clone();
|
||||
if ids.is_empty() {
|
||||
ui.add_space(24.0);
|
||||
widgets::dim_label(ui, tl!("Nothing is selected. Select the art to export, then open this tab again."));
|
||||
return;
|
||||
}
|
||||
let (r, _) = ui.allocate_exact_size(egui::vec2(120.0, 120.0), egui::Sense::hover());
|
||||
// The selection changes without a new revision: its ids name its thumbnail.
|
||||
let key = ids.iter().fold(0x005e_1ec7_u64, |h, id| h.rotate_left(7) ^ id.0);
|
||||
let a = ExportAsset { id: u64::MAX / 2 + (key % (u64::MAX / 4)), name: "Selection".into(), nodes: ids.clone() };
|
||||
crate::panels::asset_export::paint_thumb(ui, st, &a, r);
|
||||
let count = ids.len().to_string();
|
||||
widgets::dim_label(ui, &crate::i18n::fmt(tl!("{n} objects selected"), &[("n", &count)]));
|
||||
ui.add_space(6.0);
|
||||
let mode = d.str("selectionMode");
|
||||
if widgets::radio(ui, tl!("One File of the Selection"), mode != "each", true) {
|
||||
d.fields.insert("selectionMode".into(), json!("one"));
|
||||
}
|
||||
if widgets::radio(ui, tl!("A File per Object"), mode == "each", true) {
|
||||
d.fields.insert("selectionMode".into(), json!("each"));
|
||||
}
|
||||
}
|
||||
|
||||
/// The preset, the Format Settings gear (which sets `__settings`), the format rows (scale, suffix,
|
||||
@@ -516,15 +860,26 @@ pub(crate) fn formats(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
edited = true;
|
||||
}
|
||||
let (format, quality) = (row["format"].as_str().unwrap_or("png"), row["quality"].as_u64());
|
||||
let current = FORMATS.iter().position(|(_, f, q)| *f == format && q.is_none_or(|q| Some(u64::from(q)) == quality));
|
||||
let marked = row.get("marks").is_some_and(|m| !m.is_null());
|
||||
let current =
|
||||
FORMATS.iter().position(|(_, f, q, print)| *f == format && *print == marked && q.is_none_or(|q| Some(u64::from(q)) == quality));
|
||||
let label = current.map_or_else(|| format.to_uppercase(), |i| FORMAT_LABELS[i].to_string());
|
||||
if let Some((_, f, q)) = widgets::dropdown(ui, ("efs-fmt", i), &label, &FORMAT_LABELS, format_w).and_then(|i| FORMATS.get(i)) {
|
||||
if let Some((_, f, q, print)) = widgets::dropdown(ui, ("efs-fmt", i), &label, &FORMAT_LABELS, format_w).and_then(|i| FORMATS.get(i)) {
|
||||
row["format"] = json!(f);
|
||||
if let Some(o) = row.as_object_mut() {
|
||||
match q {
|
||||
Some(q) => _ = o.insert("quality".into(), json!(q)),
|
||||
None => _ = o.remove("quality"),
|
||||
}
|
||||
if *print {
|
||||
o.insert("marks".into(), print_marks());
|
||||
// A file of its own next to the plain PDF's.
|
||||
if o.get("suffix").and_then(Value::as_str).is_none_or(str::is_empty) {
|
||||
o.insert("suffix".into(), json!("-print"));
|
||||
}
|
||||
} else {
|
||||
o.remove("marks");
|
||||
}
|
||||
}
|
||||
edited = true;
|
||||
}
|
||||
|
||||
@@ -529,3 +529,99 @@ fn tiff_dropdown_exports_scaled_artboards_with_options_and_reopens() {
|
||||
h.choose(&mut app, "SVG", "PNG");
|
||||
assert_eq!(field(&app, "formats")[0]["format"], "png");
|
||||
}
|
||||
|
||||
/// Batch exporting (#1935): a file name pattern and a folder per format, the Layers and Selection
|
||||
/// tabs, and export presets applied and saved from the dialog.
|
||||
#[test]
|
||||
fn batch_naming_folders_layers_selection_and_presets() {
|
||||
let (mut app, _) = app(3, false);
|
||||
let dir = temp_folder("batch");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
let folder = dir.to_string_lossy().to_string();
|
||||
open(&mut app);
|
||||
assert_eq!(field(&app, "folders"), json!("none"));
|
||||
// Every tab draws; the OK button says what is exported.
|
||||
for (tab, ok) in [("layers", "Export Layer"), ("selection", "Export Selection"), ("assets", "Export Asset"), ("artboards", "Export Artboard")] {
|
||||
set(&mut app, "tab", json!(tab));
|
||||
frame(&mut app);
|
||||
assert_eq!((spec(export_for_screens::KIND).ok_label.unwrap())(&app), ok);
|
||||
}
|
||||
// Artboards 1–2 as SVG and print PDF, named by pattern, a folder per format.
|
||||
set(&mut app, "folder", json!(folder));
|
||||
set(&mut app, "select", json!("range"));
|
||||
set(&mut app, "range", json!("1-2"));
|
||||
set(&mut app, "name", json!("ACME-<name>-##"));
|
||||
set(&mut app, "folders", json!("format"));
|
||||
set(
|
||||
&mut app,
|
||||
"formats",
|
||||
json!([{"format": "svg", "scale": "1x", "suffix": ""}, {"format": "pdf", "scale": "1x", "suffix": "-print", "marks": {"trim": true}}]),
|
||||
);
|
||||
frame(&mut app);
|
||||
{
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!(export_for_screens::example_name(&app, d), "svg/ACME-Artboard 1-01.svg");
|
||||
}
|
||||
let r = confirm(&mut app).unwrap();
|
||||
let files: Vec<String> = r["files"].as_array().unwrap().iter().map(|f| f.as_str().unwrap().to_string()).collect();
|
||||
assert_eq!(
|
||||
files,
|
||||
[
|
||||
format!("{folder}/svg/ACME-Artboard 1-01.svg"),
|
||||
format!("{folder}/pdf/ACME-Artboard 1-01-print.pdf"),
|
||||
format!("{folder}/svg/ACME-Artboard 2-02.svg"),
|
||||
format!("{folder}/pdf/ACME-Artboard 2-02-print.pdf")
|
||||
]
|
||||
);
|
||||
// It reopens on the pattern and the folders.
|
||||
open(&mut app);
|
||||
assert_eq!((field(&app, "name"), field(&app, "folders")), (json!("ACME-<name>-##"), json!("format")));
|
||||
|
||||
// The Layers tab: one file of a layer's art.
|
||||
let layer = app.session.active().unwrap().doc.layers[0].id.0;
|
||||
set(&mut app, "folder", json!(folder));
|
||||
set(&mut app, "tab", json!("layers"));
|
||||
set(&mut app, "layers", json!([layer]));
|
||||
set(&mut app, "name", json!(""));
|
||||
set(&mut app, "folders", json!("none"));
|
||||
set(&mut app, "formats", json!([{"format": "png", "scale": "1x", "suffix": ""}]));
|
||||
let r = confirm(&mut app).unwrap();
|
||||
assert_eq!(r["files"][0], json!(format!("{folder}/Layer-1.png")));
|
||||
|
||||
// The Selection tab: nothing selected keeps the dialog open; a file per selected object.
|
||||
open(&mut app);
|
||||
app.run("select.none", json!({})).unwrap();
|
||||
set(&mut app, "folder", json!(folder));
|
||||
set(&mut app, "tab", json!("selection"));
|
||||
frame(&mut app);
|
||||
assert!(confirm(&mut app).is_err());
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
set(&mut app, "selectionMode", json!("each"));
|
||||
set(&mut app, "name", json!("piece-#"));
|
||||
set(&mut app, "formats", json!([{"format": "svg", "scale": "1x", "suffix": ""}]));
|
||||
frame(&mut app);
|
||||
let r = confirm(&mut app).unwrap();
|
||||
assert_eq!(r["files"][0], json!(format!("{folder}/piece-1.svg")));
|
||||
|
||||
// Presets: the Logo Package sets the formats, naming and folders; the dialog saves its own.
|
||||
open(&mut app);
|
||||
let presets = field(&app, "__exportPresets");
|
||||
let logo = presets.as_array().unwrap().iter().find(|p| p["name"] == "Logo Package").unwrap().clone();
|
||||
export_for_screens::apply_preset(app.ui.dialog.as_mut().unwrap(), &logo);
|
||||
assert_eq!(field(&app, "formats").as_array().unwrap().len(), 5);
|
||||
assert_eq!(
|
||||
(field(&app, "folders"), field(&app, "name"), field(&app, "exportPreset")),
|
||||
(json!("format"), json!("<filename>-<name>"), json!("Logo Package"))
|
||||
);
|
||||
frame(&mut app);
|
||||
set(&mut app, "folder", json!(folder));
|
||||
set(&mut app, "__savePreset", json!("Client Pack"));
|
||||
frame(&mut app);
|
||||
let saved = app
|
||||
.run("document.exportPresets.save", json!({"name": "Client Pack", "settings": {"formats": field(&app, "formats"), "folders": "item"}}))
|
||||
.unwrap();
|
||||
assert_eq!(saved["name"], "Client Pack");
|
||||
let list = export_for_screens::export_presets(&mut app);
|
||||
assert!(list.as_array().unwrap().iter().any(|p| p["name"] == "Client Pack" && p["builtIn"] == false));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
@@ -958,6 +958,32 @@
|
||||
Export All… Експортиране на всичко…
|
||||
Export and Clipboard Transparency Flattener Settings Настройки за сплескване на прозрачността за експортиране и клипборд
|
||||
Export Artboard Експортиране на работна област
|
||||
A File per Object Отделен файл за всеки обект
|
||||
Example: {name} Пример: {name}
|
||||
Export Preset: Набор за експортиране:
|
||||
Folders: Папки:
|
||||
Insert Вмъкване
|
||||
Nothing is selected. Select the art to export, then open this tab again. Нищо не е избрано. Изберете графиката за експортиране и отворете този раздел отново.
|
||||
One File of the Selection Един файл от избраното
|
||||
Start at: Начало от:
|
||||
{n} objects selected Избрани обекти: {n}
|
||||
Delete Export Preset Изтриване на набор за експортиране
|
||||
Export Presets Набори за експортиране
|
||||
Save Export Preset Запазване на набор за експортиране
|
||||
Export Layer Експортиране на слой
|
||||
Export Selection Експортиране на избраното
|
||||
All in One Folder Всичко в една папка
|
||||
By Size or Row По размер или ред
|
||||
By Format По формат
|
||||
By Artboard or Item По монтажна област или елемент
|
||||
By Item, Then Format По елемент, после по формат
|
||||
Document Name Име на документа
|
||||
Artboard or Item Name Име на монтажната област или елемента
|
||||
Number (####) Номер (####)
|
||||
Date (YYYYMMDD) Дата (ГГГГММДД)
|
||||
Date (YYYY-MM-DD) Дата (ГГГГ-ММ-ДД)
|
||||
Time (hhmmss) Час (ччммсс)
|
||||
PDF Print PDF за печат
|
||||
Export As Експортиране като
|
||||
Export As PNG… Експортиране като PNG…
|
||||
Export As SVG… Експортиране като SVG…
|
||||
@@ -3644,6 +3670,8 @@ axis Both И двете
|
||||
@msg cannot split an open path at its end point отворен път не може да бъде разделен в крайната му точка
|
||||
@msg closing needs three anchors затварянето изисква три опорни точки
|
||||
@msg check an asset to export изберете актив за експортиране
|
||||
@msg check a layer to export отметнете слой за експортиране
|
||||
@msg select the art to export first първо изберете графиката за експортиране
|
||||
@msg choice must be "merge" or "add", not `{choice}` изборът трябва да бъде "merge" или "add", а не `{choice}`
|
||||
@msg choose a direction to divide in изберете посока за разделяне
|
||||
@msg choose a folder to export to изберете папка за експортиране
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2667,6 +2667,32 @@ lighting Light Světlo
|
||||
Summary Souhrn
|
||||
Download Stáhnout
|
||||
Export Artboard Exportovat pracovní plochu
|
||||
A File per Object Jeden soubor na objekt
|
||||
Example: {name} Příklad: {name}
|
||||
Export Preset: Přednastavení exportu:
|
||||
Folders: Složky:
|
||||
Insert Vložit
|
||||
Nothing is selected. Select the art to export, then open this tab again. Nic není vybráno. Vyberte kresbu k exportu a znovu otevřete tuto kartu.
|
||||
One File of the Selection Jeden soubor z výběru
|
||||
Start at: Začít od:
|
||||
{n} objects selected Vybráno objektů: {n}
|
||||
Delete Export Preset Odstranit přednastavení exportu
|
||||
Export Presets Přednastavení exportu
|
||||
Save Export Preset Uložit přednastavení exportu
|
||||
Export Layer Exportovat vrstvu
|
||||
Export Selection Exportovat výběr
|
||||
All in One Folder Vše v jedné složce
|
||||
By Size or Row Podle velikosti nebo řádku
|
||||
By Format Podle formátu
|
||||
By Artboard or Item Podle kreslicí plochy nebo položky
|
||||
By Item, Then Format Podle položky, pak formátu
|
||||
Document Name Název dokumentu
|
||||
Artboard or Item Name Název kreslicí plochy nebo položky
|
||||
Number (####) Číslo (####)
|
||||
Date (YYYYMMDD) Datum (RRRRMMDD)
|
||||
Date (YYYY-MM-DD) Datum (RRRR-MM-DD)
|
||||
Time (hhmmss) Čas (hhmmss)
|
||||
PDF Print PDF pro tisk
|
||||
Encoding: Kódování:
|
||||
UTF-16 (Unicode) UTF-16 (Unicode)
|
||||
Line Endings: Konce řádků:
|
||||
@@ -4018,6 +4044,8 @@ imageTrace Outline Obrys
|
||||
@msg cannot split an open path at its end point otevřenou cestu nelze rozdělit v jejím koncovém bodu
|
||||
@msg closing needs three anchors uzavření vyžaduje tři kotevní body
|
||||
@msg check an asset to export zaškrtněte prvek k exportu
|
||||
@msg check a layer to export zaškrtněte vrstvu k exportu
|
||||
@msg select the art to export first nejprve vyberte kresbu k exportu
|
||||
@msg choice must be "merge" or "add", not `{choice}` volba musí být "merge" nebo "add", ne `{choice}`
|
||||
@msg choose a direction to divide in zvolte směr rozdělení
|
||||
@msg choose a folder to export to zvolte složku pro export
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -959,6 +959,32 @@
|
||||
Export All… Alle exportieren…
|
||||
Export and Clipboard Transparency Flattener Settings Einstellungen für die Transparenzreduzierung bei Export und Zwischenablage
|
||||
Export Artboard Zeichenfläche exportieren
|
||||
A File per Object Eine Datei pro Objekt
|
||||
Example: {name} Beispiel: {name}
|
||||
Export Preset: Exportvorgabe:
|
||||
Folders: Ordner:
|
||||
Insert Einfügen
|
||||
Nothing is selected. Select the art to export, then open this tab again. Nichts ausgewählt. Wählen Sie das zu exportierende Bildmaterial aus und öffnen Sie diese Registerkarte erneut.
|
||||
One File of the Selection Eine Datei der Auswahl
|
||||
Start at: Beginnen bei:
|
||||
{n} objects selected {n} Objekte ausgewählt
|
||||
Delete Export Preset Exportvorgabe löschen
|
||||
Export Presets Exportvorgaben
|
||||
Save Export Preset Exportvorgabe speichern
|
||||
Export Layer Ebene exportieren
|
||||
Export Selection Auswahl exportieren
|
||||
All in One Folder Alles in einem Ordner
|
||||
By Size or Row Nach Größe oder Zeile
|
||||
By Format Nach Format
|
||||
By Artboard or Item Nach Zeichenfläche oder Element
|
||||
By Item, Then Format Nach Element, dann Format
|
||||
Document Name Dokumentname
|
||||
Artboard or Item Name Name der Zeichenfläche oder des Elements
|
||||
Number (####) Nummer (####)
|
||||
Date (YYYYMMDD) Datum (JJJJMMTT)
|
||||
Date (YYYY-MM-DD) Datum (JJJJ-MM-TT)
|
||||
Time (hhmmss) Uhrzeit (hhmmss)
|
||||
PDF Print PDF Druck
|
||||
Export As Exportieren als
|
||||
Export As PNG… Als PNG exportieren…
|
||||
Export As SVG… Als SVG exportieren…
|
||||
@@ -3622,6 +3648,8 @@ theme Appearance Erscheinungsbild
|
||||
@msg cannot split an open path at its end point ein offener Pfad kann nicht an seinem Endpunkt geteilt werden
|
||||
@msg closing needs three anchors zum Schließen sind drei Ankerpunkte nötig
|
||||
@msg check an asset to export markieren Sie ein Asset für den Export
|
||||
@msg check a layer to export wählen Sie eine Ebene für den Export
|
||||
@msg select the art to export first wählen Sie zuerst das zu exportierende Bildmaterial aus
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice muss "merge" oder "add" sein, nicht `{choice}`
|
||||
@msg choose a direction to divide in wählen Sie eine Richtung für die Unterteilung
|
||||
@msg choose a folder to export to wählen Sie einen Ordner für den Export
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -924,6 +924,32 @@
|
||||
Export All… Εξαγωγή όλων…
|
||||
Export and Clipboard Transparency Flattener Settings Ρυθμίσεις ισοπέδωσης διαφάνειας εξαγωγής και πρόχειρου
|
||||
Export Artboard Εξαγωγή πίνακα σχεδίασης
|
||||
A File per Object Ένα αρχείο ανά αντικείμενο
|
||||
Example: {name} Παράδειγμα: {name}
|
||||
Export Preset: Προκαθορισμένο εξαγωγής:
|
||||
Folders: Φάκελοι:
|
||||
Insert Εισαγωγή
|
||||
Nothing is selected. Select the art to export, then open this tab again. Δεν υπάρχει επιλογή. Επιλέξτε το γραφικό για εξαγωγή και ανοίξτε ξανά αυτή την καρτέλα.
|
||||
One File of the Selection Ένα αρχείο της επιλογής
|
||||
Start at: Έναρξη από:
|
||||
{n} objects selected {n} αντικείμενα επιλεγμένα
|
||||
Delete Export Preset Διαγραφή προκαθορισμένου εξαγωγής
|
||||
Export Presets Προκαθορισμένα εξαγωγής
|
||||
Save Export Preset Αποθήκευση προκαθορισμένου εξαγωγής
|
||||
Export Layer Εξαγωγή επιπέδου
|
||||
Export Selection Εξαγωγή επιλογής
|
||||
All in One Folder Όλα σε έναν φάκελο
|
||||
By Size or Row Κατά μέγεθος ή γραμμή
|
||||
By Format Κατά μορφή
|
||||
By Artboard or Item Κατά πινακίδα ή στοιχείο
|
||||
By Item, Then Format Κατά στοιχείο, έπειτα μορφή
|
||||
Document Name Όνομα εγγράφου
|
||||
Artboard or Item Name Όνομα πινακίδας ή στοιχείου
|
||||
Number (####) Αριθμός (####)
|
||||
Date (YYYYMMDD) Ημερομηνία (ΕΕΕΕΜΜΗΗ)
|
||||
Date (YYYY-MM-DD) Ημερομηνία (ΕΕΕΕ-ΜΜ-ΗΗ)
|
||||
Time (hhmmss) Ώρα (ωωλλδδ)
|
||||
PDF Print PDF για εκτύπωση
|
||||
Export As Εξαγωγή ως
|
||||
Export As PNG… Εξαγωγή ως PNG…
|
||||
Export As SVG… Εξαγωγή ως SVG…
|
||||
@@ -3682,6 +3708,8 @@ theme Appearance Εμφάνιση
|
||||
@msg cannot split an open path at its end point δεν είναι δυνατή η διαίρεση ανοιχτής διαδρομής στο τελικό σημείο της
|
||||
@msg closing needs three anchors το κλείσιμο απαιτεί τρία σημεία αγκύρωσης
|
||||
@msg check an asset to export επιλέξτε στοιχείο για εξαγωγή
|
||||
@msg check a layer to export επιλέξτε ένα επίπεδο για εξαγωγή
|
||||
@msg select the art to export first επιλέξτε πρώτα το γραφικό για εξαγωγή
|
||||
@msg choice must be "merge" or "add", not `{choice}` η επιλογή πρέπει να είναι "merge" ή "add", όχι `{choice}`
|
||||
@msg choose a direction to divide in επιλέξτε κατεύθυνση διαίρεσης
|
||||
@msg choose a folder to export to επιλέξτε φάκελο εξαγωγής
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 1897 and column 19.
|
@@ -975,6 +975,32 @@
|
||||
Export All… Exportar todo…
|
||||
Export and Clipboard Transparency Flattener Settings Ajustes del acoplador de transparencia para exportar y portapapeles
|
||||
Export Artboard Exportar mesa de trabajo
|
||||
A File per Object Un archivo por objeto
|
||||
Example: {name} Ejemplo: {name}
|
||||
Export Preset: Ajuste de exportación:
|
||||
Folders: Carpetas:
|
||||
Insert Insertar
|
||||
Nothing is selected. Select the art to export, then open this tab again. No hay nada seleccionado. Seleccione la ilustración que desea exportar y vuelva a abrir esta pestaña.
|
||||
One File of the Selection Un archivo de la selección
|
||||
Start at: Empezar en:
|
||||
{n} objects selected {n} objetos seleccionados
|
||||
Delete Export Preset Eliminar ajuste de exportación
|
||||
Export Presets Ajustes de exportación
|
||||
Save Export Preset Guardar ajuste de exportación
|
||||
Export Layer Exportar capa
|
||||
Export Selection Exportar selección
|
||||
All in One Folder Todo en una carpeta
|
||||
By Size or Row Por tamaño o fila
|
||||
By Format Por formato
|
||||
By Artboard or Item Por mesa de trabajo o elemento
|
||||
By Item, Then Format Por elemento y luego formato
|
||||
Document Name Nombre del documento
|
||||
Artboard or Item Name Nombre de la mesa de trabajo o del elemento
|
||||
Number (####) Número (####)
|
||||
Date (YYYYMMDD) Fecha (AAAAMMDD)
|
||||
Date (YYYY-MM-DD) Fecha (AAAA-MM-DD)
|
||||
Time (hhmmss) Hora (hhmmss)
|
||||
PDF Print PDF para imprenta
|
||||
Export As Exportar como
|
||||
Export As PNG… Exportar como PNG…
|
||||
Export As SVG… Exportar como SVG…
|
||||
@@ -3719,6 +3745,8 @@ axis Both Ambos
|
||||
@msg cannot split an open path at its end point no se puede dividir un trazado abierto en su punto final
|
||||
@msg closing needs three anchors cerrar necesita tres puntos de ancla
|
||||
@msg check an asset to export marca un recurso para exportar
|
||||
@msg check a layer to export marque una capa para exportar
|
||||
@msg select the art to export first seleccione primero la ilustración que desea exportar
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice debe ser "merge" o "add", no `{choice}`
|
||||
@msg choose a direction to divide in elige una dirección en la que dividir
|
||||
@msg choose a folder to export to elige una carpeta a la que exportar
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2622,6 +2622,32 @@ lighting Light Valo
|
||||
Summary Yhteenveto
|
||||
Download Lataa
|
||||
Export Artboard Vie piirtoalusta
|
||||
A File per Object Yksi tiedosto objektia kohden
|
||||
Example: {name} Esimerkki: {name}
|
||||
Export Preset: Vientiesiasetus:
|
||||
Folders: Kansiot:
|
||||
Insert Lisää
|
||||
Nothing is selected. Select the art to export, then open this tab again. Mitään ei ole valittu. Valitse vietävä kuvitus ja avaa tämä välilehti uudelleen.
|
||||
One File of the Selection Yksi tiedosto valinnasta
|
||||
Start at: Aloita luvusta:
|
||||
{n} objects selected {n} objektia valittu
|
||||
Delete Export Preset Poista vientiesiasetus
|
||||
Export Presets Vientiesiasetukset
|
||||
Save Export Preset Tallenna vientiesiasetus
|
||||
Export Layer Vie taso
|
||||
Export Selection Vie valinta
|
||||
All in One Folder Kaikki yhteen kansioon
|
||||
By Size or Row Koon tai rivin mukaan
|
||||
By Format Muodon mukaan
|
||||
By Artboard or Item Piirtoalueen tai kohteen mukaan
|
||||
By Item, Then Format Kohteen, sitten muodon mukaan
|
||||
Document Name Asiakirjan nimi
|
||||
Artboard or Item Name Piirtoalueen tai kohteen nimi
|
||||
Number (####) Numero (####)
|
||||
Date (YYYYMMDD) Päivämäärä (VVVVKKPP)
|
||||
Date (YYYY-MM-DD) Päivämäärä (VVVV-KK-PP)
|
||||
Time (hhmmss) Aika (ttmmss)
|
||||
PDF Print Paino-PDF
|
||||
Encoding: Koodaus:
|
||||
UTF-16 (Unicode) UTF-16 (Unicode)
|
||||
Line Endings: Rivinvaihdot:
|
||||
@@ -3941,6 +3967,8 @@ imageTrace Outline Ääriviiva
|
||||
@msg cannot split an open path at its end point avointa polkua ei voi jakaa sen päätepisteestä
|
||||
@msg closing needs three anchors sulkeminen vaatii kolme ankkuria
|
||||
@msg check an asset to export valitse vietävä resurssi
|
||||
@msg check a layer to export valitse vietävä taso
|
||||
@msg select the art to export first valitse ensin vietävä kuvitus
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice-arvon on oltava "merge" tai "add", ei `{choice}`
|
||||
@msg choose a direction to divide in valitse jakosuunta
|
||||
@msg choose a folder to export to valitse kansio, johon viedään
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -975,6 +975,32 @@
|
||||
Export All… Tout exporter…
|
||||
Export and Clipboard Transparency Flattener Settings Paramètres d’aplatissement des transparences pour l’exportation et le presse-papiers
|
||||
Export Artboard Exporter le plan de travail
|
||||
A File per Object Un fichier par objet
|
||||
Example: {name} Exemple : {name}
|
||||
Export Preset: Paramètre prédéfini d’export :
|
||||
Folders: Dossiers :
|
||||
Insert Insérer
|
||||
Nothing is selected. Select the art to export, then open this tab again. Aucune sélection. Sélectionnez l’illustration à exporter, puis rouvrez cet onglet.
|
||||
One File of the Selection Un fichier de la sélection
|
||||
Start at: Commencer à :
|
||||
{n} objects selected {n} objets sélectionnés
|
||||
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 Layer Exporter le calque
|
||||
Export Selection Exporter la sélection
|
||||
All in One Folder Tout dans un dossier
|
||||
By Size or Row Par taille ou ligne
|
||||
By Format Par format
|
||||
By Artboard or Item Par plan de travail ou élément
|
||||
By Item, Then Format Par élément, puis format
|
||||
Document Name Nom du document
|
||||
Artboard or Item Name Nom du plan de travail ou de l’élément
|
||||
Number (####) Numéro (####)
|
||||
Date (YYYYMMDD) Date (AAAAMMJJ)
|
||||
Date (YYYY-MM-DD) Date (AAAA-MM-JJ)
|
||||
Time (hhmmss) Heure (hhmmss)
|
||||
PDF Print PDF impression
|
||||
Export As Exporter sous
|
||||
Export As PNG… Exporter au format PNG…
|
||||
Export As SVG… Exporter au format SVG…
|
||||
@@ -3716,6 +3742,8 @@ axis Both Les deux
|
||||
@msg cannot split an open path at its end point impossible de scinder un tracé ouvert à son extrémité
|
||||
@msg closing needs three anchors la fermeture nécessite trois points d’ancrage
|
||||
@msg check an asset to export cochez une ressource à exporter
|
||||
@msg check a layer to export cochez un calque à exporter
|
||||
@msg select the art to export first sélectionnez d’abord l’illustration à exporter
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice doit valoir "merge" ou "add", pas `{choice}`
|
||||
@msg choose a direction to divide in choisissez une direction de division
|
||||
@msg choose a folder to export to choisissez un dossier d’exportation
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1257,6 +1257,32 @@
|
||||
Export All… Összes exportálása…
|
||||
Export and Clipboard Transparency Flattener Settings Átlátszóság lapítása exportáláskor és vágólapra másoláskor
|
||||
Export Artboard Rajztábla exportálása
|
||||
A File per Object Objektumonként egy fájl
|
||||
Example: {name} Példa: {name}
|
||||
Export Preset: Exportálási előbeállítás:
|
||||
Folders: Mappák:
|
||||
Insert Beszúrás
|
||||
Nothing is selected. Select the art to export, then open this tab again. Nincs kijelölés. Jelölje ki az exportálandó grafikát, majd nyissa meg újra ezt a lapot.
|
||||
One File of the Selection Egy fájl a kijelölésből
|
||||
Start at: Kezdőérték:
|
||||
{n} objects selected {n} objektum kijelölve
|
||||
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 Layer Réteg exportálása
|
||||
Export Selection Kijelölés exportálása
|
||||
All in One Folder Mind egy mappába
|
||||
By Size or Row Méret vagy sor szerint
|
||||
By Format Formátum szerint
|
||||
By Artboard or Item Rajztábla vagy elem szerint
|
||||
By Item, Then Format Elem, majd formátum szerint
|
||||
Document Name Dokumentum neve
|
||||
Artboard or Item Name Rajztábla vagy elem neve
|
||||
Number (####) Szám (####)
|
||||
Date (YYYYMMDD) Dátum (ÉÉÉÉHHNN)
|
||||
Date (YYYY-MM-DD) Dátum (ÉÉÉÉ-HH-NN)
|
||||
Time (hhmmss) Idő (óóppmm)
|
||||
PDF Print Nyomdai PDF
|
||||
Export As Exportálás másként
|
||||
Export As PNG… Exportálás PNG-ként…
|
||||
Export As SVG… Exportálás SVG-ként…
|
||||
@@ -4236,6 +4262,8 @@ imageTrace Outline Körvonal
|
||||
@msg cannot read PDF: {0} a PDF nem olvasható: {0}
|
||||
@msg cannot split an open path at its end point nyitott útvonal a végpontjánál nem vágható ketté
|
||||
@msg check an asset to export jelölj be egy exportálandó elemet
|
||||
@msg check a layer to export jelöljön be egy exportálandó réteget
|
||||
@msg select the art to export first először jelölje ki az exportálandó grafikát
|
||||
@msg choice must be "merge" or "add", not `{choice}` a choice értéke "merge" vagy "add" lehet, ez nem az: `{choice}`
|
||||
@msg choose a direction to divide in válaszd ki a felosztás irányát
|
||||
@msg choose a folder to export to válaszd ki az exportálás célmappáját
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -975,6 +975,32 @@
|
||||
Export All… Esporta tutto…
|
||||
Export and Clipboard Transparency Flattener Settings Impostazioni di appiattimento trasparenza per esportazione e Appunti
|
||||
Export Artboard Esporta tavola da disegno
|
||||
A File per Object Un file per oggetto
|
||||
Example: {name} Esempio: {name}
|
||||
Export Preset: Predefinito di esportazione:
|
||||
Folders: Cartelle:
|
||||
Insert Inserisci
|
||||
Nothing is selected. Select the art to export, then open this tab again. Nessuna selezione. Seleziona la grafica da esportare, poi riapri questa scheda.
|
||||
One File of the Selection Un file della selezione
|
||||
Start at: Inizia da:
|
||||
{n} objects selected {n} oggetti selezionati
|
||||
Delete Export Preset Elimina predefinito di esportazione
|
||||
Export Presets Predefiniti di esportazione
|
||||
Save Export Preset Salva predefinito di esportazione
|
||||
Export Layer Esporta livello
|
||||
Export Selection Esporta selezione
|
||||
All in One Folder Tutto in una cartella
|
||||
By Size or Row Per dimensione o riga
|
||||
By Format Per formato
|
||||
By Artboard or Item Per tavola o elemento
|
||||
By Item, Then Format Per elemento, poi formato
|
||||
Document Name Nome del documento
|
||||
Artboard or Item Name Nome della tavola o dell’elemento
|
||||
Number (####) Numero (####)
|
||||
Date (YYYYMMDD) Data (AAAAMMGG)
|
||||
Date (YYYY-MM-DD) Data (AAAA-MM-GG)
|
||||
Time (hhmmss) Ora (hhmmss)
|
||||
PDF Print PDF per la stampa
|
||||
Export As Esporta come
|
||||
Export As PNG… Esporta come PNG…
|
||||
Export As SVG… Esporta come SVG…
|
||||
@@ -3722,6 +3748,8 @@ axis Both Entrambi
|
||||
@msg cannot split an open path at its end point impossibile dividere un tracciato aperto nel suo punto finale
|
||||
@msg closing needs three anchors per chiudere servono tre punti di ancoraggio
|
||||
@msg check an asset to export spunta una risorsa da esportare
|
||||
@msg check a layer to export seleziona un livello da esportare
|
||||
@msg select the art to export first seleziona prima la grafica da esportare
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice deve essere "merge" o "add", non `{choice}`
|
||||
@msg choose a direction to divide in scegli una direzione in cui dividere
|
||||
@msg choose a folder to export to scegli una cartella in cui esportare
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1370,6 +1370,32 @@
|
||||
Expand: 拡張:
|
||||
Export All… すべて書き出し…
|
||||
Export Artboard アートボードを書き出し
|
||||
A File per Object オブジェクトごとに 1 ファイル
|
||||
Example: {name} 例:{name}
|
||||
Export Preset: 書き出しプリセット:
|
||||
Folders: フォルダー:
|
||||
Insert 挿入
|
||||
Nothing is selected. Select the art to export, then open this tab again. 何も選択されていません。書き出すアートを選択してから、このタブを開き直してください。
|
||||
One File of the Selection 選択範囲を 1 ファイルに
|
||||
Start at: 開始番号:
|
||||
{n} objects selected {n} 個のオブジェクトを選択中
|
||||
Delete Export Preset 書き出しプリセットを削除
|
||||
Export Presets 書き出しプリセット
|
||||
Save Export Preset 書き出しプリセットを保存
|
||||
Export Layer レイヤーを書き出し
|
||||
Export Selection 選択範囲を書き出し
|
||||
All in One Folder すべて 1 つのフォルダーに
|
||||
By Size or Row サイズまたは行ごと
|
||||
By Format 形式ごと
|
||||
By Artboard or Item アートボードまたは項目ごと
|
||||
By Item, Then Format 項目ごと、次に形式ごと
|
||||
Document Name ドキュメント名
|
||||
Artboard or Item Name アートボード名または項目名
|
||||
Number (####) 番号(####)
|
||||
Date (YYYYMMDD) 日付(YYYYMMDD)
|
||||
Date (YYYY-MM-DD) 日付(YYYY-MM-DD)
|
||||
Time (hhmmss) 時刻(hhmmss)
|
||||
PDF Print 印刷用 PDF
|
||||
Export As 形式を指定して書き出し
|
||||
Export Assets アセットを書き出し
|
||||
Export CSS CSS を書き出し
|
||||
@@ -4024,6 +4050,8 @@ swatch Bright Violet ビビッドバイオレット
|
||||
@msg nothing to export: the document has no visible art (or turn on Use Artboards) 書き出すものがありません: ドキュメントに表示されているアートがありません (または「アートボードを使用」をオンにしてください)
|
||||
@msg add a format to export 書き出す形式を追加してください
|
||||
@msg check an asset to export 書き出すアセットにチェックを入れてください
|
||||
@msg check a layer to export 書き出すレイヤーにチェックを入れてください
|
||||
@msg select the art to export first 先に書き出すアートを選択してください
|
||||
@msg `{tool}` isn't a freehand tool `{tool}` はフリーハンドツールではありません
|
||||
@msg bad colour `{hex}` (#rrggbb) カラー `{hex}` が正しくありません (#rrggbb の形式で指定してください)
|
||||
@msg no gradient stop or freeform point is selected グラデーションの分岐点またはフリーフォームポイントが選択されていません
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1365,6 +1365,32 @@
|
||||
Expand: 확장:
|
||||
Export All… 모두 내보내기…
|
||||
Export Artboard 아트보드 내보내기
|
||||
A File per Object 오브젝트마다 파일 1개
|
||||
Example: {name} 예: {name}
|
||||
Export Preset: 내보내기 사전 설정:
|
||||
Folders: 폴더:
|
||||
Insert 삽입
|
||||
Nothing is selected. Select the art to export, then open this tab again. 선택된 항목이 없습니다. 내보낼 아트를 선택한 다음 이 탭을 다시 여십시오.
|
||||
One File of the Selection 선택 영역을 파일 1개로
|
||||
Start at: 시작 번호:
|
||||
{n} objects selected 오브젝트 {n}개 선택됨
|
||||
Delete Export Preset 내보내기 사전 설정 삭제
|
||||
Export Presets 내보내기 사전 설정
|
||||
Save Export Preset 내보내기 사전 설정 저장
|
||||
Export Layer 레이어 내보내기
|
||||
Export Selection 선택 영역 내보내기
|
||||
All in One Folder 모두 폴더 하나에
|
||||
By Size or Row 크기 또는 행별
|
||||
By Format 형식별
|
||||
By Artboard or Item 대지 또는 항목별
|
||||
By Item, Then Format 항목별, 그다음 형식별
|
||||
Document Name 문서 이름
|
||||
Artboard or Item Name 대지 또는 항목 이름
|
||||
Number (####) 번호(####)
|
||||
Date (YYYYMMDD) 날짜(YYYYMMDD)
|
||||
Date (YYYY-MM-DD) 날짜(YYYY-MM-DD)
|
||||
Time (hhmmss) 시간(hhmmss)
|
||||
PDF Print 인쇄용 PDF
|
||||
Export As 다른 형식으로 내보내기
|
||||
Export Assets 에셋 내보내기
|
||||
Export CSS CSS 내보내기
|
||||
@@ -3956,6 +3982,8 @@ theme Appearance 외관
|
||||
@msg nothing to export: the document has no visible art (or turn on Use Artboards) 내보낼 항목이 없습니다: 문서에 보이는 아트웍이 없습니다(또는 아트보드 사용을 켜세요)
|
||||
@msg add a format to export 내보낼 형식을 추가하세요
|
||||
@msg check an asset to export 내보낼 자산을 체크하세요
|
||||
@msg check a layer to export 내보낼 레이어를 선택하십시오
|
||||
@msg select the art to export first 먼저 내보낼 아트를 선택하십시오
|
||||
@msg `{tool}` isn't a freehand tool `{tool}`은(는) 프리핸드 도구가 아닙니다
|
||||
@msg bad colour `{hex}` (#rrggbb) 잘못된 색상 `{hex}`(#rrggbb)
|
||||
@msg no gradient stop or freeform point is selected 선택된 그레이디언트 정지점이나 자유형 점이 없습니다
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2543,6 +2543,32 @@ lighting Light Światło
|
||||
Summary Podsumowanie
|
||||
Download Pobierz
|
||||
Export Artboard Eksportuj obszar kompozycji
|
||||
A File per Object Jeden plik na obiekt
|
||||
Example: {name} Przykład: {name}
|
||||
Export Preset: Ustawienie eksportu:
|
||||
Folders: Foldery:
|
||||
Insert Wstaw
|
||||
Nothing is selected. Select the art to export, then open this tab again. Nic nie zaznaczono. Zaznacz grafikę do eksportu i ponownie otwórz tę kartę.
|
||||
One File of the Selection Jeden plik z zaznaczenia
|
||||
Start at: Zacznij od:
|
||||
{n} objects selected Zaznaczone obiekty: {n}
|
||||
Delete Export Preset Usuń ustawienie eksportu
|
||||
Export Presets Ustawienia eksportu
|
||||
Save Export Preset Zapisz ustawienie eksportu
|
||||
Export Layer Eksportuj warstwę
|
||||
Export Selection Eksportuj zaznaczenie
|
||||
All in One Folder Wszystko w jednym folderze
|
||||
By Size or Row Według rozmiaru lub wiersza
|
||||
By Format Według formatu
|
||||
By Artboard or Item Według obszaru roboczego lub elementu
|
||||
By Item, Then Format Według elementu, potem formatu
|
||||
Document Name Nazwa dokumentu
|
||||
Artboard or Item Name Nazwa obszaru roboczego lub elementu
|
||||
Number (####) Numer (####)
|
||||
Date (YYYYMMDD) Data (RRRRMMDD)
|
||||
Date (YYYY-MM-DD) Data (RRRR-MM-DD)
|
||||
Time (hhmmss) Godzina (hhmmss)
|
||||
PDF Print PDF do druku
|
||||
Encoding: Kodowanie:
|
||||
Line Endings: Znaki końca wiersza:
|
||||
Text Export Options Opcje eksportu tekstu
|
||||
@@ -3833,6 +3859,8 @@ imageTrace Outline Kontur
|
||||
@msg cannot split an open path at its end point nie można podzielić otwartej ścieżki w jej punkcie końcowym
|
||||
@msg closing needs three anchors zamknięcie wymaga trzech punktów
|
||||
@msg check an asset to export zaznacz zasób do eksportu
|
||||
@msg check a layer to export zaznacz warstwę do eksportu
|
||||
@msg select the art to export first najpierw zaznacz grafikę do eksportu
|
||||
@msg choice must be "merge" or "add", not `{choice}` wybór musi mieć wartość "merge" lub "add", nie `{choice}`
|
||||
@msg choose a direction to divide in wybierz kierunek podziału
|
||||
@msg choose a folder to export to wybierz folder docelowy eksportu
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -868,6 +868,32 @@
|
||||
Export Exportar
|
||||
Export All… Exportar Tudo…
|
||||
Export Artboard Exportar Mesa de Trabalho
|
||||
A File per Object Um arquivo por objeto
|
||||
Example: {name} Exemplo: {name}
|
||||
Export Preset: Predefinição de exportação:
|
||||
Folders: Pastas:
|
||||
Insert Inserir
|
||||
Nothing is selected. Select the art to export, then open this tab again. Nada selecionado. Selecione a arte a exportar e abra esta guia novamente.
|
||||
One File of the Selection Um arquivo da seleção
|
||||
Start at: Começar em:
|
||||
{n} objects selected {n} objetos selecionados
|
||||
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 Layer Exportar camada
|
||||
Export Selection Exportar seleção
|
||||
All in One Folder Tudo em uma pasta
|
||||
By Size or Row Por tamanho ou linha
|
||||
By Format Por formato
|
||||
By Artboard or Item Por prancheta ou item
|
||||
By Item, Then Format Por item, depois formato
|
||||
Document Name Nome do documento
|
||||
Artboard or Item Name Nome da prancheta ou do item
|
||||
Number (####) Número (####)
|
||||
Date (YYYYMMDD) Data (AAAAMMDD)
|
||||
Date (YYYY-MM-DD) Data (AAAA-MM-DD)
|
||||
Time (hhmmss) Hora (hhmmss)
|
||||
PDF Print PDF para impressão
|
||||
Export As Exportar Como
|
||||
Export As PNG… Exportar Como PNG…
|
||||
Export As SVG… Exportar Como SVG…
|
||||
@@ -3312,6 +3338,8 @@ axis Both Ambos
|
||||
@msg cannot split an open path at its end point não é possível dividir um traçado aberto em sua extremidade
|
||||
@msg closing needs three anchors fechar exige três âncoras
|
||||
@msg check an asset to export marque um recurso para exportar
|
||||
@msg check a layer to export marque uma camada para exportar
|
||||
@msg select the art to export first selecione primeiro a arte a exportar
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice deve ser "merge" ou "add", não `{choice}`
|
||||
@msg choose a direction to divide in escolha uma direção para dividir
|
||||
@msg choose a folder to export to escolha uma pasta para exportar
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -973,6 +973,32 @@
|
||||
Export All… Экспортировать всё…
|
||||
Export and Clipboard Transparency Flattener Settings Параметры объединения прозрачностей для экспорта и буфера обмена
|
||||
Export Artboard Экспортировать монтажную область
|
||||
A File per Object Отдельный файл для каждого объекта
|
||||
Example: {name} Пример: {name}
|
||||
Export Preset: Набор настроек экспорта:
|
||||
Folders: Папки:
|
||||
Insert Вставить
|
||||
Nothing is selected. Select the art to export, then open this tab again. Ничего не выделено. Выделите объекты для экспорта и снова откройте эту вкладку.
|
||||
One File of the Selection Один файл из выделения
|
||||
Start at: Начать с:
|
||||
{n} objects selected Выделено объектов: {n}
|
||||
Delete Export Preset Удалить набор настроек экспорта
|
||||
Export Presets Наборы настроек экспорта
|
||||
Save Export Preset Сохранить набор настроек экспорта
|
||||
Export Layer Экспортировать слой
|
||||
Export Selection Экспортировать выделение
|
||||
All in One Folder Всё в одну папку
|
||||
By Size or Row По размеру или строке
|
||||
By Format По формату
|
||||
By Artboard or Item По монтажной области или элементу
|
||||
By Item, Then Format По элементу, затем по формату
|
||||
Document Name Имя документа
|
||||
Artboard or Item Name Имя монтажной области или элемента
|
||||
Number (####) Номер (####)
|
||||
Date (YYYYMMDD) Дата (ГГГГММДД)
|
||||
Date (YYYY-MM-DD) Дата (ГГГГ-ММ-ДД)
|
||||
Time (hhmmss) Время (ччммсс)
|
||||
PDF Print PDF для печати
|
||||
Export As Экспортировать как
|
||||
Export As PNG… Экспортировать как PNG…
|
||||
Export As SVG… Экспортировать как SVG…
|
||||
@@ -3718,6 +3744,8 @@ axis Both Оба
|
||||
@msg cannot split an open path at its end point нельзя разделить открытый контур в его конечной точке
|
||||
@msg closing needs three anchors для замыкания нужны три опорные точки
|
||||
@msg check an asset to export отметьте ресурс для экспорта
|
||||
@msg check a layer to export отметьте слой для экспорта
|
||||
@msg select the art to export first сначала выделите объекты для экспорта
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice должно быть "merge" или "add", а не `{choice}`
|
||||
@msg choose a direction to divide in выберите направление для разделения
|
||||
@msg choose a folder to export to выберите папку для экспорта
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -960,6 +960,32 @@
|
||||
Export All… Exportera alla…
|
||||
Export and Clipboard Transparency Flattener Settings Inställningar för transparensförenkling vid export och urklipp
|
||||
Export Artboard Exportera rityta
|
||||
A File per Object En fil per objekt
|
||||
Example: {name} Exempel: {name}
|
||||
Export Preset: Exportförinställning:
|
||||
Folders: Mappar:
|
||||
Insert Infoga
|
||||
Nothing is selected. Select the art to export, then open this tab again. Inget är markerat. Markera teckningen som ska exporteras och öppna fliken igen.
|
||||
One File of the Selection En fil av markeringen
|
||||
Start at: Börja på:
|
||||
{n} objects selected {n} objekt markerade
|
||||
Delete Export Preset Ta bort exportförinställning
|
||||
Export Presets Exportförinställningar
|
||||
Save Export Preset Spara exportförinställning
|
||||
Export Layer Exportera lager
|
||||
Export Selection Exportera markering
|
||||
All in One Folder Allt i en mapp
|
||||
By Size or Row Efter storlek eller rad
|
||||
By Format Efter format
|
||||
By Artboard or Item Efter rityta eller objekt
|
||||
By Item, Then Format Efter objekt, sedan format
|
||||
Document Name Dokumentnamn
|
||||
Artboard or Item Name Ritytans eller objektets namn
|
||||
Number (####) Nummer (####)
|
||||
Date (YYYYMMDD) Datum (ÅÅÅÅMMDD)
|
||||
Date (YYYY-MM-DD) Datum (ÅÅÅÅ-MM-DD)
|
||||
Time (hhmmss) Tid (ttmmss)
|
||||
PDF Print Tryck-PDF
|
||||
Export As Exportera som
|
||||
Export As PNG… Exportera som PNG…
|
||||
Export As SVG… Exportera som SVG…
|
||||
@@ -3644,6 +3670,8 @@ axis Both Båda
|
||||
@msg cannot split an open path at its end point kan inte dela en öppen bana vid dess slutpunkt
|
||||
@msg closing needs three anchors att sluta banan kräver tre ankarpunkter
|
||||
@msg check an asset to export kryssa för en resurs att exportera
|
||||
@msg check a layer to export markera ett lager att exportera
|
||||
@msg select the art to export first markera först teckningen som ska exporteras
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice måste vara "merge" eller "add", inte `{choice}`
|
||||
@msg choose a direction to divide in välj en riktning att dela upp i
|
||||
@msg choose a folder to export to välj en mapp att exportera till
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -969,6 +969,32 @@
|
||||
Export All… Експортувати все…
|
||||
Export and Clipboard Transparency Flattener Settings Параметри зведення прозорості для експорту та буфера обміну
|
||||
Export Artboard Експортувати монтажну область
|
||||
A File per Object Окремий файл для кожного об’єкта
|
||||
Example: {name} Приклад: {name}
|
||||
Export Preset: Набір параметрів експорту:
|
||||
Folders: Теки:
|
||||
Insert Вставити
|
||||
Nothing is selected. Select the art to export, then open this tab again. Нічого не виділено. Виділіть об’єкти для експорту й знову відкрийте цю вкладку.
|
||||
One File of the Selection Один файл із виділення
|
||||
Start at: Почати з:
|
||||
{n} objects selected Виділено об’єктів: {n}
|
||||
Delete Export Preset Видалити набір параметрів експорту
|
||||
Export Presets Набори параметрів експорту
|
||||
Save Export Preset Зберегти набір параметрів експорту
|
||||
Export Layer Експортувати шар
|
||||
Export Selection Експортувати виділення
|
||||
All in One Folder Усе в одну теку
|
||||
By Size or Row За розміром або рядком
|
||||
By Format За форматом
|
||||
By Artboard or Item За монтажною областю або елементом
|
||||
By Item, Then Format За елементом, потім за форматом
|
||||
Document Name Назва документа
|
||||
Artboard or Item Name Назва монтажної області або елемента
|
||||
Number (####) Номер (####)
|
||||
Date (YYYYMMDD) Дата (РРРРММДД)
|
||||
Date (YYYY-MM-DD) Дата (РРРР-ММ-ДД)
|
||||
Time (hhmmss) Час (ггххсс)
|
||||
PDF Print PDF для друку
|
||||
Export As Експортувати як
|
||||
Export As PNG… Експортувати як PNG…
|
||||
Export As SVG… Експортувати як SVG…
|
||||
@@ -3706,6 +3732,8 @@ axis Both Обидві
|
||||
@msg cannot split an open path at its end point не можна розділити відкритий контур у його кінцевій точці
|
||||
@msg closing needs three anchors для замикання потрібні три опорні точки
|
||||
@msg check an asset to export позначте ресурс для експорту
|
||||
@msg check a layer to export позначте шар для експорту
|
||||
@msg select the art to export first спочатку виділіть об’єкти для експорту
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice має бути "merge" або "add", а не `{choice}`
|
||||
@msg choose a direction to divide in виберіть напрямок для поділу
|
||||
@msg choose a folder to export to виберіть папку для експорту
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 720 and column 40.
|
@@ -1135,6 +1135,32 @@
|
||||
Export All… Xuất tất cả…
|
||||
Export and Clipboard Transparency Flattener Settings Cài đặt làm phẳng độ trong suốt khi xuất và dán
|
||||
Export Artboard Xuất bảng vẽ
|
||||
A File per Object Mỗi đối tượng một tệp
|
||||
Example: {name} Ví dụ: {name}
|
||||
Export Preset: Cài đặt sẵn xuất:
|
||||
Folders: Thư mục:
|
||||
Insert Chèn
|
||||
Nothing is selected. Select the art to export, then open this tab again. Chưa chọn gì. Hãy chọn hình cần xuất rồi mở lại thẻ này.
|
||||
One File of the Selection Một tệp cho vùng chọn
|
||||
Start at: Bắt đầu từ:
|
||||
{n} objects selected Đã chọn {n} đối tượng
|
||||
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 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
|
||||
By Size or Row Theo kích thước hoặc hàng
|
||||
By Format Theo định dạng
|
||||
By Artboard or Item Theo bảng vẽ hoặc mục
|
||||
By Item, Then Format Theo mục, rồi theo định dạng
|
||||
Document Name Tên tài liệu
|
||||
Artboard or Item Name Tên bảng vẽ hoặc mục
|
||||
Number (####) Số (####)
|
||||
Date (YYYYMMDD) Ngày (YYYYMMDD)
|
||||
Date (YYYY-MM-DD) Ngày (YYYY-MM-DD)
|
||||
Time (hhmmss) Giờ (hhmmss)
|
||||
PDF Print PDF in ấn
|
||||
Export As Xuất thành
|
||||
Export As PNG… Xuất thành PNG…
|
||||
Export As SVG… Xuất thành SVG…
|
||||
@@ -3867,6 +3893,8 @@ theme Appearance Giao diện
|
||||
@msg cannot read PDF: {0} không thể đọc PDF: {0}
|
||||
@msg cannot split an open path at its end point không thể tách đường dẫn mở tại điểm cuối
|
||||
@msg check an asset to export hãy chọn một asset để xuất
|
||||
@msg check a layer to export hãy đánh dấu một lớp để xuất
|
||||
@msg select the art to export first hãy chọn hình cần xuất trước
|
||||
@msg choice must be "merge" or "add", not `{choice}` lựa chọn phải là "merge" hoặc "add", không phải `{choice}`
|
||||
@msg choose a direction to divide in hãy chọn hướng để chia
|
||||
@msg choose a folder to export to hãy chọn thư mục để xuất vào
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3773 and column 10.
|
@@ -924,6 +924,32 @@
|
||||
Export All… 导出全部…
|
||||
Export and Clipboard Transparency Flattener Settings 导出与剪贴板透明度拼合器设置
|
||||
Export Artboard 导出画板
|
||||
A File per Object 每个对象一个文件
|
||||
Example: {name} 示例:{name}
|
||||
Export Preset: 导出预设:
|
||||
Folders: 文件夹:
|
||||
Insert 插入
|
||||
Nothing is selected. Select the art to export, then open this tab again. 未选择任何内容。请选择要导出的图稿,然后重新打开此选项卡。
|
||||
One File of the Selection 选区导出为一个文件
|
||||
Start at: 起始编号:
|
||||
{n} objects selected 已选择 {n} 个对象
|
||||
Delete Export Preset 删除导出预设
|
||||
Export Presets 导出预设
|
||||
Save Export Preset 存储导出预设
|
||||
Export Layer 导出图层
|
||||
Export Selection 导出选区
|
||||
All in One Folder 全部放在一个文件夹
|
||||
By Size or Row 按尺寸或行
|
||||
By Format 按格式
|
||||
By Artboard or Item 按画板或项目
|
||||
By Item, Then Format 按项目,再按格式
|
||||
Document Name 文档名称
|
||||
Artboard or Item Name 画板或项目名称
|
||||
Number (####) 编号(####)
|
||||
Date (YYYYMMDD) 日期(YYYYMMDD)
|
||||
Date (YYYY-MM-DD) 日期(YYYY-MM-DD)
|
||||
Time (hhmmss) 时间(hhmmss)
|
||||
PDF Print 印刷用 PDF
|
||||
Export As 导出为
|
||||
Export As PNG… 导出为 PNG…
|
||||
Export As SVG… 导出为 SVG…
|
||||
@@ -4058,6 +4084,8 @@ lighting Light 光源
|
||||
Destination Folder: 目标文件夹:
|
||||
Overrides 覆盖
|
||||
Uses every data set of the open document. 使用打开文档的每个数据集。
|
||||
@msg check a layer to export 请勾选要导出的图层
|
||||
@msg select the art to export first 请先选择要导出的图稿
|
||||
|
||||
# User Interface › Accent Color
|
||||
Accent Color 强调色
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -946,6 +946,32 @@
|
||||
Export All… 全部匯出…
|
||||
Export and Clipboard Transparency Flattener Settings 匯出與剪貼簿透明度平面化設定
|
||||
Export Artboard 匯出工作區域
|
||||
A File per Object 每個物件一個檔案
|
||||
Example: {name} 範例:{name}
|
||||
Export Preset: 轉存預設集:
|
||||
Folders: 資料夾:
|
||||
Insert 插入
|
||||
Nothing is selected. Select the art to export, then open this tab again. 未選取任何內容。請選取要轉存的圖稿,然後重新開啟此標籤。
|
||||
One File of the Selection 選取範圍轉存為一個檔案
|
||||
Start at: 起始編號:
|
||||
{n} objects selected 已選取 {n} 個物件
|
||||
Delete Export Preset 刪除轉存預設集
|
||||
Export Presets 轉存預設集
|
||||
Save Export Preset 儲存轉存預設集
|
||||
Export Layer 轉存圖層
|
||||
Export Selection 轉存選取範圍
|
||||
All in One Folder 全部放在一個資料夾
|
||||
By Size or Row 依尺寸或列
|
||||
By Format 依格式
|
||||
By Artboard or Item 依工作區域或項目
|
||||
By Item, Then Format 依項目,再依格式
|
||||
Document Name 文件名稱
|
||||
Artboard or Item Name 工作區域或項目名稱
|
||||
Number (####) 編號(####)
|
||||
Date (YYYYMMDD) 日期(YYYYMMDD)
|
||||
Date (YYYY-MM-DD) 日期(YYYY-MM-DD)
|
||||
Time (hhmmss) 時間(hhmmss)
|
||||
PDF Print 印刷用 PDF
|
||||
Export As 匯出為
|
||||
Export As PNG… 匯出為 PNG…
|
||||
Export As SVG… 匯出為 SVG…
|
||||
@@ -3845,6 +3871,8 @@ lighting Light 光源
|
||||
Destination Folder: 目的地資料夾:
|
||||
Overrides 覆寫
|
||||
Uses every data set of the open document. 使用開啟文件的每個資料集。
|
||||
@msg check a layer to export 請勾選要轉存的圖層
|
||||
@msg select the art to export first 請先選取要轉存的圖稿
|
||||
|
||||
# User Interface › Accent Color
|
||||
Accent Color 強調色
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -115,7 +115,7 @@ the two can still win that race.
|
||||
| `engine.execute` `document.exportForOffice` | `{}` / `{path?, ppi?: 72 \| 150 \| 300, transparent?, artboard?}` | File → Save for Office Documents…: without params opens `saveForOffice` (fields `ppi`, `transparent`, `artboard`; OK picks the file); with params writes the artboard as a PNG (`path`, else a picked file) → `{path}`. Headless it returns `{dataBase64, width, height}` |
|
||||
| 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` | 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, `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. With the preferences off (`prefs.set`), saves return once 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`), `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 store-only zip) as `dataBase64`; with `openLocation` the first file is shown in the file manager |
|
||||
| `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 store-only zip) as `dataBase64`; with `openLocation` the first file is shown in the file manager |
|
||||
| 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`) |
|
||||
| `engine.execute` `file.exportSelection` | `{multiple?}` | File › Export Selection…: collects the selected objects as assets (`assets.add`; one per object unless `multiple` is false) 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}` |
|
||||
|
||||
File diff suppressed because one or more lines are too long
+2
-1
File diff suppressed because one or more lines are too long
+16
@@ -2620,7 +2620,23 @@ art), `includeBleed` grows each artboard by the document's bleed, `subfolders` p
|
||||
sub-folders) replaces the rows, and `settings: {png: {…}, jpg: {…}, svg: {…}, pdf: {preset}}` gives every row of a
|
||||
format its options. The document remembers the last params: `document.exportSettings`.
|
||||
|
||||
Batch exports (#1935):
|
||||
- **What:** `layers: [id…]` (each layer's art alone) or `selection: "one" | "each"` instead of artboards
|
||||
(`assets`, `fullDocument` as before).
|
||||
- **Names:** `name` is a pattern for every file: `<filename>` (the document), `<name>` (the artboard, asset, layer
|
||||
or object), `<index>`, `####` (a counter zero-padded to the run of `#`, from `counterStart`; `counterPer: "item"`
|
||||
or `"file"`), `YYYYMMDD`, `YYYY-MM-DD`, `<date>`, `<time>`, `<format>`, `<scale>` and `<suffix>` (else the row's
|
||||
suffix ends the name; `date` sets the day the pattern uses). Different files it names alike get `-2`, `-3`….
|
||||
- **Folders:** `folders: "none" | "rows" | "format" | "item" | "itemFormat"` (all in one, a folder per row, per
|
||||
extension, per item, or item then extension).
|
||||
- **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.
|
||||
|
||||
```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}}]}}}
|
||||
{"name":"run_command","arguments":{"command":"document.exportForScreens","params":{"exportPreset":"Logo Package","layers":[2,9]}}}
|
||||
{"name":"run_command","arguments":{"command":"document.exportForScreens","params":{"preset":"density","zip":true}}}
|
||||
{"name":"run_command","arguments":{"command":"document.exportForScreens","params":{"range":"1-2","formats":[{"format":"png","scale":"512w"},{"format":"jpg","quality":80,"scale":"2x"},{"format":"svg"}],"settings":{"png":{"background":"white"}}}}}
|
||||
{"name":"run_command","arguments":{"command":"document.exportSettings","params":{}}}
|
||||
|
||||
+2
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user