mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 19:57:05 -06:00
Read Illustrator EPS and .ai layers from their editing data
An Illustrator EPS is a flattened page plus, after %%EOF, an editing copy of the document; a PDF-compatible .ai carries the same in its private data. Importing only the page lost layer names, order, visibility, hidden layers and hidden objects and hidden text. - eps: read the editing copy (Zstandard %AI24_DataStream, older zlib %AI9_DataStream) into layers, sublayers, groups, compound paths, clip groups and hidden objects, run by the existing PostScript interpreter. Falls back to the page, with a warning, when a shown layer uses an operator that isn't read or the art differs from the page by over 5%. - eps: read the text document (ate.rs) to place type on hidden layers and objects; the page's type is matched to the file's text objects by position (pieces of a line included) and kept with the nearest one when none claims it. Area type and type on a path are left out with a note. - pdf/engine: .ai files go through the same reader. - engine/cli: a file whose import left things out isn't overwritten unless asked (acknowledgeLoss, --replace-lossy). - eps: font names re-encoded with a *<n> suffix resolve to their family. - i18n: es/it/ru for the new import messages; ROADMAP updated. Container and text-document formats were inferred from user-owned files; the Adobe file format specification was not read. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
f149f10059
commit
ec43ac5a1e
@@ -40,3 +40,5 @@ crc32fast = { workspace = true }
|
||||
vectorcraft-testkit = { workspace = true }
|
||||
proptest = { workspace = true }
|
||||
wat = { workspace = true }
|
||||
# Zstandard streams for the Illustrator editing data fuzz tests.
|
||||
ruzstd = { workspace = true }
|
||||
|
||||
@@ -45,6 +45,9 @@ pub(super) fn serialize(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
pub(super) fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let path = str_param(p, "path");
|
||||
if let Some(path) = path {
|
||||
super::check_not_lossy_overwrite(s.doc()?, path, p, "document.export")?;
|
||||
}
|
||||
let f = writable("document.export", str_param(p, "format"), path)?;
|
||||
let expanded = expand_presets(s, "document.export", p)?;
|
||||
let p = &*expanded;
|
||||
|
||||
@@ -133,6 +133,23 @@ pub fn load(name: &str, bytes: &[u8]) -> Result<Loaded> {
|
||||
load_with(name, bytes, &LoadOptions::default())
|
||||
}
|
||||
|
||||
/// A PDF-compatible `.ai` read through the editor's own copy of its art, which it carries: its
|
||||
/// layers and hidden objects as they were, and the art outside its artboard (its PDF part has
|
||||
/// only what is on the artboards), as an EPS of the editor is read. Anything else as it was.
|
||||
fn through_editing_data(bytes: &[u8], opts: &LoadOptions, doc: Document, warnings: Vec<String>) -> (Document, Vec<String>) {
|
||||
if opts.pages.is_some() || !opts.layers {
|
||||
return (doc, warnings);
|
||||
}
|
||||
let Some(private) = vectorcraft_pdf::illustrator_data(bytes, opts.password.as_deref()) else { return (doc, warnings) };
|
||||
let (doc, mut warnings) = vectorcraft_eps::layered_ai(&private, doc, warnings);
|
||||
// The note that the art outside the artboard is lost is the reason the layers weren't read.
|
||||
if warnings.iter().any(|w| w.starts_with("the file's layers weren't read from its editing data")) {
|
||||
return (doc, warnings);
|
||||
}
|
||||
warnings.retain(|w| w != vectorcraft_pdf::OFF_ARTBOARD_NOTE);
|
||||
(doc, warnings)
|
||||
}
|
||||
|
||||
/// [`load`] with `document.open` options (the PDF pages, box and password; the DXF options).
|
||||
pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded> {
|
||||
let format = detect(name, bytes).ok_or_else(|| match super::unsupported(&super::extension(name)) {
|
||||
@@ -171,7 +188,10 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
|
||||
.then(|| vectorcraft_pdf::editing_with(bytes, opts.password.as_deref()))
|
||||
.flatten()
|
||||
.map(|e| (e.intact, move || Some(e.data)));
|
||||
restore_or_import(editing, || super::pdfimport::import(bytes, opts))?
|
||||
restore_or_import(editing, || {
|
||||
let (doc, warnings) = super::pdfimport::import(bytes, opts)?;
|
||||
Ok(through_editing_data(bytes, opts, doc, warnings))
|
||||
})?
|
||||
}
|
||||
"dxf" => {
|
||||
let (doc, warnings) = super::dxfimport::import(bytes, &opts.dxf)?;
|
||||
@@ -244,6 +264,9 @@ pub fn open_template(s: &mut Session, name: &str, bytes: &[u8], path: Option<&st
|
||||
/// Make a loaded file the new active document; `opts` are the options it was read with.
|
||||
fn open_loaded(s: &mut Session, loaded: Loaded, path: Option<String>, opts: &LoadOptions, as_template: bool) -> Result<Value> {
|
||||
let Loaded { mut doc, format, warnings, restored, converted, .. } = loaded;
|
||||
// What reading the file left out of the document, if anything.
|
||||
let losses: Vec<String> = warnings.iter().filter(|w| is_loss(w)).cloned().collect();
|
||||
let source_path = path.clone().filter(|_| !losses.is_empty());
|
||||
let links = crate::cmd::links::resolve(&mut doc, path.as_deref(), s.prefs.update_links == "automatically");
|
||||
// A template (saved by Save as Template, or an .ait/.vctemplate file) opens as a new untitled
|
||||
// document.
|
||||
@@ -266,6 +289,8 @@ fn open_loaded(s: &mut Session, loaded: Loaded, path: Option<String>, opts: &Loa
|
||||
st.format = format.id;
|
||||
}
|
||||
st.converted = converted;
|
||||
st.imported_from = source_path;
|
||||
st.import_losses = losses;
|
||||
let title = s.documents()[index].title();
|
||||
Ok(super::merge(json!({ "index": index, "title": title, "format": format.id, "warnings": warnings, "restored": restored }), links.to_json()))
|
||||
}
|
||||
@@ -360,3 +385,36 @@ pub(super) fn raster_doc(name: &str, bytes: &[u8]) -> Result<Document> {
|
||||
d.insert(Some(layer), 0, n).map_err(err)?;
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// Does this import note say that the file has something the document doesn't?
|
||||
fn is_loss(note: &str) -> bool {
|
||||
note == vectorcraft_pdf::OFF_ARTBOARD_NOTE || vectorcraft_eps::is_loss(note)
|
||||
}
|
||||
|
||||
/// Writing the file `target` for document `st`: not over the file `st` was read from when reading
|
||||
/// it left something out (hidden text, art or layers), unless `p` has `acknowledgeLoss: true`.
|
||||
/// Another name keeps the original file, with all it has.
|
||||
pub(crate) fn check_not_lossy_overwrite(st: &crate::DocState, target: &str, p: &Value, cmd: &str) -> Result<()> {
|
||||
let Some(source) = st.imported_from.as_deref().filter(|_| !st.import_losses.is_empty()) else { return Ok(()) };
|
||||
if bool_or(p, "acknowledgeLoss", false) || !same_file(source, target) {
|
||||
return Ok(());
|
||||
}
|
||||
let mut what = st.import_losses.iter().take(3).map(|n| format!("“{n}”")).collect::<Vec<_>>().join("; ");
|
||||
if st.import_losses.len() > 3 {
|
||||
what.push_str(&format!("; {} more", st.import_losses.len() - 3));
|
||||
}
|
||||
Err(bad(
|
||||
cmd,
|
||||
format!(
|
||||
"{target} is the file this document was read from, and reading it left things out ({what}): writing over it would lose them for good. Write another file instead, or pass acknowledgeLoss: true to replace it anyway"
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
/// Do `a` and `b` name one file (spelled differently, or through a link)?
|
||||
fn same_file(a: &str, b: &str) -> bool {
|
||||
match (std::fs::canonicalize(a), std::fs::canonicalize(b)) {
|
||||
(Ok(a), Ok(b)) => a == b,
|
||||
_ => a == b,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ pub use encode::{ARTBOARD_PARAMS, ArtboardPick, Encoded, encode, encode_all, enc
|
||||
pub(crate) use encode::{anti_alias, background, with_single_artboard};
|
||||
pub use export::export_source;
|
||||
pub(crate) use export::isolated;
|
||||
pub(crate) use load::check_not_lossy_overwrite;
|
||||
use load::err;
|
||||
pub(crate) use load::import_svg;
|
||||
pub(crate) use load::native_file;
|
||||
|
||||
@@ -232,6 +232,9 @@ pub fn save_plan(s: &Session, mode: SaveMode, p: &Value) -> Result<SavePlan> {
|
||||
_ => st.path.as_deref().and_then(parent_folder),
|
||||
};
|
||||
let modified = date_param(p, "modified", cmd)?;
|
||||
if let Some(path) = &path {
|
||||
super::check_not_lossy_overwrite(st, path, p, cmd)?;
|
||||
}
|
||||
Ok(SavePlan { mode, path, format, options, name, folder, modified })
|
||||
}
|
||||
|
||||
|
||||
@@ -155,6 +155,11 @@ pub struct DocState {
|
||||
pub print_tiling: bool,
|
||||
/// The rows open in the Layers panel (view state: not undoable; native files keep it).
|
||||
pub layers_open: OpenRows,
|
||||
/// The file this document was read from, and what reading it left out (hidden text, art or
|
||||
/// layers it could not read): writing the document over that file would lose those for good,
|
||||
/// so an export or save to it asks first ([`cmd::fileio::check_not_lossy_overwrite`]). Not saved.
|
||||
pub imported_from: Option<String>,
|
||||
pub import_losses: Vec<String>,
|
||||
/// Transform Again after a perspective move or scale (Perspective Selection tool): the
|
||||
/// `perspective.transform` params it repeats. `None` once an ordinary transform follows.
|
||||
pub last_perspective: Option<Value>,
|
||||
@@ -243,6 +248,8 @@ impl DocState {
|
||||
recovery: None,
|
||||
print_tiling: false,
|
||||
layers_open,
|
||||
imported_from: None,
|
||||
import_losses: vec![],
|
||||
last_perspective: None,
|
||||
}
|
||||
}
|
||||
@@ -1016,6 +1023,8 @@ impl Session {
|
||||
st.format = old.format;
|
||||
st.save_options = old.save_options.clone();
|
||||
st.converted = old.converted;
|
||||
st.imported_from = old.imported_from.clone();
|
||||
st.import_losses = old.import_losses.clone();
|
||||
st.view = old.view.clone();
|
||||
st.layers_open = old.layers_open.clone();
|
||||
let old = std::mem::replace(&mut self.docs[index], st);
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
//! must load as an error or as a document that then renders and exports, without a panic; nor may
|
||||
//! bitmaps, PDF and text pasted from other apps, nor EMF and WMF pictures (damaged files, records
|
||||
//! of every kind with random contents) opened, placed or pasted, nor EPS and PostScript files
|
||||
//! (damaged ones, hostile programs) read by the PostScript interpreter, nor Affinity documents
|
||||
//! (damaged ones, hostile programs) read by the PostScript interpreter, nor the editing data of
|
||||
//! Illustrator EPS and `.ai` files (the layers they carry, damaged or hostile), nor Affinity documents
|
||||
//! (mutated object streams, archives and indexed PNG previews, hostile image dimensions).
|
||||
//!
|
||||
//! `PROPTEST_CASES=20000 cargo test -p vectorcraft-engine --test import_fuzz` runs a deeper search.
|
||||
@@ -1584,3 +1585,125 @@ proptest! {
|
||||
perspective_presets("mutated perspective presets", &mutate_text(&text, cut, &edits))?;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- the editing data of Illustrator files ----------
|
||||
|
||||
/// `data` as ASCII85, up to the `~>`.
|
||||
fn ascii85(data: &[u8]) -> String {
|
||||
let mut out = String::new();
|
||||
for chunk in data.chunks(4) {
|
||||
let mut x = chunk.iter().enumerate().fold(0u32, |v, (i, b)| v | u32::from(*b) << (24 - 8 * i));
|
||||
let mut d = [0u8; 5];
|
||||
for c in d.iter_mut().rev() {
|
||||
*c = (x % 85) as u8 + b'!';
|
||||
x /= 85;
|
||||
}
|
||||
out.extend(d.iter().take(chunk.len() + 1).map(|c| char::from(*c)));
|
||||
}
|
||||
out.push_str("~>");
|
||||
out
|
||||
}
|
||||
|
||||
/// The editing data of a file with every sort of object the layers reader knows, and some it doesn't.
|
||||
fn editing_text() -> String {
|
||||
let square = |x: u32| format!("0 0 1 0 k\n{x} 10 m\n{} 10 L\n{} 14 L\n{x} 14 L\nf", x + 4, x + 4);
|
||||
let body = [
|
||||
"u".to_string(),
|
||||
square(10),
|
||||
"U".into(),
|
||||
"*u".into(),
|
||||
square(20),
|
||||
square(30),
|
||||
"*U".into(),
|
||||
"q".into(),
|
||||
square(40),
|
||||
"50 10 m 60 10 L 60 20 L 50 20 L h W f".into(),
|
||||
"Q".into(),
|
||||
"1 Xw".into(),
|
||||
"u".into(),
|
||||
"/AI11Text :\n0 /FreeUndo ,\n;".into(),
|
||||
"U".into(),
|
||||
"0 Xw".into(),
|
||||
"1 0 0 0 1 0 Bg".into(),
|
||||
"0 1 w 2 J 0 j 4 M [3 2]0 d 1 D".into(),
|
||||
]
|
||||
.join("\n");
|
||||
let layer = |name: &str, visible: u8, body: &str| {
|
||||
format!("%AI5_BeginLayer\n{visible} 1 1 1 0 0 1 0 79 128 255 0 50 0 Lb\n({name}) Ln\n{body}\nLB\n%AI5_EndLayer--\n")
|
||||
};
|
||||
format!(
|
||||
"%!PS-Adobe-3.0 \n%%BoundingBox: 0 0 100 100\n%%HiResBoundingBox: 0 0 100 100\n%AI3_Cropmarks: 0 0 100 100\n{}{}%%Trailer\n",
|
||||
layer("One", 1, &format!("{body}\n{}", layer("Sub", 1, &square(70)))),
|
||||
layer("Two", 0, &square(80))
|
||||
)
|
||||
}
|
||||
|
||||
fn zstd(text: &str) -> Vec<u8> {
|
||||
ruzstd::encoding::compress_to_vec(text.as_bytes(), ruzstd::encoding::CompressionLevel::Fastest)
|
||||
}
|
||||
|
||||
/// An EPS drawing a square whose private data holds `editing`.
|
||||
fn editing_eps(editing: &str) -> Vec<u8> {
|
||||
let lines: Vec<String> = ascii85(&zstd(editing)).as_bytes().chunks(60).map(|c| format!("%{}", String::from_utf8_lossy(c))).collect();
|
||||
format!(
|
||||
"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n0 0 1 0 setcmykcolor 10 10 moveto 14 10 lineto 14 14 lineto closepath fill\nshowpage\n%%EOF\n%AI9_PrivateDataBegin\n%AI24_DataStream\n{}\n%AI9_PrivateDataEnd\n",
|
||||
lines.join("\n")
|
||||
)
|
||||
.into_bytes()
|
||||
}
|
||||
|
||||
/// The text document of a story of centred type with two styles, as an editing copy keeps it.
|
||||
fn text_document_text() -> String {
|
||||
"/0 << /1 << /0 [ << /0 << /0 << /0 (Helvetica) >> >> >> ] >> /8 << /0 [ << /0 << /0 [ 0 0 ] /2 << /2 [ 1 0 0 1 3 4 ] >> >> >> ] >> >>\n\
|
||||
/1 << /1 [ << /0 << /0 (Hi\\rthere\\r) /5 << /0 [ << /0 << /0 << /0 () /5 << /0 2 >> /6 0 >> >> /1 9 >> ] >> \
|
||||
/6 << /0 [ << /0 << /0 << /0 () /5 0 /6 << /0 0 /1 14.0 /53 << /99 /CAITextPaint /0 << /0 2 /1 [ 1.0 0.0 1.0 0.0 0.0 ] >> >> >> >> >> /1 9 >> ] >> >> \
|
||||
/1 << /0 [ << /0 0 >> ] /2 [ << /99 /PC /6 [ << /99 /F /0 << /0 [ 8200.0 8180.0 ] >> /6 [ << /99 /R /6 [ << /99 /R /6 [ << /99 /L /6 [ \
|
||||
<< /99 /S /0 << /0 [ -5.0 0.0 ] >> /15 << /0 3 >> >> ] >> << /99 /L /0 << /0 [ 0.0 16.8 ] >> /6 [ << /99 /S /0 << /0 [ -9.0 0.0 ] >> /15 << /0 6 >> >> ] >> ] >> ] >> ] >> ] >> ] >> >> ] /2 << /1 12.0 >> >>\n"
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// An EPS with a hidden text object whose story is in `document`.
|
||||
fn text_eps(document: &str) -> Vec<u8> {
|
||||
let lines: Vec<String> = ascii85(document.as_bytes()).as_bytes().chunks(60).map(|c| format!("%{}", String::from_utf8_lossy(c))).collect();
|
||||
let editing = format!(
|
||||
"%!PS-Adobe-3.0 \n%%BoundingBox: 0 0 100 100\n%%HiResBoundingBox: 0 0 100 100\n%AI3_Cropmarks: 0 0 100 100\n%AI3_TemplateBox: 50 50 50 50\n\
|
||||
%AI5_BeginLayer\n0 1 1 1 0 0 1 0 79 128 255 0 50 0 Lb\n(Spare) Ln\n/AI11Text :\n0 /FrameIndex ,\n0 /StoryIndex ,\n;\nLB\n%AI5_EndLayer--\n\
|
||||
%AI11_BeginTextDocument\n/AI11TextDocument : /ASCII85Decode ,\n{}\n%AI11_EndTextDocument\n%%Trailer\n",
|
||||
lines.join("\n")
|
||||
);
|
||||
editing_eps(&editing)
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(config())]
|
||||
|
||||
/// An Illustrator EPS whose text document is damaged or hostile: read as its type, or without it.
|
||||
#[test]
|
||||
fn eps_text_document_never_panics(cut in 0usize..1_500, edits in prop::collection::vec((0usize..1_500, prop::sample::select(vec!['0', '9', '-', '.', ' ', '\n', '(', ')', '/', '[', ']', '<', '>', '\\', 'e', '1'])), 0..12)) {
|
||||
let text = mutate_text(&text_document_text(), cut, &edits);
|
||||
let bytes = text_eps(&text);
|
||||
survive("mutated EPS text document", || vectorcraft_eps::import(&bytes).ok().map(|r| r.document))?;
|
||||
}
|
||||
|
||||
/// An Illustrator EPS whose editing data is damaged or hostile: read as its layers, or as its page.
|
||||
#[test]
|
||||
fn eps_editing_data_never_panics(cut in 0usize..3_000, edits in prop::collection::vec((0usize..3_000, prop::sample::select(vec!['0', '9', '-', '.', ' ', '\n', '(', ')', '/', ':', ';', '[', ']', '%', 'q', 'Q', 'W', 'u', 'U', 'L', 'k', 'x'])), 0..12)) {
|
||||
let text = mutate_text(&editing_text(), cut, &edits);
|
||||
let bytes = editing_eps(&text);
|
||||
survive("mutated EPS editing data", || vectorcraft_eps::import(&bytes).ok().map(|r| r.document))?;
|
||||
}
|
||||
|
||||
/// A `.ai` whose editing data is damaged or hostile.
|
||||
#[test]
|
||||
fn ai_editing_data_never_panics(cut in 0usize..3_000, edits in prop::collection::vec((0usize..3_000, prop::sample::select(vec!['0', '9', '-', '.', ' ', '\n', '(', ')', '/', ':', ';', '[', ']', '%', 'q', 'Q', 'W', 'u', 'U', 'L', 'k', 'x'])), 0..12), damage in prop::collection::vec((0usize..4_000, any::<u8>()), 0..4)) {
|
||||
let text = mutate_text(&editing_text(), cut, &edits);
|
||||
let mut private = [b"%AI24_ZStandard_Data".as_slice(), &zstd(&text)].concat();
|
||||
for (at, b) in damage {
|
||||
let n = private.len();
|
||||
if let Some(x) = private.get_mut(at % n) {
|
||||
*x = b;
|
||||
}
|
||||
}
|
||||
survive("mutated .ai editing data", || Some(vectorcraft_eps::layered_ai(&private, Document::new(100.0, 100.0), vec![]).0))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
//! A file whose import left something out isn't written over silently: its hidden text, art or
|
||||
//! layers would be gone for good.
|
||||
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
fn ascii85(data: &[u8]) -> String {
|
||||
let mut out = String::new();
|
||||
for chunk in data.chunks(4) {
|
||||
let mut x = chunk.iter().enumerate().fold(0u32, |v, (i, b)| v | u32::from(*b) << (24 - 8 * i));
|
||||
let mut d = [0u8; 5];
|
||||
for c in d.iter_mut().rev() {
|
||||
*c = (x % 85) as u8 + b'!';
|
||||
x /= 85;
|
||||
}
|
||||
out.extend(d.iter().take(chunk.len() + 1).map(|c| char::from(*c)));
|
||||
}
|
||||
out.push_str("~>");
|
||||
out
|
||||
}
|
||||
|
||||
/// A square that is gradient-filled in the layers (which can't be read) and flat on the page: the
|
||||
/// file comes in as its page, and its hidden layer is left out.
|
||||
fn lossy_eps() -> Vec<u8> {
|
||||
let layer = |name: &str, visible: u8, body: &str| {
|
||||
format!("%AI5_BeginLayer\n{visible} 1 1 1 0 0 1 0 79 128 255 0 50 0 Lb\n({name}) Ln\n{body}\nLB\n%AI5_EndLayer--\n")
|
||||
};
|
||||
let editing = format!(
|
||||
"%!PS-Adobe-3.0 \n%%BoundingBox: 0 0 100 100\n%%HiResBoundingBox: 0 0 100 100\n%AI3_Cropmarks: 0 0 100 100\n{}{}%%Trailer\n",
|
||||
layer("Art", 1, "1 0 0 0 1 0 Bg\n0 0 1 0 k\n10 10 m\n14 10 L\n14 14 L\n10 14 L\nf"),
|
||||
layer("Hidden", 0, "0 0 1 0 k\n50 50 m\n54 50 L\n54 54 L\n50 54 L\nf"),
|
||||
);
|
||||
let packed = ruzstd::encoding::compress_to_vec(editing.as_bytes(), ruzstd::encoding::CompressionLevel::Fastest);
|
||||
let lines: Vec<String> = ascii85(&packed).as_bytes().chunks(60).map(|c| format!("%{}", String::from_utf8_lossy(c))).collect();
|
||||
format!(
|
||||
"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n0 0 1 0 setcmykcolor 10 10 moveto 14 10 lineto 14 14 lineto 10 14 lineto closepath fill\nshowpage\n%%EOF\n%AI9_PrivateDataBegin\n%AI24_DataStream\n{}\n%AI9_PrivateDataEnd\n",
|
||||
lines.join("\n")
|
||||
)
|
||||
.into_bytes()
|
||||
}
|
||||
|
||||
fn dir(name: &str) -> std::path::PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("vc-lossy-{name}-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_the_import_left_things_out_of_is_not_written_over_unasked() {
|
||||
let d = dir("a");
|
||||
let src = d.join("art.eps");
|
||||
let original = lossy_eps();
|
||||
std::fs::write(&src, &original).unwrap();
|
||||
let mut s = Session::new();
|
||||
let opened = s.execute("document.open", &json!({ "path": src.to_string_lossy() })).unwrap();
|
||||
assert!(opened["warnings"].to_string().contains("layers weren't read"), "{opened}");
|
||||
|
||||
// Over the same file: refused, and the file is as it was.
|
||||
let e = s.execute("document.export", &json!({ "path": src.to_string_lossy(), "format": "eps" })).unwrap_err().to_string();
|
||||
assert!(e.contains("left things out") && e.contains("acknowledgeLoss"), "{e}");
|
||||
assert_eq!(std::fs::read(&src).unwrap(), original);
|
||||
// However the path is spelled.
|
||||
let spelled = d.join(".").join("art.eps");
|
||||
assert!(s.execute("document.export", &json!({ "path": spelled.to_string_lossy(), "format": "eps" })).is_err());
|
||||
|
||||
// Another name is fine, and so is replacing the file when asked to.
|
||||
let other = d.join("art copy.eps");
|
||||
s.execute("document.export", &json!({ "path": other.to_string_lossy(), "format": "eps" })).unwrap();
|
||||
assert!(other.exists());
|
||||
s.execute("document.export", &json!({ "path": src.to_string_lossy(), "format": "eps", "acknowledgeLoss": true })).unwrap();
|
||||
assert_ne!(std::fs::read(&src).unwrap(), original);
|
||||
std::fs::remove_dir_all(d).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_the_import_read_whole_is_written_over_as_before() {
|
||||
let d = dir("b");
|
||||
let src = d.join("plain.eps");
|
||||
std::fs::write(&src, "%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n0 0 1 0 setcmykcolor 10 10 moveto 14 10 lineto 14 14 lineto closepath fill\nshowpage\n%%EOF\n").unwrap();
|
||||
let mut s = Session::new();
|
||||
s.execute("document.open", &json!({ "path": src.to_string_lossy() })).unwrap();
|
||||
s.execute("document.export", &json!({ "path": src.to_string_lossy(), "format": "eps" })).unwrap();
|
||||
std::fs::remove_dir_all(d).ok();
|
||||
}
|
||||
Reference in New Issue
Block a user