mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 12:17:14 -06:00
Merge pull request #4293 from mockydevs/fix/eps-original-format-save
Support EPS Save and Save As with editable data
This commit is contained in:
@@ -13,13 +13,16 @@ use vectorcraft_eps::{EpsOptions, Level, Overprint, Preview, Raster};
|
||||
use vectorcraft_geom::{Point, Rect};
|
||||
|
||||
use super::super::flatten::{FlattenOptions, flatten_document};
|
||||
use super::super::{CommandSpec, EngineError, Result, Session, bad, cmd, has_doc, str_param};
|
||||
use super::super::{CommandSpec, EngineError, Result, Session, bad, bool_or, cmd, has_doc, str_param};
|
||||
use super::{ArtboardPick, Encoded, FormatOption};
|
||||
|
||||
const C: &str = "document.export";
|
||||
|
||||
/// Most pixels along a side of a preview.
|
||||
const MAX_PREVIEW: f64 = 2048.0;
|
||||
/// Save needs a lightweight compatibility preview, not an uncompressed 16 MiB image beside
|
||||
/// a small vector. TIFF format choices are unchanged; there is no public resolution option.
|
||||
const SAVE_PREVIEW: f64 = 1024.0;
|
||||
/// Most pixels along a side of a thumbnail.
|
||||
const THUMBNAIL: f64 = 128.0;
|
||||
|
||||
@@ -260,7 +263,20 @@ fn native(doc: &Document, board: Option<usize>, include_linked: bool) -> Vec<u8>
|
||||
/// artboard (after a central file of the whole document when there are several and art shows).
|
||||
/// Transparency is flattened first (once for all of them); the previews show `doc` itself.
|
||||
pub(super) fn encode(doc: &Document, p: &Value) -> Result<Encoded> {
|
||||
encode_document(doc, p, false)
|
||||
}
|
||||
|
||||
/// Save keeps a single page's original frame and all native editing data, unlike Export's
|
||||
/// optional art-bound crop or separate file per artboard. Existing EPS loss warnings still apply.
|
||||
pub(super) fn encode_save(doc: &Document, p: &Value) -> Result<Encoded> {
|
||||
encode_document(doc, p, true)
|
||||
}
|
||||
|
||||
fn encode_document(doc: &Document, p: &Value, saving: bool) -> Result<Encoded> {
|
||||
let set = parse(p)?;
|
||||
if saving && (set.use_artboards == Some(false) || bool_or(p, "selectedOnly", false)) {
|
||||
return Err(bad(C, "EPS Save retains the artboard and all artwork: use document.export for cropped or selected-only EPS"));
|
||||
}
|
||||
let original = doc;
|
||||
let (full, warnings) = crate::cmd::place::document::full_documents(doc);
|
||||
let doc = &*full;
|
||||
@@ -272,7 +288,13 @@ pub(super) fn encode(doc: &Document, p: &Value) -> Result<Encoded> {
|
||||
} else {
|
||||
Cow::Borrowed(original)
|
||||
};
|
||||
let pages = pages(doc, &set)?;
|
||||
let pages = if saving {
|
||||
let board = set.boards.one(doc.artboards.len()).map_err(|e| bad(C, e))?;
|
||||
let rect = doc.artboards.get(board).ok_or_else(|| bad(C, "the document has no artboard"))?.rect;
|
||||
vec![(Some(board), rect, Point::new(rect.x0, rect.y1))]
|
||||
} else {
|
||||
pages(doc, &set)?
|
||||
};
|
||||
if pages.is_empty() {
|
||||
return Err(bad(C, "the document has no artboard"));
|
||||
}
|
||||
@@ -284,13 +306,14 @@ pub(super) fn encode(doc: &Document, p: &Value) -> Result<Encoded> {
|
||||
if transparent && flat.is_some() {
|
||||
enc.warnings.push("transparency is flattened into opaque art and images (EPS has none): see the flattener preset".into());
|
||||
}
|
||||
let preview_limit = if saving { SAVE_PREVIEW } else { MAX_PREVIEW };
|
||||
let created = vectorcraft_doc::metadata::output_time();
|
||||
let mut renderer = vectorcraft_render::Renderer::new();
|
||||
for (board, region, origin) in pages {
|
||||
let rect = vectorcraft_eps::preview_rect(region, origin);
|
||||
let preview = match set.eps.preview {
|
||||
Preview::None => None,
|
||||
kind => Some(render(&mut renderer, doc, rect, MAX_PREVIEW, set.eps.transparent_preview && kind == Preview::TiffColor)?),
|
||||
kind => Some(render(&mut renderer, doc, rect, preview_limit, set.eps.transparent_preview && kind == Preview::TiffColor)?),
|
||||
};
|
||||
let thumbnail = set.thumbnails.then(|| render(&mut renderer, doc, rect, THUMBNAIL, false)).transpose()?;
|
||||
let o = EpsOptions {
|
||||
@@ -298,7 +321,7 @@ pub(super) fn encode(doc: &Document, p: &Value) -> Result<Encoded> {
|
||||
origin,
|
||||
title: doc.title.clone(),
|
||||
created,
|
||||
native: Some(native(&editing, board, set.include_linked)),
|
||||
native: Some(native(&editing, if saving { None } else { 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))?;
|
||||
|
||||
@@ -430,8 +430,19 @@ pub fn open_template(s: &mut Session, name: &str, bytes: &[u8], path: Option<&st
|
||||
/// `opts` are the options it was read with.
|
||||
fn open_loaded(s: &mut Session, loaded: Loaded, links: Resolved, path: Option<String>, opts: &LoadOptions, as_template: bool) -> Result<Value> {
|
||||
let Loaded { mut doc, format, warnings, restored, converted, .. } = loaded.with_grid_prefs(&s.prefs);
|
||||
// What reading the file left out of the document, if anything.
|
||||
let losses: Vec<String> = warnings.iter().filter(|w| is_loss(w)).cloned().collect();
|
||||
// EPS now has an ordinary Save path. Its preview, partial-program and damaged-editing
|
||||
// fallbacks must ask before overwriting the richer source they could not fully read.
|
||||
let losses: Vec<String> = warnings
|
||||
.iter()
|
||||
.filter(|w| {
|
||||
is_loss(w)
|
||||
|| (format.id == "eps"
|
||||
&& (matches!(w.as_str(), EDITING_DAMAGED | EDITING_STALE)
|
||||
|| w.starts_with("this file's PostScript couldn't be read")
|
||||
|| w.starts_with("the file was read up to an error")))
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
let source_path = path.clone().filter(|_| !losses.is_empty());
|
||||
// A template (saved by Save as Template, or an .ait/.vctemplate file) opens as a new untitled
|
||||
// document.
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::{DocState, Prefs};
|
||||
|
||||
/// The formats Save As offers, in menu order (append-only). The other writable formats are exports:
|
||||
/// they never become the document's own file.
|
||||
pub const SAVE_FORMATS: &[&str] = &["vectorcraft", "template", "pdf", "svg", "svgz", "ai"];
|
||||
pub const SAVE_FORMATS: &[&str] = &["vectorcraft", "template", "pdf", "svg", "svgz", "ai", "eps"];
|
||||
|
||||
/// The formats Save as Template writes, the default first: a native template, or a
|
||||
/// PDF-compatible `.ait` (written as a `.ai` file is).
|
||||
@@ -431,9 +431,9 @@ pub fn reads_option(f: &Format, key: &str) -> bool {
|
||||
}
|
||||
|
||||
/// Formats that carry the whole native document: they lose nothing, and record links relative to
|
||||
/// where they are written. A `.ai` or `.ait` file is a PDF carrying it.
|
||||
/// where they are written. A `.ai` or `.ait` file is a PDF carrying it; EPS carries it in comments.
|
||||
fn is_native(f: &Format) -> bool {
|
||||
matches!(f.id, "vectorcraft" | "template" | "ai" | "ait")
|
||||
matches!(f.id, "vectorcraft" | "template" | "ai" | "ait" | "eps")
|
||||
}
|
||||
|
||||
fn is_pdf(f: &Format) -> bool {
|
||||
@@ -520,7 +520,7 @@ impl<W: std::io::Write> std::io::Write for Counted<W> {
|
||||
/// `separateArtboards`, those `range` names (default all).
|
||||
fn separate_boards(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Result<Vec<usize>> {
|
||||
// A legacy .ai file is a file per artboard already.
|
||||
if !is_native(f) || !bool_or(p, "separateArtboards", false) || super::legacy_ai::wanted(cmd, f, p)? {
|
||||
if !is_native(f) || f.id == "eps" || !bool_or(p, "separateArtboards", false) || super::legacy_ai::wanted(cmd, f, p)? {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let range = str_param(p, "range").filter(|r| !r.trim().is_empty()).map(str::to_string);
|
||||
@@ -735,8 +735,12 @@ impl SaveJob {
|
||||
/// 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 matches!(f.id, "ai" | "ait") { encode_ai(cmd, f, doc, &self.params) } else { encode_all(doc, f.id, &self.params) }
|
||||
.map_err(|e| named_for_save(cmd, e))?;
|
||||
let enc = match f.id {
|
||||
"ai" | "ait" => encode_ai(cmd, f, doc, &self.params),
|
||||
"eps" => super::eps::encode_save(doc, &self.params),
|
||||
_ => encode_all(doc, f.id, &self.params),
|
||||
}
|
||||
.map_err(|e| named_for_save(cmd, e))?;
|
||||
// A legacy .ai file per artboard.
|
||||
let per_artboard = f.id == "ai" && super::legacy_ai::wanted(cmd, f, &self.params)?;
|
||||
if enc.files.len() != 1 && !per_artboard {
|
||||
@@ -950,7 +954,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} (an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), …the SVG and PDF options given flat instead (an option given flat or in svg and in options with different values is an error), compress?: bool (native, also in options: gzip; default: the useCompression preference; warnings say when the format doesn't read compress, version or preview given flat), 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), separateArtboards?: false (native and .ai: also save each artboard of `range` to <name>-<artboard>.<ext> beside the file, holding that artboard and the art touching it; the result's files lists every file written), range?: \"1-3, 5\"|\"all\" (default; with separateArtboards), 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; a .ai file's compress, default true, is its 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|eps (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} (an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), …the SVG and PDF options given flat instead (an option given flat or in svg and in options with different values is an error), compress?: bool (native, also in options: gzip; default: the useCompression preference; warnings say when the format doesn't read compress, version or preview given flat), 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), separateArtboards?: false (native and .ai: also save each artboard of `range` to <name>-<artboard>.<ext> beside the file, holding that artboard and the art touching it; the result's files lists every file written), range?: \"1-3, 5\"|\"all\" (default; with separateArtboards), 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; a .ai file's compress, default true, is its 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. EPS saves the chosen artboard (first by default), retaining its page margins and the whole editable document; its PostScript appearance flattens transparency, as EPS requires. Without a path it writes the document's own file in its own format: a document opened from or saved as SVG/PDF/EPS 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)
|
||||
),
|
||||
@@ -959,7 +963,7 @@ pub(super) fn specs() -> Vec<CommandSpec> {
|
||||
"Save As…",
|
||||
["File"],
|
||||
Some("Cmd+Shift+S"),
|
||||
"{…as document.save} {path?, format?: vectorcraft|template|pdf|svg|svgz|ai (default: the path's extension, else the document's own; ait, a .ait template, is written by file.saveAsTemplate), options?, svg?, modified?} the document takes on the new path, name and format (except a template, which is always a copy) → {path, format, bytes, warnings}; no path → {dataBase64, format, name, folder?, warnings}",
|
||||
"{…as document.save} {path?, format?: vectorcraft|template|pdf|svg|svgz|ai|eps (default: the path's extension, else the document's own; ait, a .ait template, is written by file.saveAsTemplate), options?, svg?, modified?} the document takes on the new path, name and format (except a template, which is always a copy) → {path, format, bytes, warnings}; no path → {dataBase64, format, name, folder?, warnings}",
|
||||
has_doc,
|
||||
|s, p| save(s, SaveMode::SaveAs, p)
|
||||
),
|
||||
|
||||
@@ -84,7 +84,7 @@ fn save_as_takes_on_path_format_and_options() {
|
||||
let svg = s.execute("file.formatOptions", &json!({"format": "svg"})).unwrap();
|
||||
assert_eq!(svg["options"]["outlineText"]["value"], false, "another format shows its defaults");
|
||||
let ids: Vec<&str> = o["saveFormats"].as_array().unwrap().iter().map(|f| f["id"].as_str().unwrap()).collect();
|
||||
assert_eq!(ids, ["vectorcraft", "template", "pdf", "svg", "svgz", "ai"]);
|
||||
assert_eq!(ids, ["vectorcraft", "template", "pdf", "svg", "svgz", "ai", "eps"]);
|
||||
// SVGZ is gzip-compressed SVG and reopens as SVGZ.
|
||||
let svgz = path(&d, "doc.svgz");
|
||||
s.execute("file.saveAs", &json!({"path": svgz})).unwrap();
|
||||
@@ -405,3 +405,141 @@ fn transparency_grid_round_trips_without_dirtying() {
|
||||
assert!(!s.doc().unwrap().transparency_grid);
|
||||
let _ = std::fs::remove_dir_all(d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opened_eps_saves_to_its_own_file_with_editable_data_and_page_margins() {
|
||||
let d = dir("eps-original-format");
|
||||
let eps_path = path(&d, "Artwork.eps");
|
||||
// Original, contributor-authored PostScript: the artwork occupies only part of the page.
|
||||
let source = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 200 100\n0 0 1 setrgbcolor 30 20 40 30 rectfill\nshowpage\n%%EOF\n";
|
||||
std::fs::write(&eps_path, source).unwrap();
|
||||
let mut s = Session::new();
|
||||
s.execute("document.open", &json!({"path": eps_path})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().format, "eps");
|
||||
assert_eq!(s.doc().unwrap().path.as_deref(), Some(eps_path.as_str()));
|
||||
let board = s.doc().unwrap().doc.artboards[0].rect;
|
||||
assert_eq!((board.width(), board.height()), (200.0, 100.0));
|
||||
rect(&mut s);
|
||||
let edited_layers = s.doc().unwrap().doc.layers.clone();
|
||||
let r = s.execute("document.save", &json!({"options": {"previewFormat": "none", "thumbnails": false}})).unwrap();
|
||||
assert_eq!((r["path"].as_str(), r["format"].as_str()), (Some(eps_path.as_str()), Some("eps")));
|
||||
let bytes = std::fs::read(&eps_path).unwrap();
|
||||
assert!(bytes.starts_with(b"%!PS-Adobe-3.0 EPSF-3.0"));
|
||||
assert!(std::str::from_utf8(vectorcraft_eps::sections(&bytes).unwrap().0).unwrap().contains("%%BoundingBox: 0 0 200 100"));
|
||||
let editing = vectorcraft_format::load(&vectorcraft_eps::native(&bytes).unwrap()).unwrap();
|
||||
assert_eq!(editing.artboards[0].rect, board);
|
||||
assert_eq!(editing.layers, edited_layers);
|
||||
assert!(!s.doc().unwrap().is_dirty());
|
||||
let opened = s.execute("document.open", &json!({"path": eps_path})).unwrap();
|
||||
assert_eq!(opened["restored"], true);
|
||||
assert_eq!(s.doc().unwrap().doc.layers, edited_layers);
|
||||
assert_eq!(s.doc().unwrap().doc.artboards[0].rect, board);
|
||||
rect(&mut s);
|
||||
s.execute("document.save", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().path.as_deref(), Some(eps_path.as_str()));
|
||||
std::fs::remove_dir_all(d).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eps_save_keeps_all_editing_artboards_while_printing_one_page() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 100, "artboards": 2})).unwrap();
|
||||
rect(&mut s);
|
||||
let r = s.execute("document.save", &json!({"format": "eps", "artboard": 1, "previewFormat": "none", "thumbnails": false})).unwrap();
|
||||
let bytes = b64(&r);
|
||||
let native = vectorcraft_format::load(&vectorcraft_eps::native(&bytes).unwrap()).unwrap();
|
||||
assert_eq!(native.artboards.len(), 2);
|
||||
assert_eq!(native.layers, s.doc().unwrap().doc.layers);
|
||||
assert!(s.execute("document.save", &json!({"format": "eps", "range": "all"})).is_err(), "EPS is one printed page");
|
||||
for options in [json!({"useArtboards": false}), json!({"selectedOnly": true})] {
|
||||
assert!(s.execute("document.save", &json!({"format": "eps", "options": options})).is_err(), "cropping is Export's job");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eps_save_retains_lossy_import_overwrite_protection_and_transparency_warning() {
|
||||
let d = dir("eps-import-warning");
|
||||
let eps_path = path(&d, "Artwork.eps");
|
||||
let mut s = session();
|
||||
rect(&mut s);
|
||||
s.execute("transparency.set", &json!({"opacity": 50})).unwrap();
|
||||
let r = s.execute("document.save", &json!({"path": eps_path, "previewFormat": "none", "thumbnails": false})).unwrap();
|
||||
assert!(r["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("flatten")), "{r}");
|
||||
let original = std::fs::read(&eps_path).unwrap();
|
||||
let st = s.doc_mut().unwrap();
|
||||
st.imported_from = Some(eps_path.clone());
|
||||
st.import_losses.push("test import omitted artwork".into());
|
||||
rect(&mut s);
|
||||
assert!(s.execute("document.save", &json!({})).unwrap_err().to_string().contains("acknowledgeLoss"));
|
||||
assert_eq!(std::fs::read(&eps_path).unwrap(), original);
|
||||
s.execute("document.save", &json!({"acknowledgeLoss": true})).unwrap();
|
||||
assert_ne!(std::fs::read(&eps_path).unwrap(), original);
|
||||
std::fs::remove_dir_all(d).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eps_save_supports_an_empty_artboard_without_cropping_its_page() {
|
||||
let mut s = session();
|
||||
let r = s.execute("document.save", &json!({"format": "eps", "previewFormat": "none", "thumbnails": false})).unwrap();
|
||||
let bytes = b64(&r);
|
||||
let native = vectorcraft_format::load(&vectorcraft_eps::native(&bytes).unwrap()).unwrap();
|
||||
assert_eq!(native.artboards[0].rect, s.doc().unwrap().doc.artboards[0].rect);
|
||||
assert_eq!(native.node_count(), s.doc().unwrap().doc.node_count());
|
||||
assert!(std::str::from_utf8(vectorcraft_eps::sections(&bytes).unwrap().0).unwrap().contains("%%BoundingBox: 0 0 200 100"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partially_read_eps_requires_confirmation_before_save_back() {
|
||||
let d = dir("eps-partial-import");
|
||||
let eps_path = path(&d, "partial.eps");
|
||||
let original = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 200 100\n0 0 1 setrgbcolor 30 20 40 30 rectfill\nunsupportedMockyOperator\n1 0 0 setrgbcolor 80 20 40 30 rectfill\nshowpage\n%%EOF\n";
|
||||
std::fs::write(&eps_path, original).unwrap();
|
||||
let mut s = Session::new();
|
||||
let opened = s.execute("document.open", &json!({"path": eps_path})).unwrap();
|
||||
assert!(opened["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().starts_with("the file was read up to an error")), "{opened}");
|
||||
rect(&mut s);
|
||||
assert!(s.execute("document.save", &json!({})).unwrap_err().to_string().contains("acknowledgeLoss"));
|
||||
assert_eq!(std::fs::read(&eps_path).unwrap(), original);
|
||||
s.execute("document.save", &json!({"acknowledgeLoss": true})).unwrap();
|
||||
assert!(vectorcraft_eps::native(&std::fs::read(&eps_path).unwrap()).is_some());
|
||||
std::fs::remove_dir_all(d).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eps_with_damaged_editing_data_requires_confirmation_before_save_back() {
|
||||
let d = dir("eps-damaged-editing");
|
||||
let eps_path = path(&d, "damaged.eps");
|
||||
let mut s = session();
|
||||
rect(&mut s);
|
||||
let r = s.execute("document.exportEps", &json!({"previewFormat": "none", "thumbnails": false, "cmykPostScript": false})).unwrap();
|
||||
let mut bytes = b64(&r);
|
||||
let at = bytes.windows(9).position(|w| w == b"native\n% ").unwrap() + 12;
|
||||
bytes[at] = b'z';
|
||||
std::fs::write(&eps_path, &bytes).unwrap();
|
||||
let opened = s.execute("document.open", &json!({"path": eps_path})).unwrap();
|
||||
assert_eq!(opened["restored"], false);
|
||||
assert!(opened["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("editing data can't be read")), "{opened}");
|
||||
rect(&mut s);
|
||||
assert!(s.execute("document.save", &json!({})).unwrap_err().to_string().contains("acknowledgeLoss"));
|
||||
assert_eq!(std::fs::read(&eps_path).unwrap(), bytes);
|
||||
std::fs::remove_dir_all(d).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eps_save_bounds_large_page_preview_without_cropping_vector_or_editing_data() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 4000, "height": 4000})).unwrap();
|
||||
rect(&mut s);
|
||||
// Explicit colour preview remains a colour preview; Save bounds its resolution, not the art.
|
||||
let r = s.execute("document.save", &json!({"format": "eps", "previewFormat": "tiffColor"})).unwrap();
|
||||
let bytes = b64(&r);
|
||||
let (ps, tiff) = vectorcraft_eps::sections(&bytes).unwrap();
|
||||
let tiff = tiff.unwrap();
|
||||
assert!(tiff.len() < 4 * 1024 * 1024 + 64 * 1024, "compatibility preview grew to {} bytes", tiff.len());
|
||||
let preview = image::load_from_memory_with_format(tiff, image::ImageFormat::Tiff).unwrap();
|
||||
assert_eq!((preview.width(), preview.height()), (1024, 1024));
|
||||
assert!(std::str::from_utf8(ps).unwrap().contains("%%BoundingBox: 0 0 4000 4000"));
|
||||
let native = vectorcraft_format::load(&vectorcraft_eps::native(&bytes).unwrap()).unwrap();
|
||||
assert_eq!(native.artboards, s.doc().unwrap().doc.artboards);
|
||||
assert_eq!(native.layers, s.doc().unwrap().doc.layers);
|
||||
}
|
||||
|
||||
@@ -87,6 +87,9 @@ fn shown(f: &Format) -> impl Iterator<Item = &'static FormatOption> {
|
||||
|| (range && o.name == "artboard")
|
||||
|| (separate && o.name == "range")
|
||||
|| (matches!(f.id, "ai" | "ait") && o.name == "useArtboards")
|
||||
// EPS Save keeps a page and the complete editing document; cropped and
|
||||
// selected-only output belongs to Export, never this save dialog.
|
||||
|| (f.id == "eps" && matches!(o.name, "useArtboards" | "selectedOnly"))
|
||||
|| (f.id == "ait" && o.name == "aiVersion"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ fn save_as_svg_opens_svg_options_then_writes_svg() {
|
||||
// The save panel offers every save format, the document's own first.
|
||||
let pick = &picks.borrow()[0];
|
||||
let labels: Vec<&str> = pick.filters.iter().map(|f| f.0.as_str()).collect();
|
||||
assert_eq!(labels, ["VectorCraft", "VectorCraft Template", "PDF", "SVG", "SVG Compressed", "PDF-compatible .ai"]);
|
||||
assert_eq!(labels, ["VectorCraft", "VectorCraft Template", "PDF", "SVG", "SVG Compressed", "PDF-compatible .ai", "EPS"]);
|
||||
assert_eq!(pick.name, "Untitled-1.vectorcraft");
|
||||
let d = dialog(&app);
|
||||
assert_eq!((d.kind.as_str(), d.str("path").as_str(), d.str("mode").as_str()), ("svgOptions", "art.svg", "save"));
|
||||
@@ -332,3 +332,18 @@ fn save_as_pdf_asks_with_the_save_pdf_dialog_and_remembers_its_options() {
|
||||
assert_eq!(dialog(&app).str("compatibility"), "1.5");
|
||||
assert_eq!(dialog(&app).str("__save"), "file.saveCopy");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eps_save_as_keeps_page_options_and_writes_eps_without_export_only_switches() {
|
||||
let (mut app, written, _) = desktop("art.eps");
|
||||
app.run("file.saveAs", json!({})).unwrap();
|
||||
let names: Vec<_> = shown(fileio::format("eps").unwrap()).map(|option| option.name).collect();
|
||||
assert!(!names.contains(&"useArtboards") && !names.contains(&"selectedOnly"));
|
||||
assert!(names.contains(&"previewFormat"));
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none());
|
||||
let saved = written.borrow();
|
||||
assert_eq!(saved[0].0, "art.eps");
|
||||
assert!(vectorcraft_eps::native(&saved[0].1).is_some());
|
||||
assert_eq!(app.session.active().unwrap().format, "eps");
|
||||
}
|
||||
|
||||
@@ -303,3 +303,22 @@ fn native_saves_stream_into_the_file() {
|
||||
assert_eq!(*streamed.lock().unwrap(), [native]);
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_save_writes_opened_eps_back_without_another_save_as_dialog() {
|
||||
let d = dir("eps-original-format");
|
||||
let path = d.join("Artwork.eps").to_string_lossy().to_string();
|
||||
std::fs::write(&path, b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 200 100\n0 0 1 setrgbcolor 30 20 40 30 rectfill\nshowpage\n%%EOF\n").unwrap();
|
||||
let mut app = app();
|
||||
app.run("document.open", json!({"path":path})).unwrap();
|
||||
rect(&mut app);
|
||||
app.run("file.save", json!({})).unwrap();
|
||||
crate::background::wait_all(&mut app);
|
||||
assert!(app.ui.dialog.is_none(), "ordinary EPS Save does not require another format dialog");
|
||||
let bytes = std::fs::read(&path).unwrap();
|
||||
assert!(vectorcraft_eps::sections(&bytes).unwrap().0.starts_with(b"%!PS-Adobe-3.0 EPSF-3.0"));
|
||||
assert!(vectorcraft_eps::native(&bytes).is_some());
|
||||
assert_eq!(app.session.active().unwrap().format, "eps");
|
||||
assert_eq!(app.session.active().unwrap().path.as_deref(), Some(path.as_str()));
|
||||
std::fs::remove_dir_all(d).unwrap();
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
+8
-1
File diff suppressed because one or more lines are too long
+7
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user