V: Separations Preview and ink limit view

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-03 03:43:22 -04:00
1 parent 43ba0bf086
commit adb054df46
6 files changed
+78 -6

No files matched your search

+1
View File
@@ -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.
+42
View File
@@ -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<u8>, ink_limit: Option<f32>) {
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::<f32>() > 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());
+4 -1
View File
@@ -465,7 +465,10 @@ fn render_worker_spawn(ctx: &egui::Context) -> Option<crate::render_worker::Work
crate::render_worker::Worker::spawn(ctx.clone())
}
fn upload(app: &mut DesignApp, ctx: &egui::Context, img: designcraft_render::Rendered) {
fn upload(app: &mut DesignApp, ctx: &egui::Context, mut img: designcraft_render::Rendered) {
if app.ui.separation.is_some() || app.ui.ink_limit.is_some() {
designcraft_render::separation_view(&mut img, app.ui.separation, app.ui.ink_limit);
}
let ci = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels);
match &mut app.canvas.texture {
Some(tex) => tex.set(ci, egui::TextureOptions::LINEAR),
+7
View File
@@ -102,6 +102,11 @@ 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 › Separations Preview: a process plate (0–3) and/or an ink limit (total, 0–4).
#[serde(skip)]
pub separation: Option<u8>,
#[serde(skip)]
pub ink_limit: Option<f32>,
/// 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,
+19
View File
@@ -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<Result<Value,
"view.textThreads" => 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)),
+5 -5
View File
@@ -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 |