diff --git a/ROADMAP.md b/ROADMAP.md index d08c22028..d87c9fbba 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -108,6 +108,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope - 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. - Effects: Inner Glow (edge or centre), Bevel and Emboss (inner bevel lit from the light angle), Satin (`object.innerGlow`, `object.bevel`, `object.satin`); PDF export now keeps soft effects: objects with shadows, glows, feathers, bevels or satin go out as 300 ppi transparent images of their appearance (threaded text frames excepted). - Directional Feather (`object.directionalFeather {widths: [top, left, bottom, right]}`): each side fades over its own width. +- View ▸ Flattener Preview (`view.flattenerPreview`): objects involving transparency highlighted in red; knockout groups export as images (PDF output has no knockout groups). - 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. diff --git a/crates/engine/src/cmd/export.rs b/crates/engine/src/cmd/export.rs index db33a7c0e..5a7917854 100644 --- a/crates/engine/src/cmd/export.rs +++ b/crates/engine/src/cmd/export.rs @@ -237,7 +237,9 @@ fn threaded(d: &designcraft_doc::Document, it: &designcraft_doc::Item) -> bool { /// Soft effects other than the gradient feather (which PDF export draws itself). fn raster_effects(it: &designcraft_doc::Item) -> bool { let e = &it.effects; - e.drop_shadow.on + // Knockout groups too: PDF output here has no knockout transparency groups. + it.knockout + || e.drop_shadow.on || e.feather > 0.0 || e.inner_shadow.on || e.outer_glow.on @@ -408,7 +410,7 @@ fn export_pdf(s: &mut Session, p: &Value) -> Result { } } if fx_count > 0 { - r.warnings.push(format!("{fx_count} object(s) with soft effects (shadows, glows, feathers, bevels) exported as images")); + r.warnings.push(format!("{fx_count} object(s) with soft effects (shadows, glows, feathers, bevels) or knockout groups exported as images")); } if layer_count > 0 { r.warnings.push(format!("{layer_count} placed PDF(s) with hidden layers exported as images")); diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index 95c297b6f..c7c977f0c 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -2188,6 +2188,13 @@ mod fitting_tests { let df = shot(&s); assert!(df.pixel(102, 200)[0] > 200, "left edge faded to paper: {:?}", df.pixel(102, 200)); assert_eq!(df.pixel(298, 200), plain.pixel(298, 200), "right edge untouched"); + // A knockout group exports as an image too. + let other = s.execute("frame.create", &json!({"rect": [400, 100, 450, 150]})).unwrap()["id"].clone(); + let other2 = s.execute("frame.create", &json!({"rect": [420, 120, 470, 170]})).unwrap()["id"].clone(); + let g = s.execute("object.group", &json!({"ids": [other, other2]})).unwrap()["id"].clone(); + s.execute("object.transparencyGroup", &json!({"ids": [g], "knockout": true})).unwrap(); + let r = s.execute("file.exportPdf", &json!({})).unwrap(); + assert!(r["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("2 object(s)")), "{r}"); // PDF export carries effects (as an image of the object). let r = s.execute("file.exportPdf", &json!({})).unwrap(); assert!(r["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("soft effects")), "{r}"); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 96bfcc3df..455781d86 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -306,6 +306,9 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { if app.ui.hidden_characters { draw_hidden_characters(app, &painter, &xf, &doc, &layout); } + if app.ui.flattener_preview { + draw_flattener_preview(&painter, &xf, &doc, &layout); + } if app.ui.dynamic_spelling && !preview { draw_dynamic_spelling(app, ui.ctx(), &painter, &xf, &doc, &layout); } @@ -1469,6 +1472,22 @@ fn draw_hidden_characters(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc } } +/// View › Flattener Preview: red over every object that involves transparency (opacity, blend +/// modes, effects, knockout) — what flattening or export rasterising affects. +fn draw_flattener_preview(painter: &egui::Painter, xf: &Xf, doc: &Document, layout: &CanvasLayout) { + fn transparent(it: &Item) -> bool { + it.opacity < 1.0 || it.blend != Default::default() || it.effects.any() || it.knockout || it.children().iter().any(|c| transparent(c)) + } + let red = Color32::from_rgba_unmultiplied(230, 30, 30, 110); + for slot in &layout.slots { + let Some(sp) = doc.spread(slot.spread) else { continue }; + for it in sp.items.iter().filter(|it| !it.hidden && transparent(it)) { + let r = it.bounds().inflate(designcraft_render::effect_outset(it), designcraft_render::effect_outset(it)); + painter.rect_filled(xf.rect(r + slot.offset), 0.0, red); + } + } +} + /// Edit › Spelling › Dynamic Spelling: red squiggles under misspelled words (checked once per /// story version). fn draw_dynamic_spelling(app: &DesignApp, ctx: &egui::Context, painter: &egui::Painter, xf: &Xf, doc: &Document, layout: &CanvasLayout) { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index fe490badc..2de24547e 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -103,6 +103,9 @@ pub struct UiState { /// Edit › Menus: hidden menu items (`menu/label`), and Show Full Menus. pub hidden_menu_items: Vec, pub show_full_menus: bool, + /// View › Flattener Preview: objects that involve transparency highlighted in red. + #[serde(skip)] + pub flattener_preview: bool, /// Tab / Shift+Tab: 1 = every panel hidden, 2 = all but the Tools panel (0 = shown). pub hidden_panels: u8, /// Edit › Color Settings (re-applied at start). @@ -186,6 +189,7 @@ impl Default for UiState { hidden_menu_items: Vec::new(), show_full_menus: false, story_editor_size: 14.0, + flattener_preview: false, hidden_panels: 0, color_settings: None, proof_colors: false, diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index bf5ff4824..f865be4af 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -78,6 +78,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ ("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.flattenerPreview", "Flattener Preview", None, "{on?: bool} — highlight objects that involve transparency"), ("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"), ( @@ -352,6 +353,7 @@ pub const MENUS: &[(&str, &[&str])] = &[ "ui:view.proofSetup|Simulate Paper Color|{\"simulatePaper\": \"toggle\"}", "<", "ui:view.proofColors", + "ui:view.flattenerPreview", ">Separations Preview", "ui:view.separations|Off|{\"plate\": null, \"inkLimit\": null}", "ui:view.separations|Cyan|{\"plate\": \"cyan\"}", @@ -1168,6 +1170,7 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option>()})) } "window.controlBar" => flag(&mut app.ui.control_bar), + "view.flattenerPreview" => flag(&mut app.ui.flattener_preview), "edit.dynamicSpelling" => flag(&mut app.ui.dynamic_spelling), "window.split" | "window.newWindow" => { let on = if id == "window.newWindow" { true } else { p.get("on").and_then(Value::as_bool).unwrap_or(!app.split) }; @@ -1561,6 +1564,7 @@ pub fn checked(app: &DesignApp, id: &str, params: &Value) -> Option { "window.controlBar" => app.ui.control_bar, "window.split" => app.split, "view.proofColors" => app.ui.proof_colors, + "view.flattenerPreview" => app.ui.flattener_preview, "view.rotateSpread" => match params.get("angle").and_then(Value::as_i64) { Some(0) => app.view().is_some_and(|v| v.rotation == 0), _ => return None, diff --git a/docs/parity.md b/docs/parity.md index c50221b88..b7ed0c90b 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -5,13 +5,13 @@ The summary is generated by `cargo xtask parity` from the table at the bottom of Each row of the InDesign feature catalogue is scored **D** (done), **P** (partial: works but lacks options or UI) or **M** (missing), weighted by priority (**P0** core = 3, **P1** = 2, **P2** = 1). Out-of-scope rows (Adobe cloud services, plug-ins, Typekit, Flash) are excluded. -**Weighted breadth parity: 96%** over 275 features. +**Weighted breadth parity: 97%** over 275 features. | Priority | Features | Done | Partial | Missing | Parity | |---|---|---|---|---|---| | P0 | 102 | 100 | 2 | 0 | 99% | | P1 | 97 | 94 | 3 | 0 | 98% | -| P2 | 76 | 47 | 29 | 0 | 81% | +| P2 | 76 | 49 | 27 | 0 | 82% | | Area | Parity | |---|---| @@ -21,7 +21,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia | Frames, shapes & paths | 100% | | Transform | 100% | | Fill, stroke, colour | 99% | -| Effects & transparency | 83% | +| Effects & transparency | 88% | | Placing & links | 91% | | Type & text frames | 99% | | Typography | 98% | @@ -29,7 +29,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia | Tables | 98% | | Long documents | 96% | | Interactivity & digital | 90% | -| Output & production | 80% | +| Output & production | 82% | | XML & automation | 83% | | View & navigation | 95% | | Undo, history, saving | 100% | @@ -159,7 +159,7 @@ Open P0 items: Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print) | Fill, stroke, colour | Eyedropper | P1 | D | | Fill, stroke, colour | Appearance of Black prefs | P1 | D | | Effects & transparency | Opacity & blend modes | P0 | D | -| Effects & transparency | Isolate Blending / Knockout Group | P2 | P | +| Effects & transparency | Isolate Blending / Knockout Group | P2 | D | | Effects & transparency | Drop Shadow | P1 | D | | Effects & transparency | Inner Shadow, Outer Glow, Inner Glow, Bevel, Satin | P2 | D | | Effects & transparency | Basic/Directional/Gradient Feather | P2 | D | @@ -287,7 +287,7 @@ Open P0 items: Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print) | Output & production | Export EPUB/HTML | P2 | P | | Output & production | Export RTF/TXT/Tagged text | P2 | P | | Output & production | Export XML | P2 | P | -| Output & production | Separations / Flattener / Ink limit preview | P2 | P | +| Output & production | Separations / Flattener / Ink limit preview | P2 | D | | XML & automation | Tags panel, Structure | P2 | P | | XML & automation | Import/Export XML | P2 | P | | XML & automation | Data Merge | P2 | D |