Files
vectorcraft/crates/pdf/src/tests/settings.rs
T
dotCooCoo 766e291e3c Save and Export: notes of what the file loses come before notes on options
Each writer marks its notes as a loss (something of the document that the
file leaves out or that reaches it changed) or as a note on the options
(an option it didn't apply or applied another way while the art reaches
the file as it looks, or something an option turns off). The PDF, SVG,
EPS, legacy .ai, DXF and HP-GL writers return their notes on the options
in a list of their own (`setting_notes`) beside `warnings`, and the
engine's `Encoded` does the same.

The replies of the save and export commands, and of
`clipboard.exportPdf` and `clipboard.exportPng`, list the losses first,
each kind in the order the writers gave them, across every file that
Export for Screens writes. The status bar names the first note, so an
export to lossy WebP of type in a font that isn't installed now names the
fallback-font note instead of the lossy WebP note. A print to a file
keeps the order its notes are given in.
2026-11-12 08:33:57 -08:00

118 lines
6.7 KiB
Rust

//! Save PDF settings: JSON shape, checks and the warnings for options not applied yet.
use serde_json::json;
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{Appearance, Document, Node};
use vectorcraft_geom::{Rect, shapes};
use crate::*;
fn settings(v: serde_json::Value) -> PdfSettings {
serde_json::from_value(v).unwrap()
}
fn doc() -> Document {
let mut d = Document::new(100.0, 100.0);
let layer = d.default_layer().unwrap();
let id = d.alloc_id();
let n = Node::path(id, shapes::rectangle(Rect::new(10.0, 10.0, 50.0, 50.0)), Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0));
d.insert(Some(layer), 0, n).unwrap();
d
}
#[test]
fn json_is_camel_case_and_partial_objects_keep_defaults() {
let d = PdfSettings::default();
let v = serde_json::to_value(&d).unwrap();
assert_eq!(v["compatibility"], "1.7");
assert_eq!(v["standard"], "none");
assert_eq!(v["compression"]["compressText"], true);
assert_eq!(v["compression"]["color"]["abovePpi"], 450.0);
assert_eq!(v["advanced"]["overprint"], "preserve");
assert_eq!(serde_json::from_value::<PdfSettings>(v).unwrap(), d, "round-trips");
let s = settings(json!({"compatibility": "1.5", "marks": {"trim": true}, "compression": {"gray": {"downsample": "bicubic"}}}));
assert_eq!(s.compatibility, Compatibility::Pdf15);
assert!(s.marks.trim && s.marks.weight == 0.25);
assert_eq!(s.compression.gray.downsample, Downsample::Bicubic);
assert_eq!(s.compression.gray.ppi, 300.0);
assert!(s.compression.compress_text);
for bad in [json!({"compatibility": "1.2"}), json!({"standard": "pdfX9"}), json!({"marks": {"trim": "yes"}})] {
assert!(serde_json::from_value::<PdfSettings>(bad.clone()).is_err(), "{bad}");
}
}
#[test]
fn passwords_are_never_serialized() {
let s = settings(json!({"security": {"openPassword": "secret", "permissionsPassword": "owner"}}));
assert_eq!(s.security.open_password, "secret");
let text = serde_json::to_string(&s).unwrap();
assert!(!text.contains("secret") && !text.contains("owner") && !text.contains("Password"), "{text}");
}
#[test]
fn checks_refuse_what_the_writer_cant_honour() {
assert_eq!(PdfSettings::default().check(), Ok(()));
let refused = |v: serde_json::Value| settings(v).check().unwrap_err();
// PDF/X-4 is a PDF 1.6 standard: the default 1.7 is refused.
assert!(matches!(refused(json!({"standard": "pdfX4"})), PdfError::BadSetting(m) if m.contains("PDF 1.7")));
assert_eq!(settings(json!({"standard": "pdfX4", "compatibility": "1.6"})).check(), Ok(()));
// Passwords are written (the file is encrypted); a password with a standard is not.
assert_eq!(settings(json!({"security": {"openPassword": "x"}})).check(), Ok(()));
assert_eq!(settings(json!({"security": {"permissionsPassword": "x"}})).check(), Ok(()));
assert!(matches!(refused(json!({"standard": "pdfA2b", "security": {"openPassword": "x"}})), PdfError::BadSetting(_)));
assert!(matches!(refused(json!({"compression": {"color": {"ppi": 5}}})), PdfError::BadSetting(m) if m.contains("compression.color.ppi")));
assert!(matches!(refused(json!({"bleed": {"left": 100}})), PdfError::BadSetting(m) if m.contains("bleed.left")));
assert!(matches!(refused(json!({"marks": {"weight": 0}})), PdfError::BadSetting(_)));
assert!(matches!(refused(json!({"advanced": {"fontSubsetPercent": 120}})), PdfError::BadSetting(_)));
// PDF/A-2 is a PDF 1.7 standard: earlier versions are fine, 2.0 is not.
assert!(matches!(refused(json!({"standard": "pdfA2b", "compatibility": "2.0"})), PdfError::BadSetting(m) if m.contains("PDF/A-2b")));
assert_eq!(settings(json!({"standard": "pdfA2b", "compatibility": "1.4"})).check(), Ok(()));
assert!(Compatibility::ALL.iter().all(|c| Standard::None.allows(*c)));
assert!(export(&doc(), &PdfOptions { settings: settings(json!({"standard": "pdfX1a"})), ..Default::default() }).is_err());
assert!(export(&doc(), &PdfOptions { settings: settings(json!({"standard": "pdfX1a", "compatibility": "1.4"})), ..Default::default() }).is_ok());
}
#[test]
fn options_not_applied_yet_come_back_as_setting_notes() {
assert!(PdfSettings::default().warnings().is_empty());
for (v, word) in [
(json!({"createLayers": true, "compatibility": "1.4"}), "PDF 1.5"),
(json!({"advanced": {"outlineText": false, "fontSubsetPercent": 35}}), "subset"),
(json!({"output": {"outputIntent": "No Such Press"}}), "without embedding"),
(json!({"output": {"registry": "http://example.com"}}), "condition identifier"),
(json!({"standard": "pdfA2b", "output": {"trapped": true}}), "PDF/A"),
(json!({"security": {"printing": "low"}}), "permissions"),
] {
let w = settings(v.clone()).warnings();
assert!(w.len() == 1 && w[0].contains(word), "{v}: {w:?}");
}
// Bleed and marks, the default view/overprint choices and applied options (image compression
// too: a codec the writer lacks is reported when an image needs it; colour output; real text)
// warn about nothing.
for v in [
json!({"output": {"conversion": "destination", "destination": vectorcraft_color::cms::GENERIC_CMYK, "profiles": "all"}}),
json!({"output": {"conversion": "preserveNumbers", "profiles": "taggedSource"}}),
json!({"output": {"outputIntent": vectorcraft_color::cms::GENERIC_CMYK, "outputCondition": "Press", "registry": "r", "trapped": true}}),
json!({"output": {"outputConditionId": "CGATS TR 001"}}),
json!({"advanced": {"outlineText": false}}),
json!({"advanced": {"outlineText": true, "fontSubsetPercent": 35}}),
json!({"bleed": {"useDocument": true, "top": 9}}),
json!({"bleed": {"top": 9}, "marks": {"trim": true, "registration": true, "colorBars": true, "pageInfo": true}}),
json!({"includeNonPrinting": true}),
json!({"createLayers": true}),
json!({"viewAfterSaving": true}),
json!({"thumbnails": true, "fastWebView": true}),
json!({"compression": {"compressText": false}}),
json!({"compression": {"color": {"compression": "jpeg"}, "mono": {"compression": "ccittG4"}}}),
] {
assert!(settings(v.clone()).warnings().is_empty(), "{v}");
}
let r =
export_with_report(&doc(), &PdfOptions { settings: settings(json!({"thumbnails": true, "marks": {"trim": true}})), ..Default::default() })
.unwrap();
// The writer embeds the thumbnails it is given: without them, the one note on the options says so.
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
assert!(r.setting_notes.len() == 1 && r.setting_notes[0].contains("thumbnails need the pages drawn"), "{:?}", r.setting_notes);
assert!(r.bytes.starts_with(b"%PDF-1.7"));
}