Files
vectorcraft/crates/engine/src/tests/dcs.rs
T
dotCooCoo ba2531bc3f Tests: engine tests remove the temp folders they make
vectorcraft_testkit::temp_dir returns a bare path, so every engine test
that wrote into one left the folder in the system temporary folder.

- testkit: TempDir, a test's own folder made through temp_dir (same
  private creation and ownership checks) under a tag no other TempDir in
  the process uses, removed with its contents when dropped, also when
  the test panics.
- Engine unit and integration tests hold a TempDir for the whole test
  instead of calling temp_dir: graph, fileio, compare, journal, library,
  shortcut set, spelling, store, web export, cancel, containers, build,
  color settings, command sweep, font folder, removed font, model-based
  and the font search fuzz.
- The DCS and swatch library tests' own helpers and the PDF editing
  tests' shared vc-pdfedit-<pid> folder use a TempDir per test too.
- The font and link file search, store, color settings and font folder
  helpers no longer delete the private folder temp_dir checked and make
  it again with default permissions.
2026-11-13 09:33:36 -08:00

916 lines
49 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! DCS files placed with Link (`file.place`): one linked image of their plates, their spot plates
//! as spot swatches, clipped by their composite's clipping path, never opened or embedded; and
//! the Links panel's Show DCS Transparency Interactions (`links.list` with `dcsTransparency`).
//! The files are made from scratch as http://www.telegraphics.net/doc/DCS.txt describes them.
use serde_json::{Value, json};
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{Node, NodeKind};
use crate::*;
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
s
}
/// A fresh scratch folder for one test, removed when dropped.
fn temp_dir(name: &str) -> vectorcraft_testkit::TempDir {
vectorcraft_testkit::TempDir::new(&format!("dcs-{name}"))
}
/// A plate program: one 2 × 2 grayscale image (`gray`, row by row) on a 20 × 10 pt page.
fn plate_eps(gray: [u8; 4]) -> String {
let hex: String = gray.iter().map(|g| format!("{g:02x}")).collect();
format!(
"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 20 10\n%%EndComments\n20 10 scale\n2 2 8 [2 0 0 -2 0 2] <{hex}> image\nshowpage\n%%Trailer\n%%EOF\n"
)
}
/// A single-file DCS 2.0 file of `plates` (name, plate program), with the header lines `extra`; its
/// composite is drawn through a clipping path, the left half of the page.
fn single_file(plates: &[(&str, String)], extra: &str) -> Vec<u8> {
let header = |offsets: &[usize]| {
let mut s = String::from("%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 20 10\n");
s.push_str(extra);
for ((name, eps), at) in plates.iter().zip(offsets) {
s.push_str(&format!("%%PlateFile: ({name}) EPS #{at:010} {:010}\n", eps.len()));
}
s.push_str("%%EndComments\ngsave newpath 0 0 moveto 10 0 lineto 10 10 lineto 0 10 lineto closepath clip newpath\n");
s.push_str("20 10 scale 2 2 8 [2 0 0 -2 0 2] <808080808080808080808080> false 3 colorimage grestore showpage\n%%EOF\n");
s
};
let main = header(&vec![0; plates.len()]).len();
let mut offsets = vec![];
let mut at = main;
for (_, eps) in plates {
offsets.push(at);
at += eps.len();
}
let mut out = header(&offsets).into_bytes();
for (_, eps) in plates {
out.extend(eps.as_bytes());
}
out
}
/// Cyan solid on the top row, PANTONE 185 C at 50% on the left column.
fn two_plates() -> Vec<u8> {
single_file(
&[("Cyan", plate_eps([0, 0, 255, 255])), ("PANTONE 185 C", plate_eps([127, 255, 127, 255]))],
"%%DocumentCustomColors: (PANTONE 185 C)\n%%CMYKCustomColor: 0 .91 .76 0 (PANTONE 185 C)\n",
)
}
fn write(dir: &std::path::Path, name: &str, bytes: &[u8]) -> String {
let p = dir.join(name);
std::fs::write(&p, bytes).unwrap();
p.to_string_lossy().into_owned()
}
/// The image inside placed object `id` (itself, or the image of its clipping group).
fn image_of(s: &Session, id: &Value) -> Node {
let n = s.doc().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap().clone();
let mut found = None;
n.walk(&mut |c: &Node| {
if found.is_none() && matches!(c.kind, NodeKind::Image(_)) {
found = Some(c.clone());
}
});
found.expect("an image")
}
fn error(s: &mut Session, cmd: &str, p: Value) -> String {
s.execute(cmd, &p).unwrap_err().to_string()
}
#[test]
fn a_dcs_file_places_as_one_linked_image_of_its_plates() {
let dir = temp_dir("place");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
assert_eq!(r["linked"], true);
assert_eq!(r["warnings"], json!([]));
let id = &r["ids"][0];
let placed = s.doc().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap().clone();
let NodeKind::Group { children, clip: true } = &placed.kind else { panic!("a clipping group: {:?}", placed.kind) };
assert!(matches!(children[0].kind, NodeKind::Path { clipping: true, .. }), "the composite's clipping path");
assert_eq!(children[0].geometric_bounds().map(|b| b.width().round()), Some(10.0));
assert_eq!(r["width"].as_f64().map(f64::round), Some(10.0), "clipped to the left half of its 20 pt page");
let image = image_of(&s, id);
let NodeKind::Image(im) = &image.kind else { not_an_image() };
assert!(im.link.as_ref().is_some_and(|l| l.path.ends_with("pic.eps")));
let doc = &s.doc().unwrap().doc;
let plates = doc.images[&im.key].separated().expect("its plates are kept");
assert_eq!(plates.plates.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["Cyan", "PANTONE 185 C"]);
assert_eq!(plates.plates[1].data, [128, 0, 128, 0]);
let sw = doc.swatch("PANTONE 185 C").expect("a spot swatch for the spot plate");
assert!(sw.spot);
assert_eq!(sw.paint, Paint::solid(Color::cmyk(0.0, 0.91, 0.76, 0.0)));
assert!(!doc.swatches_iter().any(|w| w.spot && w.name == "Cyan"), "process plates add no spot swatch");
let list = s.execute("links.list", &json!({})).unwrap();
assert_eq!(list["links"][0]["format"], "DCS");
assert_eq!(list["links"][0]["status"], "ok");
let info = s.execute("file.place.info", &json!({ "path": path })).unwrap();
assert_eq!(info["plates"], json!(["Cyan", "PANTONE 185 C"]));
assert_eq!(info["colorMode"], "CMYK");
assert_eq!(info["pixelWidth"], 2);
// One undo step takes the swatch away too.
s.execute("edit.undo", &json!({})).unwrap();
assert!(s.doc().unwrap().doc.swatch("PANTONE 185 C").is_none());
}
fn not_an_image() -> ! {
panic!("an image")
}
#[test]
fn a_dcs_file_is_never_opened_or_embedded() {
let dir = temp_dir("refused");
let bytes = two_plates();
let path = write(&dir, "pic.eps", &bytes);
let mut s = session();
let only = "DCS files can only be placed linked";
assert!(error(&mut s, "file.place", json!({ "path": path, "link": false })).contains(only));
let data = json!({ "name": "pic.eps", "dataBase64": vectorcraft_format::base64_encode(&bytes) });
assert!(error(&mut s, "file.place", data).contains(only), "bytes alone can't be linked");
assert!(error(&mut s, "document.open", json!({ "path": path })).contains(only));
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
let image = image_of(&s, &r["ids"][0]);
let e = error(&mut s, "links.embed", json!({ "ids": [image.id.0] }));
assert!(e.contains(only), "{e}");
let NodeKind::Image(im) = &image_of(&s, &r["ids"][0]).kind else { not_an_image() };
assert!(im.link.is_some(), "still linked");
}
#[test]
fn a_swatch_of_the_same_name_is_kept_with_a_warning() {
let dir = temp_dir("swatch");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
s.execute("swatch.new", &json!({"name": "PANTONE 185 C", "color": {"c": 0.0, "m": 1.0, "y": 1.0, "k": 0.0}, "spot": true})).unwrap();
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
assert_eq!(r["warnings"], json!(["the document's swatch `PANTONE 185 C` is kept: it is a different color"]));
let sw = s.doc().unwrap().doc.swatch("PANTONE 185 C").unwrap().clone();
assert_eq!(sw.paint.color(), Some(Color::cmyk(0.0, 1.0, 1.0, 0.0)));
}
#[test]
fn plate_files_of_their_own_are_read_from_the_dcs_files_folder() {
let dir = temp_dir("files");
write(&dir, "pic.C", plate_eps([0, 255, 255, 255]).as_bytes());
write(&dir, "pic.K", plate_eps([255, 255, 255, 0]).as_bytes());
let main = "%!PS-Adobe-2.0 EPSF-1.2\n%%BoundingBox: 0 0 20 10\n%%CyanPlate: OldDisk:Scans:pic.C\n%%BlackPlate: pic.K\n%%EndComments\nshowpage\n";
let path = write(&dir, "pic.eps", main.as_bytes());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
let image = image_of(&s, &r["ids"][0]);
assert!(matches!(image.kind, NodeKind::Image(_)), "no clipping path without a composite");
let NodeKind::Image(im) = &image.kind else { not_an_image() };
let plates = s.doc().unwrap().doc.images[&im.key].separated().unwrap();
assert_eq!(plates.plates[0].data, [255, 0, 0, 0], "the plate file found by its name in the folder");
assert_eq!(plates.plates[1].data, [0, 0, 0, 255]);
std::fs::remove_file(dir.join("pic.K")).unwrap();
let e = error(&mut s, "file.place", json!({ "path": path }));
assert!(e.contains("the Black plate can't be read") && e.contains("`pic.K` is not in the DCS file's folder"), "{e}");
}
#[test]
fn update_link_reads_the_plates_again() {
let dir = temp_dir("update");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
let id = image_of(&s, &r["ids"][0]).id;
let changed = single_file(&[("Cyan", plate_eps([0, 0, 0, 0])), ("Varnish", plate_eps([255; 4]))], "%%CMYKCustomColor: 0 0 .2 0 (Varnish)\n");
std::fs::write(&path, &changed).unwrap();
let u = s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
assert_eq!(u["updated"], json!([id.0]));
let NodeKind::Image(im) = &s.doc().unwrap().doc.node(id).unwrap().kind else { not_an_image() };
let plates = s.doc().unwrap().doc.images[&im.key].separated().unwrap();
assert_eq!(plates.plates.len(), 2);
assert_eq!(plates.plates[0].data, [255; 4]);
let sw = s.doc().unwrap().doc.swatch("Varnish").expect("a spot swatch for the plate the file gained");
assert!(sw.spot);
assert_eq!(sw.paint, Paint::solid(Color::cmyk(0.0, 0.0, 0.2, 0.0)));
}
#[test]
fn update_link_takes_the_files_new_clipping_path() {
let dir = temp_dir("update-clip");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
let group = NodeId(r["ids"][0].as_u64().unwrap());
let id = image_of(&s, &r["ids"][0]).id;
let clip_width = |s: &Session| {
let NodeKind::Group { children, .. } = &s.doc().unwrap().doc.node(group).unwrap().kind else { panic!("a clipping group") };
children[0].geometric_bounds().map(|b| b.width())
};
let before = clip_width(&s).unwrap();
// The same file, its composite now drawn through the left quarter of the page (same length,
// so the plates stay where the header says).
let text = String::from_utf8(two_plates()).unwrap().replacen("10 0 lineto 10 10 lineto", " 5 0 lineto 5 10 lineto", 1);
std::fs::write(&path, text.as_bytes()).unwrap();
let u = s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
assert_eq!(u["updated"], json!([id.0]));
let after = clip_width(&s).unwrap();
assert!((after - before / 2.0).abs() < 1e-6, "the clipping path follows the file: {before} → {after}");
}
#[test]
fn update_link_clips_an_image_whose_file_gained_a_clipping_path() {
let dir = temp_dir("update-gained-clip");
// The composite drawn without its clipping path (same length).
let unclipped = String::from_utf8(two_plates()).unwrap().replacen("closepath clip newpath", "closepath newpath ", 1);
let path = write(&dir, "pic.eps", unclipped.as_bytes());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
let id = NodeId(r["ids"][0].as_u64().unwrap());
assert!(matches!(s.doc().unwrap().doc.node(id).unwrap().kind, NodeKind::Image(_)), "no clipping path, no group");
std::fs::write(&path, two_plates()).unwrap();
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
let doc = &s.doc().unwrap().doc;
let group = doc.node(doc.parent_of(id).expect("a parent")).unwrap();
let NodeKind::Group { children, clip: true } = &group.kind else { panic!("a clipping group: {:?}", group.kind) };
assert_eq!(children[0].geometric_bounds().map(|b| b.width().round()), Some(10.0), "the left half of the page");
}
#[test]
fn a_changed_plate_file_marks_the_link_modified() {
let dir = temp_dir("plate-changed");
write(&dir, "pic.C", plate_eps([0, 255, 255, 255]).as_bytes());
let main = "%!PS-Adobe-2.0 EPSF-1.2\n%%BoundingBox: 0 0 20 10\n%%CyanPlate: pic.C\n%%EndComments\nshowpage\n";
let path = write(&dir, "pic.eps", main.as_bytes());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
let id = image_of(&s, &r["ids"][0]).id;
assert_eq!(s.execute("links.list", &json!({})).unwrap()["links"][0]["status"], "ok");
// Only the plate file changes (a size of its own, so the change shows whatever the clock).
write(&dir, "pic.C", format!("{}% more\n", plate_eps([255, 255, 255, 0])).as_bytes());
assert_eq!(s.execute("links.list", &json!({})).unwrap()["links"][0]["status"], "modified");
let u = s.execute("links.update", &json!({})).unwrap();
assert_eq!(u["updated"], json!([id.0]));
let NodeKind::Image(im) = &s.doc().unwrap().doc.node(id).unwrap().kind else { not_an_image() };
assert_eq!(s.doc().unwrap().doc.images[&im.key].separated().unwrap().plates[0].data, [0, 0, 0, 255]);
assert_eq!(s.execute("links.list", &json!({})).unwrap()["links"][0]["status"], "ok");
std::fs::remove_file(dir.join("pic.C")).unwrap();
assert_eq!(s.execute("links.list", &json!({})).unwrap()["links"][0]["status"], "modified", "a plate file that is gone");
}
#[test]
fn the_link_watch_reads_the_link_again_when_only_a_plate_file_changes() {
let dir = temp_dir("plate-watch");
write(&dir, "pic.C", plate_eps([0, 255, 255, 255]).as_bytes());
let main = "%!PS-Adobe-2.0 EPSF-1.2\n%%BoundingBox: 0 0 20 10\n%%CyanPlate: pic.C\n%%EndComments\nshowpage\n";
let path = write(&dir, "pic.eps", main.as_bytes());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
let id = image_of(&s, &r["ids"][0]).id;
let files: Vec<String> = crate::link_watch::linked_files(&s.doc().unwrap().doc).into_iter().map(|(p, _)| p).collect();
assert!(files.iter().any(|p| p.ends_with("pic.C")), "the plate file is watched too: {files:?}");
// The first look only remembers the files.
assert_eq!(s.execute("links.updateChanged", &json!({})).unwrap()["updated"], json!([]));
write(&dir, "pic.C", format!("{}% more\n", plate_eps([255, 255, 255, 0])).as_bytes());
assert_eq!(s.execute("links.updateChanged", &json!({})).unwrap()["updated"], json!([id.0]));
let NodeKind::Image(im) = &s.doc().unwrap().doc.node(id).unwrap().kind else { not_an_image() };
assert_eq!(s.doc().unwrap().doc.images[&im.key].separated().unwrap().plates[0].data, [0, 0, 0, 255]);
}
/// The width of the clipping path of the clip group holding the image in the art of symbol
/// `name`; `None` when no clip group holds it.
fn symbol_clip_width(s: &Session, name: &str) -> Option<f64> {
fn find(n: &Node) -> Option<Option<f64>> {
if let NodeKind::Group { children, clip: true } = &n.kind
&& let [c, im] = &children[..]
&& matches!(im.kind, NodeKind::Image(_))
{
return Some(c.geometric_bounds().map(|b| b.width()));
}
if matches!(n.kind, NodeKind::Image(_)) {
return Some(None);
}
n.children().and_then(|ch| ch.iter().find_map(|c| find(c)))
}
let doc = &s.doc().unwrap().doc;
find(&doc.symbols.iter().find(|w| w.name == name).expect("the symbol").art).expect("an image in the symbol")
}
#[test]
fn update_link_takes_the_files_clipping_path_into_symbols_too() {
let dir = temp_dir("update-symbol-clip");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
let id = image_of(&s, &r["ids"][0]).id;
let other = s.execute("file.place", &json!({ "path": path, "at": [200, 100] })).unwrap();
s.execute("symbol.new", &json!({ "name": "Pic", "ids": [other["ids"][0]] })).unwrap();
let before = symbol_clip_width(&s, "Pic").expect("the file's clipping path");
// The composite now drawn through the left quarter of the page (same length).
let text = String::from_utf8(two_plates()).unwrap().replacen("10 0 lineto 10 10 lineto", " 5 0 lineto 5 10 lineto", 1);
std::fs::write(&path, text.as_bytes()).unwrap();
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
let after = symbol_clip_width(&s, "Pic").expect("still clipped");
assert!((after - before / 2.0).abs() < 1e-6, "the symbol's clipping path follows the file: {before} → {after}");
// The file without a clipping path (same length): the symbol's image is no longer clipped.
let unclipped = text.replacen("closepath clip newpath", "closepath newpath ", 1);
std::fs::write(&path, unclipped.as_bytes()).unwrap();
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
assert_eq!(symbol_clip_width(&s, "Pic"), None);
}
#[test]
fn update_and_relink_by_id_keep_the_clipping_path_of_a_dcs_image_in_a_symbol() {
let dir = temp_dir("symbol-clip-by-id");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
s.execute("symbol.new", &json!({ "name": "Pic", "ids": [r["ids"][0]] })).unwrap();
let list = s.execute("links.list", &json!({})).unwrap();
let row = list["links"].as_array().unwrap().iter().find(|r| r["owner"]["kind"] == "symbol").cloned().expect("the symbol's image is listed");
let id = row["id"].as_u64().unwrap();
assert_eq!(row["format"], "DCS", "{list}");
let before = symbol_clip_width(&s, "Pic").expect("the file's clipping path");
// The composite now drawn through the left quarter of the page (same length).
let text = String::from_utf8(two_plates()).unwrap().replacen("10 0 lineto 10 10 lineto", " 5 0 lineto 5 10 lineto", 1);
std::fs::write(&path, text.as_bytes()).unwrap();
assert_eq!(s.execute("links.update", &json!({ "ids": [id] })).unwrap()["updated"], json!([id]));
let after = symbol_clip_width(&s, "Pic").expect("still clipped");
assert!((after - before / 2.0).abs() < 1e-6, "the symbol's clipping path follows the file: {before} → {after}");
// Relinked by its id to a plain image: the DCS file's clipping path goes.
let png = dir.join("plain.png");
image::RgbImage::from_pixel(2, 2, image::Rgb([9, 9, 9])).save(&png).unwrap();
s.execute("links.relink", &json!({ "ids": [id], "path": png.to_string_lossy() })).unwrap();
assert_eq!(symbol_clip_width(&s, "Pic"), None);
}
#[test]
fn a_file_that_lost_its_clipping_path_unclips_the_image() {
let dir = temp_dir("update-lost-clip");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
let id = image_of(&s, &r["ids"][0]).id;
let unclipped = String::from_utf8(two_plates()).unwrap().replacen("closepath clip newpath", "closepath newpath ", 1);
std::fs::write(&path, unclipped.as_bytes()).unwrap();
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
let doc = &s.doc().unwrap().doc;
let placed = NodeId(r["ids"][0].as_u64().unwrap());
assert_eq!(doc.node(placed).and_then(plain_group_of_image), Some((false, true)), "the placed group stays, without the clip");
let parent = doc.parent_of(id).expect("a parent");
assert!(!matches!(doc.node(parent).unwrap().kind, NodeKind::Group { clip: true, .. }), "the image is not clipped");
// Relinked to a plain image: no clip group either.
let png = dir.join("plain.png");
image::RgbImage::from_pixel(2, 2, image::Rgb([9, 9, 9])).save(&png).unwrap();
std::fs::write(&path, two_plates()).unwrap();
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
assert!(
s.doc().unwrap().doc.parent_of(id).is_some_and(|p| matches!(s.doc().unwrap().doc.node(p).unwrap().kind, NodeKind::Group { clip: true, .. }))
);
s.execute("links.relink", &json!({ "ids": [id.0], "path": png.to_string_lossy() })).unwrap();
let doc = &s.doc().unwrap().doc;
assert!(
!matches!(doc.node(doc.parent_of(id).unwrap()).unwrap().kind, NodeKind::Group { clip: true, .. }),
"a plain image isn't clipped by the DCS file's path"
);
}
#[test]
fn a_file_that_gains_its_clipping_path_again_clips_the_group_that_stayed() {
let dir = temp_dir("update-regained-clip");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
let placed = NodeId(r["ids"][0].as_u64().unwrap());
let id = image_of(&s, &r["ids"][0]).id;
let other = s.execute("file.place", &json!({ "path": path, "at": [200, 100] })).unwrap();
s.execute("symbol.new", &json!({ "name": "Pic", "ids": [other["ids"][0]] })).unwrap();
let unclipped = String::from_utf8(two_plates()).unwrap().replacen("closepath clip newpath", "closepath newpath ", 1);
// The symbol's art: how deep the image is.
let depth = |s: &Session| {
fn find(n: &Node, at: usize) -> Option<usize> {
if matches!(n.kind, NodeKind::Image(_)) {
return Some(at);
}
n.children().and_then(|ch| ch.iter().find_map(|c| find(c, at + 1)))
}
find(&s.doc().unwrap().doc.symbols.iter().find(|w| w.name == "Pic").expect("the symbol").art, 0)
};
let clipped_depth = depth(&s);
for _ in 0..2 {
std::fs::write(&path, unclipped.as_bytes()).unwrap();
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
std::fs::write(&path, two_plates()).unwrap();
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
let doc = &s.doc().unwrap().doc;
assert_eq!(doc.parent_of(id), Some(placed), "the placed group holds the image itself");
let NodeKind::Group { children, clip: true } = &doc.node(placed).unwrap().kind else { panic!("a clip group again") };
assert!(matches!(&children[..], [c, im] if matches!(c.kind, NodeKind::Path { clipping: true, .. }) && im.id == id));
assert_eq!(depth(&s), clipped_depth, "the symbol's image is not nested deeper");
assert!(symbol_clip_width(&s, "Pic").is_some(), "the symbol's image is clipped again");
}
}
/// The kind of group `g` is, as (clip, the kinds of its children are just one image).
fn plain_group_of_image(g: &Node) -> Option<(bool, bool)> {
match &g.kind {
NodeKind::Group { children, clip } => Some((*clip, matches!(&children[..], [c] if matches!(c.kind, NodeKind::Image(_))))),
_ => None,
}
}
#[test]
fn a_hidden_or_transparent_clip_group_loses_the_files_old_clipping_path() {
let dir = temp_dir("lost-clip-kept-group");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
let place = |s: &mut Session, x: f64| s.execute("file.place", &json!({ "path": path, "at": [x, 100] })).unwrap();
let (faded, hidden, in_symbol) = (place(&mut s, 100.0), place(&mut s, 200.0), place(&mut s, 300.0));
let (faded, hidden) = (NodeId(faded["ids"][0].as_u64().unwrap()), NodeId(hidden["ids"][0].as_u64().unwrap()));
let image = image_of(&s, &json!(faded.0)).id;
s.execute("transparency.set", &json!({"ids": [faded.0, in_symbol["ids"][0]], "opacity": 50})).unwrap();
s.edit("hide", |d, _| {
if let Some(n) = d.node_mut(hidden) {
n.visible = false;
}
Ok(())
})
.unwrap();
s.execute("symbol.new", &json!({ "name": "Pic", "ids": [in_symbol["ids"][0]] })).unwrap();
let unclipped = String::from_utf8(two_plates()).unwrap().replacen("closepath clip newpath", "closepath newpath ", 1);
std::fs::write(&path, unclipped.as_bytes()).unwrap();
let hidden_image = image_of(&s, &json!(hidden.0)).id;
s.execute("links.update", &json!({ "ids": [image.0, hidden_image.0] })).unwrap();
let doc = &s.doc().unwrap().doc;
for (g, why) in [(faded, "the transparent group"), (hidden, "the hidden group")] {
let n = doc.node(g).unwrap_or_else(|| panic!("{why} stays"));
assert_eq!(plain_group_of_image(n), Some((false, true)), "{why} holds the image alone, unclipped");
}
assert!((doc.node(faded).unwrap().opacity - 0.5).abs() < 1e-6, "its transparency kept");
assert!(!doc.node(hidden).unwrap().visible, "still hidden");
// In a symbol's art too.
let art = &doc.symbols.iter().find(|w| w.name == "Pic").expect("the symbol").art;
let mut found = None;
art.walk(&mut |n: &Node| {
if found.is_none() && n.children().is_some_and(|ch| ch.iter().any(|c| matches!(c.kind, NodeKind::Image(_)))) {
found = plain_group_of_image(n);
}
});
assert_eq!(found, Some((false, true)), "the symbol's transparent group holds the image alone, unclipped");
}
#[test]
fn a_clipping_mask_of_ones_own_stays_when_the_link_is_updated() {
let dir = temp_dir("own-mask");
let unclipped = String::from_utf8(two_plates()).unwrap().replacen("closepath clip newpath", "closepath newpath ", 1);
let path = write(&dir, "pic.eps", unclipped.as_bytes());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
let id = NodeId(r["ids"][0].as_u64().unwrap());
let rect = s.execute("shape.rectangle", &json!({"x": 95, "y": 95, "width": 4, "height": 4})).unwrap()["id"].as_u64().unwrap();
s.execute("select.set", &json!({ "ids": [id.0, rect] })).unwrap();
let g = NodeId(s.execute("object.clippingMask.make", &json!({})).unwrap()["id"].as_u64().unwrap());
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
assert_eq!(s.doc().unwrap().doc.parent_of(id), Some(g), "the clipping mask made in the document stays");
let mask_width = |s: &Session| {
let NodeKind::Group { children, .. } = &s.doc().unwrap().doc.node(g).unwrap().kind else { panic!("the mask's group") };
children[0].geometric_bounds().map(|b| b.width().round())
};
assert_eq!(mask_width(&s), Some(4.0));
// The file gains a clipping path: its clip group goes inside the mask, which keeps its path.
std::fs::write(&path, two_plates()).unwrap();
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
let doc = &s.doc().unwrap().doc;
let inner = doc.parent_of(id).expect("a parent");
assert_ne!(inner, g);
assert!(matches!(doc.node(inner).unwrap().kind, NodeKind::Group { clip: true, .. }), "the file's clip group");
assert_eq!(doc.parent_of(inner), Some(g));
assert_eq!(mask_width(&s), Some(4.0), "the mask's path is kept");
}
#[test]
fn a_dcs_file_takes_the_place_parameters_an_eps_file_takes() {
let dir = temp_dir("params");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
assert!(error(&mut s, "file.place", json!({ "path": path, "page": 0 })).contains("page"));
assert!(error(&mut s, "file.place", json!({ "path": path, "crop": "trim" })).contains("crop"));
}
#[test]
fn a_clipped_placement_answers_its_image_for_the_links_commands() {
let dir = temp_dir("image-id");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
assert_eq!(r["image"], json!(image_of(&s, &r["ids"][0]).id.0));
let info = s.execute("links.info", &json!({ "id": r["image"] })).unwrap();
assert_eq!(info["format"], "DCS");
let unclipped = String::from_utf8(two_plates()).unwrap().replacen("closepath clip newpath", "closepath newpath ", 1);
let plain = write(&dir, "plain.eps", unclipped.as_bytes());
assert!(s.execute("file.place", &json!({ "path": plain })).unwrap().get("image").is_none(), "an image placed alone is its own id");
}
#[test]
fn update_and_relink_warn_of_a_swatch_kept_for_a_new_spot_plate() {
let dir = temp_dir("update-swatch");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
let id = image_of(&s, &r["ids"][0]).id;
s.execute("swatch.new", &json!({"name": "Varnish", "color": {"c": 1.0, "m": 0.0, "y": 0.0, "k": 0.0}})).unwrap();
let changed = single_file(&[("Cyan", plate_eps([0, 0, 0, 0])), ("Varnish", plate_eps([255; 4]))], "%%CMYKCustomColor: 0 0 .2 0 (Varnish)\n");
std::fs::write(&path, &changed).unwrap();
let u = s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
let kept = "the document's swatch `Varnish` is kept: it is a process color, so the plate prints as process inks";
assert_eq!(u["warnings"], json!([kept]));
let other = write(&dir, "other.eps", &changed);
let r = s.execute("links.relink", &json!({ "ids": [id.0], "path": other })).unwrap();
assert_eq!(r["warnings"], json!([kept]));
assert!(s.execute("links.update", &json!({ "ids": [id.0] })).unwrap().get("warnings").is_some(), "every read warns");
}
#[test]
fn a_deleted_dcs_image_leaves_its_spot_swatch_unused() {
let dir = temp_dir("unused");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
let unused = |s: &mut Session| s.execute("swatch.unused", &json!({})).unwrap()["names"].as_array().unwrap().iter().any(|n| n == "PANTONE 185 C");
assert!(!unused(&mut s), "in use while the image shows it");
s.execute("select.set", &json!({ "ids": [r["ids"][0]] })).unwrap();
s.execute("edit.clear", &json!({})).unwrap();
assert!(unused(&mut s), "unused once the image is deleted");
}
#[test]
fn a_process_swatch_giving_a_dcs_plate_its_color_is_used() {
let dir = temp_dir("unused-process");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
s.execute("swatch.new", &json!({"name": "PANTONE 185 C", "color": {"c": 1.0, "m": 0.0, "y": 0.0, "k": 0.0}})).unwrap();
let unused = |s: &mut Session| s.execute("swatch.unused", &json!({})).unwrap()["names"].as_array().unwrap().iter().any(|n| n == "PANTONE 185 C");
assert!(unused(&mut s), "nothing uses it yet");
s.execute("file.place", &json!({ "path": path })).unwrap();
assert!(!unused(&mut s), "the plate of its name prints as its process inks");
}
#[test]
fn an_opened_clipping_path_is_not_the_files_and_stays() {
let dir = temp_dir("opened-clip");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
let group = NodeId(r["ids"][0].as_u64().unwrap());
let id = image_of(&s, &r["ids"][0]).id;
// The clipping path opened, its anchors where they were.
s.edit("open", |d, _| {
let clip = d.node(group).and_then(|g| g.children()).map(|ch| ch[0].id).unwrap();
if let Some(Node { kind: NodeKind::Path { path, .. }, .. }) = d.node_mut(clip) {
path.subpaths[0].closed = false;
}
Ok(())
})
.unwrap();
let unclipped = String::from_utf8(two_plates()).unwrap().replacen("closepath clip newpath", "closepath newpath ", 1);
std::fs::write(&path, unclipped.as_bytes()).unwrap();
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
assert_eq!(s.doc().unwrap().doc.parent_of(id), Some(group), "a clipping path edited in the document stays");
}
#[test]
fn a_clipping_path_given_another_fill_rule_is_not_the_files_and_stays() {
let dir = temp_dir("rule-clip");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
let group = NodeId(r["ids"][0].as_u64().unwrap());
let id = image_of(&s, &r["ids"][0]).id;
// The clipping path's fill rule changed, its anchors where they were.
s.edit("rule", |d, _| {
let clip = d.node(group).and_then(|g| g.children()).map(|ch| ch[0].id).unwrap();
if let Some(Node { kind: NodeKind::Path { rule, .. }, .. }) = d.node_mut(clip) {
*rule = vectorcraft_geom::FillRule::EvenOdd;
}
Ok(())
})
.unwrap();
let unclipped = String::from_utf8(two_plates()).unwrap().replacen("closepath clip newpath", "closepath newpath ", 1);
std::fs::write(&path, unclipped.as_bytes()).unwrap();
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
assert_eq!(s.doc().unwrap().doc.parent_of(id), Some(group), "a clipping path whose fill rule was changed stays");
}
#[test]
fn an_even_odd_clipping_path_is_placed_and_matched_under_the_nonzero_rule() {
let dir = temp_dir("eo-clip");
// The same length, so the plates' offsets hold.
let eo = String::from_utf8(two_plates()).unwrap().replacen("gsave newpath 0 0 moveto", "gsave 0 0 moveto ", 1);
let eo = eo.replacen("closepath clip newpath", "closepath eoclip newpath", 1);
assert_eq!(eo.len(), two_plates().len());
let path = write(&dir, "pic.eps", eo.as_bytes());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path })).unwrap();
let group = NodeId(r["ids"][0].as_u64().unwrap());
let id = image_of(&s, &r["ids"][0]).id;
let doc = &s.doc().unwrap().doc;
let clip = &doc.node(group).and_then(|g| g.children()).expect("the file's clip group")[0];
assert!(matches!(clip.kind, NodeKind::Path { rule: vectorcraft_geom::FillRule::NonZero, clipping: true, .. }), "{:?}", clip.kind);
// The file without its clipping path: the group's clipping path is the file's, and goes.
let unclipped = String::from_utf8(two_plates()).unwrap().replacen("closepath clip newpath", "closepath newpath ", 1);
std::fs::write(&path, unclipped.as_bytes()).unwrap();
s.execute("links.update", &json!({ "ids": [id.0] })).unwrap();
assert_eq!(s.doc().unwrap().doc.node(group).and_then(plain_group_of_image), Some((false, true)), "the file's clipping path is gone");
}
#[test]
fn files_updated_together_warn_when_they_add_one_ink_in_two_colors() {
let dir = temp_dir("update-two-inks");
let (a, b) = (write(&dir, "a.eps", &two_plates()), write(&dir, "b.eps", &two_plates()));
let mut s = session();
s.execute("file.place", &json!({ "path": a })).unwrap();
s.execute("file.place", &json!({ "path": b })).unwrap();
let varnish =
|c: &str| single_file(&[("Cyan", plate_eps([0; 4])), ("Varnish", plate_eps([255; 4]))], &format!("%%CMYKCustomColor: {c} (Varnish)\n"));
std::fs::write(&a, varnish("0 0 .2 0")).unwrap();
std::fs::write(&b, varnish("1 0 0 0")).unwrap();
let u = s.execute("links.update", &json!({})).unwrap();
assert_eq!(u["updated"].as_array().map(Vec::len), Some(2), "{u}");
assert_eq!(u["warnings"], json!(["the document's swatch `Varnish` is kept: it is a different color"]));
}
#[test]
fn relink_with_copy_refuses_a_dcs_file_with_plate_files() {
let dir = temp_dir("copy");
write(&dir, "pic.C", plate_eps([0, 255, 255, 255]).as_bytes());
let main = "%!PS-Adobe-2.0 EPSF-1.2\n%%BoundingBox: 0 0 20 10\n%%CyanPlate: pic.C\n%%EndComments\nshowpage\n";
let path = write(&dir, "pic.eps", main.as_bytes());
let docs = dir.join("docs");
std::fs::create_dir_all(&docs).unwrap();
let mut s = session();
s.execute("document.save", &json!({ "path": docs.join("doc.vectorcraft").to_string_lossy() })).unwrap();
let png = dir.join("plain.png");
image::RgbImage::from_pixel(2, 2, image::Rgb([9, 9, 9])).save(&png).unwrap();
let r = s.execute("file.place", &json!({ "path": png.to_string_lossy() })).unwrap();
let id = r["ids"][0].clone();
let e = error(&mut s, "links.relink", json!({ "ids": [id], "path": path, "copy": true }));
assert!(e.contains("plate files"), "{e}");
assert!(!docs.join("pic.eps").exists(), "nothing is copied");
// A single-file DCS file has its plates inside it: it is copied.
let single = write(&dir, "single.eps", &two_plates());
s.execute("links.relink", &json!({ "ids": [id], "path": single, "copy": true })).unwrap();
assert!(docs.join("single.eps").exists());
}
#[test]
fn a_saved_document_reads_the_plates_again_and_prints_the_spot_ink() {
let dir = temp_dir("saved");
let path = write(&dir, "pic.eps", &two_plates());
let saved = dir.join("doc.vectorcraft").to_string_lossy().into_owned();
let mut s = session();
s.execute("file.place", &json!({ "path": path })).unwrap();
let unused = s.execute("swatch.unused", &json!({})).unwrap();
assert!(!unused["names"].as_array().unwrap().iter().any(|n| n == "PANTONE 185 C"), "the plate's swatch is in use: {unused}");
s.execute("document.save", &json!({ "path": saved })).unwrap();
let mut s = Session::new();
s.execute("document.open", &json!({ "path": saved })).unwrap();
let list = s.execute("links.list", &json!({})).unwrap();
assert_eq!(list["links"][0]["format"], "DCS");
assert_eq!(list["links"][0]["status"], "ok");
let id = NodeId(list["links"][0]["id"].as_u64().unwrap());
let NodeKind::Image(im) = &s.doc().unwrap().doc.node(id).unwrap().kind else { not_an_image() };
let plates = s.doc().unwrap().doc.images[&im.key].separated().expect("the plates, read from the file again");
assert_eq!(plates.plates[1].data, [128, 0, 128, 0]);
let v = s.execute("print.preview", &json!({"settings": {"output": {"mode": "separations"}}})).unwrap();
let inks: Vec<&str> = v["sheets"].as_array().unwrap().iter().filter_map(|p| p["ink"].as_str()).collect();
assert_eq!(inks, ["Cyan", "Magenta", "Yellow", "Black", "PANTONE 185 C"]);
}
#[test]
fn a_saved_document_reads_the_plates_again_with_the_low_resolution_eps_proxy() {
let dir = temp_dir("saved-proxy");
// Plates wider than a preview, so the saved document keeps a preview of the image.
let wide = |gray: u8| {
let hex = format!("{gray:02x}").repeat(300);
format!(
"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 20 10\n%%EndComments\n20 10 scale\n300 1 8 [300 0 0 -1 0 1] <{hex}> image\nshowpage\n%%Trailer\n%%EOF\n"
)
};
let file = single_file(&[("Cyan", wide(0)), ("PANTONE 185 C", wide(128))], "%%CMYKCustomColor: 0 .91 .76 0 (PANTONE 185 C)\n");
let path = write(&dir, "pic.eps", &file);
let saved = dir.join("doc.vectorcraft").to_string_lossy().into_owned();
let mut s = session();
s.execute("file.place", &json!({ "path": path })).unwrap();
s.execute("document.save", &json!({ "path": saved })).unwrap();
let mut s = Session::new();
s.execute("prefs.set", &json!({"key": "lowResProxyEps", "value": true})).unwrap();
s.execute("document.open", &json!({ "path": saved })).unwrap();
let list = s.execute("links.list", &json!({})).unwrap();
assert_eq!(list["links"][0]["format"], "DCS", "{list}");
let id = NodeId(list["links"][0]["id"].as_u64().unwrap());
let NodeKind::Image(im) = &s.doc().unwrap().doc.node(id).unwrap().kind else { not_an_image() };
assert!(s.doc().unwrap().doc.images[&im.key].separated().is_some(), "the plates, read from the file again");
}
#[test]
fn a_dcs_header_is_found_at_the_start_of_a_file_or_behind_a_binary_eps_header() {
use crate::cmd::place::dcs::{HEAD_BYTES, head_is_dcs, is_dcs};
// A whole file, as `is_dcs` reads it.
let whole = |b: &[u8]| {
let is = head_is_dcs(b, b.len() as u64);
assert_eq!(is, Some(is_dcs(b)));
is
};
let ps = two_plates();
assert_eq!(whole(&ps), Some(true));
assert_eq!(whole(b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 20 10\n%%EndComments\n"), Some(false));
let binary = |start: u32| {
let mut b = vec![0xC5, 0xD0, 0xD3, 0xC6];
b.extend(start.to_le_bytes());
b.extend((ps.len() as u32).to_le_bytes());
b.resize(start as usize, 0);
b.extend(&ps);
b
};
assert_eq!(whole(&binary(30)), Some(true));
// A binary header whose PostScript or TIFF preview ends past the end of the file names none.
let mut cut = binary(30);
cut[8..12].copy_from_slice(&(64u32 << 20).to_le_bytes());
assert_eq!(whole(&cut), Some(false));
let mut tiff = binary(30);
tiff[20..24].copy_from_slice(&30u32.to_le_bytes());
tiff[24..28].copy_from_slice(&(64u32 << 20).to_le_bytes());
assert_eq!(whole(&tiff), Some(false));
// A file longer than the bytes read, its PostScript running past them: its header is read from
// the part there is, unless the header puts the PostScript or the preview past the file's end.
let mut big = binary(30);
big.resize(HEAD_BYTES, b' ');
big[8..12].copy_from_slice(&(64u32 << 20).to_le_bytes());
assert_eq!(head_is_dcs(&big, 30 + (64 << 20)), Some(true));
assert_eq!(head_is_dcs(&big, 30 + (64 << 20) - 1), Some(false));
big[20..24].copy_from_slice(&30u32.to_le_bytes());
big[24..28].copy_from_slice(&(65u32 << 20).to_le_bytes());
assert_eq!(head_is_dcs(&big, 30 + (64 << 20)), Some(false));
// PostScript that starts past the bytes read: the whole file is needed.
let far = binary(HEAD_BYTES as u32 + 10);
assert_eq!(head_is_dcs(&far[..HEAD_BYTES], far.len() as u64), None);
// A header that goes on past the bytes read may name its plates further on.
let mut long = b"%!PS-Adobe-3.0 EPSF-3.0\n".to_vec();
while long.len() < HEAD_BYTES {
long.extend(format!("%%Title: {}\n", "x".repeat(190)).as_bytes());
}
long.truncate(HEAD_BYTES);
assert_eq!(head_is_dcs(&long, HEAD_BYTES as u64 + 1), None);
// One that ended within them names none.
long[30..45].copy_from_slice(b"\n%%EndComments\n");
assert_eq!(head_is_dcs(&long, HEAD_BYTES as u64 + 1), Some(false));
}
#[test]
fn dcs_transparency_marks_transparent_and_overlapped_dcs_images() {
let dir = temp_dir("transparency");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
let image = image_of(&s, &r["ids"][0]).id;
let marked = |s: &mut Session| s.execute("links.list", &json!({ "dcsTransparency": true })).unwrap()["links"][0]["dcsTransparency"] == true;
assert!(!marked(&mut s), "nothing transparent");
assert!(s.execute("links.list", &json!({})).unwrap()["links"][0].get("dcsTransparency").is_none());
// A half-transparent square away from it, then over it.
let far = s.execute("shape.rectangle", &json!({"x": 300, "y": 200, "width": 20, "height": 20})).unwrap()["id"].as_u64().unwrap();
s.execute("transparency.set", &json!({"ids": [far], "opacity": 50})).unwrap();
assert!(!marked(&mut s), "a transparent object elsewhere");
s.execute("shape.rectangle", &json!({"x": 90, "y": 90, "width": 20, "height": 20})).unwrap();
let over = s.execute("shape.rectangle", &json!({"x": 92, "y": 92, "width": 4, "height": 4})).unwrap()["id"].as_u64().unwrap();
assert!(!marked(&mut s), "opaque objects over it");
s.execute("transparency.set", &json!({"ids": [over], "blend": "multiply"})).unwrap();
assert!(marked(&mut s), "a blend mode over it");
s.execute("transparency.set", &json!({"ids": [over], "blend": "normal"})).unwrap();
s.execute("transparency.set", &json!({"ids": [image.0], "opacity": 80})).unwrap();
assert!(marked(&mut s), "the image itself");
assert!(error(&mut s, "links.list", json!({ "dcsTransparency": "yes" })).contains("dcsTransparency"), "a flag that isn't true or false");
}
#[test]
fn dcs_transparency_looks_only_at_the_part_of_an_image_its_clipping_path_shows() {
let dir = temp_dir("transparency-clipped");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
let group = NodeId(r["ids"][0].as_u64().unwrap());
let image = image_of(&s, &r["ids"][0]).id;
let (clip, all) = {
let doc = &s.doc().unwrap().doc;
let NodeKind::Group { children, clip: true } = &doc.node(group).unwrap().kind else { panic!("a clipping group") };
(children[0].geometric_bounds().unwrap(), doc.node(image).unwrap().geometric_bounds().unwrap())
};
assert!(all.x1 - clip.x1 > 2.0, "the file's clipping path shows the left half: {clip:?} of {all:?}");
// A half-transparent square over the part of the image the clipping path hides.
let x = (clip.x1 + all.x1) / 2.0 - 1.0;
let over = s.execute("shape.rectangle", &json!({"x": x, "y": all.y0, "width": 2, "height": 2})).unwrap()["id"].as_u64().unwrap();
s.execute("transparency.set", &json!({"ids": [over], "opacity": 50})).unwrap();
let marked = |s: &mut Session| s.execute("links.list", &json!({ "dcsTransparency": true })).unwrap()["links"][0]["dcsTransparency"] == true;
assert!(!marked(&mut s), "the square is over the hidden part only");
s.execute("transparency.set", &json!({"ids": [over], "opacity": 100})).unwrap();
let shown = s.execute("shape.rectangle", &json!({"x": clip.x0, "y": all.y0, "width": 2, "height": 2})).unwrap()["id"].as_u64().unwrap();
s.execute("transparency.set", &json!({"ids": [shown], "opacity": 50})).unwrap();
assert!(marked(&mut s), "a square over the part that shows");
}
#[test]
fn dcs_transparency_skips_a_transparent_object_its_own_clipping_paths_hide() {
let dir = temp_dir("transparency-clipped-over");
let path = write(&dir, "pic.eps", &two_plates());
// A session with the file placed, and the top-left corner of the image, inside the part its
// clipping path shows.
let placed = || {
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
let b = image_of(&s, &r["ids"][0]).geometric_bounds().unwrap();
(s, b.x0, b.y0)
};
let rect = |s: &mut Session, x: f64, y: f64, width: f64| {
s.execute("shape.rectangle", &json!({"x": x, "y": y, "width": width, "height": 2})).unwrap()["id"].as_u64().unwrap()
};
let make = |s: &mut Session, ids: [u64; 2]| {
s.execute("select.set", &json!({ "ids": ids })).unwrap();
s.execute("object.clippingMask.make", &json!({})).unwrap()["id"].as_u64().unwrap()
};
let marked = |s: &mut Session| s.execute("links.list", &json!({ "dcsTransparency": true })).unwrap()["links"][0]["dcsTransparency"] == true;
// A half-transparent square over the part of the image that shows.
let square = |s: &mut Session, x: f64, y: f64| {
let id = rect(s, x, y, 2.0);
s.execute("transparency.set", &json!({"ids": [id], "opacity": 50})).unwrap();
assert!(marked(s), "the square over the image");
id
};
// Its own clip group's clipping path is beside it.
let (mut s, x, y) = placed();
let over = square(&mut s, x, y);
let beside = rect(&mut s, x + 5.0, y, 2.0);
make(&mut s, [over, beside]);
assert!(!marked(&mut s), "a clip group that hides the square");
// Its layer's clipping path is beside it.
let (mut s, x, y) = placed();
s.execute("layer.new", &json!({})).unwrap();
square(&mut s, x, y);
rect(&mut s, x + 5.0, y, 2.0);
s.execute("select.none", &json!({})).unwrap();
assert!(s.execute("layer.clippingMask.toggle", &json!({})).unwrap()["clip"].as_bool().unwrap());
assert!(!marked(&mut s), "a clipping layer that hides the square");
// Each of two nested clipping paths shows part of the inner group, and only the inner one
// covers the square.
let (mut s, x, y) = placed();
let over = square(&mut s, x, y);
let inner = rect(&mut s, x, y, 40.0);
let group = make(&mut s, [over, inner]);
assert!(marked(&mut s), "the inner clipping path shows the square");
let outer = rect(&mut s, x + 10.0, y, 20.0);
make(&mut s, [group, outer]);
assert!(!marked(&mut s), "the two clipping paths together hide the square");
}
#[test]
fn dcs_transparency_counts_a_transparent_dcs_image_over_another() {
let dir = temp_dir("transparency-two");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
let a = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
let b = s.execute("file.place", &json!({ "path": path, "at": [103, 100] })).unwrap();
let (a, b) = (image_of(&s, &a["ids"][0]).id, image_of(&s, &b["ids"][0]).id);
s.execute("transparency.set", &json!({"ids": [a.0], "opacity": 80})).unwrap();
let list = s.execute("links.list", &json!({ "dcsTransparency": true })).unwrap();
let marked = |id: NodeId| list["links"].as_array().unwrap().iter().any(|r| r["id"] == id.0 && r["dcsTransparency"] == true);
assert!(marked(a), "the transparent image");
assert!(marked(b), "the opaque image it overlaps");
}
#[test]
fn dcs_transparency_counts_characters_with_a_transparent_gradient() {
use vectorcraft_color::{Gradient, GradientKind, GradientPaint, GradientStop};
let dir = temp_dir("transparency-text");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
let t = NodeId(s.execute("text.create", &json!({"x": 96, "y": 104, "text": "Hi", "size": 12})).unwrap()["id"].as_u64().unwrap());
let marked = |s: &mut Session| s.execute("links.list", &json!({ "dcsTransparency": true })).unwrap()["links"][0]["dcsTransparency"] == true;
assert!(!marked(&mut s), "opaque characters over it");
let mut stops = vec![GradientStop::new(0.0, Color::rgb(1.0, 0.0, 0.0)), GradientStop::new(1.0, Color::rgb(0.0, 0.0, 1.0))];
stops[1].opacity = 0.5;
let gradient = Paint::Gradient(Box::new(GradientPaint::new(Gradient::new(GradientKind::Linear, stops))));
s.edit("t", |d, _| {
if let Some(Node { kind: NodeKind::Text(text), .. }) = d.node_mut(t) {
text.runs[0].style.fill = gradient.clone();
}
Ok(())
})
.unwrap();
assert!(marked(&mut s), "characters filled with a gradient that has a transparent stop");
assert!(s.doc().unwrap().doc.node(t).unwrap().shows_transparency());
}