C: separations from source colours
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0d02222dab
commit
7b636fb9f5
|
|
@ -81,6 +81,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
|||
- 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).
|
||||
- Separations Preview plates now come from each object's own colour (CMYK as authored; RGB and images through the colour settings, images via a cached 17³ table), so pure black shows only on the black plate.
|
||||
- Books (`book.new/open/add/remove/list/styleSource/paginate/syncStyles/exportPdf`, Book panel): `.dcbook` files, continuous page numbering, style sync from a style source, one merged PDF.
|
||||
- Track Changes (`changes.track/list/acceptAll/rejectAll`, Type ▸ Track Changes): typing is marked added (highlighted on screen), deletions are kept but hidden from layout and exports until accepted or rejected. Per-change accept/reject and authors aren't there yet.
|
||||
- Track Changes: per-change Accept / Reject (`changes.accept`, `changes.reject`) and a Track Changes panel listing every change.
|
||||
|
|
|
|||
|
|
@ -2183,6 +2183,27 @@ mod attributes_tests {
|
|||
mod fitting_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn separations_come_from_source_colours() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let c = s.execute("frame.create", &json!({"rect": [100, 100, 200, 200]})).unwrap()["id"].clone();
|
||||
s.execute("object.fill", &json!({"swatch": "C=100 M=0 Y=0 K=0", "ids": [c]})).unwrap();
|
||||
let k = s.execute("frame.create", &json!({"rect": [300, 100, 400, 200]})).unwrap()["id"].clone();
|
||||
s.execute("object.fill", &json!({"swatch": "[Black]", "ids": [k]})).unwrap();
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let plate = |p: u8| {
|
||||
let mut rr = designcraft_render::Renderer::new();
|
||||
rr.threads = 0;
|
||||
rr.render_page(&d, &s.cache, 0, 1.0, false, &designcraft_render::RenderOptions { plate: Some(p), ..Default::default() }).unwrap()
|
||||
};
|
||||
let (cyan, black) = (plate(0), plate(3));
|
||||
// Cyan ink only on the cyan frame; black ink only on the black one (no rich-black guess).
|
||||
assert!(cyan.pixel(150, 150)[0] < 10 && cyan.pixel(350, 150)[0] > 245, "{:?} {:?}", cyan.pixel(150, 150), cyan.pixel(350, 150));
|
||||
assert!(black.pixel(350, 150)[0] < 10 && black.pixel(150, 150)[0] > 245);
|
||||
assert!(cyan.pixel(20, 20)[0] > 245, "paper has no ink");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_glow_bevel_and_satin_change_the_inside() {
|
||||
let mut s = Session::new();
|
||||
|
|
|
|||
|
|
@ -120,6 +120,9 @@ pub struct RenderOptions {
|
|||
pub change_markup: bool,
|
||||
/// View › Structure › Show Tag Markers: brackets around inline-tagged text (screen view).
|
||||
pub tag_markers: bool,
|
||||
/// Separations Preview: draw one process plate (0 = cyan … 3 = black) as grey ink coverage,
|
||||
/// from each object's own colour (CMYK as authored, others through the colour settings).
|
||||
pub plate: Option<u8>,
|
||||
/// View › Display Performance.
|
||||
pub quality: DisplayQuality,
|
||||
/// Preferences › Appearance of Black: show 100% K as rich (pure) black instead of the
|
||||
|
|
@ -159,6 +162,7 @@ impl Default for RenderOptions {
|
|||
note_indicators: false,
|
||||
change_markup: false,
|
||||
tag_markers: false,
|
||||
plate: None,
|
||||
quality: DisplayQuality::High,
|
||||
rich_black: false,
|
||||
overprint_preview: false,
|
||||
|
|
@ -272,6 +276,7 @@ impl Renderer {
|
|||
};
|
||||
let mt = ctx.render_settings().num_threads > 0;
|
||||
RICH_BLACK.with(|r| r.set(opts.rich_black));
|
||||
PLATE.with(|p| p.set(opts.plate));
|
||||
self.stats = FrameStats::default();
|
||||
self.stories.clear();
|
||||
self.glyphs.tick();
|
||||
|
|
@ -704,6 +709,10 @@ impl Renderer {
|
|||
let hidden = HIDDEN_LAYERS.with(|h| h.borrow().clone());
|
||||
let data = if !hidden.is_empty() && is_pdf(&asset.data) { pdf_layers::layered(&asset.data, &hidden) } else { asset.data.clone() };
|
||||
let Some(pm) = images::mip(&data, asset.page, g.size.0, on_screen) else { return };
|
||||
let pm = match PLATE.with(|p| p.get()) {
|
||||
Some(plate) => plate_pixmap(&pm, plate),
|
||||
None => pm,
|
||||
};
|
||||
let rect = Rect::new(0.0, 0.0, g.size.0, g.size.1);
|
||||
ctx.set_transform(f.view * xf * g.xf);
|
||||
let sx = g.size.0 / pm.width().max(1) as f64;
|
||||
|
|
@ -718,6 +727,78 @@ impl Renderer {
|
|||
}
|
||||
}
|
||||
|
||||
/// sRGB → working CMYK through a 17³ table built once per colour settings (trilinear).
|
||||
fn cmyk_lut(rgb: [f32; 3]) -> [f32; 4] {
|
||||
const N: usize = 17;
|
||||
type Lut = (designcraft_color::cms::ColorSettings, std::sync::Arc<Vec<[f32; 4]>>);
|
||||
static LUT: std::sync::Mutex<Option<Lut>> = std::sync::Mutex::new(None);
|
||||
let settings = designcraft_color::cms::active_settings();
|
||||
let table = {
|
||||
let mut g = LUT.lock().unwrap_or_else(|e| e.into_inner());
|
||||
match g.as_ref() {
|
||||
Some((s, t)) if *s == settings => t.clone(),
|
||||
_ => {
|
||||
let cms = designcraft_color::cms::active();
|
||||
let step = |i: usize| i as f32 / (N - 1) as f32;
|
||||
let t: Vec<[f32; 4]> = (0..N * N * N)
|
||||
.map(|i| {
|
||||
cms.srgb_to_cmyk([step(i / (N * N)), step(i / N % N), step(i % N)], designcraft_color::cms::Intent::RelativeColorimetric)
|
||||
})
|
||||
.collect();
|
||||
let t = std::sync::Arc::new(t);
|
||||
*g = Some((settings, t.clone()));
|
||||
t
|
||||
}
|
||||
}
|
||||
};
|
||||
let f = rgb.map(|v| v.clamp(0.0, 1.0) * (N - 1) as f32);
|
||||
let i = f.map(|v| (v as usize).min(N - 2));
|
||||
let d = [f[0] - i[0] as f32, f[1] - i[1] as f32, f[2] - i[2] as f32];
|
||||
let mut out = [0.0f32; 4];
|
||||
for (corner, w) in (0..8).map(|c| {
|
||||
let (a, b, cc) = (c >> 2 & 1, c >> 1 & 1, c & 1);
|
||||
let w = (if a == 1 { d[0] } else { 1.0 - d[0] }) * (if b == 1 { d[1] } else { 1.0 - d[1] }) * (if cc == 1 { d[2] } else { 1.0 - d[2] });
|
||||
((i[0] + a) * N * N + (i[1] + b) * N + i[2] + cc, w)
|
||||
}) {
|
||||
for k in 0..4 {
|
||||
out[k] += table[corner][k] * w;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// An image's ink coverage on one process plate, as grey (cached per image and plate).
|
||||
fn plate_pixmap(pm: &std::sync::Arc<vello_cpu::Pixmap>, plate: u8) -> std::sync::Arc<vello_cpu::Pixmap> {
|
||||
type Cache = std::collections::HashMap<(usize, u8), std::sync::Arc<vello_cpu::Pixmap>>;
|
||||
static CACHE: std::sync::Mutex<Option<Cache>> = std::sync::Mutex::new(None);
|
||||
let key = (std::sync::Arc::as_ptr(pm) as usize, plate);
|
||||
if let Some(v) = CACHE.lock().unwrap_or_else(|e| e.into_inner()).as_ref().and_then(|c| c.get(&key).cloned()) {
|
||||
return v;
|
||||
}
|
||||
let data: Vec<vello_cpu::color::PremulRgba8> = pm
|
||||
.data()
|
||||
.iter()
|
||||
.map(|p| {
|
||||
if p.a == 0 {
|
||||
return *p;
|
||||
}
|
||||
let k = 255.0 / p.a as f32;
|
||||
let rgb = [p.r, p.g, p.b].map(|v| (v as f32 * k / 255.0).min(1.0));
|
||||
let cmyk = cmyk_lut(rgb);
|
||||
let v = ((1.0 - cmyk[plate.min(3) as usize].clamp(0.0, 1.0)) * p.a as f32).round() as u8;
|
||||
vello_cpu::color::PremulRgba8 { r: v, g: v, b: v, a: p.a }
|
||||
})
|
||||
.collect();
|
||||
let out = std::sync::Arc::new(vello_cpu::Pixmap::from_parts(data, pm.width(), pm.height()));
|
||||
let mut c = CACHE.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let map = c.get_or_insert_with(Default::default);
|
||||
if map.len() > 32 {
|
||||
map.clear();
|
||||
}
|
||||
map.insert(key, out.clone());
|
||||
out
|
||||
}
|
||||
|
||||
fn rect_overlaps(a: Rect, b: Rect) -> bool {
|
||||
a.x0 < b.x1 && a.x1 > b.x0 && a.y0 < b.y1 && a.y1 > b.y0
|
||||
}
|
||||
|
|
@ -727,9 +808,19 @@ thread_local! {
|
|||
static HIDDEN_LAYERS: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };
|
||||
/// Appearance of Black for the render on this thread: 100% K as pure black.
|
||||
static RICH_BLACK: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
||||
/// Separations Preview plate for the render on this thread.
|
||||
static PLATE: std::cell::Cell<Option<u8>> = const { std::cell::Cell::new(None) };
|
||||
}
|
||||
|
||||
pub fn color_of(c: &designcraft_color::Color, alpha: f32) -> peniko::Color {
|
||||
if let Some(p) = PLATE.with(|p| p.get()) {
|
||||
let cmyk = match *c {
|
||||
designcraft_color::Color::Cmyk { c, m, y, k } => [c, m, y, k],
|
||||
_ => c.to_cmyk_managed(designcraft_color::cms::Intent::RelativeColorimetric),
|
||||
};
|
||||
let v = ((1.0 - cmyk[p.min(3) as usize].clamp(0.0, 1.0)) * 255.0).round() as u8;
|
||||
return peniko::Color::from_rgba8(v, v, v, (alpha.clamp(0.0, 1.0) * 255.0).round() as u8);
|
||||
}
|
||||
if RICH_BLACK.with(|r| r.get())
|
||||
&& let designcraft_color::Color::Cmyk { c: cc, m, y, k } = *c
|
||||
&& cc <= 1e-3
|
||||
|
|
|
|||
|
|
@ -414,6 +414,7 @@ fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rect, xf: &Xf,
|
|||
note_indicators: !preview && app.session.prefs.show_note_anchors,
|
||||
change_markup: !preview && app.session.prefs.show_added_text,
|
||||
tag_markers: !preview && app.ui.tag_markers,
|
||||
plate: app.ui.separation,
|
||||
highlight_keeps: !preview && app.session.prefs.highlight_keeps,
|
||||
highlight_custom_tracking: !preview && app.session.prefs.highlight_custom_tracking,
|
||||
quality: app.ui.display_quality,
|
||||
|
|
@ -526,6 +527,7 @@ fn patch_texture(app: &mut DesignApp, layout: &CanvasLayout, ppp: f64, doc_key:
|
|||
note_indicators: !preview && app.session.prefs.show_note_anchors,
|
||||
change_markup: !preview && app.session.prefs.show_added_text,
|
||||
tag_markers: !preview && app.ui.tag_markers,
|
||||
plate: app.ui.separation,
|
||||
highlight_keeps: !preview && app.session.prefs.highlight_keeps,
|
||||
highlight_custom_tracking: !preview && app.session.prefs.highlight_custom_tracking,
|
||||
quality: app.ui.display_quality,
|
||||
|
|
@ -556,8 +558,9 @@ fn upload(app: &mut DesignApp, ctx: &egui::Context, mut img: designcraft_render:
|
|||
if app.ui.proof_colors {
|
||||
designcraft_render::proof_view(&mut img, &app.ui.proof_setup);
|
||||
}
|
||||
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);
|
||||
// Plates render from source colours; the ink limit is estimated from the composite.
|
||||
if app.ui.separation.is_none() && app.ui.ink_limit.is_some() {
|
||||
designcraft_render::separation_view(&mut img, None, 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 {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
|||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 100 | 2 | 0 | 99% |
|
||||
| P1 | 97 | 94 | 3 | 0 | 98% |
|
||||
| P2 | 76 | 64 | 12 | 0 | 92% |
|
||||
| P2 | 76 | 65 | 11 | 0 | 93% |
|
||||
|
||||
| Area | Parity |
|
||||
|---|---|
|
||||
|
|
@ -20,7 +20,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
|||
| Layers | 100% |
|
||||
| Frames, shapes & paths | 100% |
|
||||
| Transform | 100% |
|
||||
| Fill, stroke, colour | 99% |
|
||||
| Fill, stroke, colour | 100% |
|
||||
| Effects & transparency | 92% |
|
||||
| Placing & links | 93% |
|
||||
| Type & text frames | 99% |
|
||||
|
|
@ -155,7 +155,7 @@ Open P0 items: Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print)
|
|||
| Fill, stroke, colour | Stroke styles | P1 | D |
|
||||
| Fill, stroke, colour | Color management | P1 | D |
|
||||
| Fill, stroke, colour | Overprint Preview | P1 | D |
|
||||
| Fill, stroke, colour | Separations Preview | P2 | P |
|
||||
| Fill, stroke, colour | Separations Preview | P2 | D |
|
||||
| Fill, stroke, colour | Eyedropper | P1 | D |
|
||||
| Fill, stroke, colour | Appearance of Black prefs | P1 | D |
|
||||
| Effects & transparency | Opacity & blend modes | P0 | D |
|
||||
|
|
|
|||
Loading…
Reference in New Issue