mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 19:27:21 -06:00
Colour, greyscale and monochrome (black-and-white) images placed above the threshold resolution are resampled to the target (average, subsample or bicubic) and compressed with ZIP, JPEG at the chosen quality, or automatically (JPEGs stay JPEG, the others lossless); images already as asked are embedded unchanged. JPEG 2000, CCITT, Run Length and no compression are written as ZIP with a warning when an image needs them; images with transparency stay lossless. Images no longer ask viewers to smooth them in PDF/A files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
5.2 KiB
Rust
104 lines
5.2 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.3"}), 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();
|
|
assert!(matches!(refused(json!({"standard": "pdfX4"})), PdfError::Unsupported(_)));
|
|
assert!(matches!(refused(json!({"security": {"openPassword": "x"}})), PdfError::Unsupported(_)));
|
|
assert!(matches!(refused(json!({"security": {"permissionsPassword": "x"}})), PdfError::Unsupported(_)));
|
|
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());
|
|
}
|
|
|
|
#[test]
|
|
fn options_not_applied_yet_come_back_as_warnings() {
|
|
assert!(PdfSettings::default().warnings().is_empty());
|
|
for (v, word) in [
|
|
(json!({"thumbnails": true}), "thumbnails"),
|
|
(json!({"fastWebView": true}), "fast web view"),
|
|
(json!({"createLayers": true}), "layers"),
|
|
(json!({"marks": {"registration": true}}), "marks"),
|
|
(json!({"bleed": {"top": 9}}), "bleed"),
|
|
(json!({"output": {"conversion": "destination"}}), "conversion"),
|
|
(json!({"output": {"profiles": "all"}}), "ICC"),
|
|
(json!({"output": {"trapped": true}}), "trapped"),
|
|
(json!({"advanced": {"outlineText": false}}), "outlines"),
|
|
(json!({"security": {"printing": "low"}}), "permissions"),
|
|
] {
|
|
let w = settings(v.clone()).warnings();
|
|
assert!(w.len() == 1 && w[0].contains(word), "{v}: {w:?}");
|
|
}
|
|
// A document bleed, the default view/overprint choices and applied options (image compression
|
|
// too: a codec the writer lacks is reported when an image needs it) warn about nothing.
|
|
for v in [
|
|
json!({"bleed": {"useDocument": true, "top": 9}}),
|
|
json!({"viewAfterSaving": 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();
|
|
assert_eq!(r.warnings.len(), 2, "{:?}", r.warnings);
|
|
assert!(r.bytes.starts_with(b"%PDF-1.7"));
|
|
}
|