PDF: distinguish all-on layer choices from file defaults

This commit is contained in:
andeecollard
2026-10-24 13:23:02 +01:00
committed by Andee Collard
parent 2633fe8b5d
commit 8d295b101e
12 changed files with 249 additions and 33 deletions
+3
View File
@@ -12,6 +12,9 @@
DesignCraft aims at full Adobe InDesign parity — and to be better: faster, open (documented JSON format + IDML), scriptable by agents (MCP), and available on the web.
- **2026-10-23** · Placed PDF layers can explicitly all be shown, separately from Use File Defaults.
Legacy commands keep their reset behavior; native saves require a capable reader only for
the new all-on state, and PDF/embedded IDML exports preserve the choice. No readiness percentage change.
- **2026-10-23** · IDML graphic-layer export rejects parents in already closed subtrees,
preserving native-edited layer order through round trips. Synthetic regression added.
- **2026-10-23** · PDF layer choices now validate before editing all targets; rejected rewrites
+20
View File
@@ -31,6 +31,17 @@ fn story_layer_options(st: &Story, depth: usize, overrides_only: bool) -> bool {
|| st.notes.iter().chain(&st.endnotes).any(|note| story_layer_options(&note.text, depth + 1, overrides_only))
}
fn item_any(it: &Item, depth: usize, f: &impl Fn(&Item) -> bool) -> bool {
depth > MAX_DEPTH || f(it) || it.children().iter().any(|child| item_any(child, depth + 1, f))
}
fn story_any(st: &Story, depth: usize, f: &impl Fn(&Item) -> bool) -> bool {
depth > MAX_DEPTH
|| st.objects.iter().any(|object| item_any(&object.item, depth + 1, f))
|| st.tables.values().flat_map(|table| &table.cells).any(|cell| story_any(&cell.text, depth + 1, f))
|| st.notes.iter().chain(&st.endnotes).any(|note| story_any(&note.text, depth + 1, f))
}
impl Document {
/// Whether saving needs a reader that retains per-placement graphic layer settings.
/// Exceeding the traversal budget conservatively requires the newer format.
@@ -39,6 +50,15 @@ impl Document {
|| self.stories.values().any(|story| story_layer_options(story, 0, false))
}
/// Whether any placed PDF, on document or parent spreads, in groups or anchored, explicitly
/// shows every layer (an override with an empty hidden list). Older readers would take that
/// for "use the file's defaults". Exceeding the traversal budget conservatively says yes.
pub fn has_explicit_all_on_pdf_layers(&self) -> bool {
let all_on = |it: &Item| it.pdf_layer_override && it.pdf_hidden_layers.is_empty();
self.spreads.iter().chain(&self.parents).flat_map(|spread| &spread.items).any(|item| item_any(item, 0, &all_on))
|| self.stories.values().any(|story| story_any(story, 0, &all_on))
}
/// Placements outside the ordinary document-spread frame walk used by preflight.
pub fn has_unwalked_graphic_layer_visibility_overrides(&self) -> bool {
self.parents.iter().flat_map(|spread| &spread.items).any(|item| item_layer_options(item, 0, true))
+13
View File
@@ -1398,6 +1398,10 @@ pub struct Item {
/// Object › Object Layer Options: layers of a placed PDF that are hidden.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub pdf_hidden_layers: Vec<String>,
/// Explicit placed-PDF visibility, including an all-on override with no hidden names.
/// False with an empty hidden list retains the file's own settings (legacy behavior).
#[serde(default, skip_serializing_if = "is_false")]
pub pdf_layer_override: bool,
/// Window › Interactive › Media: options of a placed video or sound.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub media: Option<MediaOptions>,
@@ -1430,7 +1434,15 @@ fn one() -> f32 {
1.0
}
fn is_false(value: &bool) -> bool {
!*value
}
impl Item {
pub fn has_pdf_layer_override(&self) -> bool {
self.pdf_layer_override || !self.pdf_hidden_layers.is_empty()
}
pub fn new(id: ItemId, layer: LayerId, shape: Shape, path: PathData) -> Self {
Item {
id,
@@ -1457,6 +1469,7 @@ impl Item {
xml_tag: String::new(),
button: None,
pdf_hidden_layers: Vec::new(),
pdf_layer_override: false,
live_caption: None,
form_field: None,
export_options: ExportOptions::default(),
+2 -2
View File
@@ -673,7 +673,7 @@ fn wrap_stand_in(it: &designcraft_doc::Item, depth: usize) -> designcraft_doc::I
/// A copy of `d` where placed PDFs with Object Layer Options hidden layers are 300 ppi images.
fn rasterize_layered(d: &designcraft_doc::Document) -> Result<(Option<designcraft_doc::Document>, usize)> {
let ids: Vec<designcraft_doc::ItemId> =
d.all_items().into_iter().filter(|id| d.item(*id).is_some_and(|it| !it.pdf_hidden_layers.is_empty() && it.graphic().is_some())).collect();
d.all_items().into_iter().filter(|id| d.item(*id).is_some_and(|it| it.has_pdf_layer_override() && it.graphic().is_some())).collect();
if ids.is_empty() {
return Ok((None, 0));
}
@@ -690,7 +690,7 @@ fn rasterize_layered(d: &designcraft_doc::Document) -> Result<(Option<designcraf
let [l, t, r, b] = shown.unwrap_or([0.0, 0.0, 1.0, 1.0]);
let page_side = (g.size.0 / (r - l)).max(g.size.1 / (b - t));
let side = (page_side * 300.0 / 72.0).clamp(64.0, 8000.0) as u32;
let png = designcraft_render::pdf_page_png_checked(&a.data, a.page as usize, side, &hidden, shown)
let png = designcraft_render::pdf_page_png_options_checked(&a.data, a.page as usize, side, Some(&hidden), shown)
.map_err(|e| EngineError::Other(format!("{}: {e}", a.name)))?;
let px = designcraft_render::image_size(&png);
let aid = designcraft_doc::AssetId(out.alloc());
+44 -1
View File
@@ -28,7 +28,8 @@ pub fn specs() -> Vec<CommandSpec> {
fn export_idml(s: &mut Session, p: &Value) -> Result<Value> {
let st = s.doc()?;
let opts = designcraft_idml::ExportOptions { embed_images: p.get("embedImages").and_then(Value::as_bool).unwrap_or(true) };
let bytes = designcraft_idml::export_idml_with(&st.doc, &opts);
let layer_doc = with_pdf_layer_variants(&st.doc, opts.embed_images)?;
let bytes = designcraft_idml::export_idml_with(layer_doc.as_ref().unwrap_or(&st.doc), &opts);
match str_param(p, "path") {
Some(path) => {
#[cfg(not(target_arch = "wasm32"))]
@@ -39,6 +40,48 @@ fn export_idml(s: &mut Session, p: &Value) -> Result<Value> {
}
}
/// 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.
fn with_pdf_layer_variants(doc: &Document, embed: bool) -> Result<Option<Document>> {
let ids: Vec<_> = doc
.all_items()
.into_iter()
.filter(|id| {
doc.item(*id).is_some_and(|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))
})
})
.collect();
if ids.is_empty() {
return Ok(None);
}
if !embed {
return Err(EngineError::Other("PDF layer choices require embedded images in IDML export".into()));
}
let mut out = doc.clone();
for id in ids {
let Some(it) = doc.item(id) else { continue };
let Some(asset) = it.graphic().and_then(|g| doc.assets.get(&g.asset)) else { continue };
let bytes = designcraft_render::pdf_hide_layers(&asset.data, &it.pdf_hidden_layers)
.ok_or_else(|| EngineError::Other(format!("{}: PDF layer choices could not be applied", asset.name)))?;
let mut variant = (**asset).clone();
variant.id = designcraft_doc::AssetId(out.alloc());
variant.data = std::sync::Arc::new(bytes);
variant.link = None;
let aid = variant.id;
out.assets.insert(aid, std::sync::Arc::new(variant));
if let Some(it) = out.item_mut(id) {
if let designcraft_doc::Content::Graphic(g) = &mut it.content {
g.asset = aid;
}
it.pdf_layer_override = false;
it.pdf_hidden_layers.clear();
}
}
Ok(Some(out))
}
/// [`designcraft_idml::import_idml`] with the linked images read through [`crate::io`] from
/// beside `package` (the package's file, if it has one), so the automation read root applies to
/// the paths the package names, and its PDF crops resolved.
+78 -6
View File
@@ -17,11 +17,13 @@ pub fn specs() -> Vec<CommandSpec> {
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 data = placed_pdf(s, id).ok_or_else(|| bad("object.pdfLayers", "not a placed PDF"))?;
let hidden = s.doc()?.doc.item(id).map(|i| i.pdf_hidden_layers.clone()).unwrap_or_default();
let item = s.doc()?.doc.item(id);
let hidden = item.map(|i| i.pdf_hidden_layers.clone()).unwrap_or_default();
let overridden = item.is_some_and(|i| i.has_pdf_layer_override());
Ok(Value::Array(
designcraft_render::pdf_layers(&data)
.into_iter()
.map(|(name, on)| json!({"visible": if hidden.is_empty() { on } else { !hidden.contains(&name) }, "name": name, "default": on}))
.map(|(name, on)| json!({"visible": if overridden { !hidden.contains(&name) } else { on }, "name": name, "default": on, "overridden": overridden}))
.collect(),
))
}),
@@ -30,16 +32,23 @@ pub fn specs() -> Vec<CommandSpec> {
"Object Layer Options…",
["Object"],
None,
"{ids?, hidden: [layer name]} — layers of placed PDFs to hide ([] = the file's own settings)",
"{ids?, hidden: [layer name], useDefaults?: bool} — hide layers; useDefaults:false makes [] explicitly all on; legacy [] restores file defaults",
has_selection,
|s, p| {
let ids = targets(s, p)?;
let ids: Vec<_> = targets(s, p)?.into_iter().filter(|id| placed_pdf(s, *id).is_some()).collect();
let hidden: Vec<String> = p
.get("hidden")
.and_then(Value::as_array)
.map(|a| a.iter().filter_map(Value::as_str).map(str::to_string).collect())
.unwrap_or_default();
if ids.iter().all(|id| placed_pdf(s, *id).is_none()) {
let use_defaults = match p.get("useDefaults") {
None => hidden.is_empty(),
Some(v) => v.as_bool().ok_or_else(|| bad("object.layerOptions", "useDefaults must be a boolean"))?,
};
if use_defaults && !hidden.is_empty() {
return Err(bad("object.layerOptions", "useDefaults cannot be combined with hidden layers"));
}
if ids.is_empty() {
return Err(bad("object.layerOptions", "select a placed PDF"));
}
// Validate every selected PDF before editing any item or recording undo.
@@ -62,9 +71,10 @@ pub fn specs() -> Vec<CommandSpec> {
for id in &ids {
if let Some(it) = d.item_mut(*id) {
it.pdf_hidden_layers = hidden.clone();
it.pdf_layer_override = !use_defaults;
}
}
Ok(json!({"hidden": hidden}))
Ok(json!({"hidden": hidden, "useDefaults": use_defaults}))
})
}
),
@@ -378,4 +388,66 @@ pub(crate) mod tests {
let r = s.execute("file.exportPdf", &json!({})).unwrap();
assert!(r["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("layer")), "{r}");
}
#[test]
fn placed_pdf_all_on_is_distinct_from_defaults_and_survives_exports() {
let pdf = designcraft_render::pdf_hide_layers(&two_layer_pdf(), &["Red".into()]).unwrap();
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
let id = s
.execute("file.place", &json!({"base64": super::super::file::base64_encode(&pdf), "name":"off.pdf", "x":100,"y":100,"width":100}))
.unwrap()["id"]
.clone();
let visible = |s: &mut Session| s.execute("object.pdfLayers", &json!({"id": id})).unwrap()[1]["visible"].as_bool().unwrap();
let pixel = |s: &Session| {
let mut rr = designcraft_render::Renderer::new();
rr.threads = 0;
rr.render_page(&s.doc().unwrap().doc, &s.cache, 0, 1.0, false, &Default::default()).unwrap().pixel(175, 150)
};
assert!(!visible(&mut s));
assert!(pixel(&s)[1] > 200, "red hidden by default");
s.execute("object.layerOptions", &json!({"ids":[id], "hidden":[], "useDefaults":false})).unwrap();
assert!(visible(&mut s));
assert!(pixel(&s)[0] > 200 && pixel(&s)[1] < 40, "all-on renders red");
s.execute("edit.undo", &json!({})).unwrap();
assert!(!visible(&mut s));
s.execute("edit.redo", &json!({})).unwrap();
assert!(visible(&mut s));
let bytes = designcraft_format::save(&s.doc().unwrap().doc).unwrap();
let roundtrip = designcraft_format::load(&bytes).unwrap();
let item = roundtrip.item(designcraft_doc::ItemId(id.as_u64().unwrap())).unwrap();
assert!(item.pdf_layer_override && item.pdf_hidden_layers.is_empty());
let exported = s.execute("file.exportPdf", &json!({})).unwrap();
let bytes = super::super::file::base64_decode(exported["base64"].as_str().unwrap());
let raster = designcraft_render::render_pdf_page(&bytes, 0, 842).unwrap();
let px = raster.data().get(150 * usize::from(raster.width()) + 175).unwrap();
assert!(px.r > 200 && px.g < 40, "PDF export retains red: {px:?}");
let idml = s.execute("file.exportIdml", &json!({})).unwrap();
let bytes = super::super::file::base64_decode(idml["base64"].as_str().unwrap());
let imported = designcraft_idml::import_idml(&bytes).unwrap();
let pdf_asset = imported.assets.values().find(|a| designcraft_render::is_pdf(&a.data)).unwrap();
assert!(designcraft_render::pdf_layers(&pdf_asset.data).iter().all(|(_, on)| *on));
assert!(s.execute("file.exportIdml", &json!({"embedImages":false})).is_err());
assert_eq!(s.doc().unwrap().doc.assets.values().find(|a| a.name == "off.pdf").unwrap().data.as_slice(), pdf);
s.execute("object.layerOptions", &json!({"ids":[id],"hidden":[]})).unwrap();
assert!(!visible(&mut s), "legacy empty list still resets defaults");
s.execute("object.layerOptions", &json!({"ids":[id],"hidden":[],"useDefaults":false})).unwrap();
s.execute("object.layerOptions", &json!({"ids":[id],"useDefaults":true})).unwrap();
assert!(!visible(&mut s), "explicit reset restores defaults");
}
#[test]
fn pdf_layer_options_ignore_non_pdf_targets_in_mixed_selection() {
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
let placed = s
.execute(
"file.place",
&json!({"base64": super::super::file::base64_encode(&two_layer_pdf()), "name":"art.pdf", "x":100,"y":100,"width":100}),
)
.unwrap();
let other = s.execute("frame.create", &json!({"rect":[0,0,20,20]})).unwrap();
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());
assert!(s.execute("object.layerOptions", &json!({"ids":[placed["id"]],"useDefaults":"false"})).is_err());
}
}
+38 -6
View File
@@ -22,10 +22,10 @@ use serde_json::{Value, json};
pub const MIME: &str = "application/vnd.designcraft+zip";
/// Latest reader capability. Version 2 pinned the Keep Lines Together and drop cap defaults;
/// version 3 adds rendering of retained EPS source assets; version 4 retains placed graphic layer
/// options. Version 4 is one capability shared with explicit all-on placed-PDF layer choices
/// (storytold/designcraft#1671): it must not be released until a reader understands both, and any
/// later capability needs version 5. [`required_version`] is the only place that picks a version.
/// version 3 adds rendering of retained EPS source assets; version 4 is one capability covering both
/// retained placed-graphic layer options and explicit all-on placed-PDF layer choices, so a version 4
/// reader understands both. Any later capability needs version 5. [`required_version`] is the only
/// place that picks a version.
pub const VERSION: u32 = 4;
#[derive(Debug, thiserror::Error)]
@@ -73,8 +73,10 @@ const LARGE_ENTRY: usize = (u32::MAX as usize).saturating_sub(64 << 20);
/// The oldest reader capability that keeps everything in `doc`. Documents that need nothing newer
/// keep their older requirement so existing readers still open them.
fn required_version(doc: &Document) -> u32 {
// Older readers ignore unknown Graphic fields and would silently drop layer choices.
if doc.has_graphic_layer_options() {
// Version 4 covers both of these: older readers ignore the unknown fields, so they would
// silently drop retained graphic layer choices, or read an explicit all-on PDF layer choice
// (empty hidden list) as "use the file's defaults". A nonempty hidden list stays readable.
if doc.has_graphic_layer_options() || doc.has_explicit_all_on_pdf_layers() {
return 4;
}
// Older readers would silently omit EPS artwork. Preview-only legacy assets and documents
@@ -454,6 +456,36 @@ mod tests {
buf.into_inner()
}
#[test]
fn explicit_all_on_pdf_layers_require_a_capable_reader_only_when_needed() {
let mut d = Document::new(&NewDocument::default());
let aid = AssetId(d.alloc());
d.assets.insert(
aid,
Arc::new(Asset {
id: aid,
name: "art.pdf".into(),
mime: "application/pdf".into(),
data: Arc::new(b"%PDF-1.7".to_vec()),
..Default::default()
}),
);
let id = place(&mut d, aid, 0.0);
let version = |d: &Document| {
let bytes = save(d).unwrap();
let mut z = zip::ZipArchive::new(Cursor::new(bytes)).unwrap();
let mut meta = String::new();
z.by_name("meta.json").unwrap().read_to_string(&mut meta).unwrap();
serde_json::from_str::<Value>(&meta).unwrap()["version"].as_u64().unwrap()
};
assert_eq!(version(&d), 2);
d.item_mut(id).unwrap().pdf_layer_override = true;
assert_eq!(version(&d), 4);
assert!(load(&save(&d).unwrap()).unwrap().item(id).unwrap().pdf_layer_override);
d.item_mut(id).unwrap().pdf_hidden_layers = vec!["Red".into()];
assert_eq!(version(&d), 2, "legacy nonempty overrides remain readable");
}
#[test]
fn eps_source_requires_new_reader_but_preview_documents_keep_the_older_one() {
let mut d = Document::new(&NewDocument::default());
+27 -16
View File
@@ -654,13 +654,10 @@ impl Renderer {
let g = &if it.media.is_some() { it.drawn_graphic(&f.doc.assets).unwrap_or_else(|| g.clone()) } else { g.clone() };
ctx.set_transform(f.view * xf);
ctx.push_clip_layer(bp);
if !it.pdf_hidden_layers.is_empty() {
HIDDEN_LAYERS.with(|h| *h.borrow_mut() = it.pdf_hidden_layers.clone());
}
let hidden = it.has_pdf_layer_override().then(|| it.pdf_hidden_layers.clone());
let previous = HIDDEN_LAYERS.with(|h| h.replace(hidden));
self.draw_graphic(ctx, f, g, xf);
if !it.pdf_hidden_layers.is_empty() {
HIDDEN_LAYERS.with(|h| h.borrow_mut().clear());
}
HIDDEN_LAYERS.with(|h| h.replace(previous));
ctx.pop_layer();
}
// Items pasted into the frame, clipped by it.
@@ -880,12 +877,14 @@ impl Renderer {
let on_screen = (f.view * xf * g.xf).determinant().abs().sqrt();
let on_screen = if f.opts.quality == DisplayQuality::Typical { on_screen.min(1.0) } else { on_screen };
let hidden = HIDDEN_LAYERS.with(|h| h.borrow().clone());
let data = if !hidden.is_empty() && is_pdf(&asset.data) {
// Stale persisted/relinked choices must not reveal hidden source content.
let Some(variant) = pdf_layers::layered(&asset.data, &hidden) else { return };
variant
} else {
asset.data.clone()
let data = match hidden.as_deref().filter(|_| is_pdf(&asset.data)) {
// An explicit choice (including all-on) that cannot be rewritten fails closed:
// stale persisted/relinked choices must not reveal hidden source content.
Some(hidden) => {
let Some(variant) = pdf_layers::layered(&asset.data, hidden) else { return };
variant
}
None => asset.data.clone(),
};
// A placed PDF cropped to a box: the whole page, placed so that the box spans graphic space.
let [l, t, r, b] = shown_box(asset).unwrap_or([0.0, 0.0, 1.0, 1.0]);
@@ -1025,7 +1024,7 @@ fn rect_overlaps(a: Rect, b: Rect) -> bool {
thread_local! {
/// Object Layer Options of the placed PDF being drawn.
static HIDDEN_LAYERS: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };
static HIDDEN_LAYERS: std::cell::RefCell<Option<Vec<String>>> = const { std::cell::RefCell::new(None) };
/// Appearance of Black for the render on this thread: 100% K as pure black.
static RICH_BLACK: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
/// Separations Preview plate for the render on this thread.
@@ -1296,11 +1295,23 @@ pub fn pdf_page_png(bytes: &[u8], page: usize, max_side: u32, hidden: &[String],
/// Rasterize a placed page for export, preserving available interpreter diagnostics.
/// This cannot detect backend approximations that do not emit an interpreter warning.
pub fn pdf_page_png_checked(bytes: &[u8], page: usize, max_side: u32, hidden: &[String], shown: Option<[f64; 4]>) -> Result<Vec<u8>, PdfRasterError> {
pdf_page_png_options_checked(bytes, page, max_side, (!hidden.is_empty()).then_some(hidden), shown)
}
/// Rasterize with an explicit visibility override: `Some(&[])` enables every layer;
/// `None` retains file defaults. The legacy entry point keeps its empty-list semantics.
pub fn pdf_page_png_options_checked(
bytes: &[u8],
page: usize,
max_side: u32,
hidden: Option<&[String]>,
shown: Option<[f64; 4]>,
) -> Result<Vec<u8>, PdfRasterError> {
check_pdf_input_size(bytes.len())?;
let data = if hidden.is_empty() {
std::borrow::Cow::Borrowed(bytes)
} else {
let data = if let Some(hidden) = hidden {
std::borrow::Cow::Owned(pdf_hide_layers(bytes, hidden).ok_or(PdfRasterError::LayerRewrite)?)
} else {
std::borrow::Cow::Borrowed(bytes)
};
let raster = render_pdf_raster(&data, page, max_side)?;
if raster.diagnostics.image_decode_failed {
+8 -1
View File
@@ -2411,8 +2411,15 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
&& let Some(o) = l.as_object_mut()
{
o.insert("visible".into(), json!(on));
d.fields.insert("useDefaults".into(), json!(false));
}
}
if ui.button(crate::i18n::tr(&app.ui.language, "Use File Defaults")).clicked() {
for layer in &mut layers {
layer["visible"] = layer["default"].clone();
}
d.fields.insert("useDefaults".into(), json!(true));
}
d.fields.insert("layers".into(), json!(layers));
}
"colorSettings" => {
@@ -3269,7 +3276,7 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
}
"layerOptions" => {
let hidden: Vec<Value> = d.fields.get("layers").and_then(Value::as_array).map(|a| a.iter().filter(|l| l["visible"] == false).map(|l| l["name"].clone()).collect()).unwrap_or_default();
app.run("object.layerOptions", json!({"hidden": hidden}))
app.run("object.layerOptions", json!({"hidden": if d.b("useDefaults") { Vec::<Value>::new() } else { hidden }, "useDefaults": d.b("useDefaults")}))
}
"colorSettings" => {
let r = app.run("color.settings", json!({"rgb": d.s("rgb"), "cmyk": d.s("cmyk"), "intent": d.s("intent"), "bpc": d.b("bpc")}))?;
+4 -1
View File
@@ -1556,7 +1556,10 @@ fn run_ui_inner(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value
Ok(v) => v,
Err(e) => return Some(Err(e.to_string())),
};
app.ui.dialog = Some(crate::dialogs::Dialog::new("layerOptions", json!({"layers": layers})));
app.ui.dialog = Some(crate::dialogs::Dialog::new(
"layerOptions",
json!({"useDefaults": !layers.as_array().and_then(|a| a.first()).and_then(|l| l["overridden"].as_bool()).unwrap_or(false), "layers": layers}),
));
Ok(Value::Null)
}
"view.rotateSpread" => {
+3
View File
@@ -207,6 +207,9 @@ Kind: **F** feature · **U** UI/UX · **T** typography · **FF** file format ·
- **Safety:** PDF visibility variants preserve catalog/configuration entries and nonzero-generation
references, redefining the existing catalog rather than choosing an arbitrary object number.
Encrypted, hybrid-reference and over-budget rewrites are rejected; no print-fidelity claim.
- **Layer choices:** placed PDFs distinguish explicit all-on visibility from file defaults;
choices survive native saves, raster/PDF output and embedded IDML export. IDML layer
variants are embedded copies; external-only export is rejected when choices would be lost.
- **Missing:** EPS is shown through its preview only (no PostScript interpreter); PSD layer
comps beyond visibility; the Clipping Path Options dialog (tracing into a clip that keeps the
frame, Threshold/Tolerance/Inset Frame preview, picking an alpha channel or a path saved in a
+9
View File
@@ -0,0 +1,9 @@
# Placed PDF layer visibility
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.
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.
This fixes visibility-state handling, not full InDesign layer/print parity. It does not implement editing the contents of placed PDFs or native INDD placement. Catalog/reference safety fixes are tracked separately. The concurrent graphic-layer metadata PR also changes native format capability; its version selection must be reconciled when merging both features.