E: CMYK transparency blend space on screen

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-04 02:09:26 -04:00
1 parent e856b849bb
commit 558e798dfc
6 files changed
+89 -10

No files matched your search

+6 -6
View File
@@ -49,7 +49,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
- Endnotes (`endnote.insert/edit/delete/list/options`, Type ▸ Insert Endnote): references number through the document, the endnote frame on a new last page lists them under a heading; IDML keeps the listed text only.
- Editorial notes (`note.new/edit/delete/convertToText/list`, Notes panel): anchored in text, flagged on screen, never printed or exported.
- File ▸ Print Booklet (`file.printBooklet`): saddle-stitch or 2-up consecutive printer spreads as PDF, padded with blanks, with a gap between pages.
- Edit ▸ Transparency Blend Space (`edit.transparencyBlendSpace`; RGB for web/mobile documents; IDML TransparencyPreference) — exported PDFs give pages with transparency a DeviceCMYK or DeviceRGB page group, so viewers and RIPs blend in that space; on-screen compositing is still RGB.
- Edit ▸ Transparency Blend Space (`edit.transparencyBlendSpace`; RGB for web/mobile documents; IDML TransparencyPreference) — exported PDFs give pages with transparency a DeviceCMYK or DeviceRGB page group, so viewers and RIPs blend in that space. On screen, spreads with transparency in a CMYK blend space are shown through the working CMYK profile (their colours keep to its gamut, as when composited in CMYK).
- High Contrast interface theme (fifth Interface Color Theme: black chrome, white text, yellow selection).
- 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.
@@ -167,18 +167,18 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
## How far from full parity (estimate, 2026-10-04)
**Breadth: ~99% weighted** (P0 core 99%, P1 99%, P2 96%) over the 275 features of the InDesign catalogue, scored
**Breadth: ~99% weighted** (P0 core 99%, P1 100%, P2 97%) over the 275 features of the InDesign catalogue, scored
row by row in [docs/parity.md](docs/parity.md) (`cargo xtask parity` recomputes it). Many features scored done still
lack some of InDesign's options or dialog details, so **overall parity including depth is about 82%**.
lack some of InDesign's options or dialog details, so **overall parity including depth is about 83%**.
**Remaining work: about 185 wall-clock hours of a single Claude Opus 5.5 agent** (±30%), or roughly 60–85 hours with
**Remaining work: about 170 wall-clock hours of a single Claude Opus 5.5 agent** (±30%), or roughly 60–85 hours with
four agents in parallel on separate crates:
| Work | Estimate |
|---|---|
| Open P0 (2: PDF/X-4 validation against a certified checker, PostScript interpretation for EPS) | 8 h |
| Open P1 (1 partial: on-screen blending in the blend space) | 4 h |
| Open P2 (16 partial: vertical/RTL refinements, alternate-layout linking, separations from source colours, buttons as form fields, interactive PDF extras, scripting, localization breadth, …) | 45 h |
| Open P1 (none partial) | 0 h |
| Open P2 (4 partial: localization breadth, INDD pages (not readable clean-room), in-app media playback, vertical metrics and warichu) | 14 h |
| Depth and pixel fidelity of every dialog, panel and menu against InDesign 2026 | 90 h |
| Performance (incremental composition, GPU raster) and interchange hardening (IDML/PDF corpus) | 55 h |
+10
View File
@@ -1100,6 +1100,16 @@ impl Item {
self.shape == Shape::Group && matches!(self.content, Content::Group { .. })
}
/// Does this object (or anything in it) involve transparency (opacity, a blend mode, effects,
/// knockout)?
pub fn involves_transparency(&self) -> bool {
self.opacity < 1.0
|| self.blend != Default::default()
|| self.effects.any()
|| self.knockout
|| self.children().iter().any(|c| c.involves_transparency())
}
/// A frame with items pasted into it (Edit › Paste Into): drawn clipped to its path.
pub fn has_nested_items(&self) -> bool {
self.shape != Shape::Group && matches!(&self.content, Content::Group { items } if !items.is_empty())
+1 -1
View File
@@ -226,7 +226,7 @@ pub(crate) fn options(p: &Value, page_count: usize) -> Result<PdfOptions> {
/// Does this object (or anything in it) need the transparency flattener?
fn transparent(it: &designcraft_doc::Item) -> bool {
it.opacity < 1.0 || it.blend != Default::default() || it.effects.any() || it.knockout || it.children().iter().any(|c| transparent(c))
it.involves_transparency()
}
/// Transparency Flattener: each object involving transparency becomes an opaque image of its
+67
View File
@@ -130,6 +130,9 @@ pub struct RenderOptions {
pub rich_black: bool,
/// View › Overprint Preview: overprinting inks mix with what's beneath.
pub overprint_preview: bool,
/// Screen: spreads with transparency are shown as blended in the document's blend space (a
/// CMYK blend space keeps their colours in the working CMYK gamut).
pub blend_space_view: bool,
}
/// View › Display Performance: how placed graphics and effects are drawn on screen.
@@ -166,6 +169,7 @@ impl Default for RenderOptions {
quality: DisplayQuality::High,
rich_black: false,
overprint_preview: false,
blend_space_view: false,
}
}
}
@@ -303,6 +307,17 @@ impl Renderer {
// Render straight into the returned buffer (no extra copy of the frame).
let mut pixels = vec![0u8; w as usize * h as usize * 4];
ctx.render(vello_cpu::PixmapMut::new(w, h, &mut pixels).expect("buffer size"), &mut self.resources);
if opts.blend_space_view && doc.settings.blend_space == designcraft_doc::BlendSpace::Cmyk && opts.plate.is_none() {
let mut img = Rendered { width: w as u32, height: h as u32, pixels };
for pl in spreads {
let Some(sp) = doc.spread(pl.spread) else { continue };
if sp.items.iter().any(|it| it.involves_transparency() && !it.hidden) {
let r = (view * pl.xf).transform_rect_bbox(sp.bounds());
blend_space_view(&mut img, r);
}
}
pixels = img.pixels;
}
self.ctx = Some(ctx);
self.stories.clear();
self.stats.micros = now().saturating_sub(start);
@@ -1066,6 +1081,31 @@ pub fn proof_view(img: &mut Rendered, setup: &designcraft_color::cms::ProofSetup
}
}
/// A CMYK transparency blend space on screen: the pixels of `area` (device space) through the
/// working CMYK profile and back, as a spread composited in that space looks.
fn blend_space_view(img: &mut Rendered, area: Rect) {
use designcraft_color::cms::{ProofSetup, ProofTarget};
let lut = designcraft_color::cms::active().proof_lut(&ProofSetup { target: ProofTarget::WorkingCmyk, ..Default::default() });
let (w, h) = (img.width as i64, img.height as i64);
let (x0, y0) = ((area.x0.floor() as i64).clamp(0, w), (area.y0.floor() as i64).clamp(0, h));
let (x1, y1) = ((area.x1.ceil() as i64).clamp(0, w), (area.y1.ceil() as i64).clamp(0, h));
for y in y0..y1 {
let row = &mut img.pixels[((y * w + x0) * 4) as usize..((y * w + x1) * 4) as usize];
for px in row.chunks_exact_mut(4) {
let a = px[3];
if a == 0 {
continue;
}
let k = 255.0 / a as f32;
let rgb = [px[0], px[1], px[2]].map(|v| ((v as f32 * k).round()).min(255.0) as u8);
let out = lut.apply8(rgb);
for i in 0..3 {
px[i] = ((out[i] as u32 * a as u32 + 127) / 255) as u8;
}
}
}
}
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;
@@ -1207,6 +1247,33 @@ mod tests {
assert!(on[2] < 60 && on[0] > 180, "magenta over yellow → red: {on:?}");
}
#[test]
fn cmyk_blend_space_shows_transparent_spreads_in_cmyk() {
let mut d = Document::new(&NewDocument::default());
d.swatches.push(designcraft_color::swatch::Swatch::color("Green", designcraft_color::Color::rgb8(0, 255, 0)));
let lid = d.default_layer();
let id = designcraft_doc::ItemId(d.alloc());
let mut it =
Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(100.0, 100.0, 200.0, 200.0)));
it.fill = Fill::swatch("Green");
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
let cache = Cache::new();
let mut r = Renderer::new();
r.threads = 0;
let px = |d: &Document, r: &mut Renderer| {
r.render_page(d, &cache, 0, 1.0, false, &RenderOptions { blend_space_view: true, ..Default::default() }).unwrap().pixel(150, 150)
};
// No transparency on the spread: shown as is.
assert_eq!(px(&d, &mut r)[1], 255);
// With transparency in a CMYK blend space the bright green leaves the RGB gamut's edge.
d.item_mut(id).unwrap().opacity = 0.99;
let cmyk = px(&d, &mut r);
assert!(cmyk[0] > 20 || cmyk[1] < 240, "{cmyk:?}");
d.settings.blend_space = designcraft_doc::BlendSpace::Rgb;
let rgb = px(&d, &mut r);
assert!(rgb[1] > 245 && rgb[0] < 10, "{rgb:?}");
}
#[test]
fn knockout_group_hides_its_own_objects() {
let mut d = Document::new(&NewDocument::default());
+2
View File
@@ -421,6 +421,7 @@ fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rect, xf: &Xf,
quality: app.ui.display_quality,
rich_black: app.ui.rich_black,
overprint_preview: app.ui.overprint_preview,
blend_space_view: !app.ui.proof_colors,
..Default::default()
};
(st.doc.clone(), placed, w, h, view, opts)
@@ -533,6 +534,7 @@ fn patch_texture(app: &mut DesignApp, layout: &CanvasLayout, ppp: f64, doc_key:
quality: app.ui.display_quality,
rich_black: app.ui.rich_black,
overprint_preview: app.ui.overprint_preview,
blend_space_view: !app.ui.proof_colors,
..Default::default()
};
let img = app.canvas.patcher.render(&new, &app.session.cache, &placed, (x1 - x0) as u32, (y1 - y0) as u32, view, &opts);
+3 -3
View File
@@ -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 | 100 | 2 | 0 | 99% |
| P1 | 97 | 96 | 1 | 0 | 99% |
| P1 | 97 | 97 | 0 | 0 | 100% |
| P2 | 76 | 72 | 4 | 0 | 97% |
| 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 | 100% |
| Effects & transparency | 92% |
| Effects & transparency | 100% |
| Placing & links | 93% |
| Type & text frames | 99% |
| Typography | 100% |
@@ -164,7 +164,7 @@ Open P0 items: Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print)
| Effects & transparency | Inner Shadow, Outer Glow, Inner Glow, Bevel, Satin | P2 | D |
| Effects & transparency | Basic/Directional/Gradient Feather | P2 | D |
| Effects & transparency | Global Light | P2 | D |
| Effects & transparency | Transparency blend space | P1 | P |
| Effects & transparency | Transparency blend space | P1 | D |
| Effects & transparency | Transparency flattener | P2 | D |
| Placing & links | Place | P0 | D |
| Placing & links | Place into frame / by click or drag | P0 | D |