IO.1: preserve editable export data and AI artboard options
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
f9a6c10719
commit
365e7f2896
|
|
@ -293,9 +293,9 @@ pub(crate) fn anti_alias(id: &str) -> std::result::Result<AntiAlias, String> {
|
|||
AntiAlias::from_id(id).ok_or_else(|| format!("antiAlias `{id}`: none, art or type"))
|
||||
}
|
||||
|
||||
/// The `useArtboards` param of a PDF, DXF or raster export (SVG has its own): `true` writes every
|
||||
/// The `useArtboards` param of a PDF, AI, DXF or raster export (SVG has its own): `true` writes every
|
||||
/// chosen artboard (default all), `false` the bounds of the visible art instead, absent one
|
||||
/// artboard (raster, DXF) or the chosen pages (PDF).
|
||||
/// artboard (raster, DXF) or the chosen pages (PDF, AI).
|
||||
fn use_artboards(p: &Value) -> Result<Option<bool>> {
|
||||
match p.get("useArtboards") {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
|
|
@ -364,9 +364,10 @@ pub fn encode_with_warnings(doc: &Document, format: &str, p: &Value) -> Result<(
|
|||
|
||||
/// [`encode`], with every file the export writes.
|
||||
pub fn encode_all(doc: &Document, format: &str, p: &Value) -> Result<Encoded> {
|
||||
// Placed documents output their files' art (a native file keeps what it keeps: see
|
||||
// `native::encode`).
|
||||
if !matches!(format, "vectorcraft" | "template") {
|
||||
let format = super::writable(C, Some(format), None)?.id;
|
||||
// Formats with editing data prepare their drawn output separately from their original
|
||||
// document. Other formats only need the placed files' art.
|
||||
if !matches!(format, "vectorcraft" | "template" | "ai" | "pdf" | "eps" | "svg" | "svgz") {
|
||||
let (full, warnings) = crate::cmd::place::document::full_documents(doc);
|
||||
if !warnings.is_empty() {
|
||||
let mut enc = encode_all_of(&full, format, p)?;
|
||||
|
|
@ -397,7 +398,7 @@ fn encode_files(doc: &Document, f: &Format, p: &Value) -> Result<Encoded> {
|
|||
let doc = &*doc.without_edit_modes();
|
||||
let n = doc.artboards.len();
|
||||
// SVG reads its own `useArtboards` (an SVG option).
|
||||
let use_artboards = if f.raster || matches!(f.id, "pdf" | "dxf" | "emf" | "wmf") { use_artboards(p)? } else { None };
|
||||
let use_artboards = if f.raster || matches!(f.id, "pdf" | "ai" | "dxf" | "emf" | "wmf") { use_artboards(p)? } else { None };
|
||||
if use_artboards == Some(false) {
|
||||
let bounds = vectorcraft_render::encode::art_bounds(doc).ok_or_else(|| bad(C, "nothing to export: the document has no visible art"))?;
|
||||
let mut q = super::export::without_artboards(p);
|
||||
|
|
@ -417,16 +418,11 @@ fn encode_files(doc: &Document, f: &Format, p: &Value) -> Result<Encoded> {
|
|||
"txt" => super::text::encode(doc, p)?,
|
||||
"svg" | "svgz" => return super::svg::encode(doc, p, f.id == "svgz").map_err(|e| bad(C, e)),
|
||||
"dxf" => return super::dxf::encode(doc, p, use_artboards),
|
||||
// As Save As writes it (a PDF of every artboard carrying the native document).
|
||||
"ai" => {
|
||||
let mut q = p.clone();
|
||||
super::save::ai_params(&mut q);
|
||||
return super::save::encode_ai(C, f, doc, &q);
|
||||
}
|
||||
// EPS reads its own `useArtboards` (the art's bounds unless asked).
|
||||
"eps" => return super::eps::encode(doc, p),
|
||||
"emf" => return super::metafile::encode(doc, p, use_artboards, vectorcraft_metafile::Kind::Emf),
|
||||
"wmf" => return super::metafile::encode(doc, p, use_artboards, vectorcraft_metafile::Kind::Wmf),
|
||||
"ai" => return super::save::encode_ai(C, f, doc, p),
|
||||
"pdf" => {
|
||||
let (bytes, warnings) = super::pdf::encode(C, doc, p)?;
|
||||
return Ok(Encoded { warnings, ..Encoded::one(bytes) });
|
||||
|
|
|
|||
|
|
@ -255,6 +255,17 @@ fn native(doc: &Document, board: Option<usize>, include_linked: bool) -> Vec<u8>
|
|||
/// itself.
|
||||
pub(super) fn encode(doc: &Document, p: &Value) -> Result<Encoded> {
|
||||
let set = parse(p)?;
|
||||
let original = doc;
|
||||
let (full, warnings) = crate::cmd::place::document::full_documents(doc);
|
||||
let doc = &*full;
|
||||
// Include available bytes while retaining missing, relinkable objects in the attachment.
|
||||
let editing = if set.include_linked {
|
||||
let mut d = original.clone();
|
||||
d.images = doc.images.clone();
|
||||
Cow::Owned(d)
|
||||
} else {
|
||||
Cow::Borrowed(original)
|
||||
};
|
||||
let pages = pages(doc, &set)?;
|
||||
if pages.is_empty() {
|
||||
return Err(bad(C, "the document has no artboard"));
|
||||
|
|
@ -262,7 +273,7 @@ pub(super) fn encode(doc: &Document, p: &Value) -> Result<Encoded> {
|
|||
let transparent = doc.layers.iter().any(|l| l.shows_transparency());
|
||||
let flat = flatten_document(doc, &set.flatten)?;
|
||||
let drawn = flat.as_ref().unwrap_or(doc);
|
||||
let mut enc = Encoded { joiner: Some("_"), ..Encoded::default() };
|
||||
let mut enc = Encoded { warnings, joiner: Some("_"), ..Encoded::default() };
|
||||
if transparent && flat.is_some() {
|
||||
enc.warnings.push("transparency is flattened into opaque art and images (EPS has none): see the flattener preset".into());
|
||||
}
|
||||
|
|
@ -275,8 +286,14 @@ pub(super) fn encode(doc: &Document, p: &Value) -> Result<Encoded> {
|
|||
kind => Some(render(&mut renderer, doc, rect, MAX_PREVIEW, set.eps.transparent_preview && kind == Preview::TiffColor)?),
|
||||
};
|
||||
let thumbnail = set.thumbnails.then(|| render(&mut renderer, doc, rect, THUMBNAIL, false)).transpose()?;
|
||||
let o =
|
||||
EpsOptions { region, origin, title: doc.title.clone(), created, native: Some(native(doc, board, set.include_linked)), ..set.eps.clone() };
|
||||
let o = EpsOptions {
|
||||
region,
|
||||
origin,
|
||||
title: doc.title.clone(),
|
||||
created,
|
||||
native: Some(native(&editing, board, set.include_linked)),
|
||||
..set.eps.clone()
|
||||
};
|
||||
let out = vectorcraft_eps::export(drawn, &o, preview.as_ref(), thumbnail.as_ref()).map_err(|e| bad(C, e))?;
|
||||
enc.files.push((board, out.bytes));
|
||||
for w in out.warnings {
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ const ARTBOARDS: FormatOption = FormatOption {
|
|||
name: "artboards",
|
||||
ty: "array",
|
||||
default: "null",
|
||||
description: "0-based artboards (PDF: one page each, default all; SVG, and raster formats with useArtboards: one file each)",
|
||||
description: "0-based artboards (PDF and AI: one page each, default all; SVG, and raster formats with useArtboards: one file each)",
|
||||
};
|
||||
const RANGE: FormatOption = FormatOption {
|
||||
name: "range",
|
||||
|
|
@ -260,7 +260,7 @@ const USE_ARTBOARDS: FormatOption = FormatOption {
|
|||
name: "useArtboards",
|
||||
ty: "boolean",
|
||||
default: "null",
|
||||
description: "true: every chosen artboard (default all), one file each named <file>-<artboard>.<ext> (PDF: one page each); false: the bounds of the visible art",
|
||||
description: "true: every chosen artboard (default all), one file each named <file>-<artboard>.<ext> (PDF and AI: one page each); false: the bounds of the visible art",
|
||||
};
|
||||
const PPI: FormatOption = FormatOption {
|
||||
name: "ppi",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
//! artboard to a separate file, Include Linked Files, Embed ICC Profiles and Create
|
||||
//! PDF-Compatible File.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
|
@ -25,8 +26,8 @@ pub const OPTIONS: &[FormatOption] = &[
|
|||
FormatOption {
|
||||
name: "version",
|
||||
ty: "integer",
|
||||
default: "3",
|
||||
description: "the format version to write: 3 (current), or 2 or 1 for older VectorCraft versions (not compressed; newer features they don't know are lost there)",
|
||||
default: "4",
|
||||
description: "the format version to write: 4 (current), or 3, 2 or 1 for older VectorCraft versions (1 and 2 are never compressed; newer features they don't know are lost there)",
|
||||
},
|
||||
FormatOption {
|
||||
name: "preview",
|
||||
|
|
@ -71,11 +72,19 @@ const EMBED_PROFILES: FormatOption = FormatOption {
|
|||
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).
|
||||
/// The save and export options of a `.ai` file (a PDF carrying the native document; its PDF
|
||||
/// options come on top, see document.exportPdf). Saves always retain every artboard.
|
||||
pub const AI_OPTIONS: &[FormatOption] = &[
|
||||
super::ARTBOARD,
|
||||
super::ARTBOARDS,
|
||||
super::USE_ARTBOARDS,
|
||||
FormatOption {
|
||||
name: "range",
|
||||
ty: "string",
|
||||
default: "null",
|
||||
description: "1-based artboards such as \"1-3, 5\", or \"all\": the PDF pages to export, or with separateArtboards the files to save separately",
|
||||
},
|
||||
SEPARATE_ARTBOARDS,
|
||||
SEPARATE_RANGE,
|
||||
INCLUDE_LINKED,
|
||||
EMBED_PROFILES,
|
||||
FormatOption {
|
||||
|
|
@ -133,15 +142,23 @@ pub(super) fn encode(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Result
|
|||
} else {
|
||||
std::borrow::Cow::Borrowed(doc)
|
||||
};
|
||||
let doc = &*full;
|
||||
if extras.preview {
|
||||
so.preview = preview_png(doc)?;
|
||||
so.preview = preview_png(&full)?;
|
||||
}
|
||||
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);
|
||||
so.pdf = Some(super::pdf::encode(cmd, &full, &json!({ "range": "all", "preserveEditing": false }))?.0);
|
||||
}
|
||||
vectorcraft_format::save_with(doc, &so).map_err(|e| bad(cmd, e.to_string()))
|
||||
let native = match full {
|
||||
Cow::Owned(full) if so.include_linked && so.version >= vectorcraft_format::VERSION => {
|
||||
// Include available bytes without replacing missing, relinkable placed objects.
|
||||
let mut original = doc.clone();
|
||||
original.images = full.images;
|
||||
Cow::Owned(original)
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
vectorcraft_format::save_with(&native, &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
|
||||
|
|
@ -172,16 +189,40 @@ pub(super) fn install_profiles(profiles: BTreeMap<String, Vec<u8>>) -> Vec<Strin
|
|||
.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>> {
|
||||
/// Check AI options even when chosen PDF pages omit the native editing attachment.
|
||||
pub(super) fn ai_options(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Result<SaveOptions> {
|
||||
let mut q = p.clone();
|
||||
if let Some(o) = q.as_object_mut() {
|
||||
o.retain(|k, _| !matches!(k.as_str(), "compress" | "version" | "preview" | "pdfCompatible"));
|
||||
// AI carries the current native format without a separate preview. Its compression
|
||||
// and PDF-content flags are still validated as booleans.
|
||||
o.retain(|k, _| !matches!(k.as_str(), "version" | "preview"));
|
||||
if o.get("pdfCompatible").is_some_and(Value::is_null) {
|
||||
o.remove("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()))
|
||||
so.compress = false; // The PDF compresses its editing attachment.
|
||||
Ok(so)
|
||||
}
|
||||
|
||||
/// The original native document carried by an AI file, with validated save options.
|
||||
pub(super) fn ai_native(cmd: &str, doc: &Document, so: &SaveOptions) -> Result<Vec<u8>> {
|
||||
let native = if so.include_linked {
|
||||
match crate::cmd::place::document::full_documents(doc).0 {
|
||||
Cow::Borrowed(d) => Cow::Borrowed(d),
|
||||
Cow::Owned(full) => {
|
||||
// Include available files, retaining placed objects even when their files
|
||||
// are missing. Only drawn output substitutes unlinked preview images.
|
||||
let mut original = doc.clone();
|
||||
original.images = full.images;
|
||||
Cow::Owned(original)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Cow::Borrowed(doc)
|
||||
};
|
||||
vectorcraft_format::save_with(&native, so).map_err(|e| bad(cmd, e.to_string()))
|
||||
}
|
||||
|
||||
/// The first artboard (else the art) as a PNG fitted into [`PREVIEW_MAX`] pixels (`None`: nothing
|
||||
|
|
|
|||
|
|
@ -249,7 +249,11 @@ 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)))
|
||||
// Keep the editable object tree independent of preview fallbacks used by the drawn pages.
|
||||
let (full, mut warnings) = crate::cmd::place::document::full_documents(doc);
|
||||
let (bytes, drawn_warnings) = encode_carrying(cmd, &full, p, || Ok(vectorcraft_format::save(doc, false)))?;
|
||||
warnings.extend(drawn_warnings);
|
||||
Ok((bytes, warnings))
|
||||
}
|
||||
|
||||
/// [`encode`] the pages of `doc`, carrying `native()` as the editing data (a `.ai` file: the
|
||||
|
|
|
|||
|
|
@ -353,36 +353,41 @@ fn blank_pages(doc: &Document) -> Document {
|
|||
/// 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";
|
||||
|
||||
/// The encoder's params for a `.ai` file, from a save's or an export's: a PDF of every artboard
|
||||
/// with the PDF options, always carrying the native document, its layers and sublayers PDF layers
|
||||
/// (hidden ones off) unless asked otherwise; Use Compression compresses the PDF's content.
|
||||
pub(super) fn ai_params(params: &mut Value) {
|
||||
let Some(o) = params.as_object_mut() else { return };
|
||||
o.extend([("preserveEditing".into(), json!(true)), ("range".into(), json!("all"))]);
|
||||
// Apps that read the PDF part (not the native document) then find the layers, the hidden ones
|
||||
// with their art (#372); a PDF standard decides for itself.
|
||||
if o.get("standard").is_none_or(Value::is_null) {
|
||||
o.entry("createLayers").or_insert(json!(true));
|
||||
}
|
||||
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));
|
||||
/// The shared Save and Export encoder for PDF-compatible `.ai` files.
|
||||
pub(super) fn encode_ai(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Result<Encoded> {
|
||||
let mut params = if p.is_object() { p.clone() } else { json!({}) };
|
||||
if let Some(o) = params.as_object_mut() {
|
||||
o.insert("preserveEditing".into(), json!(true));
|
||||
// Keep upstream AI PDF layers, including hidden layers, unless a standard decides.
|
||||
if o.get("standard").is_none_or(Value::is_null) {
|
||||
o.entry("createLayers").or_insert(json!(true));
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
let native_options = super::native::ai_options(cmd, f, doc, ¶ms)?;
|
||||
let pdf_compatible = bool_or(¶ms, "pdfCompatible", true);
|
||||
if !pdf_compatible {
|
||||
let picked: ArtboardPick = super::encode::options(f, ¶ms)?;
|
||||
let picked = picked.resolve(doc.artboards.len()).map_err(|e| bad(cmd, e))?;
|
||||
if picked.is_some_and(|v| !v.into_iter().eq(0..doc.artboards.len())) {
|
||||
return Err(bad(cmd, "pdfCompatible: false needs every artboard to keep the editable artwork"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode `doc` as a PDF-compatible `.ai` file (`params` as [`ai_params`] makes them) for command
|
||||
/// `cmd`: its pages (blank with `pdfCompatible: false`, which it then notes), carrying the native
|
||||
/// document. File › Save As writes it, and so do the exports (`document.export`, the CLI).
|
||||
pub(super) fn encode_ai(cmd: &str, f: &Format, doc: &Document, params: &Value) -> Result<Encoded> {
|
||||
let compatible = bool_or(params, "pdfCompatible", true);
|
||||
let pages = doc.without_edit_modes();
|
||||
let pages = if compatible { pages } else { std::borrow::Cow::Owned(blank_pages(&pages)) };
|
||||
let native = || super::native::ai_native(cmd, f, doc, params);
|
||||
let (bytes, mut warnings) = super::pdf::encode_carrying(cmd, &pages, params, native)?;
|
||||
if !compatible {
|
||||
warnings.insert(0, NOT_PDF_COMPATIBLE.to_string());
|
||||
// Resolve placed files only for the drawn pages. Missing files become preview images there;
|
||||
// the native attachment keeps the original placed objects and their relinkable metadata.
|
||||
let (pages, placed_warnings) =
|
||||
if pdf_compatible { crate::cmd::place::document::full_documents(&pages) } else { (std::borrow::Cow::Owned(blank_pages(&pages)), Vec::new()) };
|
||||
let native = || super::native::ai_native(cmd, doc, &native_options);
|
||||
let (bytes, mut warnings) = super::pdf::encode_carrying(cmd, &pages, ¶ms, native)?;
|
||||
warnings.extend(placed_warnings);
|
||||
if !pdf_compatible {
|
||||
warnings.push(NOT_PDF_COMPATIBLE.to_string());
|
||||
}
|
||||
Ok(Encoded { warnings, ..Encoded::one(bytes) })
|
||||
}
|
||||
|
|
@ -421,8 +426,9 @@ pub(crate) fn job_for(s: &Session, st: &DocState, plan: SavePlan) -> Result<Save
|
|||
}
|
||||
let boards = separate_boards(cmd, plan.format, &doc, ¶ms)?;
|
||||
let mut params = super::pdf::expand_preset(s, cmd, ¶ms)?.into_owned();
|
||||
if plan.format.id == "ai" {
|
||||
ai_params(&mut params);
|
||||
if let (Some(o), "ai") = (params.as_object_mut(), plan.format.id) {
|
||||
// Save keeps every artboard; exports may choose a subset instead.
|
||||
o.insert("range".into(), json!("all"));
|
||||
}
|
||||
Ok(SaveJob { plan, doc, params, snapshot: st.doc.clone(), boards })
|
||||
}
|
||||
|
|
@ -596,7 +602,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), 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",
|
||||
"{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?: 4 (3, 2 or 1: for older VectorCraft versions; 1 and 2 are 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)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -153,10 +153,14 @@ pub(super) fn encode(doc: &Document, p: &Value, compressed: bool) -> Result<Enco
|
|||
let (opts, boards, chosen) = plan(doc, p)?;
|
||||
// Preserve editing embeds the native document (once, shared by every file).
|
||||
let native = opts.preserve_editing.then(|| vectorcraft_format::save(doc, false));
|
||||
// Draw linked files as vectors when available, or their previews with a warning. Keep
|
||||
// that preparation out of the native attachment so missing links remain relinkable.
|
||||
let (full, warnings) = crate::cmd::place::document::full_documents(doc);
|
||||
let doc = &*full;
|
||||
// SVG has no filters for the Photoshop-style effects: their objects go in as images.
|
||||
let flat = crate::cmd::rasterfx::flatten_pixel_effects(doc);
|
||||
let doc = flat.as_ref().unwrap_or(doc);
|
||||
let mut enc = Encoded::default();
|
||||
let mut enc = Encoded { warnings, ..Encoded::default() };
|
||||
for artboard in boards {
|
||||
// A chosen artboard's file holds the art over it (#550).
|
||||
let over = artboard.filter(|_| chosen).and_then(|b| doc.artboards.get(b)).map(|a| super::export::art_over(doc, a.rect));
|
||||
|
|
|
|||
|
|
@ -190,3 +190,64 @@ fn a_pdf_of_some_artboards_carries_no_editing_data() {
|
|||
assert_eq!(warned, !kept, "{range}: {v}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ai_export_of_some_artboards_keeps_only_those_pages() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 100, "height": 80, "artboards": 3})).unwrap();
|
||||
let r = s.execute("document.export", &json!({"format": "ai", "range": "2"})).unwrap();
|
||||
let bytes = b64(&r);
|
||||
assert!(vectorcraft_pdf::editing(&bytes).is_none());
|
||||
assert!(r["warnings"].as_array().unwrap().contains(&json!(super::pdf::EDITING_NEEDS_EVERY_ARTBOARD)));
|
||||
open(&mut s, "one.ai", &bytes);
|
||||
assert_eq!(s.doc().unwrap().doc.artboards.len(), 1);
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"artboards": 3})).unwrap();
|
||||
assert!(s.execute("document.export", &json!({"format": "ai", "range": "2", "pdfCompatible": false})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ai_export_validates_options_even_when_editing_data_is_omitted() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"artboards": 3})).unwrap();
|
||||
let before = comparable(&s.doc().unwrap().doc);
|
||||
for command in ["document.export", "document.serialize"] {
|
||||
for range in ["all", "2"] {
|
||||
for option in ["includeLinked", "embedProfiles", "compress", "pdfCompatible"] {
|
||||
let mut params = json!({"format": "ai", "range": range});
|
||||
params[option] = json!("no");
|
||||
assert!(s.execute(command, ¶ms).is_err(), "{command}: {params}");
|
||||
assert_eq!(comparable(&s.doc().unwrap().doc), before);
|
||||
}
|
||||
assert!(
|
||||
s.execute(
|
||||
command,
|
||||
&json!({"format": "ai", "range": range, "includeLinked": false, "embedProfiles": false, "compress": false, "pdfCompatible": true})
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ai_export_without_artboards_keeps_art_outside_the_original_pages() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 100, "height": 80, "artboards": 2})).unwrap();
|
||||
s.execute("paint.setStroke", &json!({"none": true})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 180, "y": 100, "width": 30, "height": 20})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 1000, "y": 1000, "width": 50, "height": 40})).unwrap();
|
||||
s.execute("object.hide", &json!({})).unwrap();
|
||||
let before = comparable(&s.doc().unwrap().doc);
|
||||
let r = s.execute("document.export", &json!({"format": "ai", "useArtboards": false, "range": "2"})).unwrap();
|
||||
let bytes = b64(&r);
|
||||
let pages = vectorcraft_pdf::import(&bytes).unwrap();
|
||||
assert_eq!(pages.artboards.len(), 1);
|
||||
assert_eq!((pages.artboards[0].rect.width(), pages.artboards[0].rect.height()), (30.0, 20.0));
|
||||
let editing = vectorcraft_pdf::editing(&bytes).unwrap();
|
||||
let exported = vectorcraft_format::load(&editing.data).unwrap();
|
||||
assert_eq!(exported.artboards.len(), 1);
|
||||
assert_eq!(exported.artboards[0].rect, vectorcraft_geom::Rect::new(180.0, 100.0, 210.0, 120.0));
|
||||
assert_eq!(comparable(&s.doc().unwrap().doc), before);
|
||||
assert!(s.execute("document.export", &json!({"format": "ai", "useArtboards": "yes"})).is_err());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,6 +198,154 @@ fn the_preview_is_saved_so_the_document_shows_it_without_its_file() {
|
|||
assert!(old.contains("\"image\"") && !old.contains("placeddocument"), "{old}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ai_export_preserves_placed_links_while_drawing_full_art_or_previews() {
|
||||
for missing in [false, true] {
|
||||
let dir = Folder::new(&format!("placed-ai-editable-{missing}"));
|
||||
let src = dir.file("logo.vectorcraft");
|
||||
star_source(&src, &format!("placed-ai-editable-{missing}"));
|
||||
let mut s = session();
|
||||
let id = place(&mut s, &src);
|
||||
let parent = dir.file("parent.vectorcraft");
|
||||
save(&mut s, &parent);
|
||||
if missing {
|
||||
std::fs::remove_file(&src).unwrap();
|
||||
}
|
||||
open(&mut s, &parent);
|
||||
let original = placed(&s, id);
|
||||
for pdf_compatible in [true, false] {
|
||||
for (command, include_linked) in
|
||||
[("document.export", false), ("document.export", true), ("document.serialize", false), ("document.serialize", true)]
|
||||
{
|
||||
let out = s.execute(command, &json!({"format": "ai", "pdfCompatible": pdf_compatible, "includeLinked": include_linked})).unwrap();
|
||||
let bytes = decode(&out);
|
||||
let editing = vectorcraft_pdf::editing(&bytes).unwrap();
|
||||
let native = vectorcraft_format::load(&editing.data).unwrap();
|
||||
assert_eq!(
|
||||
native.images[&original.key].is_proxy(),
|
||||
missing || !include_linked,
|
||||
"Include Linked Files retains available source bytes"
|
||||
);
|
||||
if pdf_compatible {
|
||||
let (images, paths) = pdf_contents(&bytes);
|
||||
if missing {
|
||||
assert!(images > 0, "the missing file draws its preview");
|
||||
assert!(out["warnings"].to_string().contains("output as their previews"));
|
||||
} else {
|
||||
assert_eq!(images, 0, "the PDF uses the file's vectors");
|
||||
assert!(paths > 0);
|
||||
}
|
||||
}
|
||||
let mut restored = session();
|
||||
let result = restored
|
||||
.execute("document.open", &json!({"name": "export.ai", "dataBase64": vectorcraft_format::base64_encode(&bytes)}))
|
||||
.unwrap();
|
||||
assert_eq!(result["restored"], true);
|
||||
assert_eq!(placed(&restored, id), original, "link metadata stays editable");
|
||||
assert_eq!(placed(&s, id), original, "export leaves the source intact");
|
||||
if missing {
|
||||
assert_eq!(restored.execute("links.check", &json!({})).unwrap()["missing"], 1);
|
||||
let replacement = dir.file("replacement.vectorcraft");
|
||||
source(&replacement, 100.0, 50.0, BLUE, 1);
|
||||
let relinked = restored.execute("links.relink", &json!({"ids": [id.0], "path": replacement})).unwrap();
|
||||
assert_eq!(relinked["relinked"], json!([id.0]));
|
||||
assert_eq!(placed(&restored, id).link.path, replacement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ai_export_public_aliases_preserve_missing_placed_links() {
|
||||
let dir = Folder::new("placed-ai-public-aliases-latest");
|
||||
let src = dir.file("logo.vectorcraft");
|
||||
star_source(&src, "placed-ai-public-aliases-latest-unique");
|
||||
let mut s = session();
|
||||
let id = place(&mut s, &src);
|
||||
let parent = dir.file("parent.vectorcraft");
|
||||
save(&mut s, &parent);
|
||||
std::fs::remove_file(&src).unwrap();
|
||||
open(&mut s, &parent);
|
||||
let original = placed(&s, id);
|
||||
assert!(s.doc().unwrap().doc.images[&original.key].is_proxy());
|
||||
|
||||
for alias in
|
||||
["vectorcraft", ".vectorcraft", "VECTORCRAFT", "drawcraft", ".DRAWCRAFT", "template", "vctemplate", ".VCTEMPLATE", "ai", ".ai", "AI", ".AI"]
|
||||
{
|
||||
let canonical = cmd::fileio::format(alias).unwrap().id;
|
||||
let enc = cmd::fileio::encode_all(&s.doc().unwrap().doc, alias, &json!({"pdfCompatible": false, "includeLinked": false})).unwrap();
|
||||
let bytes = &enc.files[0].1;
|
||||
let raw = if canonical == "ai" { vectorcraft_pdf::editing(bytes).unwrap().data } else { bytes.clone() };
|
||||
let native = vectorcraft_format::load(&raw).unwrap();
|
||||
assert!(
|
||||
matches!(&native.node(id).unwrap().kind, NodeKind::PlacedDocument(p) if **p == original),
|
||||
"{alias}: preparation discarded the editable placed link"
|
||||
);
|
||||
assert!(native.images[&original.key].is_proxy(), "{alias}");
|
||||
assert_eq!(placed(&s, id), original, "{alias}: source changed");
|
||||
}
|
||||
}
|
||||
|
||||
/// Include Linked Files must retain relinkable objects when only their previews are available.
|
||||
#[test]
|
||||
fn native_included_links_keep_missing_placed_documents_editable() {
|
||||
for missing in [true, false] {
|
||||
let dir = Folder::new(&format!("native-included-links-{missing}"));
|
||||
let src = dir.file("logo.vectorcraft");
|
||||
star_source(&src, &format!("native-included-links-{missing}"));
|
||||
let source_bytes = std::fs::read(&src).unwrap();
|
||||
let mut s = session();
|
||||
let id = place(&mut s, &src);
|
||||
let parent = decode(&s.execute("document.serialize", &json!({"format": "vectorcraft"})).unwrap());
|
||||
if missing {
|
||||
std::fs::remove_file(&src).unwrap();
|
||||
}
|
||||
s.execute("document.open", &json!({"name": "parent.vectorcraft", "dataBase64": vectorcraft_format::base64_encode(&parent)})).unwrap();
|
||||
let original = placed(&s, id);
|
||||
assert!(s.doc().unwrap().doc.images[&original.key].is_proxy());
|
||||
let before = s.execute("document.json", &json!({})).unwrap();
|
||||
for format in ["vectorcraft", "template"] {
|
||||
for command in ["document.serialize", "document.export", "document.save"] {
|
||||
for include_linked in [true, false] {
|
||||
let out = s.execute(command, &json!({"format": format, "includeLinked": include_linked, "compress": false})).unwrap();
|
||||
let bytes = decode(&out);
|
||||
let native = vectorcraft_format::load(&bytes).unwrap();
|
||||
assert_eq!(file_json(&bytes)["version"], vectorcraft_format::VERSION);
|
||||
assert!(
|
||||
matches!(&native.node(id).unwrap().kind, NodeKind::PlacedDocument(p) if **p == original),
|
||||
"{command}/{format}/{missing}/{include_linked}: discarded the editable link"
|
||||
);
|
||||
let blob = &native.images[&original.key];
|
||||
assert_eq!(blob.is_proxy(), missing || !include_linked);
|
||||
if !missing && include_linked {
|
||||
assert_eq!(blob.bytes.as_slice(), source_bytes.as_slice());
|
||||
}
|
||||
assert_eq!(s.execute("document.json", &json!({})).unwrap(), before);
|
||||
if missing && include_linked {
|
||||
let mut restored = Session::new();
|
||||
restored
|
||||
.execute(
|
||||
"document.open",
|
||||
&json!({"name": "included.vectorcraft", "dataBase64": vectorcraft_format::base64_encode(&bytes)}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(placed(&restored, id), original);
|
||||
assert_eq!(restored.execute("links.check", &json!({})).unwrap()["missing"], 1);
|
||||
let replacement = dir.file("replacement.vectorcraft");
|
||||
source(&replacement, 100.0, 50.0, BLUE, 1);
|
||||
assert_eq!(
|
||||
restored.execute("links.relink", &json!({"ids": [id.0], "path": replacement})).unwrap()["relinked"],
|
||||
json!([id.0])
|
||||
);
|
||||
assert_eq!(placed(&restored, id).link.path, replacement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_document_saved_with_previews_outputs_its_files_art() {
|
||||
let dir = Folder::new("placed-preview-output");
|
||||
|
|
@ -479,3 +627,256 @@ fn link_info_gives_the_artboards_size_not_pixels() {
|
|||
"{i}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Save As reads its defaults from the catalog; using them must retain current editable links.
|
||||
#[test]
|
||||
fn native_save_catalog_defaults_keep_placed_documents_editable() {
|
||||
for missing in [false, true] {
|
||||
let dir = Folder::new(if missing { "native-default-missing" } else { "native-default-linked" });
|
||||
let src = dir.file("logo.vectorcraft");
|
||||
star_source(&src, if missing { "native-default-missing" } else { "native-default-linked" });
|
||||
let mut s = session();
|
||||
let id = place(&mut s, &src);
|
||||
let original = placed(&s, id);
|
||||
if missing {
|
||||
std::fs::remove_file(&src).unwrap();
|
||||
}
|
||||
let options = s.execute("file.formatOptions", &json!({"format": "vectorcraft"})).unwrap();
|
||||
let saved = dir.file("default-options.vectorcraft");
|
||||
s.execute("document.save", &json!({"path": saved, "version": options["options"]["version"]["value"]})).unwrap();
|
||||
let bytes = std::fs::read(&saved).unwrap();
|
||||
let native = vectorcraft_format::load(&bytes).unwrap();
|
||||
let mut written = original.clone();
|
||||
// Native saves record a sibling link relative to the destination as well as its
|
||||
// absolute path. This is relocation metadata, not a change to the placed object.
|
||||
written.link.relative = Some("logo.vectorcraft".into());
|
||||
assert!(matches!(&native.node(id).unwrap().kind, NodeKind::PlacedDocument(p) if **p == written), "source missing: {missing}");
|
||||
assert_eq!(file_json(&bytes)["version"], vectorcraft_format::VERSION);
|
||||
assert_eq!(placed(&s, id), original, "saving does not rewrite the live link");
|
||||
let mut reopened = Session::new();
|
||||
open(&mut reopened, &saved);
|
||||
assert_eq!(placed(&reopened, id).link.path, src);
|
||||
assert_eq!(reopened.execute("links.check", &json!({})).unwrap()["missing"], usize::from(missing));
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic and dedicated PDF exports share editable links and prepared drawn pages.
|
||||
#[test]
|
||||
fn pdf_editing_keeps_placed_links_separate_from_drawn_previews() {
|
||||
for missing in [true, false] {
|
||||
let dir = Folder::new(&format!("pdf-editable-links-{missing}"));
|
||||
let src = dir.file("logo.vectorcraft");
|
||||
star_source(&src, &format!("pdf-editable-links-{missing}"));
|
||||
let mut s = session();
|
||||
parent_star(&mut s);
|
||||
let id = place(&mut s, &src);
|
||||
s.execute("artboard.new", &json!({"x":600,"y":0,"width":100,"height":100})).unwrap();
|
||||
let parent = decode(&s.execute("document.serialize", &json!({"format": "vectorcraft"})).unwrap());
|
||||
if missing {
|
||||
std::fs::remove_file(&src).unwrap();
|
||||
}
|
||||
s.execute("document.open", &json!({"name": "parent.vectorcraft", "dataBase64": vectorcraft_format::base64_encode(&parent)})).unwrap();
|
||||
let original = placed(&s, id);
|
||||
assert!(s.doc().unwrap().doc.images[&original.key].is_proxy());
|
||||
let before = s.execute("document.json", &json!({})).unwrap();
|
||||
for command in ["document.serialize", "document.export", "document.save", "document.exportPdf"] {
|
||||
let out = s.execute(command, &json!({"format":"pdf","preserveEditing":true,"range":"all","thumbnails":false})).unwrap();
|
||||
let bytes = decode(&out);
|
||||
let editing = vectorcraft_pdf::editing(&bytes).unwrap();
|
||||
assert!(editing.intact);
|
||||
let native = vectorcraft_format::load(&editing.data).unwrap();
|
||||
assert!(matches!(&native.node(id).unwrap().kind, NodeKind::PlacedDocument(p) if **p == original), "{command}: discarded editable link");
|
||||
let (images, paths) = pdf_contents(&bytes);
|
||||
assert_eq!(images > 0, missing, "{command}: pages still draw previews or vectors");
|
||||
if !missing {
|
||||
assert!(paths > 0);
|
||||
}
|
||||
assert_eq!(
|
||||
out["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("placed document(s) output as their previews")),
|
||||
missing,
|
||||
"{out}"
|
||||
);
|
||||
let mut restored = Session::new();
|
||||
assert_eq!(
|
||||
restored.execute("document.open", &json!({"name":"editable.pdf","dataBase64":vectorcraft_format::base64_encode(&bytes)})).unwrap()["restored"],
|
||||
true
|
||||
);
|
||||
assert_eq!(placed(&restored, id), original);
|
||||
assert_eq!(restored.execute("links.check", &json!({})).unwrap()["missing"], usize::from(missing));
|
||||
assert_eq!(s.execute("document.json", &json!({})).unwrap(), before);
|
||||
if missing {
|
||||
let replacement = dir.file("replacement.vectorcraft");
|
||||
source(&replacement, 100.0, 50.0, BLUE, 1);
|
||||
assert_eq!(restored.execute("links.relink", &json!({"ids":[id.0],"path":replacement})).unwrap()["relinked"], json!([id.0]));
|
||||
}
|
||||
}
|
||||
for alias in ["PDF", ".pdf"] {
|
||||
let enc = cmd::fileio::encode_all(&s.doc().unwrap().doc, alias, &json!({"preserveEditing":true})).unwrap();
|
||||
let editing = vectorcraft_pdf::editing(&enc.files[0].1).unwrap();
|
||||
let native = vectorcraft_format::load(&editing.data).unwrap();
|
||||
assert!(matches!(&native.node(id).unwrap().kind, NodeKind::PlacedDocument(p) if **p == original), "{alias}");
|
||||
}
|
||||
for params in [json!({"format":"pdf","preserveEditing":false}), json!({"format":"pdf","preserveEditing":true,"range":"1"})] {
|
||||
let out = s.execute("document.serialize", ¶ms).unwrap();
|
||||
let bytes = decode(&out);
|
||||
assert!(vectorcraft_pdf::editing(&bytes).is_none(), "{params}");
|
||||
assert_eq!(pdf_contents(&bytes).0 > 0, missing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// EPS draws hydrated artwork but carries the original link metadata with requested source bytes.
|
||||
#[test]
|
||||
fn eps_editing_keeps_placed_links_and_requested_source_bytes() {
|
||||
for missing in [true, false] {
|
||||
let dir = Folder::new(&format!("eps-editable-links-{missing}"));
|
||||
let src = dir.file("logo.vectorcraft");
|
||||
star_source(&src, &format!("eps-editable-links-{missing}"));
|
||||
let source_bytes = std::fs::read(&src).unwrap();
|
||||
let mut s = session();
|
||||
parent_star(&mut s);
|
||||
let id = place(&mut s, &src);
|
||||
let parent = decode(&s.execute("document.serialize", &json!({"format":"vectorcraft"})).unwrap());
|
||||
if missing {
|
||||
std::fs::remove_file(&src).unwrap();
|
||||
}
|
||||
s.execute("document.open", &json!({"name":"parent.vectorcraft","dataBase64":vectorcraft_format::base64_encode(&parent)})).unwrap();
|
||||
let original = placed(&s, id);
|
||||
assert!(s.doc().unwrap().doc.images[&original.key].is_proxy());
|
||||
let before = s.execute("document.json", &json!({})).unwrap();
|
||||
for include_linked in [false, true] {
|
||||
let params = json!({"format":"eps","includeLinkedFiles":include_linked,"previewFormat":"none","thumbnails":false});
|
||||
for command in ["document.serialize", "document.export", "document.exportEps"] {
|
||||
let out = s.execute(command, ¶ms).unwrap();
|
||||
let bytes = decode(&out);
|
||||
let raw = vectorcraft_eps::native(&bytes).unwrap();
|
||||
let native = vectorcraft_format::load(&raw).unwrap();
|
||||
assert!(
|
||||
matches!(&native.node(id).unwrap().kind, NodeKind::PlacedDocument(p) if **p == original),
|
||||
"{command}/{missing}/{include_linked}: editable link lost"
|
||||
);
|
||||
let blob = &native.images[&original.key];
|
||||
assert_eq!(blob.is_proxy(), missing || !include_linked);
|
||||
if !missing && include_linked {
|
||||
assert_eq!(blob.bytes.as_slice(), source_bytes.as_slice());
|
||||
}
|
||||
assert_eq!(
|
||||
out["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("placed document(s) output as their previews")),
|
||||
missing,
|
||||
"{out}"
|
||||
);
|
||||
// Read the printed page, independently of the native editing attachment.
|
||||
let drawn = vectorcraft_eps::import_with(&bytes, false).unwrap();
|
||||
assert!(!drawn.preview);
|
||||
let (mut images, mut paths) = (0, 0);
|
||||
drawn.document.walk(|n| match n.kind {
|
||||
NodeKind::Image(_) => images += 1,
|
||||
NodeKind::Path { .. } | NodeKind::Compound { .. } => paths += 1,
|
||||
_ => {}
|
||||
});
|
||||
assert_eq!(images > 0, missing, "{command}: drawn output uses vectors or the preview");
|
||||
if !missing {
|
||||
assert!(paths > 0);
|
||||
}
|
||||
let mut restored = Session::new();
|
||||
assert_eq!(
|
||||
restored
|
||||
.execute("document.open", &json!({"name":"editable.eps","dataBase64":vectorcraft_format::base64_encode(&bytes)}))
|
||||
.unwrap()["restored"],
|
||||
true
|
||||
);
|
||||
assert_eq!(placed(&restored, id), original);
|
||||
assert_eq!(restored.execute("links.check", &json!({})).unwrap()["missing"], usize::from(missing));
|
||||
assert_eq!(s.execute("document.json", &json!({})).unwrap(), before);
|
||||
if missing {
|
||||
let replacement = dir.file("replacement.vectorcraft");
|
||||
source(&replacement, 100.0, 50.0, BLUE, 1);
|
||||
assert_eq!(restored.execute("links.relink", &json!({"ids":[id.0],"path":replacement})).unwrap()["relinked"], json!([id.0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
for alias in ["EPS", ".eps"] {
|
||||
let enc = cmd::fileio::encode_all(&s.doc().unwrap().doc, alias, &json!({"previewFormat":"none","thumbnails":false})).unwrap();
|
||||
let native = vectorcraft_format::load(&vectorcraft_eps::native(&enc.files[0].1).unwrap()).unwrap();
|
||||
assert!(matches!(&native.node(id).unwrap().kind, NodeKind::PlacedDocument(p) if **p == original), "{alias}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An SVG's editing attachment keeps links even when its visible output must use previews.
|
||||
#[test]
|
||||
fn svg_editing_keeps_placed_links_separate_from_drawn_previews() {
|
||||
for missing in [true, false] {
|
||||
let dir = Folder::new(&format!("svg-editable-links-{missing}"));
|
||||
let src = dir.file("logo.vectorcraft");
|
||||
star_source(&src, &format!("svg-editable-links-{missing}"));
|
||||
let mut s = session();
|
||||
parent_star(&mut s);
|
||||
let id = place(&mut s, &src);
|
||||
let parent = decode(&s.execute("document.serialize", &json!({"format": "vectorcraft"})).unwrap());
|
||||
if missing {
|
||||
std::fs::remove_file(&src).unwrap();
|
||||
}
|
||||
s.execute("document.open", &json!({"name": "parent.vectorcraft", "dataBase64": vectorcraft_format::base64_encode(&parent)})).unwrap();
|
||||
let original = placed(&s, id);
|
||||
assert!(s.doc().unwrap().doc.images[&original.key].is_proxy());
|
||||
let before = s.execute("document.json", &json!({})).unwrap();
|
||||
for format in ["svg", "svgz"] {
|
||||
for command in ["document.serialize", "document.export", "document.save"] {
|
||||
let out = s.execute(command, &json!({"format": format, "svg": {"preserveEditing": true, "range": "1"}})).unwrap();
|
||||
let bytes = out
|
||||
.get("dataBase64")
|
||||
.map(|b| vectorcraft_format::base64_decode(b.as_str().unwrap()).unwrap())
|
||||
.unwrap_or_else(|| out["text"].as_str().unwrap().as_bytes().to_vec());
|
||||
let text = vectorcraft_svg::text_of(&bytes).unwrap();
|
||||
assert_eq!(text.contains("<image"), missing, "{format}/{command}: {text}");
|
||||
assert_eq!(
|
||||
out["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("placed document(s) output as their previews")),
|
||||
missing,
|
||||
"{out}"
|
||||
);
|
||||
let mut restored = Session::new();
|
||||
let opened = restored
|
||||
.execute("document.open", &json!({"name": format!("editable.{format}"), "dataBase64": vectorcraft_format::base64_encode(&bytes)}))
|
||||
.unwrap();
|
||||
assert_eq!(opened["restored"], true, "{opened}");
|
||||
eprintln!("SVG_EDITING_RESTORED_READY missing={missing} format={format} command={command}");
|
||||
assert_eq!(placed(&restored, id), original, "{format}/{command}: link or transform lost");
|
||||
assert_eq!(restored.execute("links.check", &json!({})).unwrap()["missing"], usize::from(missing));
|
||||
assert_eq!(s.execute("document.json", &json!({})).unwrap(), before, "source unchanged");
|
||||
if missing {
|
||||
let replacement = dir.file("replacement.vectorcraft");
|
||||
source(&replacement, 100.0, 50.0, BLUE, 1);
|
||||
assert_eq!(restored.execute("links.relink", &json!({"ids": [id.0], "path": replacement})).unwrap()["relinked"], json!([id.0]));
|
||||
assert_eq!(placed(&restored, id).link.path, replacement);
|
||||
}
|
||||
}
|
||||
let out = s.execute("document.serialize", &json!({"format": format, "preserveEditing": false})).unwrap();
|
||||
let bytes = out
|
||||
.get("dataBase64")
|
||||
.map(|b| vectorcraft_format::base64_decode(b.as_str().unwrap()).unwrap())
|
||||
.unwrap_or_else(|| out["text"].as_str().unwrap().as_bytes().to_vec());
|
||||
assert_eq!(vectorcraft_svg::text_of(&bytes).unwrap().contains("<image"), missing);
|
||||
let mut reopened = Session::new();
|
||||
assert_eq!(
|
||||
reopened
|
||||
.execute("document.open", &json!({"name": format!("plain.{format}"), "dataBase64": vectorcraft_format::base64_encode(&bytes)}))
|
||||
.unwrap()["restored"],
|
||||
false
|
||||
);
|
||||
}
|
||||
// Dot and case aliases must reach the same original/prepared document boundary.
|
||||
for alias in ["SVG", ".svg", "SVGZ"] {
|
||||
let enc = cmd::fileio::encode_all(&s.doc().unwrap().doc, alias, &json!({"preserveEditing": true})).unwrap();
|
||||
let format = cmd::fileio::format(alias).unwrap().id;
|
||||
let mut restored = Session::new();
|
||||
restored
|
||||
.execute(
|
||||
"document.open",
|
||||
&json!({"name": format!("alias.{format}"), "dataBase64": vectorcraft_format::base64_encode(&enc.files[0].1)}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(placed(&restored, id), original, "{alias}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,12 @@ fn shown(f: &Format) -> impl Iterator<Item = &'static FormatOption> {
|
|||
let range = f.options.iter().any(|o| o.name == "range");
|
||||
let separate = separates(f);
|
||||
let options = if own_dialog(f) { &[][..] } else { f.options };
|
||||
options.iter().filter(move |o| !(matches!(o.ty, "array" | "object") || (range && o.name == "artboard") || (separate && o.name == "range")))
|
||||
options.iter().filter(move |o| {
|
||||
!(matches!(o.ty, "array" | "object")
|
||||
|| (range && o.name == "artboard")
|
||||
|| (separate && o.name == "range")
|
||||
|| (f.id == "ai" && o.name == "useArtboards"))
|
||||
})
|
||||
}
|
||||
|
||||
/// An option's label: the save options as the reference app's options dialog names them, the
|
||||
|
|
|
|||
Loading…
Reference in New Issue