mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 01:14:29 -06:00
Print as Bitmap limited each page's image to 65,535 pixels a side, but the renderer draws at most 32,768. A 9000 x 100 pt artboard at a 300 ppi raster effects resolution printed as an image that stopped at 7,864 pt and was placed as if whole. The resolution is now lowered until the longest side fits.
182 lines
9.7 KiB
Rust
182 lines
9.7 KiB
Rust
//! 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 crate::*;
|
||
|
||
/// 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 = crate::cmd::printadvanced::prepare("file.print", doc(&s), &settings(json!({})), &saved, false).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 = crate::cmd::printadvanced::prepare("file.print", doc(&s), &set, &saved, false).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 = crate::cmd::printadvanced::prepare("file.print", doc(&s), &set, &saved, false).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!(crate::cmd::printadvanced::prepare("file.print", doc(&s), &set, &saved, false).unwrap().is_none());
|
||
}
|
||
|
||
/// Print as Bitmap of an artboard longer than the renderer draws at the raster effects resolution
|
||
/// (32,768 pixels a side) prints an image of all of it, at a lower resolution.
|
||
#[test]
|
||
fn print_as_bitmap_prints_all_of_a_long_artboard() {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 9000, "height": 100})).unwrap();
|
||
s.execute("document.rasterEffectsSettings", &json!({"resolution": 300})).unwrap();
|
||
s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 9000, "height": 100})).unwrap();
|
||
let set = settings(json!({"advanced": {"printAsBitmap": true}}));
|
||
let saved = s.prefs.flattener_presets.clone();
|
||
let bitmap = crate::cmd::printadvanced::prepare("file.print", doc(&s), &set, &saved, false).unwrap().unwrap();
|
||
let mut placed = vec![];
|
||
bitmap.walk(|n| {
|
||
if let NodeKind::Image(im) = &n.kind {
|
||
placed.push((im.width, im.xf.transform_rect_bbox(Rect::new(0.0, 0.0, f64::from(im.width), f64::from(im.height)))));
|
||
}
|
||
});
|
||
let [(width, b)] = placed[..] else { panic!("one image: {placed:?}") };
|
||
assert!(width <= vectorcraft_render::MAX_RASTER_SIDE, "{width} pixels");
|
||
assert!(b.x0 <= 0.0 && b.x1 >= 9000.0, "the image covers the artboard: {b:?}");
|
||
}
|
||
|
||
/// Appearance of Black › Printing / Exporting (#394): Print as Bitmap draws pure CMYK black as RGB
|
||
/// black when `rich_black` is on.
|
||
#[test]
|
||
fn print_as_bitmap_honours_rich_black() {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 20, "height": 20, "colorMode": "cmyk"})).unwrap();
|
||
s.execute("paint.setStroke", &json!({"none": true})).unwrap();
|
||
s.execute("paint.setFill", &json!({"color": {"c": 0, "m": 0, "y": 0, "k": 1}})).unwrap();
|
||
s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 20, "height": 20})).unwrap();
|
||
let set = settings(json!({"advanced": {"printAsBitmap": true}}));
|
||
let saved = s.prefs.flattener_presets.clone();
|
||
let centre = |prepared: &Document| {
|
||
let mut key = None;
|
||
prepared.walk(|n| {
|
||
if let NodeKind::Image(im) = &n.kind {
|
||
key = Some(im.key.clone());
|
||
}
|
||
});
|
||
let bytes = &prepared.images[key.as_ref().expect("bitmap image")].bytes;
|
||
let img = image::load_from_memory(bytes).unwrap().to_rgba8();
|
||
img.get_pixel(img.width() / 2, img.height() / 2).0
|
||
};
|
||
let accurate = centre(&crate::cmd::printadvanced::prepare("file.print", doc(&s), &set, &saved, false).unwrap().unwrap());
|
||
assert!(accurate[0] > 10, "accurate: dark grey, {accurate:?}");
|
||
let rich = centre(&crate::cmd::printadvanced::prepare("file.print", doc(&s), &set, &saved, true).unwrap().unwrap());
|
||
assert_eq!(rich, [0, 0, 0, 255], "rich: RGB black");
|
||
}
|