Files
vectorcraft/crates/engine/src/cmd/fileio/tests_pdf.rs
T

473 lines
27 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.
//! `document.exportPdf`, `document.pdfSettings` and the PDF options of every PDF export.
use serde_json::{Value, json};
use super::pdf::DEFAULT_PRESET;
use super::*;
fn session(artboards: usize) -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 100, "height": 80, "artboards": artboards})).unwrap();
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 40, "height": 30})).unwrap();
s
}
fn b64(v: &Value) -> Vec<u8> {
vectorcraft_format::base64_decode(v["dataBase64"].as_str().expect("dataBase64")).unwrap()
}
fn export(s: &mut Session, p: Value) -> Value {
s.execute("document.exportPdf", &p).unwrap_or_else(|e| panic!("{p}: {e}"))
}
fn warnings(v: &Value) -> Vec<String> {
v["warnings"].as_array().expect("warnings").iter().map(|w| w.as_str().unwrap().to_string()).collect()
}
fn pages(bytes: &[u8]) -> usize {
vectorcraft_pdf::import(bytes).unwrap().artboards.len()
}
/// `p` plus `format: pdf` (the same options through `document.export`).
fn as_export(p: &Value) -> Value {
let mut o = p.as_object().cloned().unwrap_or_default();
o.insert("format".into(), json!("pdf"));
Value::Object(o)
}
#[test]
fn export_pdf_writes_the_compatibility_header_and_the_range() {
let mut s = session(3);
let v = export(&mut s, json!({}));
let pdf = b64(&v);
assert!(pdf.starts_with(b"%PDF-1.7"));
assert_eq!(pages(&pdf), 3);
assert_eq!(v["bytes"], pdf.len());
assert!(warnings(&v).is_empty(), "{v}");
assert!(b64(&export(&mut s, json!({"compatibility": "1.5"}))).starts_with(b"%PDF-1.5"));
assert!(b64(&export(&mut s, json!({"compatibility": "2.0"}))).starts_with(b"%PDF-2.0"));
assert_eq!(pages(&b64(&export(&mut s, json!({"range": "1,3"})))), 2);
assert_eq!(pages(&b64(&export(&mut s, json!({"artboards": [1]})))), 1);
// The same options through document.export and document.serialize.
let v = s.execute("document.export", &as_export(&json!({"compatibility": "1.4", "range": "2"}))).unwrap();
assert!(b64(&v).starts_with(b"%PDF-1.4") && pages(&b64(&v)) == 1);
assert!(v["warnings"].is_array());
let v = s.execute("document.serialize", &as_export(&json!({"compatibility": "1.6"}))).unwrap();
assert!(b64(&v).starts_with(b"%PDF-1.6"));
}
/// `useArtboards: false` writes one page, the bounds of the visible art, as `document.export`
/// does, whatever artboards are chosen. Art longer than 200 in keeps its size with
/// `longPagesInPoints`. A document with no visible art and a value that isn't a boolean are
/// refused.
#[test]
fn use_artboards_false_writes_one_page_of_the_arts_bounds() {
let mut s = session(3);
let page = |bytes: &[u8]| {
let d = vectorcraft_pdf::import(bytes).unwrap();
(d.artboards.len(), d.artboards[0].rect.size())
};
let through_export = page(&b64(&s.execute("document.export", &as_export(&json!({"useArtboards": false}))).unwrap()));
assert!(through_export.0 == 1 && through_export.1.width < 50.0 && through_export.1.height < 40.0, "{through_export:?}");
for p in [
json!({"useArtboards": false}),
json!({"useArtboards": false, "range": "2"}),
json!({"useArtboards": false, "artboards": [0, 1]}),
json!({"useArtboards": false, "artboard": 2}),
] {
assert_eq!(page(&b64(&export(&mut s, p.clone()))), through_export, "{p}");
}
assert_eq!(pages(&b64(&export(&mut s, json!({"useArtboards": true})))), 3);
assert_eq!(pages(&b64(&export(&mut s, json!({"useArtboards": null})))), 3);
// A 10 m drawing on a small artboard: its page is 10 m long, in points.
s.execute("shape.rectangle", &json!({"x": 0, "y": 200, "width": 28_346.46, "height": 100})).unwrap();
let v = export(&mut s, json!({"useArtboards": false, "longPagesInPoints": true}));
let pdf = b64(&v);
let (n, size) = page(&pdf);
assert!(n == 1 && size.width > 28_346.0 && size.width < 28_350.0, "{size:?}");
assert!(!String::from_utf8_lossy(&pdf).contains("/UserUnit"));
assert!(warnings(&v).iter().any(|w| w.contains("written without a UserUnit, in points")), "{v}");
// The Summary's export in memory writes the same page.
let mut long = |p: Value| warnings(&s.execute("document.pdfSettings", &p).unwrap()).iter().any(|w| w.contains("longer than PDF's page limit"));
assert!(long(json!({"includeDocument": true, "useArtboards": false})));
assert!(!long(json!({"includeDocument": true})), "the artboards are small");
let e = s.execute("document.pdfSettings", &json!({"useArtboards": "no"})).unwrap_err();
assert!(matches!(e, crate::EngineError::BadParams { .. }), "without includeDocument too: {e}");
s.execute("select.all", &json!({})).unwrap();
s.execute("edit.clear", &json!({})).unwrap();
for p in [json!({"useArtboards": false}), json!({"useArtboards": "no"})] {
let e = s.execute("document.exportPdf", &p).unwrap_err();
assert!(matches!(e, crate::EngineError::BadParams { .. }), "{p}: {e}");
}
}
/// In a PDF without a standard, the warning for a page longer than 14,400 pt written with a
/// `UserUnit` above 1 names "Write pages longer than 200 in at full size in points": at PDF 1.5
/// (written as 1.6) and later, through `document.exportPdf`, `document.export`, a page of the
/// art's bounds and the Summary's export, with one such warning per page. PDF/A files and `.ai`
/// saves keep the warning without the option's name. Pages written in points (the option on, PDF
/// 1.4, PDF/X, a page too narrow for a `UserUnit` above 1) and a page too long and narrow for
/// 14,400 units have other warnings, which don't name it.
#[test]
fn a_page_with_a_user_unit_names_the_option_that_writes_it_in_points() {
use super::pdf::LONG_PAGES_IN_POINTS;
const SMALLER: &str = "so apps that ignore UserUnit show it that many times smaller";
let mut s = Session::new();
s.execute("file.new", &json!({"width": 20_000, "height": 1_000})).unwrap();
s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 19_000, "height": 500})).unwrap();
let warnings = |v: &Value| -> Vec<String> {
v["warnings"].as_array().unwrap().iter().filter_map(|w| w.as_str()).filter(|w| w.contains("UserUnit")).map(str::to_string).collect()
};
let named = |v: &Value| warnings(v).iter().any(|w| w.contains(LONG_PAGES_IN_POINTS));
for p in [
json!({"longPagesInPoints": false}),
json!({"longPagesInPoints": false, "compatibility": "1.5"}),
json!({"longPagesInPoints": false, "compatibility": "2.0"}),
json!({"longPagesInPoints": false, "useArtboards": false}),
] {
let mut q = p.clone();
q["includeDocument"] = json!(true);
for v in [export(&mut s, p.clone()), s.execute("document.export", &as_export(&p)).unwrap(), s.execute("document.pdfSettings", &q).unwrap()] {
let page: Vec<String> = warnings(&v).into_iter().filter(|w| w.starts_with("the page of artboard")).collect();
assert_eq!(page.len(), 1, "{p}: {v}");
assert!(page[0].ends_with(&format!("{SMALLER}{LONG_PAGES_IN_POINTS}")), "{p}: {page:?}");
assert_eq!(warnings(&v).iter().filter(|w| w.contains("apps that ignore UserUnit")).count(), 1, "{p}: {v}");
}
}
for p in [
json!({"longPagesInPoints": true}),
json!({"longPagesInPoints": false, "compatibility": "1.4"}),
json!({"longPagesInPoints": false, "preset": "PDF/X-4:2010"}),
] {
assert!(!named(&export(&mut s, p.clone())), "{p}");
}
for p in
[json!({"longPagesInPoints": false, "standard": "pdfA2b"}), json!({"longPagesInPoints": false, "standard": "pdfA2b", "compatibility": "1.4"})]
{
let v = export(&mut s, p.clone());
assert!(warnings(&v).iter().any(|w| w.ends_with(SMALLER)) && !named(&v), "{p}: {v}");
}
let ai = s.execute("document.export", &json!({"format": "ai", "longPagesInPoints": false})).unwrap();
assert!(warnings(&ai).iter().any(|w| w.ends_with(SMALLER)) && !named(&ai), "{ai}");
// The art's bounds are within 14,400 pt.
s.execute("select.all", &json!({})).unwrap();
s.execute("edit.clear", &json!({})).unwrap();
s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 500, "height": 500})).unwrap();
assert!(!named(&export(&mut s, json!({"longPagesInPoints": false, "useArtboards": false}))));
// A page 3 pt high keeps a UserUnit of 1, so it is written in points either way; a page
// 85,040 × 17 pt gets a UserUnit above 1 that leaves its long side past 14,400 units.
for (w, h) in [(20_000, 3), (85_040, 17)] {
let mut s = Session::new();
s.execute("file.new", &json!({"width": w, "height": h})).unwrap();
s.execute("shape.rectangle", &json!({"x": 100, "y": 0, "width": w - 1_000, "height": h})).unwrap();
let v = export(&mut s, json!({"longPagesInPoints": false}));
assert!(!named(&v), "{w} × {h}: {v}");
}
}
/// A PDF of the art's bounds carries no Preserve Editing data, through `document.exportPdf`,
/// `document.export` and the Summary's export in memory, and says so in a warning: reopened, its
/// editing data would replace the document's artboards with the one around the art. A PDF of the
/// artboards keeps the data, and it reopens with every artboard. With Preserve Editing off, there
/// is no warning.
#[test]
fn a_pdf_of_the_arts_bounds_carries_no_editing_data() {
let mut s = session(3);
let left_out = |v: &Value| warnings(v).iter().any(|w| w == super::pdf::EDITING_NEEDS_THE_ARTBOARDS);
let art = json!({"useArtboards": false});
for v in [export(&mut s, art.clone()), s.execute("document.export", &as_export(&art)).unwrap()] {
let pdf = b64(&v);
assert!(vectorcraft_pdf::editing(&pdf).is_none(), "{:?}", warnings(&v));
assert!(left_out(&v), "{:?}", warnings(&v));
assert_eq!(pages(&pdf), 1);
}
let summary = s.execute("document.pdfSettings", &json!({"includeDocument": true, "useArtboards": false})).unwrap();
assert!(left_out(&summary), "{summary}");
let quiet = export(&mut s, json!({"useArtboards": false, "preserveEditing": false}));
assert!(!left_out(&quiet), "{:?}", warnings(&quiet));
for v in [export(&mut s, json!({})), export(&mut s, json!({"useArtboards": true}))] {
let editing = vectorcraft_pdf::editing(&b64(&v)).expect("editing data");
let reopened = vectorcraft_format::load(&editing.data).unwrap();
assert_eq!(reopened.artboards.len(), 3);
assert!(!left_out(&v), "{:?}", warnings(&v));
}
}
/// While an opacity mask is edited, a PDF of the art's bounds measures the art without the
/// editing layer, as `document.export` does: a mask shape drawn past the masked object doesn't
/// widen the page.
#[test]
fn a_pdf_of_the_art_leaves_out_the_mask_editing_layer() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 300, "height": 300})).unwrap();
let obj = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 200, "height": 200})).unwrap()["id"].clone();
let mask = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 100, "height": 200})).unwrap()["id"].clone();
s.execute("paint.setFill", &json!({"color": "#ffffff", "ids": [mask]})).unwrap();
s.execute("select.set", &json!({"ids": [obj, mask]})).unwrap();
s.execute("transparency.makeOpacityMask", &json!({})).unwrap();
s.execute("transparency.editOpacityMask", &json!({})).unwrap();
s.execute("shape.ellipse", &json!({"x": 250, "y": 250, "width": 40, "height": 40})).unwrap();
assert!(s.doc().unwrap().doc.mask_edit.is_some());
let width = |bytes: &[u8]| vectorcraft_pdf::import(bytes).unwrap().artboards[0].rect.width();
let through_export = width(&b64(&s.execute("document.export", &as_export(&json!({"useArtboards": false}))).unwrap()));
assert!(through_export < 250.0, "{through_export}");
assert_eq!(width(&b64(&export(&mut s, json!({"useArtboards": false})))), through_export);
}
/// While an opacity mask is edited, a PDF of the artboards leaves out the editing layer too, as
/// `document.export` does: from the drawn page and from the Preserve Editing data.
#[test]
fn a_pdf_of_the_artboards_leaves_out_the_mask_editing_layer() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 300, "height": 300})).unwrap();
let obj = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 200, "height": 200})).unwrap()["id"].clone();
let mask = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 100, "height": 200})).unwrap()["id"].clone();
s.execute("paint.setFill", &json!({"color": "#ffffff", "ids": [mask]})).unwrap();
s.execute("select.set", &json!({"ids": [obj, mask]})).unwrap();
s.execute("transparency.makeOpacityMask", &json!({})).unwrap();
s.execute("transparency.editOpacityMask", &json!({})).unwrap();
s.execute("shape.ellipse", &json!({"x": 250, "y": 250, "width": 40, "height": 40})).unwrap();
assert!(s.doc().unwrap().doc.mask_edit.is_some());
let nodes = |bytes: &[u8]| vectorcraft_pdf::import(bytes).unwrap().node_count();
let plain = json!({"preserveEditing": false});
let through_export = nodes(&b64(&s.execute("document.export", &as_export(&plain)).unwrap()));
assert_eq!(nodes(&b64(&export(&mut s, plain))), through_export, "the drawn page");
let layers = |v: &Value| vectorcraft_format::load(&vectorcraft_pdf::editing(&b64(v)).unwrap().data).unwrap().layers;
let through_export = layers(&s.execute("document.export", &as_export(&json!({}))).unwrap());
assert_eq!(layers(&export(&mut s, json!({}))), through_export, "the Preserve Editing data");
}
#[test]
fn bad_options_are_refused() {
let mut s = session(2);
for p in [
json!({"compatibility": "1.2"}),
json!({"compatibility": "1.3", "standard": "pdfA2b"}),
json!({"compatibility": "1.3", "flattenerPreset": "Nope"}),
json!({"compatibility": 1.5}),
json!({"range": "1-3"}),
json!({"range": "0"}),
json!({"artboards": []}),
json!({"preset": "Nope"}),
json!({"standard": "pdfX4", "compatibility": "1.7"}),
json!({"standard": "pdfA2b", "compatibility": "2.0"}),
json!({"compression": {"color": {"ppi": 0}}}),
json!({"compression": "zip"}),
json!({"standard": "pdfA2b", "security": {"openPassword": "x"}}),
json!({"security": {"openPassword": "x", "permissionsPassword": "x"}}),
] {
let e = s.execute("document.exportPdf", &p).unwrap_err();
assert!(matches!(e, crate::EngineError::BadParams { .. }), "{p}: {e}");
assert!(s.execute("document.export", &as_export(&p)).is_err(), "document.export {p}");
}
}
#[test]
fn options_apply_over_the_preset() {
let mut s = session(1);
let a = b64(&export(&mut s, json!({"preset": DEFAULT_PRESET, "compression": {"compressText": false}})));
let b = b64(&export(&mut s, json!({"preset": "default"})));
assert!(a.len() > b.len(), "content streams left uncompressed");
assert!(String::from_utf8_lossy(&a).contains(" re") || String::from_utf8_lossy(&a).contains(" l"));
// `null` (the default document.formats lists) keeps the preset's value, at any depth.
let p = json!({"compatibility": null, "compression": null, "security": {"openPassword": null}, "marks": {"weight": null}});
let v = s.execute("document.pdfSettings", &p).unwrap();
assert_eq!(v["changed"], json!([]), "{v}");
assert!(b64(&export(&mut s, p)).starts_with(b"%PDF-1.7"));
}
#[test]
fn pdf_a_refuses_pdf_2() {
let mut s = session(1);
let e = s.execute("document.pdfSettings", &json!({"standard": "pdfA2b", "compatibility": "2.0"})).unwrap_err();
assert!(matches!(e, crate::EngineError::BadParams { .. }), "{e}");
let pdf = b64(&export(&mut s, json!({"standard": "pdfA2b", "compatibility": "1.6"})));
assert!(pdf.starts_with(b"%PDF-1.6") && String::from_utf8_lossy(&pdf).contains("pdfaid"));
}
#[test]
fn options_not_applied_yet_and_document_features_warn() {
let mut s = session(1);
// Printer's marks are drawn, thumbnails embedded and the file linearised: nothing to report.
let v = export(&mut s, json!({"thumbnails": true, "fastWebView": true, "marks": {"trim": true}}));
assert_eq!(warnings(&v), Vec::<String>::new());
let text = String::from_utf8_lossy(&b64(&v)).into_owned();
assert!(text.contains("/Thumb ") && text.contains("/Linearized 1"));
// A pattern stroke is written as its tiles clipped to the stroke: nothing to report.
let tile = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap();
s.execute("select.set", &json!({"ids": [tile]})).unwrap();
s.execute("object.pattern.make", &json!({"name": "Dots", "width": 20, "height": 20})).unwrap();
s.execute("object.pattern.done", &json!({})).unwrap();
let frame = s.execute("shape.rectangle", &json!({"x": 20, "y": 20, "width": 50, "height": 40})).unwrap()["id"].as_u64().unwrap();
s.execute("paint.setStroke", &json!({"ids": [frame], "swatch": "Dots"})).unwrap();
assert_eq!(warnings(&export(&mut s, json!({}))).len(), 0);
// A knockout group is written as a PDF knockout group: nothing to report.
s.execute("select.set", &json!({"ids": [frame]})).unwrap();
s.execute("object.group", &json!({})).unwrap();
s.execute("transparency.set", &json!({"knockout": "on"})).unwrap();
let knockout = |w: Vec<String>| w.iter().any(|w| w.contains("knockout groups"));
assert!(!knockout(warnings(&export(&mut s, json!({})))));
// One whose soft masks would draw a brushed stroke is approximated: its warning comes back
// from every PDF path.
let rect =
|s: &mut Session, x: i32| s.execute("shape.rectangle", &json!({"x": x, "y": 45, "width": 40, "height": 30})).unwrap()["id"].as_u64().unwrap();
let (lower, upper) = (rect(&mut s, 20), rect(&mut s, 40));
s.execute("brush.apply", &json!({"ids": [upper], "name": "3 pt. Round"})).unwrap();
s.execute("object.setProps", &json!({"ids": [lower, upper], "opacity": 50, "knockoutShape": true})).unwrap();
s.execute("select.set", &json!({"ids": [lower, upper]})).unwrap();
s.execute("object.group", &json!({})).unwrap();
s.execute("transparency.set", &json!({"knockout": "on"})).unwrap();
assert!(knockout(warnings(&export(&mut s, json!({})))));
assert!(knockout(warnings(&s.execute("document.export", &as_export(&json!({}))).unwrap())));
// Other formats have none.
assert!(warnings(&s.execute("document.export", &json!({"format": "png"})).unwrap()).is_empty());
}
/// A placed 16-bit PNG is written at 8 bits a sample in PDF 1.3 and 1.4 files (PDF/X-3 and the
/// pages of a .ai file too), and keeps 16 bits from PDF 1.5.
#[test]
fn a_16_bit_png_is_written_at_8_bits_before_pdf_1_5() {
let mut s = session(1);
let deep = image::ImageBuffer::from_fn(8, 8, |x, y| image::Rgb([x as u16 * 8000, y as u16 * 8000, 40000]));
let mut png = vec![];
image::DynamicImage::ImageRgb16(deep).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
s.execute("file.place", &json!({"name": "deep.png", "dataBase64": vectorcraft_format::base64_encode(&png), "at": [50, 40]})).unwrap();
let readable = json!({"compressText": false});
for (cmd, p, depth) in [
("document.exportPdf", json!({"compatibility": "1.3", "compression": readable}), 8),
("document.exportPdf", json!({"compatibility": "1.4", "compression": readable}), 8),
("document.exportPdf", json!({"standard": "pdfX3", "compression": readable}), 8),
("file.saveCopy", json!({"format": "ai", "compatibility": "1.4", "compress": false}), 8),
("document.exportPdf", json!({"compatibility": "1.5", "compression": readable}), 16),
("document.exportPdf", json!({"compression": readable}), 16),
] {
let v = s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}"));
let text = String::from_utf8_lossy(&b64(&v)).into_owned();
let (eight, sixteen) = (text.contains("/BitsPerComponent 8"), text.contains("/BitsPerComponent 16"));
assert_eq!((eight, sixteen), (depth == 8, depth == 16), "{cmd} {p}");
assert!(warnings(&v).iter().all(|w| !w.contains("decoded")), "{cmd} {p}: {}", v["warnings"]);
}
}
/// A placed image whose pixel data can't be decoded is left out of every PDF the engine writes,
/// and the rest of the page is written. Each reply that carries the PDF's warnings has the note
/// raster exports give, once.
#[test]
fn every_pdf_leaves_out_an_image_that_cant_be_decoded() {
const NOTE: &str = "an image couldn't be decoded and is missing from the picture";
let mut s = session(1);
let cut = crate::tests_menucmds::place_undecodable_png(&mut s);
// Whether each object on the PDF's pages is an image. The pages should hold the rectangle and
// no image.
let drawn = |bytes: &[u8]| {
let mut images = vec![];
vectorcraft_pdf::import(bytes).unwrap().walk(|n| {
if !n.is_container() && !matches!(n.kind, vectorcraft_doc::NodeKind::Path { clipping: true, .. }) {
images.push(matches!(n.kind, vectorcraft_doc::NodeKind::Image(_)));
}
});
images
};
for (cmd, p) in [
("document.exportPdf", json!({})),
("document.exportPdf", json!({"preset": "High Quality Print"})),
("document.exportPdf", json!({"standard": "pdfX1a"})),
("document.export", json!({"format": "pdf"})),
("file.saveCopy", json!({"format": "pdf"})),
("file.saveCopy", json!({"format": "ai"})),
("file.print", json!({})),
] {
let v = s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}"));
assert_eq!(warnings(&v).iter().filter(|w| *w == NOTE).count(), 1, "{cmd} {p}: {}", v["warnings"]);
assert_eq!(drawn(&b64(&v)), [false], "{cmd} {p}");
}
// The .ai file reopens with the image as it was placed, from the image bytes in its editing data.
let ai = b64(&s.execute("file.saveCopy", &json!({"format": "ai"})).unwrap());
let placed = s.doc().unwrap().doc.images.clone();
s.execute("document.open", &json!({"name": "cut.ai", "dataBase64": vectorcraft_format::base64_encode(&ai)})).unwrap();
let reopened = &s.doc().unwrap().doc;
assert!(reopened.node(cut).is_some_and(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Image(_))));
assert_eq!(reopened.images, placed);
// Export for Screens, a native file carrying a PDF, and the PDF that Copy offers are written.
let r = s.execute("document.exportForScreens", &json!({"formats": [{"format": "pdf"}]})).unwrap();
assert_eq!(drawn(&b64(&r["files"][0])), [false]);
assert!(s.execute("file.saveCopy", &json!({"format": "vectorcraft", "pdfCompatible": true})).is_ok());
s.execute("select.set", &json!({"ids": [cut.0]})).unwrap();
s.execute("edit.copy", &json!({})).unwrap();
assert!(b64(&s.execute("clipboard.exportPdf", &json!({})).unwrap()).starts_with(b"%PDF"));
s.execute("prefs.set", &json!({"key": "copyAsPdf", "value": true})).unwrap();
assert!(s.clipboard_flavours().iter().any(|f| f.mime == crate::cmd::clipboard::PDF));
}
#[test]
fn export_pdf_writes_a_path() {
let mut s = session(2);
let dir = std::env::temp_dir().join(format!("vc-exportpdf-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("out.pdf").to_string_lossy().to_string();
let v = export(&mut s, json!({"path": path, "range": "2"}));
assert_eq!(v["path"], path.as_str());
assert!(v.get("dataBase64").is_none());
let bytes = std::fs::read(&path).unwrap();
assert_eq!(v["bytes"], bytes.len());
assert_eq!(pages(&bytes), 1);
assert_eq!(s.doc().unwrap().path, None, "exporting never retargets the document");
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn pdf_settings_summarise_changes_and_warnings() {
let mut s = Session::new();
let v = s.execute("document.pdfSettings", &json!({})).unwrap();
assert_eq!(v["settings"]["compatibility"], "1.7");
assert_eq!(v["presets"][0], DEFAULT_PRESET);
assert_eq!(v["changed"], json!([]));
assert_eq!(v["warnings"], json!([]));
let p = json!({"compatibility": "1.5", "compression": {"compressText": false}, "createLayers": true, "security": {"copy": false, "openPassword": ""}});
let v = s.execute("document.pdfSettings", &p).unwrap();
let changed: Vec<&str> = v["changed"].as_array().unwrap().iter().map(|c| c["option"].as_str().unwrap()).collect();
assert_eq!(changed, ["compatibility", "compression.compressText", "createLayers", "security.copy"], "key order");
assert_eq!(v["changed"][0]["value"], "1.5");
assert_eq!(v["warnings"].as_array().unwrap().len(), 1, "permissions (PDF layers are written at 1.5): {}", v["warnings"]);
assert!(!v.to_string().contains("Password"), "passwords never come back");
assert!(s.execute("document.pdfSettings", &json!({"compatibility": "9"})).is_err());
// With the document: the in-memory export adds the document's own warnings.
let mut s = session(1);
s.execute("select.all", &json!({})).unwrap();
s.execute("object.group", &json!({})).unwrap();
let g = s.doc().unwrap().selection.objects[0].0;
// Raster effects are written as images: nothing to report.
s.execute("effect.apply", &json!({"effect": "stylize.dropShadow", "ids": [g]})).unwrap();
assert_eq!(s.execute("document.pdfSettings", &json!({"includeDocument": true})).unwrap()["warnings"], json!([]));
s.execute("transparency.set", &json!({"knockout": "on"})).unwrap();
// A knockout group is written as a PDF knockout group: nothing to report.
assert_eq!(s.execute("document.pdfSettings", &json!({"includeDocument": true})).unwrap()["warnings"], json!([]));
// One whose soft masks would draw a brushed stroke is approximated.
let rect =
|s: &mut Session, x: i32| s.execute("shape.rectangle", &json!({"x": x, "y": 45, "width": 40, "height": 30})).unwrap()["id"].as_u64().unwrap();
let (lower, upper) = (rect(&mut s, 20), rect(&mut s, 40));
s.execute("brush.apply", &json!({"ids": [upper], "name": "3 pt. Round"})).unwrap();
s.execute("object.setProps", &json!({"ids": [lower, upper], "opacity": 50, "knockoutShape": true})).unwrap();
s.execute("select.set", &json!({"ids": [lower, upper]})).unwrap();
s.execute("object.group", &json!({})).unwrap();
s.execute("transparency.set", &json!({"knockout": "on"})).unwrap();
assert_eq!(s.execute("document.pdfSettings", &json!({})).unwrap()["warnings"], json!([]));
let full = s.execute("document.pdfSettings", &json!({"includeDocument": true})).unwrap();
assert!(warnings(&full).iter().any(|w| w.contains("knockout groups")), "{}", full["warnings"]);
}
#[test]
fn formats_list_the_pdf_options() {
let mut s = Session::new();
let v = s.execute("document.formats", &json!({})).unwrap();
let pdf = v["formats"].as_array().unwrap().iter().find(|f| f["id"] == "pdf").unwrap();
for k in ["artboard", "range", "preset", "standard", "compatibility", "compression", "security"] {
assert!(pdf["options"].get(k).is_some(), "{k}");
}
assert_eq!(pdf["options"]["compatibility"]["default"], "1.7");
assert_eq!(pdf["options"]["preset"]["default"], DEFAULT_PRESET);
}