Export: hidden PDF layers place symbol art effects when a long page raises the version

A PDF/A-2b export at PDF 1.4 with Create PDF Layers and a page longer
than 14,400 pt is written as PDF 1.6, which has PDF layers. The export
decided which layers place their instances' symbol art before it raised
the version, so a hidden layer, written as a PDF layer that is off, kept
its instances' art without the art's live effects.

The version a long page needs is now settled before the document is
baked. The pages come from the artboards, which baking leaves as they
are.
This commit is contained in:
dotCooCoo
2026-11-14 13:28:34 -08:00
parent 107078ad10
commit 9fb5abfbc6
2 changed files with 36 additions and 3 deletions
+5 -3
View File
@@ -45,6 +45,11 @@ fn export_with_report_inner(doc: &Document, opts: &PdfOptions, left_out: &mut Ha
let set = &opts.settings;
set.check()?;
vectorcraft_doc::cancel::check().map_err(|_| PdfError::Cancelled)?;
// The pages come from the artboards, which baking leaves as they are. The version a long page
// raises the file to decides whether the layers are written, so it is settled before baking.
let pages = pages(doc, opts)?;
let raised = for_long_pages(set, &pages)?;
let set = raised.as_ref().unwrap_or(set);
// Live geometry effects export as their result, in symbols' art too; raster effects are
// reported below.
// Only the layers this export writes place their instances' art (see `ex.non_printing` and
@@ -57,9 +62,6 @@ fn export_with_report_inner(doc: &Document, opts: &PdfOptions, left_out: &mut Ha
// Baking stops early when cancelled, leaving a half-baked copy that must not be written.
vectorcraft_doc::cancel::check().map_err(|_| PdfError::Cancelled)?;
let doc = baked.as_ref().unwrap_or(doc);
let pages = pages(doc, opts)?;
let raised = for_long_pages(set, &pages)?;
let set = raised.as_ref().unwrap_or(set);
// The file's Title is File Info's; the page information names the document, as its tab does.
let title = crate::pdfx::title(set.standard, opts.title.clone().unwrap_or_else(|| doc.info_title().to_owned()));
+31
View File
@@ -131,6 +131,37 @@ fn pdf_layers_import_back_as_layers() {
}
}
/// A hidden layer written as a PDF layer places the live effects of its instances' symbol art,
/// also in a PDF/A-2b file at PDF 1.4 whose 17,000 pt page has it written as PDF 1.6: the symbol's
/// square from x = 10 to 40, which a Transform effect moves 100 pt right, is written from x = 110
/// to 140.
#[test]
fn a_hidden_layer_places_symbol_art_effects_when_a_long_page_raises_the_version() {
let mut d = Document::new(17_000.0, 300.0);
d.layers.clear();
let mut square = rect(&mut d, 10.0, RED);
square.appearance.effects.push(vectorcraft_doc::Effect { id: "distort.transform".into(), params: json!({"moveH": 100.0}), visible: true });
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Moved".into(), art: square.into() });
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Moved".into(), xf: vectorcraft_geom::Affine::IDENTITY });
let mut l = Node::layer(d.alloc_id(), "Hidden", LayerColor::Preset(0));
l.visible = false;
if let NodeKind::Layer { children, .. } = &mut l.kind {
children.push(instance.into());
}
d.layers.push(l.into());
for compatibility in [Compatibility::Pdf14, Compatibility::Pdf16] {
let settings = PdfSettings { standard: Standard::PdfA2b, compatibility, create_layers: true, ..PdfSettings::default() };
let r = export_with_report(&d, &PdfOptions { settings, created: Some(1_791_200_000), ..Default::default() }).unwrap();
assert!(r.bytes.starts_with(b"%PDF-1.6"), "{compatibility:?}");
let back = import_with_report(&r.bytes, &ImportOptions::default()).unwrap().document;
let hidden = back.layers.iter().find(|l| l.name.as_deref() == Some("Hidden")).expect("the hidden layer");
let art = hidden.children().unwrap();
assert_eq!(art.len(), 1, "{compatibility:?}");
let b = art[0].geometric_bounds().unwrap();
assert!((b.x0 - 110.0).abs() < 0.1 && (b.x1 - 140.0).abs() < 0.1, "{compatibility:?}: {b:?}");
}
}
/// The rows of `nodes` in paint order, depth first: `depth name visible` for layers, `depth fill`
/// for art.
fn tree(nodes: &[std::sync::Arc<Node>], depth: usize, out: &mut Vec<String>) {