From eb2e8d8b4fbf66a231ac224afb425b1e0dd0fb54 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 2 Oct 2026 23:51:51 -0400 Subject: [PATCH] =?UTF-8?q?C:=20Appearance=20of=20Black=20=E2=80=94=20outp?= =?UTF-8?q?ut=20rich=20black,=20Overprint=20[Black]=20at=20100%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/doc/src/lib.rs | 3 +++ crates/engine/src/cmd/prefs.rs | 3 ++- crates/engine/src/lib.rs | 4 ++++ crates/idml/src/export.rs | 2 +- crates/idml/src/import.rs | 3 +++ crates/render/src/lib.rs | 26 +++++++++++++++++++++++++- crates/ui-egui/src/dialogs.rs | 22 +++++++++++++++++++++- crates/ui-egui/src/menus.rs | 3 ++- docs/parity.md | 6 +++--- 10 files changed, 65 insertions(+), 8 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 6a6f4ac7e..d7b774fd8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -54,6 +54,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope - Place Excel workbooks (.xlsx): the first worksheet's used range becomes a table (shared/inline strings, numbers, booleans). - Object Library (`library.new/open/add/place/remove/list/json/close`, Library panel): `.dclib` files of snippets that keep stories, styles, swatches and images. - Ink Manager (`ink.list`, `ink.options`; Swatches panel menu): All Spots to Process, per-ink conversion and ink aliases in PDF output; IDML ConvertToProcess/AliasInkName. +- Appearance of Black complete: Printing/Exporting rich black for RGB output (PNG), Overprint [Black] Swatch at 100% (document, IDML OverprintBlack). - 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/doc/src/lib.rs b/crates/doc/src/lib.rs index 67485dbed..95b111819 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -253,6 +253,8 @@ pub struct DocSettings { pub advanced_type: AdvancedType, /// Edit › Transparency Blend Space. pub blend_space: BlendSpace, + /// Preferences › Appearance of Black: [Black] at 100% overprints (Overprint Preview, output). + pub overprint_black: bool, } impl Default for DocSettings { @@ -281,6 +283,7 @@ impl Default for DocSettings { global_light: 120.0, advanced_type: AdvancedType::default(), blend_space: BlendSpace::Cmyk, + overprint_black: true, } } } diff --git a/crates/engine/src/cmd/prefs.rs b/crates/engine/src/cmd/prefs.rs index 50f46a788..54ecc2abe 100644 --- a/crates/engine/src/cmd/prefs.rs +++ b/crates/engine/src/cmd/prefs.rs @@ -19,6 +19,7 @@ const DOC_KEYS: &[&str] = &[ "bleedColor", "slugColor", "advancedType", + "overprintBlack", ]; pub fn specs() -> Vec { @@ -46,7 +47,7 @@ pub fn specs() -> Vec { "Preferences", [], None, - "{showHiddenCharacters?, typographersQuotes?, polygonSides?, starInset?, scaleStrokes?, dimensionsIncludeStroke?, transformationsAreTotals?, absolutePageNumbers?, highlightHj?, highlightKeeps?, highlightCustomTracking?, highlightSubstitutedFonts?} → all application preferences", + "{showHiddenCharacters?, typographersQuotes?, polygonSides?, starInset?, scaleStrokes?, dimensionsIncludeStroke?, transformationsAreTotals?, absolutePageNumbers?, highlightHj?, highlightKeeps?, highlightCustomTracking?, highlightSubstitutedFonts?, richBlackOutput?} → all application preferences", super::always, |s, p| { let cur = serde_json::to_value(&s.prefs).map_err(|e| bad("prefs.set", e.to_string()))?; diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index c3bfe282e..5c5e5b4d4 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -144,6 +144,9 @@ pub struct Prefs { pub highlight_keeps: bool, pub highlight_custom_tracking: bool, pub highlight_substituted_fonts: bool, + /// Preferences › Appearance of Black › Printing / Exporting: 100% K as rich (pure) black in + /// RGB output (PNG) instead of the accurate dark grey. + pub rich_black_output: bool, /// Preferences › Type › Smart Text Reflow: pages follow the primary text frame's story /// (added while it oversets, empty ones at the end removed). pub smart_text_reflow: bool, @@ -164,6 +167,7 @@ impl Default for Prefs { highlight_keeps: false, highlight_custom_tracking: false, highlight_substituted_fonts: true, + rich_black_output: false, smart_text_reflow: true, } } diff --git a/crates/idml/src/export.rs b/crates/idml/src/export.rs index 737a720ca..68b39fd52 100644 --- a/crates/idml/src/export.rs +++ b/crates/idml/src/export.rs @@ -659,7 +659,7 @@ impl<'a> Ex<'a> { .attr("DocumentSlugUniformSize", bool_s(s.slug.iter().all(|b| *b == s.slug[0]))) .attr("PreserveLayoutWhenShuffling", "true") .attr("AllowPageShuffle", "true") - .attr("OverprintBlack", "true") + .attr("OverprintBlack", bool_s(s.overprint_black)) .attr("PageBinding", "LeftToRight") .attr("ColumnDirection", "Horizontal") .attr( diff --git a/crates/idml/src/import.rs b/crates/idml/src/import.rs index 7d2d281c9..30d6663d9 100644 --- a/crates/idml/src/import.rs +++ b/crates/idml/src/import.rs @@ -394,6 +394,9 @@ impl<'r> Importer<'r> { fn doc_prefs(&mut self, e: &El) { let s = &mut self.settings; + if let Some(v) = e.get("OverprintBlack") { + s.overprint_black = v != "false"; + } if let Some(v) = e.num("PageWidth") { s.page_width = v; } diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 410cf162c..35b03b930 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -708,7 +708,7 @@ fn set_fill_paint(ctx: &mut RenderContext, doc: &Document, fill: &designcraft_do /// Overprint Preview: does this paint overprint (its own flag, or 100% [Black], which /// overprints by default)? It's then multiplied over what's beneath. pub(crate) fn overprints(f: &Frame, swatch: &str, tint: f32, flag: bool) -> bool { - f.opts.overprint_preview && (flag || (swatch == designcraft_color::swatch::BLACK && tint >= 0.999)) + f.opts.overprint_preview && (flag || (f.doc.settings.overprint_black && swatch == designcraft_color::swatch::BLACK && tint >= 0.999)) } pub fn blend_mode(b: DcBlend) -> BlendMode { @@ -977,6 +977,30 @@ mod tests { } } + #[test] + fn black_overprints_only_with_the_preference() { + let mut d = Document::new(&NewDocument::default()); + let lid = d.default_layer(); + for (sw, r) in + [("C=100 M=0 Y=0 K=0", Rect::new(100.0, 100.0, 200.0, 200.0)), (designcraft_color::swatch::BLACK, Rect::new(150.0, 100.0, 250.0, 200.0))] + { + let id = designcraft_doc::ItemId(d.alloc()); + let mut it = Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(r)); + it.fill = Fill::swatch(sw); + d.insert_item(SpreadRef::Doc(0), it, None).unwrap(); + } + let cache = Cache::new(); + let mut r = Renderer::new(); + r.threads = 0; + let o = RenderOptions { overprint_preview: true, ..Default::default() }; + let on = r.render_page(&d, &cache, 0, 1.0, false, &o).unwrap().pixel(175, 150); + let alone = r.render_page(&d, &cache, 0, 1.0, false, &o).unwrap().pixel(225, 150); + d.settings.overprint_black = false; + let off = r.render_page(&d, &cache, 0, 1.0, false, &o).unwrap().pixel(175, 150); + assert_eq!(off, alone, "knocks out: black alone"); + assert_ne!(on, alone, "overprints: the cyan shows through"); + } + #[test] fn appearance_of_black_on_screen() { let mut d = Document::new(&NewDocument::default()); diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 9c07f8c87..f7fb14d21 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -417,6 +417,25 @@ fn preferences(ui: &mut egui::Ui, d: &mut Dialog) { } }); }); + ui.horizontal(|ui| { + ui.label("Printing / Exporting:"); + let rich = d.b("richBlackOutput"); + egui::ComboBox::from_id_salt("pref_black_out") + .selected_text(if rich { "Output All Blacks as Rich Black" } else { "Output All Blacks Accurately" }) + .width(240.0) + .show_ui(ui, |ui| { + for (v, l) in [(false, "Output All Blacks Accurately"), (true, "Output All Blacks as Rich Black")] { + if ui.selectable_label(v == rich, l).clicked() { + d.fields.insert("richBlackOutput".into(), json!(v)); + } + } + }); + }); + if d.fields.contains_key("overprintBlack") { + ui.add_space(6.0); + ui.label(egui::RichText::new("[Black] Overprint").font(semibold(12.0))); + check(ui, d, "overprintBlack", "Overprint [Black] Swatch at 100%"); + } } "interface" => { ui.label(egui::RichText::new("UI Scaling").font(semibold(12.0))); @@ -963,7 +982,7 @@ pub fn confirm(app: &mut DesignApp) -> Result { "preferences" => { app.run( "prefs.set", - json!({"scaleStrokes": d.b("scaleStrokes"), "dimensionsIncludeStroke": d.b("dimensionsIncludeStroke"), "transformationsAreTotals": d.b("transformationsAreTotals"), "absolutePageNumbers": d.b("absolutePageNumbers"), "highlightHj": d.b("highlightHj"), "highlightKeeps": d.b("highlightKeeps"), "highlightCustomTracking": d.b("highlightCustomTracking"), "highlightSubstitutedFonts": d.b("highlightSubstitutedFonts"), "typographersQuotes": d.b("typographersQuotes"), "smartTextReflow": d.b("smartTextReflow")}), + json!({"scaleStrokes": d.b("scaleStrokes"), "dimensionsIncludeStroke": d.b("dimensionsIncludeStroke"), "transformationsAreTotals": d.b("transformationsAreTotals"), "absolutePageNumbers": d.b("absolutePageNumbers"), "highlightHj": d.b("highlightHj"), "highlightKeeps": d.b("highlightKeeps"), "highlightCustomTracking": d.b("highlightCustomTracking"), "highlightSubstitutedFonts": d.b("highlightSubstitutedFonts"), "richBlackOutput": d.b("richBlackOutput"), "typographersQuotes": d.b("typographersQuotes"), "smartTextReflow": d.b("smartTextReflow")}), )?; // Highlight options change the screen view only. app.canvas.shown = None; @@ -1020,6 +1039,7 @@ pub fn confirm(app: &mut DesignApp) -> Result { } } doc["advancedType"] = adv; + doc["overprintBlack"] = json!(d.b("overprintBlack")); app.run("document.preferences", doc) } "qrCode" => { diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 170b814fb..bf9f66c51 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -671,6 +671,7 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option Result { page, scale, false, - &designcraft_render::RenderOptions { printing_only: true, ..Default::default() }, + &designcraft_render::RenderOptions { printing_only: true, rich_black: app.session.prefs.rich_black_output, ..Default::default() }, ) .ok_or("no such page")?; let png = img.to_png(); diff --git a/docs/parity.md b/docs/parity.md index 77418dc24..38be43511 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -10,7 +10,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia | Priority | Features | Done | Partial | Missing | Parity | |---|---|---|---|---|---| | P0 | 102 | 99 | 3 | 0 | 99% | -| P1 | 97 | 75 | 21 | 1 | 88% | +| P1 | 97 | 76 | 20 | 1 | 89% | | P2 | 76 | 23 | 13 | 40 | 39% | | Area | Parity | @@ -20,7 +20,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia | Layers | 95% | | Frames, shapes & paths | 93% | | Transform | 100% | -| Fill, stroke, colour | 90% | +| Fill, stroke, colour | 93% | | Effects & transparency | 71% | | Placing & links | 82% | | Type & text frames | 92% | @@ -157,7 +157,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t | Fill, stroke, colour | Overprint Preview | P1 | D | | Fill, stroke, colour | Separations Preview | P2 | M | | Fill, stroke, colour | Eyedropper | P1 | D | -| Fill, stroke, colour | Appearance of Black prefs | P1 | P | +| 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 | Drop Shadow | P1 | D |