V: Flattener Preview; knockout groups kept in PDF export

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-03 08:58:27 -04:00
1 parent 84d8f498eb
commit b655a5f75c
7 files changed
+45 -8

No files matched your search

+1
View File
@@ -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.
+4 -2
View File
@@ -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<Value> {
}
}
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"));
+7
View File
@@ -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}");
+19
View File
@@ -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) {
+4
View File
@@ -103,6 +103,9 @@ pub struct UiState {
/// Edit › Menus: hidden menu items (`menu/label`), and Show Full Menus.
pub hidden_menu_items: Vec<String>,
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,
+4
View File
@@ -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<Result<Value,
Ok(json!({"floating": app.ui.floating.iter().map(|(p, _)| p.clone()).collect::<Vec<_>>()}))
}
"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<bool> {
"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,
+6 -6
View File
@@ -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.
<!-- SUMMARY -->
**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 |