P: Object Layer Options for placed PDFs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-03 08:38:23 -04:00
co-authored by Claude Opus 5.5
parent d37d2e5237
commit 37f0cd99e1
10 changed files with 327 additions and 5 deletions
+1
View File
@@ -105,6 +105,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
- View ▸ Rotate Spread (`view.rotateSpread`: 90° CW / CCW, 180°, clear): the view turns in quarter turns about its centre; tools, hit testing, the render texture and overlays follow; rulers hide while rotated. InDesign turns just the one spread.
- Vertical type (Vertical Type Tool, Type ▸ Story Direction, `object.textFrameOptions {vertical}`, IDML StoryOrientation): lines run top to bottom and follow each other right to left; CJK characters stay upright and other text turns, on screen and in PDF; CJK line breaking with basic kinsoku (also in horizontal text). Vertical punctuation forms (`vert`), tate-chu-yoko and vertical metrics aren't there yet.
- Math expressions (Type ▸ Insert Math Expression…, `math.insert`, `math.svg`): a LaTeX subset (fractions, scripts, roots, big operators with limits, Greek, relations and operators with math spacing, `\left…\right`, `\text`) typeset by our own engine into SVG, placed inline at the cursor or as a frame; the LaTeX is kept as alt text and can be edited.
- Object Layer Options for placed PDFs (`object.pdfLayers`, `object.layerOptions`, Object ▸ Object Layer Options…): the file's optional-content layers listed and shown or hidden per frame on screen; in exported PDFs such graphics go out as 300 ppi images of their visible layers.
- Drag and drop text editing: drag selected text to move it (Alt copies) with its formatting (`text.release`).
- Image Import Options: place any page of a multi-page PDF (`file.place {pdfPage}`; the app asks which page), shown on screen and embedded as that page in PDF export.
- Define Lists: named numbered lists that continue across stories in page order (`list.define`, paragraph `listName` / `startAt`), round-tripped through IDML; `null` in `type.para` / `type.char` now removes an override.
+4
View File
@@ -811,6 +811,9 @@ pub struct Item {
/// Liquid Layout (object-based pages): pins and resize permissions.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub liquid: Option<ObjectLiquid>,
/// 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>,
/// Window › Interactive › Media: options of a placed video or sound.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub media: Option<MediaOptions>,
@@ -866,6 +869,7 @@ impl Item {
alt_text: String::new(),
xml_tag: String::new(),
button: None,
pdf_hidden_layers: Vec::new(),
media: None,
liquid: None,
states: Vec::new(),
+46 -1
View File
@@ -229,6 +229,45 @@ fn flatten(d: &designcraft_doc::Document, cache: &designcraft_compose::Cache, pp
(out, n)
}
/// A copy of `d` where placed PDFs with Object Layer Options hidden layers are 300 ppi images.
fn rasterize_layered(d: &designcraft_doc::Document) -> (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();
if ids.is_empty() {
return (None, 0);
}
let mut out = d.clone();
let mut n = 0;
for id in ids {
let Some((g, hidden)) = out.item(id).and_then(|it| Some((it.graphic()?.clone(), it.pdf_hidden_layers.clone()))) else { continue };
let Some(a) = out.assets.get(&g.asset).cloned() else { continue };
if !designcraft_render::is_pdf(&a.data) {
continue;
}
let side = (g.size.0.max(g.size.1) * 300.0 / 72.0).clamp(64.0, 8000.0) as u32;
let Some(png) = designcraft_render::pdf_page_png(&a.data, a.page as usize, side, &hidden) else { continue };
let px = designcraft_render::image_size(&png);
let aid = designcraft_doc::AssetId(out.alloc());
out.assets.insert(
aid,
std::sync::Arc::new(designcraft_doc::Asset {
page: 0,
id: aid,
name: format!("{}.png", a.name),
mime: "image/png".into(),
link: None,
data: std::sync::Arc::new(png),
pixels: px,
}),
);
if let Some(designcraft_doc::Content::Graphic(gg)) = out.item_mut(id).map(|it| &mut it.content) {
gg.asset = aid;
}
n += 1;
}
(Some(out), n)
}
fn export_pdf(s: &mut Session, p: &Value) -> Result<Value> {
let st = s.doc()?;
let opts = options(p, st.doc.page_count())?;
@@ -244,7 +283,10 @@ fn export_pdf(s: &mut Session, p: &Value) -> Result<Value> {
),
};
let flattened = ppi.map(|ppi| flatten(&st.doc, &s.cache, ppi));
let doc: &designcraft_doc::Document = flattened.as_ref().map_or(&st.doc, |f| &f.0);
// Placed PDFs with hidden layers go out as images showing just their visible layers.
let base: &designcraft_doc::Document = flattened.as_ref().map_or(&st.doc, |f| &f.0);
let (layered, layer_count) = rasterize_layered(base);
let doc: &designcraft_doc::Document = layered.as_ref().unwrap_or(base);
let r = designcraft_pdf::export_pdf_with_report(doc, &s.cache, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
let mut r = r;
// Transparency Blend Space: the page group of pages with transparency.
@@ -265,6 +307,9 @@ fn export_pdf(s: &mut Session, p: &Value) -> Result<Value> {
None => r.warnings.push("page transitions couldn't be added to this PDF".into()),
}
}
if layer_count > 0 {
r.warnings.push(format!("{layer_count} placed PDF(s) with hidden layers exported as images"));
}
if let Some((_, n)) = &flattened
&& *n > 0
{
+2
View File
@@ -36,6 +36,7 @@ mod overrides;
mod package;
mod path_type;
mod paths;
mod pdflayers;
mod place_text;
pub mod preflight;
mod prefs;
@@ -166,6 +167,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
v.extend(linked::specs());
v.extend(mathexpr::specs());
v.extend(media::specs());
v.extend(pdflayers::specs());
v.extend(liquid::specs());
v.extend(xml::specs());
v.extend(strokes::specs());
+119
View File
@@ -0,0 +1,119 @@
//! Object › Object Layer Options: show or hide the layers (optional content) of a placed PDF.
use serde_json::{Value, json};
use super::{CommandSpec, bad, cmd, has_selection, targets};
use crate::Session;
fn placed_pdf(s: &Session, id: designcraft_doc::ItemId) -> Option<std::sync::Arc<Vec<u8>>> {
let d = &s.active()?.doc;
let g = d.item(id)?.graphic()?;
let a = d.assets.get(&g.asset)?;
designcraft_render::is_pdf(&a.data).then(|| a.data.clone())
}
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 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();
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}))
.collect(),
))
}),
cmd!(
"object.layerOptions",
"Object Layer Options…",
["Object"],
None,
"{ids?, hidden: [layer name]} — layers of placed PDFs to hide ([] = the file's own settings)",
has_selection,
|s, p| {
let ids = targets(s, p)?;
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()) {
return Err(bad("object.layerOptions", "select a placed PDF"));
}
s.edit(|d, _| {
for id in &ids {
if let Some(it) = d.item_mut(*id) {
it.pdf_hidden_layers = hidden.clone();
}
}
Ok(json!({"hidden": hidden}))
})
}
),
]
}
#[cfg(test)]
pub(crate) mod tests {
use serde_json::json;
use crate::Session;
/// A one-page PDF (100×100 pt) with two layers: "Blue" (left half) and "Red" (right half).
pub(crate) fn two_layer_pdf() -> Vec<u8> {
let content = "/OC /a BDC 0 0 1 rg 0 0 50 100 re f EMC /OC /b BDC 1 0 0 rg 50 0 50 100 re f EMC";
let objs = [
"<</Type/Catalog/Pages 2 0 R/OCProperties<</OCGs[5 0 R 6 0 R]/D<</Order[5 0 R 6 0 R]>>>>>>".to_string(),
"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_string(),
"<</Type/Page/Parent 2 0 R/MediaBox[0 0 100 100]/Resources<</Properties<</a 5 0 R/b 6 0 R>>>>/Contents 4 0 R>>".to_string(),
format!("<</Length {}>>\nstream\n{content}\nendstream", content.len()),
"<</Type/OCG/Name(Blue)>>".to_string(),
"<</Type/OCG/Name(Red)>>".to_string(),
];
let mut out = b"%PDF-1.7\n".to_vec();
let mut offs = Vec::new();
for (i, o) in objs.iter().enumerate() {
offs.push(out.len());
out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
}
let x = out.len();
let mut t = format!("xref\n0 {}\n0000000000 65535 f\r\n", objs.len() + 1);
for o in offs {
t.push_str(&format!("{o:010} 00000 n\r\n"));
}
t.push_str(&format!("trailer\n<</Size {}/Root 1 0 R>>\nstartxref\n{x}\n%%EOF\n", objs.len() + 1));
out.extend_from_slice(t.as_bytes());
out
}
#[test]
fn placed_pdf_layers_hide() {
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
let pdf = two_layer_pdf();
let r = s
.execute("file.place", &json!({"base64": super::super::file::base64_encode(&pdf), "name": "art.pdf", "x": 100, "y": 100, "width": 100}))
.unwrap();
let id = r["id"].clone();
let l = s.execute("object.pdfLayers", &json!({"id": id})).unwrap();
assert_eq!(l.as_array().unwrap().len(), 2);
assert_eq!(l[0]["name"], "Blue");
let px = |s: &Session, x: u32| {
let d = s.doc().unwrap().doc.clone();
let mut rr = designcraft_render::Renderer::new();
rr.threads = 0;
rr.render_page(&d, &s.cache, 0, 1.0, false, &Default::default()).unwrap().pixel(x, 150)
};
assert!(px(&s, 125)[2] > 200 && px(&s, 175)[0] > 200, "both layers show");
s.execute("object.layerOptions", &json!({"ids": [id], "hidden": ["Red"]})).unwrap();
let right = px(&s, 175);
assert!(right[0] > 200 && right[1] > 200 && right[2] > 200, "the red layer is hidden: {right:?}");
assert!(px(&s, 125)[2] > 200, "blue still shows");
assert_eq!(s.execute("object.pdfLayers", &json!({"id": id})).unwrap()[1]["visible"], false);
// PDF export carries the choice (the graphic goes out as an image).
let r = s.execute("file.exportPdf", &json!({})).unwrap();
assert!(r["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("layer")), "{r}");
}
}
+21 -1
View File
@@ -25,6 +25,8 @@ pub use vello_cpu;
pub mod damage;
mod fx;
pub mod glyphs;
mod pdf_layers;
pub use pdf_layers::{pdf_hide_layers, pdf_layers};
pub mod images;
mod text;
@@ -508,7 +510,13 @@ 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());
}
self.draw_graphic(ctx, f, g, xf);
if !it.pdf_hidden_layers.is_empty() {
HIDDEN_LAYERS.with(|h| h.borrow_mut().clear());
}
ctx.pop_layer();
}
Content::Text(tf) => {
@@ -663,7 +671,9 @@ impl Renderer {
// Pick a mip level close to the on-screen size (Typical: a 72 ppi proxy).
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 Some(pm) = images::mip(&asset.data, asset.page, g.size.0, on_screen) else { return };
let hidden = HIDDEN_LAYERS.with(|h| h.borrow().clone());
let data = if !hidden.is_empty() && is_pdf(&asset.data) { pdf_layers::layered(&asset.data, &hidden) } else { asset.data.clone() };
let Some(pm) = images::mip(&data, asset.page, g.size.0, on_screen) else { return };
let rect = Rect::new(0.0, 0.0, g.size.0, g.size.1);
ctx.set_transform(f.view * xf * g.xf);
let sx = g.size.0 / pm.width().max(1) as f64;
@@ -683,6 +693,8 @@ 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()) };
/// 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) };
}
@@ -866,6 +878,14 @@ pub fn pdf_page_size(bytes: &[u8], page: usize) -> Option<(f64, f64)> {
Some((w as f64, h as f64))
}
/// PDF page `page` as PNG, its longer side about `max_side` pixels, with the `hidden` layers off.
pub fn pdf_page_png(bytes: &[u8], page: usize, max_side: u32, hidden: &[String]) -> Option<Vec<u8>> {
let data = if hidden.is_empty() { bytes.to_vec() } else { pdf_hide_layers(bytes, hidden).unwrap_or_else(|| bytes.to_vec()) };
let pm = render_pdf_page(&data, page, max_side)?;
let pixels: Vec<u8> = pm.data().iter().flat_map(|p| [p.r, p.g, p.b, p.a]).collect();
Some(Rendered { width: pm.width() as u32, height: pm.height() as u32, pixels }.to_png())
}
/// Rasterize PDF page `page` so its longer side is about `max_side` pixels (screen display; PDF
/// export embeds the page as vectors).
pub fn render_pdf_page(bytes: &[u8], page: usize, max_side: u32) -> Option<Pixmap> {
+103
View File
@@ -0,0 +1,103 @@
//! Object Layer Options for placed PDFs: the file's optional-content layers, and a variant of the
//! file with some layers off (an appended catalog whose default configuration turns them off,
//! which the PDF renderer honours).
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use hayro::hayro_syntax::Pdf;
use hayro::hayro_syntax::object::{Array, Dict, Name, ObjectIdentifier};
/// (object number, name, on by default) of each layer.
fn ocgs(pdf: &Pdf) -> Vec<(i32, String, bool)> {
let xref = pdf.xref();
let Some(cat) = xref.get::<Dict<'_>>(xref.root_id()) else { return vec![] };
let Some(ocp) = cat.get::<Dict<'_>>(b"OCProperties") else { return vec![] };
let Some(list) = ocp.get::<Array<'_>>(b"OCGs") else { return vec![] };
let cfg = ocp.get::<Dict<'_>>(b"D");
let ids_in = |key: &[u8]| -> Vec<i32> {
cfg.as_ref()
.and_then(|c| c.get::<Array<'_>>(key))
.map(|a| a.raw_iter().filter_map(|o| o.as_obj_ref()).map(|r| r.obj_number).collect())
.unwrap_or_default()
};
let base_off = cfg.as_ref().and_then(|c| c.get::<Name<'_>>(b"BaseState")).is_some_and(|n| n.as_ref() == b"OFF");
let (on, off) = (ids_in(b"ON"), ids_in(b"OFF"));
list.raw_iter()
.filter_map(|o| o.as_obj_ref())
.filter_map(|r| {
let d = xref.get::<Dict<'_>>(ObjectIdentifier::from(r))?;
let name = d
.get::<hayro::hayro_syntax::object::String<'_>>(b"Name")
.map(|s| String::from_utf8_lossy(s.as_bytes()).to_string())
.unwrap_or_default();
let visible = if base_off { on.contains(&r.obj_number) } else { !off.contains(&r.obj_number) };
Some((r.obj_number, name, visible))
})
.collect()
}
/// The layers of a placed PDF: (name, visible by default).
pub fn pdf_layers(bytes: &[u8]) -> Vec<(String, bool)> {
Pdf::new(Arc::new(bytes.to_vec())).map(|p| ocgs(&p).into_iter().map(|(_, n, v)| (n, v)).collect()).unwrap_or_default()
}
/// `bytes` with exactly the layers named in `hidden` off (the others on). `None` when the file
/// has no such layers or can't be read.
pub fn pdf_hide_layers(bytes: &[u8], hidden: &[String]) -> Option<Vec<u8>> {
let pdf = Pdf::new(Arc::new(bytes.to_vec())).ok()?;
let layers = ocgs(&pdf);
if layers.is_empty() {
return None;
}
let xref = pdf.xref();
let cat = xref.get::<Dict<'_>>(xref.root_id())?;
let pages = cat.get_ref(b"Pages")?;
let all: Vec<String> = layers.iter().map(|(n, _, _)| format!("{n} 0 R")).collect();
let off: Vec<String> = layers.iter().filter(|(_, name, _)| hidden.contains(name)).map(|(n, _, _)| format!("{n} 0 R")).collect();
// A new catalog under a fresh object number, with the original's page tree.
let root = 9_999_000;
let mut out = bytes.to_vec();
if !out.ends_with(b"\n") {
out.push(b'\n');
}
let at = out.len();
out.extend_from_slice(
format!(
"{root} 0 obj\n<</Type/Catalog/Pages {} {} R/OCProperties<</OCGs[{}]/D<</BaseState/ON/OFF[{}]>>>>>>\nendobj\n",
pages.obj_number,
pages.gen_number,
all.join(" "),
off.join(" ")
)
.as_bytes(),
);
let xref_at = out.len();
let prev = {
let tail = String::from_utf8_lossy(&bytes[bytes.len().saturating_sub(64)..]).to_string();
tail.rsplit("startxref").next().and_then(|t| t.split_whitespace().next()).and_then(|n| n.parse::<usize>().ok())?
};
out.extend_from_slice(
format!("xref\n{root} 1\n{at:010} 00000 n\r\ntrailer\n<</Size {}/Root {root} 0 R/Prev {prev}>>\nstartxref\n{xref_at}\n%%EOF\n", root + 1)
.as_bytes(),
);
Some(out)
}
/// Shared copies of layer variants (by source address and hidden set).
pub(crate) fn layered(data: &Arc<Vec<u8>>, hidden: &[String]) -> Arc<Vec<u8>> {
type Key = (usize, Vec<String>);
static CACHE: Mutex<Option<HashMap<Key, Arc<Vec<u8>>>>> = Mutex::new(None);
let key = (Arc::as_ptr(data) as usize, hidden.to_vec());
let mut c = CACHE.lock().unwrap_or_else(|e| e.into_inner());
let map = c.get_or_insert_with(HashMap::new);
if let Some(v) = map.get(&key) {
return v.clone();
}
if map.len() > 64 {
map.clear();
}
let v = pdf_hide_layers(data, hidden).map(Arc::new).unwrap_or_else(|| data.clone());
map.insert(key, v.clone());
v
}
+18
View File
@@ -730,6 +730,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
"print" => "Print",
"pdfImport" => "Place PDF",
"colorSettings" => "Color Settings",
"layerOptions" => "Object Layer Options",
"keyboardShortcuts" => "Keyboard Shortcuts",
"colorPicker" => "Color Picker",
id => match id.strip_prefix("cmd:").and_then(designcraft_engine::find_command) {
@@ -950,6 +951,19 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
});
});
}
"layerOptions" => {
let mut layers = d.fields.get("layers").and_then(Value::as_array).cloned().unwrap_or_default();
if layers.is_empty() {
ui.label("This PDF has no layers.");
}
for l in &mut layers {
let mut on = l["visible"].as_bool().unwrap_or(true);
if ui.checkbox(&mut on, l["name"].as_str().unwrap_or("")).changed() {
l["visible"] = json!(on);
}
}
d.fields.insert("layers".into(), json!(layers));
}
"colorSettings" => {
let profiles = d.fields.get("profiles").and_then(Value::as_array).cloned().unwrap_or_default();
egui::Grid::new("color_settings").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
@@ -1324,6 +1338,10 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
let hex = d.s("hex");
app.run("object.color", json!({"color": hex, "target": d.s("target")}))
}
"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}))
}
"colorSettings" => {
let r = app.run("color.settings", json!({"rgb": d.s("rgb"), "cmyk": d.s("cmyk"), "intent": d.s("intent"), "bpc": d.b("bpc")}))?;
app.ui.color_settings = Some(designcraft_color::cms::active_settings());
+10
View File
@@ -77,6 +77,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
("window.hidePanelsExceptTools", "Show/Hide Panels Except Tools", Some("Shift+Tab"), "{}"),
("window.nextDocument", "Next Document", Some("Cmd+F6"), "{}"),
("window.previousDocument", "Previous Document", Some("Cmd+Shift+F6"), "{}"),
("app.layerOptions", "Object Layer Options…", None, "{} — show or hide the layers of the selected placed PDF"),
("view.rotateSpread", "Rotate Spread", None, "{angle: 90 (clockwise) | -90 | 180 | 0 (clear)} — turn the view in quarter turns"),
("view.proofColors", "Proof Colors", None, "{on?: bool} — simulate the proof target on screen"),
(
@@ -433,6 +434,7 @@ pub const MENUS: &[(&str, &[&str])] = &[
"cmd:object.showAll",
"-",
"cmd:object.textFrameOptions",
"ui:app.layerOptions",
"cmd:object.primaryTextFrame",
"cmd:object.cornerOptions",
"-",
@@ -1012,6 +1014,14 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
}
Ok(json!(app.session.active_index()))
}
"app.layerOptions" => {
let layers = match app.session.execute("object.pdfLayers", &json!({})) {
Ok(v) => v,
Err(e) => return Some(Err(e.to_string())),
};
app.ui.dialog = Some(crate::dialogs::Dialog::new("layerOptions", json!({"layers": layers})));
Ok(Value::Null)
}
"view.rotateSpread" => {
let angle = p.get("angle").and_then(Value::as_i64).unwrap_or(90);
let (Some(rect), Some(v)) = (app.canvas_rect, app.view().copied()) else { return Some(Err("no view".into())) };
+3 -3
View File
@@ -11,13 +11,13 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|---|---|---|---|---|---|
| P0 | 102 | 100 | 2 | 0 | 99% |
| P1 | 97 | 94 | 3 | 0 | 98% |
| P2 | 76 | 44 | 31 | 1 | 78% |
| P2 | 76 | 45 | 31 | 0 | 80% |
| Area | Parity |
|---|---|
| Application shell & workspace | 96% |
| Documents, pages, spreads | 98% |
| Layers | 95% |
| Layers | 100% |
| Frames, shapes & paths | 100% |
| Transform | 100% |
| Fill, stroke, colour | 99% |
@@ -93,7 +93,7 @@ Open P0 items: Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print)
| Layers | Hide/Lock others | P1 | D |
| Layers | Page item names in Layers panel | P1 | D |
| Layers | Layer colour drives selection colour | P0 | D |
| Layers | Object Layer Options for placed files | P2 | M |
| Layers | Object Layer Options for placed files | P2 | D |
| Frames, shapes & paths | Rectangle/Ellipse/Polygon frames | P0 | D |
| Frames, shapes & paths | Rectangle/Ellipse/Polygon shapes | P0 | D |
| Frames, shapes & paths | Polygon settings | P0 | D |