Files
vectorcraft/crates/engine/src/tests_printadvanced.rs
T
bflatasticandClaude Opus 5.5 1f922fcbbc M4.81: Print advanced options: overprints, flattener, printer profile, bitmap
The pdf crate's PrintSettings gains advanced {printAsBitmap,
overprints: preserve|discard|simulate, flattenerPreset} and
color.profile, all defaulting so saved print_setup values still load.
Composite output discards overprints (no /OP) or simulates them as
Overprint Preview draws them; separations keep them. A printer profile
converts composite colours with the chosen intent (a CMYK one writes
device CMYK) and separations separate with a CMYK one. The engine
flattens transparency with the preset, or renders each printed region
as one image for Print as Bitmap, before PDF or PostScript layout.
The Print dialog's Advanced and Color Management sections show them.

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

132 lines
7.0 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.
//! File → Print › Advanced: simulated overprints print what Overprint Preview shows, Print as
//! Bitmap prints images of the art, and a flattener preset flattens transparency first.
use serde_json::{Value, json};
use vectorcraft_doc::{Document, NodeKind};
use vectorcraft_geom::Rect;
use vectorcraft_pdf::PrintSettings;
use vectorcraft_render::{RenderOptions, Renderer};
use super::*;
/// A 200 × 100 CMYK document: a cyan square under a yellow one whose fill overprints, and a
/// half-transparent magenta one; their ids.
fn session() -> (Session, [u64; 3]) {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 100, "colorMode": "cmyk"})).unwrap();
let mut ids = [0; 3];
for (i, (x, color)) in [(10, "#00ffff"), (40, "#ffff00"), (120, "#ff00ff")].into_iter().enumerate() {
let r = s.execute("shape.rectangle", &json!({"x": x, "y": 10, "width": 50, "height": 50})).unwrap();
ids[i] = r["id"].as_u64().unwrap();
s.execute("paint.setFill", &json!({"ids": [ids[i]], "color": color})).unwrap();
s.execute("paint.setStroke", &json!({"ids": [ids[i]], "none": true})).unwrap();
}
s.execute("object.setOverprint", &json!({"ids": [ids[1]], "fill": true})).unwrap();
s.execute("transparency.set", &json!({"ids": [ids[2]], "opacity": 50, "item": null})).unwrap();
(s, ids)
}
fn settings(v: Value) -> PrintSettings {
serde_json::from_value(v).unwrap()
}
fn doc(s: &Session) -> &Document {
&s.doc().unwrap().doc
}
/// `doc` rendered over its artboard at 2 px/pt on white.
fn pixels(doc: &Document, opts: RenderOptions) -> Vec<u8> {
let opts = RenderOptions { background: Some([255; 4]), skip_templates: true, ..opts };
Renderer::new().render_region_with(doc, Rect::new(0.0, 0.0, 200.0, 100.0), 2.0, &opts).pixels
}
/// The pixel of the page `pdf` prints at document point `(x, y)` of the first sheet.
fn printed_pixel(s: &mut Session, settings: Value, (x, y): (f64, f64)) -> [u8; 4] {
let v = s.execute("file.print", &json!({ "settings": settings })).unwrap();
let bytes = vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap();
let pv = s.execute("print.preview", &json!({ "settings": settings })).unwrap();
let t: Vec<f64> = pv["sheets"][0]["transform"].as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect();
let back = vectorcraft_pdf::import(&bytes).unwrap();
let page = back.artboards[0].rect;
let (px, py) = (page.x0 + t[0] * x + t[2] * y + t[4], page.y0 + t[1] * x + t[3] * y + t[5]);
let img = Renderer::new().render_region(&back, Rect::new(px - 0.5, py - 0.5, px + 0.5, py + 0.5), 1.0, true);
img.pixels[..4].try_into().unwrap()
}
fn near(a: [u8; 4], b: [u8; 4]) -> bool {
a.iter().zip(b).all(|(a, b)| a.abs_diff(b) <= 4)
}
#[test]
fn simulated_overprints_print_what_overprint_preview_shows() {
let (mut s, _) = session();
let preview = pixels(doc(&s), RenderOptions { overprint_preview: true, ..Default::default() });
let simulated = vectorcraft_pdf::printed_document(doc(&s), &settings(json!({"advanced": {"overprints": "simulate"}})));
assert_eq!(pixels(&simulated, RenderOptions::default()), preview, "the same pixels as Overprint Preview");
assert_ne!(pixels(doc(&s), RenderOptions::default()), preview, "overprinting shows");
// On the printed page too: where yellow overprints cyan it prints green; discarded, yellow.
let at = (55.0, 30.0);
let i = ((30.0 * 2.0) as usize * 400 + (55.0 * 2.0) as usize) * 4;
let shown: [u8; 4] = preview[i..i + 4].try_into().unwrap();
let sim = printed_pixel(&mut s, json!({"advanced": {"overprints": "simulate"}}), at);
assert!(near(sim, shown), "{sim:?} vs {shown:?}");
let discarded = printed_pixel(&mut s, json!({"advanced": {"overprints": "discard"}}), at);
assert!(!near(discarded, shown) && discarded[2] < 64 && discarded[0] > 192, "yellow knocks out: {discarded:?}");
}
#[test]
fn a_flattener_preset_flattens_transparency_before_printing() {
let (mut s, _) = session();
let saved = s.prefs.flattener_presets.clone();
let transparent = |d: &Document| {
let mut any = false;
d.walk(|n| any |= n.opacity < 1.0);
any
};
assert!(transparent(doc(&s)));
let none = super::cmd::printadvanced::prepare("file.print", doc(&s), &settings(json!({})), &saved).unwrap();
assert!(none.is_none(), "transparency prints live without a preset");
for name in ["High Resolution", "low"] {
let set = settings(json!({"advanced": {"flattenerPreset": name}}));
let flat = super::cmd::printadvanced::prepare("file.print", doc(&s), &set, &saved).unwrap().unwrap();
assert!(!transparent(&flat), "{name}: flattened");
}
// A saved preset by name; an unknown one is refused by preview and print alike.
s.execute("flattener.presets.save", &json!({"name": "Proofs", "preset": "low"})).unwrap();
let ok = json!({"settings": {"advanced": {"flattenerPreset": "Proofs"}}});
assert!(s.execute("print.preview", &ok).is_ok() && s.execute("file.print", &ok).is_ok());
let bad = json!({"settings": {"advanced": {"flattenerPreset": "Nope"}}});
assert!(s.execute("print.preview", &bad).is_err() && s.execute("file.print", &bad).is_err());
}
#[test]
fn print_as_bitmap_prints_an_image_of_each_artboard() {
let (mut s, _) = session();
s.execute("artboard.new", &json!({"x": 300, "y": 0, "width": 200, "height": 100})).unwrap();
let saved = s.prefs.flattener_presets.clone();
let set = settings(json!({"advanced": {"printAsBitmap": true}}));
let bitmap = super::cmd::printadvanced::prepare("file.print", doc(&s), &set, &saved).unwrap().unwrap();
let mut kinds = vec![];
bitmap.walk(|n| kinds.push(matches!(n.kind, NodeKind::Image(_))));
assert_eq!(kinds.iter().filter(|k| **k).count(), 1, "one image: the empty artboard gets none");
// The image looks like the art, at the raster effects resolution.
assert_eq!(pixels(&bitmap, RenderOptions::default()).len(), pixels(doc(&s), RenderOptions::default()).len());
let v = s.execute("file.print", &json!({"settings": {"advanced": {"printAsBitmap": true}, "skipBlank": true}})).unwrap();
assert_eq!(v["pages"], 1, "the blank artboard is skipped");
let bytes = vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap();
let back = vectorcraft_pdf::import(&bytes).unwrap();
let (mut images, mut paths) = (0, 0);
back.walk(|n| match n.kind {
NodeKind::Image(_) => images += 1,
NodeKind::Path { .. } => paths += 1,
_ => {}
});
assert_eq!((images, paths), (1, 0), "no vector art is left");
// Separations print the art as it is.
let seps = json!({"settings": {"advanced": {"printAsBitmap": true}, "output": {"mode": "separations"}}});
let pv = s.execute("print.preview", &seps).unwrap();
assert!(pv["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("Print as Bitmap")));
let set = settings(seps["settings"].clone());
assert!(super::cmd::printadvanced::prepare("file.print", doc(&s), &set, &saved).unwrap().is_none());
}