M4.38: native and .ai save options: each artboard to its own file, linked files, ICC profiles, PDF content

- document.save / Save As / Save a Copy to a native or .ai file take
  separateArtboards + range (also write <name>-<artboard>.<ext> per artboard
  beside the master file, holding that artboard and the art touching it; the
  result lists every file), includeLinked (linked images keep their files' own
  pixels), embedProfiles (ICC profiles loaded from files that the document is
  tagged with; opening installs them where missing) and pdfCompatible (native:
  also carry a PDF of every artboard; .ai: false writes blank pages around the
  native document). .ai files also take compress (compression.compressText).
- File > Save As to a native or .ai file shows the options dialog (VectorCraft
  Options) after the save panel: Save each artboard to a separate file (All or a
  range), Include Linked Files, Embed ICC Profiles, Create PDF-Compatible File,
  Use Compression, Preview; the web's Save As dialog shows them too. Save, Save a
  Copy and Save as Template reuse the options as last saved.
- Format: optional "profiles" and "pdf" members (load_file, pdf_content),
  SaveOptions.include_linked/profiles/pdf; files without them are unchanged.
- Changed test: tests_aisave::save_as_ai_writes_a_pdf_that_reopens_editable now
  confirms the options dialog Save As .ai shows before writing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-05 15:24:19 +07:00
co-authored by Claude Opus 5.5
parent 975067b44a
commit bf4614669e
19 changed files with 883 additions and 72 deletions
+13 -8
View File
@@ -85,8 +85,9 @@ impl Encoded {
}
/// Every file to write for the destination `path` (a path or a file name): the file itself
/// when there is one, else `{stem}-{artboard name}.{ext}` beside it, then the linked images
/// beside it under their own names, then the image maps as `{image stem}.html` or `.map`.
/// when there is one, else `{stem}-{artboard name}.{ext}` beside it (see `joiner`; a file of no artboard
/// among several, as Save's master file, is the file itself), then the linked images beside it
/// under their own names, then the image maps as `{image stem}.html` or `.map`.
pub fn named<'a>(&'a self, doc: &Document, path: &str) -> Vec<(String, Cow<'a, [u8]>)> {
// Siblings keep the path's own separators (this runs on every platform and on the web).
let dir = &path[..path.rfind(['/', '\\']).map_or(0, |i| i + 1)];
@@ -94,15 +95,19 @@ impl Encoded {
let mut out: Vec<(String, Cow<[u8]>)> = match self.files.as_slice() {
[(_, bytes)] => vec![(path.to_string(), Cow::Borrowed(bytes.as_slice()))],
files => {
let boards: Vec<usize> = files.iter().map(|(b, _)| b.unwrap_or(0)).collect();
let boards: Vec<usize> = files.iter().filter_map(|(b, _)| *b).collect();
let file = std::path::Path::new(&path[dir.len()..]);
let stem = file.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default();
let ext = file.extension().map(|e| format!(".{}", e.to_string_lossy())).unwrap_or_default();
super::artboard_file_names(doc, &boards)
.into_iter()
.zip(files)
.map(|(board, (_, bytes))| {
(sibling(&format!("{stem}{}{board}{ext}", self.joiner.unwrap_or("-"))), Cow::Borrowed(bytes.as_slice()))
let mut names = super::artboard_file_names(doc, &boards).into_iter();
files
.iter()
.map(|(b, bytes)| {
let name = match b {
None => path.to_string(),
Some(_) => sibling(&format!("{stem}{}{}{ext}", self.joiner.unwrap_or("-"), names.next().unwrap_or_default())),
};
(name, Cow::Borrowed(bytes.as_slice()))
})
.collect()
}
+12 -4
View File
@@ -90,8 +90,8 @@ fn restore_or_import(
let mut fallback = None;
if let Some((intact, bytes)) = editing {
let native = if intact { bytes() } else { None };
match native.and_then(|b| vectorcraft_format::load(&b).ok()) {
Some(doc) => return Ok((doc, vec![], true)),
match native.and_then(|b| native_file(&b).ok()) {
Some((doc, _, warnings)) => return Ok((doc, warnings, true)),
None => fallback = Some(if intact { EDITING_DAMAGED } else { EDITING_STALE }),
}
}
@@ -102,6 +102,14 @@ fn restore_or_import(
Ok((doc, warnings, false))
}
/// A native file's document and details, the colour profiles it carries installed where missing
/// (with a warning for each that can't be used).
pub(crate) fn native_file(bytes: &[u8]) -> Result<(Document, vectorcraft_format::FileInfo, Vec<String>)> {
let f = vectorcraft_format::load_file(bytes).map_err(err)?;
let warnings = super::native::install_profiles(f.profiles);
Ok((f.doc, f.info, warnings))
}
/// Read a file of any readable format (`name`: its file name or path, for the extension and title;
/// a path's folder is where an SVG's relative image links are found).
pub fn load(name: &str, bytes: &[u8]) -> Result<Loaded> {
@@ -123,9 +131,9 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
let mut converted = false;
let (mut doc, warnings, restored) = match format.id {
"vectorcraft" | "template" => {
let (d, info) = vectorcraft_format::load_info(bytes).map_err(err)?;
let (d, info, warnings) = native_file(bytes)?;
converted = info.is_old() || super::extension(name) == vectorcraft_format::LEGACY_EXTENSION;
(d, vec![], false)
(d, warnings, false)
}
"svg" | "svgz" => {
let text = vectorcraft_svg::text_of(bytes).map_err(err)?;
+12 -1
View File
@@ -43,6 +43,7 @@ pub(crate) use encode::{anti_alias, background, with_single_artboard};
pub use export::export_source;
use load::err;
pub(crate) use load::import_svg;
pub(crate) use load::native_file;
pub(crate) use load::source;
pub use load::{Loaded, RasterImage, detect, file_name, load, load_with, open_bytes, open_bytes_with, open_template, raster_image};
pub use native::with_compression_pref;
@@ -360,7 +361,17 @@ pub const FORMATS: &[Format] = &[
options: svg::OPTIONS,
},
Format { id: "pdf", label: "PDF", extensions: &["pdf"], mime: "application/pdf", read: true, write: true, raster: false, options: pdf::OPTIONS },
reader("ai", "PDF-compatible .ai", &["ai"], "application/pdf", false),
Format {
id: "ai",
label: "PDF-compatible .ai",
extensions: &["ai"],
mime: "application/pdf",
read: true,
// Saved (Save As), not exported.
write: false,
raster: false,
options: native::AI_OPTIONS,
},
reader("ait", "PDF-compatible .ait template", &["ait"], "application/pdf", false),
Format {
id: "png",
+127 -9
View File
@@ -1,8 +1,13 @@
//! The native format's options: compression, the version to write (older apps read older
//! versions) and an embedded preview.
//! versions), an embedded preview, and the save options native and `.ai` files share: each
//! artboard to a separate file, Include Linked Files, Embed ICC Profiles and Create
//! PDF-Compatible File.
use std::collections::BTreeMap;
use serde::Deserialize;
use serde_json::Value;
use serde_json::{Value, json};
use vectorcraft_color::cms;
use vectorcraft_doc::Document;
use vectorcraft_format::{PREVIEW_MAX, SaveOptions};
@@ -29,33 +34,146 @@ pub const OPTIONS: &[FormatOption] = &[
default: "false",
description: "embed a PNG preview of the first artboard (at most 256 pixels on its longer side) for file browsers",
},
SEPARATE_ARTBOARDS,
SEPARATE_RANGE,
INCLUDE_LINKED,
EMBED_PROFILES,
FormatOption {
name: "pdfCompatible",
ty: "boolean",
default: "false",
description: "also carry a PDF of every artboard, for apps that read PDF (a larger file)",
},
];
const SEPARATE_ARTBOARDS: FormatOption = FormatOption {
name: "separateArtboards",
ty: "boolean",
default: "false",
description: "also save each artboard (of range) to a file of its own beside this one, <name>-<artboard>.<ext>, holding that artboard and the art on it",
};
const SEPARATE_RANGE: FormatOption = FormatOption {
name: "range",
ty: "string",
default: "null",
description: "with separateArtboards: the artboards saved separately, 1-based such as \"1-3, 5\", or \"all\" (default)",
};
const INCLUDE_LINKED: FormatOption = FormatOption {
name: "includeLinked",
ty: "boolean",
default: "false",
description: "keep the linked files' own pixels in the file (they stay linked), not just a preview: the document shows them in full where the files can't be found",
};
const EMBED_PROFILES: FormatOption = FormatOption {
name: "embedProfiles",
ty: "boolean",
default: "true",
description: "carry the ICC profiles the document is tagged with that were loaded from files (Edit → Assign Profile), so the document keeps them where they aren't installed",
};
/// The save options of a `.ai` file (a PDF carrying the native document; its PDF options come on
/// top, see document.exportPdf).
pub const AI_OPTIONS: &[FormatOption] = &[
SEPARATE_ARTBOARDS,
SEPARATE_RANGE,
INCLUDE_LINKED,
EMBED_PROFILES,
FormatOption {
name: "pdfCompatible",
ty: "boolean",
default: "true",
description: "write the artwork as PDF pages that other apps show; false keeps only the native document (smaller, faster; other apps show empty pages)",
},
FormatOption {
name: "compress",
ty: "boolean",
default: "true",
description: "compress the PDF's content and the native document it carries (the PDF option compression.compressText)",
},
];
#[derive(Default, Deserialize)]
#[serde(default)]
#[serde(default, rename_all = "camelCase")]
struct NativeOptions {
compress: Option<bool>,
version: Option<u32>,
preview: bool,
include_linked: bool,
embed_profiles: Option<bool>,
pdf_compatible: bool,
}
/// The options in `p` for saving `doc`, checked (the preview is left to [`encode`]) → them and
/// whether to embed a preview.
pub(super) fn save_options(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Result<(SaveOptions, bool)> {
/// What [`save_options`] leaves to [`encode`] (rendering and encoding take time).
pub(super) struct Extras {
preview: bool,
pdf: bool,
}
/// The options in `p` for saving `doc`, checked, with the profiles to embed (the preview and PDF
/// are left to [`encode`]).
pub(super) fn save_options(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Result<(SaveOptions, Extras)> {
let o: NativeOptions = super::encode::options(f, p)?;
let mut so = SaveOptions::for_doc(doc);
so.compress = o.compress.unwrap_or(false);
so.version = o.version.unwrap_or(vectorcraft_format::VERSION);
so.include_linked = o.include_linked;
if o.embed_profiles.unwrap_or(true) {
so.profiles = embedded_profiles(doc);
}
so.check().map_err(|e| bad(cmd, e.to_string()))?;
Ok((so, o.preview))
Ok((so, Extras { preview: o.preview, pdf: o.pdf_compatible }))
}
/// The `.vectorcraft` file of `doc` with the options in `p`.
pub(super) fn encode(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Result<Vec<u8>> {
let (mut so, preview) = save_options(cmd, f, doc, p)?;
if preview {
let (mut so, extras) = save_options(cmd, f, doc, p)?;
if extras.preview {
so.preview = preview_png(doc)?;
}
if extras.pdf {
// Every artboard as PDF pages (no editing data: the file is the native document).
so.pdf = Some(super::pdf::encode(cmd, doc, &json!({ "range": "all", "preserveEditing": false }))?.0);
}
vectorcraft_format::save_with(doc, &so).map_err(|e| bad(cmd, e.to_string()))
}
/// The ICC files of the profiles `doc` is tagged with that were loaded from files (the built-in
/// ones are there wherever VectorCraft runs).
fn embedded_profiles(doc: &Document) -> BTreeMap<String, Vec<u8>> {
[&doc.color_profiles.rgb, &doc.color_profiles.cmyk]
.into_iter()
.flatten()
.filter_map(|name| cms::profile(name).filter(|p| !p.builtin))
.filter_map(|p| Some((p.name.clone(), cms::icc_bytes(&p.name).ok()?.to_vec())))
.collect()
}
/// Most profiles read from one file.
const MAX_PROFILES: usize = 8;
/// Make the profiles a file carries available (those not installed here), named as in the file →
/// a warning per profile that can't be used.
pub(super) fn install_profiles(profiles: BTreeMap<String, Vec<u8>>) -> Vec<String> {
// A document is tagged with an RGB and a CMYK profile at most: more is junk.
profiles
.into_iter()
.take(MAX_PROFILES)
.filter(|(name, _)| cms::profile(name).is_none())
.filter_map(|(name, icc)| {
cms::register_icc(&icc, Some(name.clone())).err().map(|e| format!("the colour profile “{name}” the file carries can't be used: {e}"))
})
.collect()
}
/// The native document a `.ai` file carries, with the options in `p` that apply to it
/// (`includeLinked`, `embedProfiles`; compact and uncompressed: the PDF compresses it).
pub(super) fn ai_native(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Result<Vec<u8>> {
let mut q = p.clone();
if let Some(o) = q.as_object_mut() {
o.retain(|k, _| !matches!(k.as_str(), "compress" | "version" | "preview" | "pdfCompatible"));
}
let (mut so, _) = save_options(cmd, f, doc, &q)?;
so.pretty = false;
vectorcraft_format::save_with(doc, &so).map_err(|e| bad(cmd, e.to_string()))
}
+7 -1
View File
@@ -197,11 +197,17 @@ pub const EDITING_NEEDS_EVERY_ARTBOARD: &str =
/// to images at the document's raster effects resolution. Preserve Editing embeds `doc` itself
/// when the PDF has every artboard (a PDF of some reopens as just those).
pub fn encode(cmd: &str, doc: &Document, p: &Value) -> Result<(Vec<u8>, Vec<String>)> {
encode_carrying(cmd, doc, p, || Ok(vectorcraft_format::save(doc, false)))
}
/// [`encode`] the pages of `doc`, carrying `native()` as the editing data (a `.ai` file: the
/// native document with its save options, whose pages may be left blank).
pub(super) fn encode_carrying(cmd: &str, doc: &Document, p: &Value, native: impl FnOnce() -> Result<Vec<u8>>) -> Result<(Vec<u8>, Vec<String>)> {
let mut opts = options(cmd, doc, p)?;
let mut warnings = vec![];
if opts.settings.preserve_editing {
if opts.artboards.as_ref().is_none_or(|v| v.iter().copied().eq(0..doc.artboards.len())) {
opts.native = Some(vectorcraft_format::save(doc, false));
opts.native = Some(native()?);
} else {
opts.settings.preserve_editing = false;
warnings.push(EDITING_NEEDS_EVERY_ARTBOARD.to_string());
+104 -19
View File
@@ -13,7 +13,8 @@ use vectorcraft_doc::Document;
use super::super::*;
use super::{
Encoded, Format, Loaded, encode_all, file_stem, format, format_for_name, load, read_file, with_compression_pref, write_encoded, write_file,
ArtboardPick, Encoded, Format, Loaded, encode_all, file_stem, format, format_for_name, load, read_file, with_compression_pref, write_encoded,
write_file,
};
use crate::{DocState, Prefs};
@@ -198,10 +199,7 @@ pub fn save_plan(s: &Session, mode: SaveMode, p: &Value) -> Result<SavePlan> {
/// Does a save in `f` read option `key`? PDF (and a .ai file, a PDF) also reads its General
/// settings (`createLayers`…) and presets.
fn reads_option(f: &Format, key: &str) -> bool {
match is_pdf(f) {
true => super::pdf::OPTIONS.iter().any(|o| o.name == key) || super::pdf::is_setting(key),
false => f.options.iter().any(|o| o.name == key),
}
f.options.iter().any(|o| o.name == key) || (is_pdf(f) && (super::pdf::OPTIONS.iter().any(|o| o.name == key) || super::pdf::is_setting(key)))
}
/// Formats that carry the whole native document: they lose nothing, and record links relative to
@@ -251,8 +249,62 @@ pub struct SaveJob {
params: Value,
/// The document when the job was made: what counts as saved once the file is written.
snapshot: Arc<Document>,
/// The artboards also saved to files of their own (`separateArtboards`).
boards: Vec<usize>,
}
/// The artboards a native or `.ai` save also writes to files of their own: with
/// `separateArtboards`, those `range` names (default all).
fn separate_boards(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Result<Vec<usize>> {
if !is_native(f) || !bool_or(p, "separateArtboards", false) {
return Ok(vec![]);
}
let range = str_param(p, "range").filter(|r| !r.trim().is_empty()).map(str::to_string);
let n = doc.artboards.len();
let picked = ArtboardPick { range, ..Default::default() }.resolve(n).map_err(|e| bad(cmd, e))?;
Ok(picked.unwrap_or_else(|| (0..n).collect()))
}
/// `doc` reduced to artboard `i` and the art on it (Save each artboard to a separate file):
/// objects whose bounds touch the artboard, in their layers; resources are kept.
fn artboard_doc(doc: &Document, i: usize) -> Document {
let mut d = doc.clone();
let Some(a) = doc.artboards.get(i) else { return d };
let r = a.rect;
d.artboards = vec![a.clone()];
// It opens fitted to its artboard, not at the master file's view.
d.last_view = None;
for layer in &mut d.layers {
keep_on(layer, r);
}
d
}
/// Keep in layer `n` (and its sublayers) only the objects touching `r`.
fn keep_on(n: &mut Arc<vectorcraft_doc::Node>, r: vectorcraft_geom::Rect) {
let touches = |b: vectorcraft_geom::Rect| b.x0 <= r.x1 && b.x1 >= r.x0 && b.y0 <= r.y1 && b.y1 >= r.y0;
if let Some(children) = Arc::make_mut(n).children_mut() {
children.retain(|c| c.is_layer() || c.visual_bounds().is_some_and(touches));
for c in children.iter_mut().filter(|c| c.is_layer()) {
keep_on(c, r);
}
}
}
/// `doc` with empty layers: the pages of a `.ai` file saved without PDF content.
fn blank_pages(doc: &Document) -> Document {
let mut d = doc.clone();
for layer in &mut d.layers {
if let Some(children) = Arc::make_mut(layer).children_mut() {
children.clear();
}
}
d
}
/// Why a `.ai` file saved without PDF content looks empty elsewhere.
const NOT_PDF_COMPATIBLE: &str = "saved without PDF content: VectorCraft opens it as before, other apps show empty pages";
/// Snapshot the active document for `plan` (stamping File Info's dates when the file becomes the
/// document's own). Bad options fail here, before anything is encoded.
pub fn save_job(s: &mut Session, plan: SavePlan) -> Result<SaveJob> {
@@ -285,12 +337,20 @@ pub(crate) fn job_for(s: &Session, st: &DocState, plan: SavePlan) -> Result<Save
params = with_compression_pref(&s.prefs, &params);
super::native::save_options(cmd, plan.format, &doc, &params)?;
}
let boards = separate_boards(cmd, plan.format, &doc, &params)?;
let mut params = super::pdf::expand_preset(s, cmd, &params)?.into_owned();
if let (Some(o), "ai") = (params.as_object_mut(), plan.format.id) {
// A PDF of every artboard with the PDF options, always carrying the native document.
o.extend([("preserveEditing".into(), json!(true)), ("range".into(), json!("all"))]);
// Use Compression compresses the PDF's content.
if let Some(c) = o.get("compress").and_then(Value::as_bool) {
let compression = o.entry("compression").or_insert_with(|| json!({}));
if let Some(m) = compression.as_object_mut() {
m.insert("compressText".into(), json!(c));
}
}
}
Ok(SaveJob { plan, doc, params, snapshot: st.doc.clone() })
Ok(SaveJob { plan, doc, params, snapshot: st.doc.clone(), boards })
}
impl SaveJob {
@@ -303,26 +363,45 @@ impl SaveJob {
&self.snapshot
}
/// Encode the file → it, with the warnings (what the format loses first, then the encoder's
/// own notes: PDF options not applied yet…).
/// Encode the file → it (and, with `separateArtboards`, a file per artboard after it), with
/// the warnings (what the format loses first, then the encoder's own notes: PDF options not
/// applied yet…).
pub fn encode(&self) -> Result<Encoded> {
let (cmd, plan, doc) = (self.plan.mode.command(), &self.plan, &*self.doc);
let mut enc = if plan.format.id == "ai" {
let (bytes, warnings) = super::pdf::encode(cmd, &doc.without_edit_modes(), &self.params)?;
let mut enc = self.encode_one(&self.doc)?;
for &b in &self.boards {
let one = self.encode_one(&artboard_doc(&self.doc, b))?;
enc.files.extend(one.files.into_iter().map(|(_, bytes)| (Some(b), bytes)));
}
let pdf_less = self.plan.format.id == "ai" && !bool_or(&self.params, "pdfCompatible", true);
let notes = fidelity_warning(self.plan.format).into_iter().chain(pdf_less.then(|| NOT_PDF_COMPATIBLE.to_string()));
enc.warnings.splice(0..0, notes);
Ok(enc)
}
/// Encode one file of `doc` in the job's format.
fn encode_one(&self, doc: &Document) -> Result<Encoded> {
let (cmd, f) = (self.plan.mode.command(), self.plan.format);
let enc = if f.id == "ai" {
// The pages (blank without PDF content), carrying the native document.
let pages = doc.without_edit_modes();
let pages = if bool_or(&self.params, "pdfCompatible", true) { pages } else { std::borrow::Cow::Owned(blank_pages(&pages)) };
let native = || super::native::ai_native(cmd, f, doc, &self.params);
let (bytes, warnings) = super::pdf::encode_carrying(cmd, &pages, &self.params, native)?;
Encoded { warnings, ..Encoded::one(bytes) }
} else {
encode_all(doc, plan.format.id, &self.params)?
encode_all(doc, f.id, &self.params)?
};
if enc.files.len() != 1 {
return Err(bad(cmd, "Save writes one artboard: name one, or export several with document.export"));
}
enc.warnings.splice(0..0, fidelity_warning(plan.format));
Ok(enc)
}
/// Encode the file and write it with `write` (the file, then the images an SVG links to,
/// beside it). Without a path → `{dataBase64, bytes, format, name, folder?, warnings, linked?:
/// [{name, dataBase64}]}`; with one → `{path, format, bytes, warnings, linked?: [path…]}`.
/// Encode the file and write it with `write` (the file, then its artboards' files and the
/// images an SVG links to, beside it). Without a path → `{dataBase64, bytes, format, name,
/// folder?, warnings, files?: [{name, dataBase64}] (every file, when there are several),
/// linked?: [{name, dataBase64}]}`; with one → `{path, format, bytes, warnings, files?:
/// [path…], linked?: [path…]}`.
pub fn write(&self, mut write: impl FnMut(&str, &[u8]) -> Result<()>) -> Result<Value> {
let (plan, doc) = (&self.plan, &*self.doc);
let mut enc = self.encode()?;
@@ -342,7 +421,11 @@ impl SaveJob {
}
let bytes = files.first().map_or(0, |f| f.1.len());
let mut out = super::merge(json!({ "path": path, "bytes": bytes }), extra);
if let Some(linked) = files.get(1..).filter(|l| !l.is_empty()) {
let (main, linked) = files.split_at(enc.files.len().min(files.len()));
if main.len() > 1 {
out["files"] = main.iter().map(|(p, _)| json!(p)).collect();
}
if !linked.is_empty() {
out["linked"] = linked.iter().map(|(p, _)| json!(p)).collect();
}
Ok(out)
@@ -420,7 +503,9 @@ fn format_options(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "file.formatOptions";
let st = s.active();
let id = str_param(p, "format").or(st.map(|d| d.format)).unwrap_or("vectorcraft");
let f = format(id).filter(|f| f.write).ok_or_else(|| bad(C, format!("`{id}` is no writable format (see document.formats)")))?;
let f = format(id)
.filter(|f| f.write || SAVE_FORMATS.contains(&f.id))
.ok_or_else(|| bad(C, format!("`{id}` is no writable format (see document.formats)")))?;
let saved = st.filter(|d| d.format == f.id).map(|d| &d.save_options);
// A native save compresses as Use Compression says, unless told otherwise.
let prefs = with_compression_pref(&s.prefs, &json!({}));
@@ -443,7 +528,7 @@ pub(super) fn specs() -> Vec<CommandSpec> {
"Save Document",
[],
None,
"{path?, format?: vectorcraft|template|pdf|svg|svgz|ai (default: the path's extension, else the document's own format), options?: {…the format's options, see file.formatOptions; default: as last saved}, svg?: {…SVG options} (SVG options may also be given flat; an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), native (also flat): compress?: bool (gzip; default: the useCompression preference), version?: 3 (2 or 1: for older VectorCraft versions, never compressed), preview?: false (embed a PNG of the first artboard, at most 256 px), modified?: Unix seconds|null (the File Info modified date, and created date when there is none, a save to a file stamps; default now, recorded in the journal so a replay matches; null: leave the dates)} → {path, format, bytes, warnings, linked?: [path…] (images an SVG links to)}. Save writes one artboard, except a .ai file: a PDF-compatible file of every artboard carrying the native document (preserveEditing always on; PDF options flat or in options), which document.open restores exactly. Without a path it writes the document's own file in its own format: a document opened from or saved as SVG/PDF saves as that again (warnings name what the format loses). No path known (never saved, converted from an older version, restored by Data Recovery, or another format) → {dataBase64, format, name, folder?, warnings} and the document stays modified",
"{path?, format?: vectorcraft|template|pdf|svg|svgz|ai (default: the path's extension, else the document's own format), options?: {…the format's options, see file.formatOptions; default: as last saved}, svg?: {…SVG options} (SVG options may also be given flat; an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), native (also flat): compress?: bool (gzip; default: the useCompression preference), version?: 3 (2 or 1: for older VectorCraft versions, never compressed), preview?: false (embed a PNG of the first artboard, at most 256 px), native and .ai: separateArtboards?: false (also save each artboard of range?: \"1-3, 5\"|\"all\" (default) to <name>-<artboard>.<ext> beside the file, holding that artboard and the art touching it; the result's files lists every file written), includeLinked?: false (keep linked files' own pixels, not just their previews; they stay linked), embedProfiles?: true (carry the ICC profiles loaded from files that the document is tagged with; opening the file installs them where missing), pdfCompatible?: false for native (also carry a PDF of every artboard) | true for .ai (false: blank pages, only the native document), .ai: compress?: true (compression.compressText), modified?: Unix seconds|null (the File Info modified date, and created date when there is none, a save to a file stamps; default now, recorded in the journal so a replay matches; null: leave the dates)} → {path, format, bytes, warnings, files?: [path…] (the file and its artboards' files), linked?: [path…] (images an SVG links to)}. Save writes one artboard, except a .ai file: a PDF-compatible file of every artboard carrying the native document (preserveEditing always on; PDF options flat or in options), which document.open restores exactly; separateArtboards writes more files, one per artboard. Without a path it writes the document's own file in its own format: a document opened from or saved as SVG/PDF saves as that again (warnings name what the format loses). No path known (never saved, converted from an older version, restored by Data Recovery, or another format) → {dataBase64, format, name, folder?, warnings} and the document stays modified",
has_doc,
|s, p| save(s, SaveMode::Save, p)
),
+3 -3
View File
@@ -735,7 +735,7 @@ fn restore_one(s: &mut Session, store: &Arc<dyn RecoveryStore>, file: &str) -> R
let bytes = store.read(&format!("{file}{DOC_EXT}")).map_err(EngineError::Other)?;
let meta_bytes = store.read(&format!("{file}{META_EXT}")).unwrap_or_default();
let meta: Meta = serde_json::from_slice(&meta_bytes).unwrap_or_default();
let mut doc = vectorcraft_format::load(&bytes).map_err(|e| EngineError::Other(format!("recovery copy `{file}`: {e}")))?;
let (mut doc, _, warnings) = fileio::native_file(&bytes).map_err(|e| EngineError::Other(format!("recovery copy `{file}`: {e}")))?;
let area = claim(s, store)?;
let mut taken: Vec<String> = copies_in(&store.list().map_err(EngineError::Other)?).into_iter().map(|(_, f)| f).collect();
taken.extend(open_copies(s));
@@ -761,7 +761,7 @@ fn restore_one(s: &mut Session, store: &Arc<dyn RecoveryStore>, file: &str) -> R
// The copy stays until the document is saved or closed.
st.recovery = Some(RecoveryCopy { file: mine.clone(), doc: st.doc.clone() });
let title = st.title();
Ok(super::fileio::merge(json!({ "index": index, "title": title, "file": mine, "path": meta.path }), links.to_json()))
Ok(super::fileio::merge(json!({ "index": index, "title": title, "file": mine, "path": meta.path, "warnings": warnings }), links.to_json()))
}
fn restore(s: &mut Session, p: &Value) -> Result<Value> {
@@ -823,7 +823,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Restore Recovered Documents",
[],
None,
"{file?} open a copy left behind by a crash (default: every one; a running VectorCraft's copies are never taken) as a new document titled \"<name> [Recovered]\": modified, and Save asks where to save it (suggesting its original file) → {restored: [{index, title, file, path, missingLinks, modifiedLinks, updatedLinks}], failed: [{file, error}] (damaged copies, when restoring every one; a named one fails the command)}. The copy moves to this app's area and stays until the document is saved or closed",
"{file?} open a copy left behind by a crash (default: every one; a running VectorCraft's copies are never taken) as a new document titled \"<name> [Recovered]\": modified, and Save asks where to save it (suggesting its original file) → {restored: [{index, title, file, path, warnings, missingLinks, modifiedLinks, updatedLinks}], failed: [{file, error}] (damaged copies, when restoring every one; a named one fails the command)}. The copy moves to this app's area and stays until the document is saved or closed",
has_store,
restore
),