mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-27 11:46:51 -06:00
Keep source defaults and per-frame hidden layers in plain and PDF/X-4 vector exports, including independent repeated placements, booklet output and book merging. Accept usage-application arrays and resource-only groups without a catalog configuration. Use file-named warnings and all-ON defaults for recoverable default-configuration problems and unsupported base states. Add readable groups omitted from OCGs without discarding their visibility, combine concurrent recovery warnings, and generate missing names only when no frame override needs name matching. Retain errors for contradictory states, unresolved references and ambiguous name overrides. Cache prepared variants per asset and hidden-name set while replaying recovery warnings through the existing deduplicating channel. Keep original source bytes when every group is visible and named, so reader-repaired cross-reference tables remain usable without an incremental layer update. When hidden groups or generated names require writing, reject an unusable cross-reference table with a file-named damage error and retain the first placement failure. Cover visibility, recovery, names, independent placements, repaired cross-reference tables and vector output with 46 optional-content integration tests, plus preparation-cache and shared-buffer unit tests.
239 lines
8.4 KiB
Rust
239 lines
8.4 KiB
Rust
#[path = "support/optional_content.rs"]
|
|
mod support;
|
|
|
|
#[path = "support/optional_content_fallback.rs"]
|
|
mod fallback;
|
|
|
|
#[path = "support/optional_content_xref.rs"]
|
|
mod xref;
|
|
|
|
use designcraft_compose::Cache;
|
|
use designcraft_pdf::{PdfOptions, Standard, export_pdf, export_pdf_with_report};
|
|
use support::{catalog, document, fixture, painted};
|
|
|
|
const DEFAULT: &str = "/BaseState/ON/OFF[6 3 R]";
|
|
const USAGE_APPLICATIONS: &str = "/BaseState/ON/ON[5 0 R]/OFF[6 3 R]/AS[<</Event/View/Category[/View]/OCGs[5 0 R 6 3 R]>><</Event/Print/Category[/Print]/OCGs[5 0 R 6 3 R]>><</Event/Export/Category[/Export]/OCGs[5 0 R 6 3 R]>>]";
|
|
|
|
#[test]
|
|
fn source_layer_dictionaries_are_readable() {
|
|
let pdf = hayro_syntax::Pdf::new(fixture(DEFAULT, "Hidden")).unwrap();
|
|
for id in [(5, 0), (6, 3)] {
|
|
assert!(pdf.xref().get::<hayro_syntax::object::Dict<'_>>(hayro_syntax::object::ObjectIdentifier::new(id.0, id.1)).is_some(), "{id:?}");
|
|
}
|
|
}
|
|
|
|
fn exported(config: &str, standard: Standard) -> Vec<u8> {
|
|
let doc = document(fixture(config, "Hidden"), &[&[]]);
|
|
let report = export_pdf_with_report(&doc, &Cache::new(), &PdfOptions { standard, created: Some(0), ..Default::default() }).unwrap();
|
|
assert!(report.warnings.is_empty(), "{:?}", report.warnings);
|
|
report.bytes
|
|
}
|
|
|
|
#[test]
|
|
fn source_default_off_renders_before_export() {
|
|
let bytes = fixture(DEFAULT, "Hidden");
|
|
|
|
painted(&bytes, &[true, false]);
|
|
}
|
|
|
|
#[test]
|
|
fn plain_export_registers_default_off_layers() {
|
|
let bytes = exported(DEFAULT, Standard::None);
|
|
|
|
assert_eq!(catalog(&bytes), [("Hidden".into(), true), ("Shown".into(), false)]);
|
|
}
|
|
|
|
#[test]
|
|
fn pdfx_export_preserves_default_off_catalog() {
|
|
let bytes = exported(DEFAULT, Standard::PdfX4);
|
|
|
|
assert_eq!(catalog(&bytes), [("Hidden".into(), true), ("Shown".into(), false)]);
|
|
assert!(designcraft_pdf::check_pdfx4(&bytes).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn plain_export_paints_only_the_shown_layer() {
|
|
let bytes = exported(DEFAULT, Standard::None);
|
|
|
|
painted(&bytes, &[true, false]);
|
|
}
|
|
|
|
#[test]
|
|
fn pdfx_export_paints_only_the_shown_layer() {
|
|
let bytes = exported(DEFAULT, Standard::PdfX4);
|
|
|
|
painted(&bytes, &[true, false]);
|
|
}
|
|
|
|
#[test]
|
|
fn plain_export_preserves_defaults_with_usage_applications() {
|
|
let bytes = exported(USAGE_APPLICATIONS, Standard::None);
|
|
|
|
assert_eq!(catalog(&bytes), [("Hidden".into(), true), ("Shown".into(), false)]);
|
|
assert_vector(&bytes);
|
|
painted(&bytes, &[true, false]);
|
|
}
|
|
|
|
#[test]
|
|
fn pdfx_export_preserves_defaults_with_usage_applications() {
|
|
let bytes = exported(USAGE_APPLICATIONS, Standard::PdfX4);
|
|
|
|
assert_eq!(catalog(&bytes), [("Hidden".into(), true), ("Shown".into(), false)]);
|
|
assert!(designcraft_pdf::check_pdfx4(&bytes).is_empty());
|
|
assert_vector(&bytes);
|
|
painted(&bytes, &[true, false]);
|
|
}
|
|
|
|
#[test]
|
|
fn plain_export_draws_groups_without_catalog_properties() {
|
|
let doc = document(support::orphaned_fixture(), &[&[]]);
|
|
|
|
let bytes = export_pdf(&doc, &Cache::new(), &PdfOptions::default()).unwrap();
|
|
|
|
assert_eq!(catalog(&bytes), [("Hidden".into(), false), ("Shown".into(), false)]);
|
|
assert_vector(&bytes);
|
|
painted(&bytes, &[true, true]);
|
|
}
|
|
|
|
#[test]
|
|
fn pdfx_export_draws_groups_without_catalog_properties() {
|
|
let doc = document(support::orphaned_fixture(), &[&[]]);
|
|
|
|
let bytes = export_pdf(&doc, &Cache::new(), &PdfOptions { standard: Standard::PdfX4, ..Default::default() }).unwrap();
|
|
|
|
assert_eq!(catalog(&bytes), [("Hidden".into(), false), ("Shown".into(), false)]);
|
|
assert!(designcraft_pdf::check_pdfx4(&bytes).is_empty());
|
|
assert_vector(&bytes);
|
|
painted(&bytes, &[true, true]);
|
|
}
|
|
|
|
#[test]
|
|
fn base_off_with_on_exception_is_preserved() {
|
|
let bytes = exported("/BaseState/OFF/ON[5 0 R]", Standard::None);
|
|
|
|
assert_eq!(catalog(&bytes), [("Hidden".into(), true), ("Shown".into(), false)]);
|
|
painted(&bytes, &[true, false]);
|
|
}
|
|
|
|
#[test]
|
|
fn same_asset_placements_keep_opposing_layer_states() {
|
|
let doc = document(fixture(DEFAULT, "Hidden"), &[&[], &["Shown"]]);
|
|
|
|
let bytes = export_pdf(&doc, &Cache::new(), &PdfOptions::default()).unwrap();
|
|
|
|
assert_eq!(catalog(&bytes), [("Hidden".into(), false), ("Hidden".into(), true), ("Shown".into(), false), ("Shown".into(), true)]);
|
|
painted(&bytes, &[true, false, false, true]);
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_names_with_distinct_generations_keep_default_states() {
|
|
let doc = document(fixture(DEFAULT, "Shown"), &[&[]]);
|
|
|
|
let bytes = export_pdf(&doc, &Cache::new(), &PdfOptions::default()).unwrap();
|
|
|
|
assert_eq!(catalog(&bytes), [("Shown".into(), false), ("Shown".into(), true)]);
|
|
painted(&bytes, &[true, false]);
|
|
}
|
|
|
|
#[test]
|
|
fn unresolved_default_off_reference_fails_export() {
|
|
let doc = document(fixture("/OFF[99 0 R]", "Hidden"), &[&[]]);
|
|
|
|
let result = export_pdf(&doc, &Cache::new(), &PdfOptions::default());
|
|
|
|
assert!(result.is_err(), "an unreadable layer state must not print hidden artwork");
|
|
}
|
|
|
|
#[test]
|
|
fn unlayered_pdf_stays_vector_without_optional_content() {
|
|
let doc = document(support::unlayered_fixture(), &[&[]]);
|
|
|
|
let bytes = export_pdf(&doc, &Cache::new(), &PdfOptions::default()).unwrap();
|
|
|
|
let pdf = hayro_syntax::Pdf::new(bytes.clone()).unwrap();
|
|
let root = pdf.xref().get::<hayro_syntax::object::Dict<'_>>(pdf.xref().root_id()).unwrap();
|
|
assert!(!root.contains_key(b"OCProperties"));
|
|
assert_vector(&bytes);
|
|
painted(&bytes, &[true, false]);
|
|
}
|
|
|
|
fn assert_vector(bytes: &[u8]) {
|
|
use hayro_syntax::object::{Name, Object};
|
|
let pdf = hayro_syntax::Pdf::new(bytes.to_vec()).unwrap();
|
|
for object in pdf.objects() {
|
|
if let Object::Stream(stream) = object {
|
|
assert_ne!(stream.dict().get::<Name<'_>>(b"Subtype").as_ref().map(AsRef::as_ref), Some(b"Image".as_slice()));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn layer_options_command_preserves_vector_overrides_in_plain_and_pdfx() {
|
|
use designcraft_engine::{
|
|
Session,
|
|
cmd::{base64_decode, base64_encode},
|
|
};
|
|
use serde_json::json;
|
|
for standard in ["none", "x4"] {
|
|
let mut session = Session::new();
|
|
session.execute("file.new", &json!({"width": 100, "height": 100, "pages": 1, "facingPages": false, "margins": 0})).unwrap();
|
|
session
|
|
.execute("file.place", &json!({"base64": base64_encode(&fixture(DEFAULT, "Hidden")), "name": "layers.pdf", "x": 0, "y": 0, "width": 100}))
|
|
.unwrap();
|
|
session.execute("object.layerOptions", &json!({"hidden": ["Shown"]})).unwrap();
|
|
|
|
let result = session.execute("file.exportPdf", &json!({"standard": standard})).unwrap();
|
|
|
|
let bytes = base64_decode(result["base64"].as_str().unwrap());
|
|
assert_eq!(catalog(&bytes), [("Hidden".into(), false), ("Shown".into(), true)]);
|
|
assert_vector(&bytes);
|
|
painted(&bytes, &[false, true]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn exported_optional_content_survives_replacement_and_book_merge() {
|
|
let first = exported(DEFAULT, Standard::None);
|
|
let doc = document(first.clone(), &[&[]]);
|
|
|
|
let replaced = export_pdf(&doc, &Cache::new(), &PdfOptions::default()).unwrap();
|
|
let merged = designcraft_pdf::merge_pdfs(&[(first, vec![(100.0, 100.0)])], None).unwrap();
|
|
|
|
for bytes in [replaced, merged] {
|
|
assert_eq!(catalog(&bytes), [("Hidden".into(), true), ("Shown".into(), false)]);
|
|
painted(&bytes, &[true, false]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn booklet_export_preserves_optional_content() {
|
|
let doc = document(fixture(DEFAULT, "Hidden"), &[&[]]);
|
|
let options = designcraft_pdf::BookletOptions { kind: designcraft_pdf::BookletKind::TwoUpConsecutive, ..Default::default() };
|
|
|
|
let report = designcraft_pdf::export_booklet(&doc, &Cache::new(), &options).unwrap();
|
|
|
|
assert!(report.warnings.is_empty());
|
|
assert_eq!(catalog(&report.bytes), [("Hidden".into(), true), ("Shown".into(), false)]);
|
|
assert_vector(&report.bytes);
|
|
painted(&report.bytes, &[true, false, false, false]);
|
|
}
|
|
|
|
#[test]
|
|
fn archival_export_still_rejects_unvalidated_placed_pdf() {
|
|
let doc = document(fixture(DEFAULT, "Hidden"), &[&[]]);
|
|
|
|
let error = export_pdf(&doc, &Cache::new(), &PdfOptions { standard: Standard::PdfA2b, ..Default::default() }).unwrap_err();
|
|
|
|
assert!(error.to_string().contains("EmbeddedPDF"), "{error}");
|
|
}
|
|
|
|
#[test]
|
|
fn source_xref_stream_preserves_layer_states() {
|
|
let doc = document(support::stream_fixture(DEFAULT), &[&[]]);
|
|
|
|
let bytes = export_pdf(&doc, &Cache::new(), &PdfOptions::default()).unwrap();
|
|
|
|
assert_eq!(catalog(&bytes), [("Hidden".into(), true), ("Shown".into(), false)]);
|
|
painted(&bytes, &[true, false]);
|
|
}
|