diff --git a/ROADMAP.md b/ROADMAP.md index bbdf404..7a14c03 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -80,6 +80,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope - Content Collector and Placer tools (B; `conveyor.collect/place/list/clear`): copies of objects (with stories, styles, images) ride a conveyor between pages and documents; Alt-click keeps them on it. - Linked stories (Edit ▸ Place and Link, `story.placeAndLink/links/updateLink/unlink`): child stories copy a parent, show out of date in the Links panel and update. - Edit ▸ Menus (`window.hideMenuItem`): hide menu items; menus with hidden items end in Show All Menu Items. +- View ▸ Separations Preview (`view.separations`): one process plate as ink density, or areas over an ink limit in red (a GCR preview of the rendered view; spot plates and the flattener preview aren't there). - 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/render/src/lib.rs b/crates/render/src/lib.rs index a8b897d..6e67fd5 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -884,6 +884,34 @@ pub fn pdf_page_box(bytes: &[u8], page: usize, kind: &str) -> Option<(f64, f64, Some(rel(visible, b)) } +/// Separations Preview on a rendered image: `plate` 0–3 shows that process plate (C, M, Y, K) as +/// ink density in grey; with an `ink_limit` (0–4, total ink) areas over it are shown in red. +/// Pixels are separated with a plain GCR (black = 1 − max(r, g, b)), so it's a preview. +pub fn separation_view(img: &mut Rendered, plate: Option, ink_limit: Option) { + for px in img.pixels.chunks_exact_mut(4) { + let a = px[3] as f32 / 255.0; + // Composite over paper white, then separate. + let ch = |v: u8| (v as f32 / 255.0) + (1.0 - a); + let (r, g, b) = (ch(px[0]).min(1.0), ch(px[1]).min(1.0), ch(px[2]).min(1.0)); + let k = 1.0 - r.max(g).max(b); + let inks = + if k >= 0.999 { [0.0, 0.0, 0.0, 1.0] } else { [(1.0 - r - k) / (1.0 - k), (1.0 - g - k) / (1.0 - k), (1.0 - b - k) / (1.0 - k), k] }; + let mut out = match plate { + Some(p) => { + let v = ((1.0 - inks[p.min(3) as usize].clamp(0.0, 1.0)) * 255.0).round() as u8; + [v, v, v, 255] + } + None => [(r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8, 255], + }; + if let Some(limit) = ink_limit + && inks.iter().sum::() > limit + { + out = [230, 40, 40, 255]; + } + px.copy_from_slice(&out); + } +} + pub fn image_mime(bytes: &[u8]) -> &'static str { designcraft_images::mime(bytes) } @@ -1014,6 +1042,20 @@ mod tests { } } + #[test] + fn separations_split_plates_and_flag_ink_limits() { + // Pure cyan (0, 255, 255) and rich black-ish (20, 20, 20). + let mut img = Rendered { width: 2, height: 1, pixels: vec![0, 255, 255, 255, 20, 20, 20, 255] }; + let mut c = img.clone(); + separation_view(&mut c, Some(0), None); + assert_eq!(c.pixel(0, 0)[0], 0, "cyan plate solid under cyan"); + let mut m = img.clone(); + separation_view(&mut m, Some(1), None); + assert_eq!(m.pixel(0, 0)[0], 255, "no magenta"); + separation_view(&mut img, None, Some(0.5)); + assert_eq!(img.pixel(0, 0), [230, 40, 40, 255], "100% over a 50% limit"); + } + #[test] fn black_overprints_only_with_the_preference() { let mut d = Document::new(&NewDocument::default()); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index c69a9c0..0a5890a 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -465,7 +465,10 @@ fn render_worker_spawn(ctx: &egui::Context) -> Option tex.set(ci, egui::TextureOptions::LINEAR), diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 453c45b..2b7c8e1 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -102,6 +102,11 @@ 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 › Separations Preview: a process plate (0–3) and/or an ink limit (total, 0–4). + #[serde(skip)] + pub separation: Option, + #[serde(skip)] + pub ink_limit: Option, /// View › Display Performance. pub display_quality: designcraft_render::DisplayQuality, /// Preferences › Interface › UI scaling (1 = 100%). @@ -164,6 +169,8 @@ impl Default for UiState { tagged_frames: false, hidden_menu_items: Vec::new(), show_full_menus: false, + separation: None, + ink_limit: None, display_quality: designcraft_render::DisplayQuality::High, ui_scale: 1.0, rich_black: false, diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 34d8c2f..d8faa32 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -64,6 +64,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ ("view.textThreads", "Show/Hide Text Threads", Some("Cmd+Alt+Y"), "{}"), ("view.hiddenCharacters", "Show/Hide Hidden Characters", Some("Cmd+Alt+I"), "{}"), ("view.taggedFrames", "Show/Hide Tagged Frames", None, "{} — XML-tagged frames outlined in their tag colour"), + ("view.separations", "Separations Preview", None, "{plate?: cyan|magenta|yellow|black|null, inkLimit?: percent|null}"), ("view.overprintPreview", "Overprint Preview", Some("Cmd+Alt+Shift+Y"), "{} — show how overprinting inks mix"), ("view.fastDisplay", "Fast Display", Some("Cmd+Alt+Shift+Z"), "{} — placed graphics as grey boxes, no effects"), ("view.typicalDisplay", "Typical Display", Some("Cmd+Alt+Z"), "{} — low-resolution image proxies"), @@ -294,6 +295,14 @@ pub const MENUS: &[(&str, &[&str])] = &[ "-", "ui:view.hiddenCharacters", "ui:view.taggedFrames", + ">Separations Preview", + "ui:view.separations|Off|{\"plate\": null, \"inkLimit\": null}", + "ui:view.separations|Cyan|{\"plate\": \"cyan\"}", + "ui:view.separations|Magenta|{\"plate\": \"magenta\"}", + "ui:view.separations|Yellow|{\"plate\": \"yellow\"}", + "ui:view.separations|Black|{\"plate\": \"black\"}", + "ui:view.separations|Ink Limit 300%|{\"inkLimit\": 300}", + "<", "-", ">Hyperlinks & Cross-References", "cmd:hyperlink.create", @@ -872,6 +881,16 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option flag(&mut app.ui.text_threads), "view.hiddenCharacters" => flag(&mut app.ui.hidden_characters), "view.taggedFrames" => flag(&mut app.ui.tagged_frames), + "view.separations" => { + if let Some(v) = p.get("plate") { + app.ui.separation = v.as_str().and_then(|s| ["cyan", "magenta", "yellow", "black"].iter().position(|x| *x == s)).map(|i| i as u8); + } + if let Some(v) = p.get("inkLimit") { + app.ui.ink_limit = v.as_f64().map(|l| (l / 100.0) as f32); + } + app.canvas.shown = None; + Ok(Value::Null) + } "view.goToPage" => { match p.get("page") { Some(Value::Number(n)) => crate::canvas::go_to_page(app, (n.as_u64().unwrap_or(1) as usize).saturating_sub(1)), diff --git a/docs/parity.md b/docs/parity.md index 3aea863..d755e1b 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -11,7 +11,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia |---|---|---|---|---|---| | P0 | 102 | 99 | 3 | 0 | 99% | | P1 | 97 | 92 | 4 | 1 | 97% | -| P2 | 76 | 33 | 18 | 25 | 55% | +| P2 | 76 | 33 | 20 | 23 | 57% | | Area | Parity | |---|---| @@ -20,7 +20,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia | Layers | 95% | | Frames, shapes & paths | 99% | | Transform | 100% | -| Fill, stroke, colour | 95% | +| Fill, stroke, colour | 96% | | Effects & transparency | 71% | | Placing & links | 90% | | Type & text frames | 94% | @@ -29,7 +29,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia | Tables | 92% | | Long documents | 88% | | Interactivity & digital | 75% | -| Output & production | 78% | +| Output & production | 80% | | XML & automation | 83% | | View & navigation | 88% | | Undo, history, saving | 100% | @@ -155,7 +155,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t | Fill, stroke, colour | Stroke styles | P1 | D | | Fill, stroke, colour | Color management | P1 | P | | Fill, stroke, colour | Overprint Preview | P1 | D | -| Fill, stroke, colour | Separations Preview | P2 | M | +| Fill, stroke, colour | Separations Preview | P2 | P | | Fill, stroke, colour | Eyedropper | P1 | D | | Fill, stroke, colour | Appearance of Black prefs | P1 | D | | Effects & transparency | Opacity & blend modes | P0 | D | @@ -287,7 +287,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t | 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 | M | +| Output & production | Separations / Flattener / Ink limit preview | P2 | P | | XML & automation | Tags panel, Structure | P2 | P | | XML & automation | Import/Export XML | P2 | P | | XML & automation | Data Merge | P2 | D |