mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 14:16:56 -06:00
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:
co-authored by
Claude Opus 5.5
parent
975067b44a
commit
bf4614669e
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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, ¶ms);
|
||||
super::native::save_options(cmd, plan.format, &doc, ¶ms)?;
|
||||
}
|
||||
let boards = separate_boards(cmd, plan.format, &doc, ¶ms)?;
|
||||
let mut params = super::pdf::expand_preset(s, cmd, ¶ms)?.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)
|
||||
),
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user