mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-30 05:06:48 -06:00
PDF: validate layer overrides and query mixed selections
This commit is contained in:
committed by
Andee Collard
parent
8d295b101e
commit
57658368af
@@ -25,6 +25,10 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
regression tests cover metadata, output-intent references, nonzero generations and
|
||||
forward Prev links; repeated cross-reference offsets are rejected explicitly. No readiness percentage change.
|
||||
|
||||
- **2026-10-23** · PDF layer commands reject overrides on layerless files atomically;
|
||||
default layer queries find a placed PDF anywhere in mixed selections. Regression tests cover
|
||||
IDML export after rejection and both selection orders.
|
||||
|
||||
## Status (2026-10-01)
|
||||
|
||||
**Working today**
|
||||
|
||||
@@ -15,7 +15,7 @@ fn placed_pdf(s: &Session, id: designcraft_doc::ItemId) -> Option<std::sync::Arc
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(query "object.pdfLayers", "Object Layers", [], None, "{id?} → [{name, visible, default}] — the layers of the selected placed PDF", has_selection, |s, p| {
|
||||
let id = super::id_param(p, "id").or_else(|| s.active().and_then(|d| d.selection.items.first().copied())).ok_or_else(|| bad("object.pdfLayers", "no selection"))?;
|
||||
let id = super::id_param(p, "id").or_else(|| s.active().and_then(|d| d.selection.items.iter().copied().find(|id| placed_pdf(s, *id).is_some()))).ok_or_else(|| bad("object.pdfLayers", "no selection"))?;
|
||||
let data = placed_pdf(s, id).ok_or_else(|| bad("object.pdfLayers", "not a placed PDF"))?;
|
||||
let item = s.doc()?.doc.item(id);
|
||||
let hidden = item.map(|i| i.pdf_hidden_layers.clone()).unwrap_or_default();
|
||||
@@ -51,6 +51,11 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
if ids.is_empty() {
|
||||
return Err(bad("object.layerOptions", "select a placed PDF"));
|
||||
}
|
||||
// A layerless/unreadable PDF cannot carry a visibility variant. Validate
|
||||
// every target before editing so a mixed request remains atomic.
|
||||
if !use_defaults && ids.iter().any(|id| placed_pdf(s, *id).is_none_or(|data| designcraft_render::pdf_layers(&data).is_empty())) {
|
||||
return Err(bad("object.layerOptions", "the selected PDF has no readable optional-content layers"));
|
||||
}
|
||||
// Validate every selected PDF before editing any item or recording undo.
|
||||
// Repeated placements of the same source need only one rewrite check.
|
||||
if !hidden.is_empty() {
|
||||
@@ -434,6 +439,27 @@ pub(crate) mod tests {
|
||||
s.execute("object.layerOptions", &json!({"ids":[id],"useDefaults":true})).unwrap();
|
||||
assert!(!visible(&mut s), "explicit reset restores defaults");
|
||||
}
|
||||
#[test]
|
||||
fn layerless_pdf_rejects_overrides_without_breaking_idml_export() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let pdf = s.execute("file.exportPdf", &json!({})).unwrap()["base64"].clone();
|
||||
let id = s.execute("file.place", &json!({"base64":pdf,"name":"plain.pdf","x":0,"y":0,"width":100})).unwrap()["id"].clone();
|
||||
let layered = s
|
||||
.execute(
|
||||
"file.place",
|
||||
&json!({"base64":super::super::file::base64_encode(&two_layer_pdf()),"name":"layered.pdf","x":120,"y":0,"width":100}),
|
||||
)
|
||||
.unwrap()["id"]
|
||||
.clone();
|
||||
let before = serde_json::to_value(&*s.doc().unwrap().doc).unwrap();
|
||||
assert!(s.execute("object.layerOptions", &json!({"ids":[layered,id],"hidden":[],"useDefaults":false})).is_err());
|
||||
assert_eq!(serde_json::to_value(&*s.doc().unwrap().doc).unwrap(), before);
|
||||
assert!(s.execute("file.exportIdml", &json!({})).is_ok());
|
||||
s.execute("object.layerOptions", &json!({"ids":[id],"hidden":[],"useDefaults":true})).unwrap();
|
||||
assert!(!s.doc().unwrap().doc.item(designcraft_doc::ItemId(id.as_u64().unwrap())).unwrap().has_pdf_layer_override());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_layer_options_ignore_non_pdf_targets_in_mixed_selection() {
|
||||
let mut s = Session::new();
|
||||
@@ -445,6 +471,11 @@ pub(crate) mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
let other = s.execute("frame.create", &json!({"rect":[0,0,20,20]})).unwrap();
|
||||
for ids in [json!([other["id"], placed["id"]]), json!([placed["id"], other["id"]])] {
|
||||
s.execute("selection.set", &json!({"ids":ids})).unwrap();
|
||||
assert_eq!(s.execute("object.pdfLayers", &json!({})).unwrap().as_array().unwrap().len(), 2);
|
||||
}
|
||||
assert!(s.execute("object.pdfLayers", &json!({"id":other["id"]})).is_err(), "explicit non-PDF id remains an error");
|
||||
s.execute("object.layerOptions", &json!({"ids":[placed["id"],other["id"]],"hidden":[],"useDefaults":false})).unwrap();
|
||||
assert!(!s.doc().unwrap().doc.item(designcraft_doc::ItemId(other["id"].as_u64().unwrap())).unwrap().has_pdf_layer_override());
|
||||
assert!(s.execute("object.layerOptions", &json!({"ids":[placed["id"]],"hidden":["Red"],"useDefaults":true})).is_err());
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Object Layer Options distinguishes a PDF's own default configuration from an explicit visibility choice, including showing every layer. Use File Defaults resets that choice. Only placed PDFs in a mixed selection are changed.
|
||||
|
||||
The engine command `object.layerOptions` accepts `hidden` names and optional `useDefaults`. Existing calls with `hidden: []` retain their reset behavior; pass `useDefaults: false` to explicitly show all layers. `useDefaults: true` resets defaults and cannot be combined with hidden names. Queries include `overridden` separately from each layer's `visible` and `default` states.
|
||||
The engine command `object.layerOptions` accepts `hidden` names and optional `useDefaults`. Existing calls with `hidden: []` retain their reset behavior; pass `useDefaults: false` to explicitly show all layers. `useDefaults: true` resets defaults and cannot be combined with hidden names. Overrides on PDFs without readable optional-content layers are rejected before any target is edited. With no explicit ID, a query selects the first placed PDF anywhere in the selection; an explicit non-PDF ID remains an error. Queries include `overridden` separately from each layer's `visible` and `default` states.
|
||||
|
||||
Native saves use format 4 only for an explicit all-on override: older readers would otherwise silently render file defaults. Existing nonempty hidden lists retain their older semantics and format requirements. Raster and PDF export honor the choice; PDF export keeps its existing warned rasterization behavior. Embedded IDML export stores a separate PDF variant per placement and leaves original linked bytes untouched. External-only IDML export rejects active PDF layer choices because the original external file would lose the choice.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user