mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 20:58:00 -06:00
IO.1: preserve editable export data and AI artboard options
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Codex
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));
|
||||
/// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user