mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
The layers reader of the editing copy (operators translated into PostScript for the interpreter, any file with RGB colours, gradients, transparency or images falling back to its page) is replaced by a reader of the whole structure (import/ai): layers and sublayers with their options, groups, compound paths, clipping groups and layer clipping, names, hidden and locked objects, RGB, CMYK, grey, named and spot colours, gradients, transparency, fill rule, overprint, embedded images with their alpha channel, guides and every artboard, and objects drawn as their look when they have several fills or strokes, effects or a brush. Containers: Zstandard, zlib or plain data in EPS and .ai. The text placement, the comparison with the page and the overwrite guard stay around it. The page's type takes its text object's place without a group; the comparison is by lightness, on nothing, without layers that don't print. Symbols, pattern fills, placed files and unknown operators make the file come in as its page where they show, and are left out of layers that don't. Co-Authored-By: Claude Opus 5.5 <[email protected]>
111 lines
5.0 KiB
Rust
111 lines
5.0 KiB
Rust
//! 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 drawn with an operator the layers' reader doesn't know 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 Zq\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();
|
|
}
|
|
|
|
#[test]
|
|
fn the_other_ways_of_writing_a_file_ask_too() {
|
|
let d = dir("c");
|
|
let src = d.join("art.eps");
|
|
let original = lossy_eps();
|
|
std::fs::write(&src, &original).unwrap();
|
|
let mut s = Session::new();
|
|
s.execute("document.open", &json!({ "path": src.to_string_lossy() })).unwrap();
|
|
s.execute("select.all", &json!({})).unwrap();
|
|
for (cmd, params) in [
|
|
("document.exportPdf", json!({})),
|
|
("document.exportSelection", json!({ "format": "eps" })),
|
|
("document.exportForOffice", json!({})),
|
|
("file.print", json!({ "format": "pdf" })),
|
|
] {
|
|
let mut p = params;
|
|
p["path"] = json!(src.to_string_lossy());
|
|
let e = s.execute(cmd, &p).unwrap_err().to_string();
|
|
assert!(e.contains("left things out"), "{cmd}: {e}");
|
|
assert_eq!(std::fs::read(&src).unwrap(), original, "{cmd}");
|
|
}
|
|
std::fs::remove_dir_all(d).ok();
|
|
}
|