diff --git a/crates/engine/src/cmd/export.rs b/crates/engine/src/cmd/export.rs index 48b9099c5..443dcbb23 100644 --- a/crates/engine/src/cmd/export.rs +++ b/crates/engine/src/cmd/export.rs @@ -686,8 +686,9 @@ fn rasterize_layered(d: &designcraft_doc::Document) -> Result<(Option Result { } } +/// Whether a placement still holds an Object Layer Options choice the exports can apply: a +/// graphic whose asset is a PDF. A choice left on an item that is no longer a placed PDF (its +/// link was replaced with another format) is not one, and must not block an export. +pub(crate) fn has_applicable_pdf_layer_overrides(doc: &Document) -> bool { + doc.placed_items().into_iter().any(|it| { + it.has_pdf_layer_override() && it.graphic().and_then(|g| doc.assets.get(&g.asset)).is_some_and(|a| designcraft_render::is_pdf(&a.data)) + }) +} + /// Each placement gets its own embedded variant so two frames sharing a PDF can /// choose different layers. The live document and original linked bytes stay untouched. /// @@ -56,9 +65,11 @@ fn with_pdf_layer_variants(doc: &Document, embed: bool) -> Result Result { let opts = designcraft_idml::ExportOptions { embed_images: false }; root.write(&path, &designcraft_idml::export_idml_with(&packed, &opts))?; files.push(path); - if packed.has_pdf_layer_overrides() { + if super::interchange::has_applicable_pdf_layer_overrides(&packed) { idml_layer_note = "\nPlaced PDF layers\nThe IDML copy links the placed PDFs, so it shows their file defaults. The DesignCraft document keeps each placement's Object Layer Options choice.\n"; } } diff --git a/crates/engine/src/cmd/pdflayers.rs b/crates/engine/src/cmd/pdflayers.rs index b4c236a39..aaf8841ea 100644 --- a/crates/engine/src/cmd/pdflayers.rs +++ b/crates/engine/src/cmd/pdflayers.rs @@ -588,6 +588,40 @@ pub(crate) mod tests { } } + /// Relinking a placed PDF to an image leaves the item's layer fields behind. They are no longer a + /// placed-PDF choice, so they must not block an export: the refusal guard asks for a graphic whose + /// asset is a PDF, the same thing the export walks ask for. + #[test] + fn a_stale_choice_on_a_relinked_image_does_not_block_exports() { + let pdf = designcraft_render::pdf_hide_layers(&two_layer_pdf(), &["Red".into()]).unwrap(); + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + let id = s + .execute("file.place", &json!({"base64": super::super::file::base64_encode(&pdf), "name": "art.pdf", "x": 0, "y": 0, "width": 100})) + .unwrap()["id"] + .clone(); + s.execute("object.layerOptions", &json!({"ids": [id], "hidden": ["Red"]})).unwrap(); + let asset = s.doc().unwrap().doc.item(designcraft_doc::ItemId(id.as_u64().unwrap())).unwrap().graphic().unwrap().asset; + let png = designcraft_render::Rendered { width: 4, height: 3, pixels: [30, 90, 180, 255].repeat(12) }.to_png(); + let path = std::env::temp_dir().join(format!( + "dc-relinked-{}-{}.png", + std::process::id(), + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos() + )); + std::fs::write(&path, &png).unwrap(); + s.execute("links.relink", &json!({"asset": asset.0, "path": path.to_string_lossy()})).unwrap(); + let d = &s.doc().unwrap().doc; + assert!(!designcraft_render::is_pdf(&d.assets[&asset].data), "the placement is an image now"); + assert!(d.item(designcraft_doc::ItemId(id.as_u64().unwrap())).unwrap().has_pdf_layer_override(), "the stale field is still on the item"); + assert!(d.has_pdf_layer_overrides(), "the field is there"); + assert!(!super::super::interchange::has_applicable_pdf_layer_overrides(d), "but it is not a placed-PDF choice"); + // Both exports still run: nothing is dropped, and there is no choice to refuse. + s.execute("file.exportIdml", &json!({})).unwrap(); + s.execute("file.exportPdf", &json!({"standard": "a2b"})).unwrap(); + s.execute("file.exportPdf", &json!({})).unwrap(); + std::fs::remove_file(&path).ok(); + } + #[test] fn layerless_pdf_rejects_overrides_without_breaking_idml_export() { let mut s = Session::new();