mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
P: Object Layer Options for placed PDFs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d37d2e5237
commit
37f0cd99e1
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user