Files
vectorcraft/crates/eps/src/tests.rs
T
bflatasticandClaude Opus 5.5 4c4850e745 M4.76: EPS export (PostScript Level 2 and 3) with EPS Options, TIFF previews and the document embedded
New crates/eps (L3): a hand-written EPS writer. A DSC header with
%%BoundingBox and %%HiResBoundingBox (y up from the origin), %%LanguageLevel
and spot colours; paths filled and stroked with caps, joins, miter limits
and dashes (strokes a plain line can't draw as filled outlines, brushes as
their art), clipping groups, linear and radial gradients as smooth
shadings at Level 3 and stepped fills at Level 2 or with compatible
gradient printing, patterns as tiles, freeform gradients and placed images
as images (Flate with masked transparent pixels at Level 3, run-length at
Level 2), type as glyph outlines, spot colours as Separation spaces and
overprints when preserved; RGB written as CMYK on request. A TIFF preview
(1-bit or colour, transparent or opaque) behind the binary header, a PNG
thumbnail and the native document in comments (eps::native and
eps::thumbnail read them back for a later import).

document.export {format: eps} and document.exportEps: transparency is
flattened first with a flattener preset (built-in or saved) and options;
the visible art's bounds, or with useArtboards one file per artboard named
{stem}_{artboard}.eps. Export As lists EPS, then EPS Options
(ui.epsOptionsDialog for agents, remembered between exports).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 13:50:50 +07:00

467 lines
21 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.
//! EPS output read back: the DSC header and bounding box, the page's operators, gradients at both
//! levels, images, colours, the preview header and TIFF, and the data the file carries.
use std::sync::Arc;
use vectorcraft_color::{Color, Gradient, GradientKind, GradientPaint, GradientStop, Paint, Swatch};
use vectorcraft_doc::{Appearance, AppearanceItem, Dash, Document, ImageBlob, ImageObject, LineCap, LineJoin, Node, NodeKind};
use vectorcraft_geom::{Affine, PathData, Point, Rect, shapes};
use crate::*;
/// A 200 × 200 pt document whose first layer holds what `nodes` makes.
fn doc_with(nodes: impl FnOnce(&mut Document) -> Vec<Node>) -> Document {
let mut d = Document::new(200.0, 200.0);
let nodes = nodes(&mut d);
let layer = d.layers[0].id;
for n in nodes {
d.insert(Some(layer), usize::MAX, n).unwrap();
}
d
}
fn path(d: &mut Document, p: PathData, fill: Paint, stroke: Paint, width: f64) -> Node {
Node::path(d.alloc_id(), p, Appearance::basic(fill, stroke, width))
}
fn red_rect(d: &mut Document) -> Node {
path(d, shapes::rectangle(Rect::new(50.0, 60.0, 150.0, 100.0)), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0)
}
/// The first artboard as the region, its bottom-left corner the origin; no preview.
fn opts(d: &Document) -> EpsOptions {
let r = d.artboards[0].rect;
EpsOptions { region: r, origin: Point::new(r.x0, r.y1), preview: Preview::None, ..EpsOptions::default() }
}
fn text(out: &EpsOutput) -> String {
String::from_utf8(sections(&out.bytes).unwrap().0.to_vec()).unwrap()
}
fn page_of(d: &Document, o: &EpsOptions) -> String {
checked_page(&text(&export(d, o, None, None).unwrap()))
}
/// The value of DSC comment `key` (`%%Key:`).
fn dsc(ps: &str, key: &str) -> Option<String> {
let prefix = format!("%%{key}:");
ps.lines().find_map(|l| l.strip_prefix(&prefix)).map(|v| v.trim().to_string())
}
/// The program, checked for balanced procedures, arrays, dictionaries, strings and graphics
/// state saves (the syntax a PostScript interpreter would reject first); image data and comments
/// are skipped.
fn checked_page(ps: &str) -> String {
let mut code = String::new();
let mut lines = ps.lines();
while let Some(l) = lines.next() {
if !l.starts_with('%') {
code.push_str(l);
code.push('\n');
}
// Image data follows the procedure that reads it, up to the ASCII85 end marker.
if l.ends_with("} exec") {
assert!(lines.any(|d| d.contains("~>")), "image data ends");
}
}
let mut stack = vec![];
let mut chars = code.chars().peekable();
while let Some(c) = chars.next() {
match c {
'(' => {
let mut depth = 1;
while depth > 0 {
match chars.next().expect("string closes") {
'\\' => {
chars.next();
}
'(' => depth += 1,
')' => depth -= 1,
_ => {}
}
}
}
'{' | '[' => stack.push(c),
'<' if chars.peek() == Some(&'<') => {
chars.next();
stack.push('<');
}
'>' if chars.peek() == Some(&'>') => {
chars.next();
assert_eq!(stack.pop(), Some('<'), "dictionary closes");
}
'}' => assert_eq!(stack.pop(), Some('{'), "procedure closes"),
']' => assert_eq!(stack.pop(), Some('['), "array closes"),
_ => {}
}
}
assert!(stack.is_empty(), "unclosed: {stack:?}");
let count = |op: &str| code.split_whitespace().filter(|t| *t == op).count();
assert_eq!(count("q"), count("Q"), "gsave and grestore pair up");
code
}
#[test]
fn the_header_follows_dsc_and_the_file_ends_with_eof() {
let d = doc_with(|d| vec![red_rect(d)]);
let out = export(&d, &EpsOptions { title: "Poster (draft)".into(), created: Some(0), ..opts(&d) }, None, None).unwrap();
let ps = text(&out);
assert!(ps.starts_with("%!PS-Adobe-3.0 EPSF-3.0\n"), "{ps}");
assert!(dsc(&ps, "Creator").unwrap().starts_with("VectorCraft"));
assert_eq!(dsc(&ps, "Title").as_deref(), Some("(Poster \\(draft\\))"));
assert_eq!(dsc(&ps, "CreationDate").as_deref(), Some("(1970-01-01 00:00:00 UTC)"));
assert_eq!(dsc(&ps, "LanguageLevel").as_deref(), Some("3"));
assert_eq!(dsc(&ps, "DocumentData").as_deref(), Some("Clean7Bit"));
assert_eq!(dsc(&ps, "Pages").as_deref(), Some("1"));
let order = ["%%EndComments", "%%BeginProlog", "%%EndProlog", "%%BeginSetup", "%%EndSetup", "%%Page: 1 1", "showpage", "%%Trailer", "%%EOF"];
let at: Vec<usize> = order.iter().map(|m| ps.find(m).unwrap_or_else(|| panic!("{m}"))).collect();
assert!(at.windows(2).all(|w| w[0] < w[1]), "sections in order");
assert!(ps.ends_with("%%EOF\n"));
assert!(ps.is_ascii() && ps.lines().all(|l| l.len() <= 255), "7-bit lines DSC readers take");
let page = checked_page(&ps);
assert!(page.contains("1 0 0 rg\n50 60 m\n150 60 l\n150 100 l\n50 100 l\n50 60 l\nh\nf\n"), "red, in document space: {page}");
assert!(out.warnings.is_empty(), "{:?}", out.warnings);
}
#[test]
fn the_bounding_box_is_y_up_from_the_origin() {
let d = doc_with(|d| vec![red_rect(d)]);
// The art's bounds on a 200 pt artboard: 60..100 down from the top is 100..140 up from the bottom.
let art = Rect::new(50.0, 60.0, 150.5, 100.0);
let out = export(&d, &EpsOptions { region: art, ..opts(&d) }, None, None).unwrap();
let ps = text(&out);
assert_eq!(dsc(&ps, "BoundingBox").as_deref(), Some("50 100 151 140"));
assert_eq!(dsc(&ps, "HiResBoundingBox").as_deref(), Some("50 100 150.5 140"));
// The page maps document space onto it: y flipped about the artboard's bottom edge.
assert!(ps.contains("[1 0 0 -1 0 200] cm"), "{ps}");
assert_eq!(bounding_box(art, Point::new(0.0, 200.0)), [50, 100, 151, 140]);
assert_eq!(preview_rect(art, Point::new(0.0, 200.0)), Rect::new(50.0, 60.0, 151.0, 100.0));
// An artboard of its own: the origin at its bottom-left corner.
let ab = Rect::new(300.0, -50.0, 400.0, 50.0);
assert_eq!(bounding_box(ab, Point::new(300.0, 50.0)), [0, 0, 100, 100]);
// Objects off the region are left out.
let off = page_of(&d, &EpsOptions { region: ab, origin: Point::new(300.0, 50.0), ..opts(&d) });
assert!(off.contains("[1 0 0 -1 -300 50] cm") && !off.contains(" rg\n"), "nothing drawn: {off}");
}
fn gradient_doc(kind: GradientKind) -> Document {
let stops = vec![
GradientStop::new(0.0, Color::rgb(1.0, 0.0, 0.0)),
GradientStop::new(0.4, Color::rgb(1.0, 1.0, 0.0)),
GradientStop::new(1.0, Color::rgb(0.0, 0.0, 1.0)),
];
let paint = Paint::Gradient(Box::new(GradientPaint::new(Gradient { kind, stops })));
doc_with(|d| vec![path(d, shapes::rectangle(Rect::new(20.0, 20.0, 180.0, 120.0)), paint, Paint::None, 0.0)])
}
#[test]
fn gradients_are_smooth_shadings_at_level_3_and_stepped_fills_at_level_2() {
let linear = gradient_doc(GradientKind::Linear);
let page = page_of(&linear, &opts(&linear));
assert!(page.contains("/ShadingType 2 /ColorSpace /DeviceRGB"), "{page}");
assert!(page.contains("/FunctionType 3") && page.contains("/Bounds [0.4 ]") && page.contains("/Encode [0 1 0 1 ]"), "{page}");
assert!(page.contains("/C0 [1 0 0] /C1 [1 1 0]") && page.contains("/C0 [1 1 0] /C1 [0 0 1]"), "{page}");
let clip = page.find("W\n").unwrap();
assert!(page[clip..].contains("shfill"), "clipped to the shape: {page}");
let radial = gradient_doc(GradientKind::Radial);
assert!(page_of(&radial, &opts(&radial)).contains("/ShadingType 3"));
for (d, o) in [
(&linear, EpsOptions { level: Level::Two, ..opts(&linear) }),
(&radial, EpsOptions { level: Level::Two, ..opts(&radial) }),
(&linear, EpsOptions { compatible_gradients: true, ..opts(&linear) }),
] {
let page = page_of(d, &o);
assert!(!page.contains("shfill") && !page.contains("ShadingType"), "{page}");
let bands = page.matches("rectfill").count() + page.matches("arc fill").count();
assert!(bands > 100, "a band per colour step: {bands}");
assert!(
page.contains("0 0 1 rg")
&& page.contains(
" 0 rg
"
),
"from red through yellow to blue"
);
}
assert!(page_of(&radial, &EpsOptions { level: Level::Two, ..opts(&radial) }).contains("0 360 newpath arc fill"));
let ps = text(&export(&linear, &EpsOptions { level: Level::Two, ..opts(&linear) }, None, None).unwrap());
assert_eq!(dsc(&ps, "LanguageLevel").as_deref(), Some("2"));
}
#[test]
fn strokes_keep_their_caps_joins_and_dashes() {
let d = doc_with(|d| {
let mut n = path(d, shapes::rectangle(Rect::new(20.0, 20.0, 80.0, 80.0)), Paint::None, Paint::solid(Color::BLACK), 4.0);
for i in &mut n.appearance.items {
if let AppearanceItem::Stroke(s) = i {
s.cap = LineCap::Round;
s.join = LineJoin::Bevel;
s.dash = Some(Dash { pattern: vec![6.0, 2.0], offset: 1.0, ..Dash::default() });
}
}
vec![n]
});
let page = page_of(&d, &opts(&d));
assert!(page.contains("4 w 1 J 2 j 10 M [6 2 ] 1 d\n"), "{page}");
assert!(page.contains("0 0 0 rg\n20 20 m\n80 20 l\n80 80 l\n20 80 l\n20 20 l\nh\nS\n"), "{page}");
}
#[test]
fn transparency_is_written_opaque_with_a_warning() {
let d = doc_with(|d| {
let mut n = red_rect(d);
n.opacity = 0.5;
vec![n]
});
let out = export(&d, &opts(&d), None, None).unwrap();
assert!(text(&out).contains("1 0 0 rg"), "drawn, opaque");
assert!(out.warnings.iter().any(|w| w.contains("written opaque")), "{:?}", out.warnings);
}
#[test]
fn spot_colours_are_separations_and_overprints_are_kept_or_dropped() {
let mut d = doc_with(|_| vec![]);
let gold = Color::cmyk(0.0, 0.2, 0.8, 0.1);
d.swatches.push(Swatch { name: "Gold Ink".into(), paint: Paint::solid(gold), global: true, spot: true });
let mut n = red_rect(&mut d);
if let Some(AppearanceItem::Fill(f)) = n.appearance.items.first_mut() {
f.paint = Paint::Solid { color: gold, swatch: Some("Gold Ink".into()), tint: 0.5 };
f.overprint = true;
}
let layer = d.layers[0].id;
d.insert(Some(layer), 0, n).unwrap();
let ps = text(&export(&d, &opts(&d), None, None).unwrap());
assert_eq!(dsc(&ps, "DocumentCustomColors").as_deref(), Some("(Gold Ink)"));
assert_eq!(dsc(&ps, "CMYKCustomColor").as_deref(), Some("0 0.2 0.8 0.1 (Gold Ink)"));
assert!(ps.contains("/VCsp0 [/Separation (Gold Ink) cvn /DeviceCMYK {dup 0 mul exch dup 0.2 mul exch dup 0.8 mul exch 0.1 mul}] def"), "{ps}");
let page = checked_page(&ps);
assert!(page.contains("true op\nVCsp0 setcolorspace 0.5 setcolor\n") && page.contains("false op"), "{page}");
assert!(!page_of(&d, &EpsOptions { overprint: Overprint::Discard, ..opts(&d) }).contains("true op"));
}
#[test]
fn rgb_is_written_as_cmyk_when_asked_and_in_cmyk_documents() {
let d = doc_with(|d| vec![red_rect(d)]);
assert!(page_of(&d, &opts(&d)).contains("1 0 0 rg"));
let cmyk = page_of(&d, &EpsOptions { cmyk: true, ..opts(&d) });
assert!(!cmyk.contains(" rg\n") && cmyk.lines().any(|l| l.ends_with(" k") && l.split(' ').count() == 5), "{cmyk}");
let mut c = d.clone();
c.color_mode = vectorcraft_doc::ColorMode::Cmyk;
assert!(!page_of(&c, &opts(&c)).contains(" rg\n"));
}
/// A 4 × 2 PNG placed 10 pt a pixel: the left half opaque green, the right half transparent.
fn png_doc() -> Document {
let mut img = image::RgbaImage::new(4, 2);
for (x, _, p) in img.enumerate_pixels_mut() {
*p = if x < 2 { image::Rgba([0, 200, 0, 255]) } else { image::Rgba([0, 0, 0, 0]) };
}
let mut png = vec![];
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
let mut d = Document::new(200.0, 200.0);
d.images.insert("pic".into(), ImageBlob::new("image/png", png));
let im = ImageObject {
key: "pic".into(),
width: 4,
height: 2,
xf: Affine::translate((10.0, 10.0)) * Affine::scale(10.0),
link: None,
placement: Default::default(),
};
let n = Node::new(d.alloc_id(), NodeKind::Image(im));
let layer = d.layers[0].id;
d.insert(Some(layer), 0, n).unwrap();
d
}
/// The image data after the first image procedure, ASCII85-decoded.
fn image_data(page: &str) -> Vec<u8> {
let at = page.find("} exec\n").unwrap() + 7;
let end = page[at..].find("~>").unwrap() + at + 2;
ps::ascii85_decode(&page[at..end]).unwrap()
}
#[test]
fn images_are_flate_with_masked_pixels_at_level_3_and_run_length_at_level_2() {
let d = png_doc();
let l3 = export(&d, &opts(&d), None, None).unwrap();
let ps = text(&l3);
checked_page(&ps);
assert!(ps.contains("[40 0 0 20 10 10] cm\n/DeviceRGB setcolorspace"), "the pixel grid on its placement: {ps}");
assert!(
ps.contains(
"/ImageType 4 /Width 4 /Height 2 /BitsPerComponent 8 /Decode [0 1 0 1 0 1] /ImageMatrix [4 0 0 2 0 0] /MaskColor [255 255 0 0 255 255]"
),
"{ps}"
);
assert!(ps.contains("/DataSource VCsrc /FlateDecode filter >> image\nVCsrc flushfile } exec\n"), "{ps}");
assert!(l3.warnings.is_empty(), "{:?}", l3.warnings);
let pixels = ps::inflate(&image_data(&ps), 1 << 20).unwrap();
let (g, key) = ([0, 200, 0], [255, 0, 255]);
let row = [g, g, key, key].concat();
assert_eq!(pixels, [row.clone(), row].concat());
let l2 = export(&d, &EpsOptions { level: Level::Two, ..opts(&d) }, None, None).unwrap();
let ps = text(&l2);
checked_page(&ps);
assert!(ps.contains("/ImageType 1") && ps.contains("/RunLengthDecode filter") && !ps.contains("MaskColor"), "{ps}");
let row = [g, g, [255; 3], [255; 3]].concat();
assert_eq!(unpack(&image_data(&ps)), [row.clone(), row].concat());
assert!(l2.warnings.iter().any(|w| w.contains("written white at PostScript Level 2")), "{:?}", l2.warnings);
}
/// Run-length data decoded up to its end marker.
fn unpack(data: &[u8]) -> Vec<u8> {
let mut out = vec![];
let mut i = 0;
while let Some(&n) = data.get(i) {
match n {
128 => break,
0..=127 => {
out.extend(&data[i + 1..i + 2 + n as usize]);
i += 2 + n as usize;
}
_ => {
out.extend(std::iter::repeat_n(data[i + 1], 257 - n as usize));
i += 2;
}
}
}
out
}
#[test]
fn run_length_and_ascii85_round_trip() {
let cycle: Vec<u8> = (0..=255u8).cycle().take(1000).collect();
for data in [vec![], vec![7], vec![1, 2, 3], vec![9; 300], cycle, [vec![0; 9], vec![1, 2, 2, 2, 2, 3]].concat()] {
let mut rl = vec![];
ps::packbits(&data, &mut rl);
rl.push(128);
assert_eq!(unpack(&rl), data);
let a85 = ps::ascii85(&data);
assert!(a85.lines().all(|l| !l.starts_with('%') && l.len() <= 80), "{a85}");
assert_eq!(ps::ascii85_decode(&a85).unwrap(), data);
}
assert_eq!(ps::ascii85(b"Man "), "9jqo^~>\n");
assert_eq!(ps::ascii85(&[0, 0, 0, 0]), "z~>\n");
assert!(ps::ascii85_decode("9jqo^").is_none(), "no end marker");
assert!(ps::ascii85_decode("9jqo^v~>").is_none(), "not base 85");
assert_eq!(ps::num(1.0 / 3.0), "0.3333");
assert_eq!((ps::num(-0.00001), ps::num(f64::NAN), ps::num(2.5)), ("0".into(), "0".into(), "2.5".into()));
assert_eq!(ps::string("a(b)\\é"), "(a\\(b\\)\\\\\\303\\251)");
}
/// A 3 × 2 preview: black, white, transparent; then red, green, blue.
fn preview() -> Raster {
let rgba = vec![0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255];
Raster { width: 3, height: 2, rgba }
}
#[test]
fn a_tiff_preview_sits_behind_the_binary_header() {
let d = doc_with(|d| vec![red_rect(d)]);
for (kind, transparent) in [(Preview::TiffColor, true), (Preview::TiffColor, false), (Preview::TiffBlackWhite, true)] {
let out = export(&d, &EpsOptions { preview: kind, transparent_preview: transparent, ..opts(&d) }, Some(&preview()), None).unwrap();
let b = &out.bytes;
let word = |at: usize| u32::from_le_bytes(b[at..at + 4].try_into().unwrap()) as usize;
assert_eq!(&b[..4], &[0xC5, 0xD0, 0xD3, 0xC6]);
let (ps_at, ps_len, tiff_at, tiff_len) = (word(4), word(8), word(20), word(24));
assert_eq!((ps_at, word(12), word(16)), (30, 0, 0), "PostScript right after the header; no metafile");
assert_eq!(tiff_at, ps_at + ps_len, "the TIFF follows the PostScript");
assert_eq!(tiff_at + tiff_len, b.len());
assert_eq!(&b[28..30], &[0xFF, 0xFF], "no checksum");
assert!(b[ps_at..].starts_with(b"%!PS-Adobe-3.0 EPSF-3.0") && b[..tiff_at].ends_with(b"%%EOF\n"));
let tiff = &b[tiff_at..];
assert!(tiff.starts_with(b"II*\0"));
assert_eq!(sections(b).unwrap().1, Some(tiff));
if kind == Preview::TiffBlackWhite {
// A bit a pixel, a byte a row, always opaque: black, white, (transparent) white; red
// and blue are darker than mid-grey, green isn't. The strip is the file's end.
let strip = &tiff[tiff.len() - 4..];
assert_eq!(unpack(&[strip, &[128]].concat()), [0b1000_0000, 0b1010_0000]);
continue;
}
let img = image::load_from_memory_with_format(tiff, image::ImageFormat::Tiff).unwrap().to_rgba8();
assert_eq!(img.dimensions(), (3, 2));
let expect: [u8; 4] = if transparent { [0, 0, 0, 0] } else { [255; 4] };
assert_eq!(img.get_pixel(2, 0).0, expect, "transparent, or on white");
assert_eq!(img.get_pixel(0, 1).0, [255, 0, 0, 255]);
}
// Asked for but not rendered: the file has no preview, and says so.
let out = export(&d, &EpsOptions { preview: Preview::TiffColor, ..opts(&d) }, None, None).unwrap();
assert!(out.bytes.starts_with(b"%!PS") && out.warnings.iter().any(|w| w.contains("preview")));
}
#[test]
fn the_native_document_and_thumbnail_come_back() {
let d = doc_with(|d| vec![red_rect(d)]);
let native: Vec<u8> = (0..20_000u32).flat_map(|i| (i % 251).to_le_bytes()).collect();
for kind in [Preview::None, Preview::TiffColor] {
let o = EpsOptions { native: Some(native.clone()), preview: kind, ..opts(&d) };
let out = export(&d, &o, Some(&preview()), Some(&preview())).unwrap();
assert_eq!(super::native(&out.bytes).as_deref(), Some(&native[..]));
let png = super::thumbnail(&out.bytes).unwrap();
assert_eq!(image::load_from_memory(&png).unwrap().to_rgba8().dimensions(), (3, 2));
let ps = text(&out);
checked_page(&ps);
assert!(ps.find("%VectorCraft_BeginData: thumbnail").unwrap() < ps.find("%%BeginProlog").unwrap());
assert!(ps.find("%VectorCraft_BeginData: native").unwrap() > ps.find("%%Trailer").unwrap());
assert!(ps.lines().all(|l| l.len() <= 255));
}
let plain = export(&d, &opts(&d), None, None).unwrap();
assert_eq!((super::native(&plain.bytes), super::thumbnail(&plain.bytes)), (None, None));
// Damaged files give nothing back.
assert_eq!(super::native(b"%VectorCraft_BeginData: native\n% 9jqo^\n%VectorCraft_EndData\n"), None);
let mut bad = vec![0xC5, 0xD0, 0xD3, 0xC6];
bad.extend([0xFF; 26]);
assert_eq!(sections(&bad), None);
}
#[test]
fn clipping_groups_clip_and_hidden_or_template_art_is_left_out() {
let mut d = doc_with(|d| {
let clip = path(d, shapes::ellipse(Rect::new(0.0, 0.0, 100.0, 100.0)), Paint::None, Paint::None, 0.0);
let inside = red_rect(d);
let mut hidden = path(d, shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None, 0.0);
hidden.visible = false;
let id = d.alloc_id();
vec![Node::new(id, NodeKind::Group { children: vec![Arc::new(clip), Arc::new(inside)], clip: true }), hidden]
});
let mut t = Node::layer(d.alloc_id(), "Template", vectorcraft_doc::LayerColor::Preset(0));
if let NodeKind::Layer { template, .. } = &mut t.kind {
*template = true;
}
let blue = path(&mut d, shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None, 0.0);
*t.children_mut().unwrap() = vec![Arc::new(blue)];
d.layers.push(Arc::new(t));
let page = page_of(&d, &opts(&d));
let clip = page.find("W\n").unwrap();
assert!(page[..clip].contains("q\n") && page[clip..].contains("1 0 0 rg"), "{page}");
assert!(!page.contains("0 0 1 rg"), "hidden and template art: {page}");
}
#[test]
fn level_preview_and_overprint_ids_read_back() {
for l in Level::ALL {
assert_eq!(Level::from_id(l.id()), Some(l));
}
assert_eq!(Level::from_id("LanguageLevel 2"), Some(Level::Two));
assert_eq!(Level::from_id("1"), None);
for p in Preview::ALL {
assert_eq!(Preview::from_id(p.id()), Some(p));
}
assert_eq!(Overprint::from_id("DISCARD"), Some(Overprint::Discard));
}
#[test]
fn a_rich_document_is_well_formed_at_both_levels() {
let d = (*vectorcraft_testkit::fixtures::rich_session().doc().unwrap().doc).clone();
for level in Level::ALL {
for cmyk in [false, true] {
let out = export(&d, &EpsOptions { level, cmyk, ..opts(&d) }, None, None).unwrap();
let page = checked_page(&text(&out));
assert!(page.lines().count() > 50, "{page}");
}
}
}