mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-28 12:16:53 -06:00
M6: refuse an export only for a choice it could have applied
The refusal guard asked whether any item still had the layer fields, while the export walks also require a graphic whose asset is a PDF. Relinking a placed PDF to an image keeps those fields on the item (links.relink replaces the asset in place), so a stale field blocked IDML and PDF/A-2b exports that used to work. The guard, and the package report, now ask the same question the walks ask: has_applicable_pdf_layer_overrides requires the PDF asset too, so a stale choice is simply ignored and the tripwire still fires if a future construct ever puts a real choice where the walk cannot reach it. Regression: a_stale_choice_on_a_relinked_image_does_not_block_exports places a layered PDF, hides a layer, relinks that asset to a PNG and exports IDML, PDF/A-2b and plain PDF - all succeed, and the document reports the stale field while the applicable helper does not.
This commit is contained in:
@@ -686,8 +686,9 @@ fn rasterize_layered(d: &designcraft_doc::Document) -> Result<(Option<designcraf
|
||||
})
|
||||
.collect();
|
||||
if choices.is_empty() {
|
||||
// Fail closed: a choice this walk cannot reach must not become the file's defaults.
|
||||
if d.has_pdf_layer_overrides() {
|
||||
// Fail closed, with the same predicate as the walk: a stale choice on an item that is no
|
||||
// longer a placed PDF must not block the export.
|
||||
if super::interchange::has_applicable_pdf_layer_overrides(d) {
|
||||
return Err(EngineError::Other("a placed PDF's layer choice sits where this raster export cannot apply it".into()));
|
||||
}
|
||||
return Ok((None, 0));
|
||||
|
||||
@@ -40,6 +40,15 @@ fn export_idml(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<Option<Documen
|
||||
})
|
||||
.collect();
|
||||
if choices.is_empty() {
|
||||
// A choice this walk cannot reach would silently become the file's defaults (a newer
|
||||
// nesting construct, say), so the copy is refused instead.
|
||||
if doc.has_pdf_layer_overrides() {
|
||||
// A choice the walk cannot reach would silently become the file's defaults (a newer nesting
|
||||
// construct, say), so the copy is refused instead. The predicate matches the walk above:
|
||||
// both ask for a graphic whose asset is a PDF, so stale fields on a relinked image never
|
||||
// block an export.
|
||||
if has_applicable_pdf_layer_overrides(doc) {
|
||||
return Err(EngineError::Other("a placed PDF's layer choice sits where this IDML export cannot apply it".into()));
|
||||
}
|
||||
return Ok(None);
|
||||
|
||||
@@ -437,7 +437,7 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user