//! Soft proofing (View → Proof Colors), Overprint Preview and Separations Preview. //! //! * **Proof Colors** post-processes the rendered frame: every pixel goes display sRGB → proof //! space (CMYK press or RGB/colour-blindness simulation) → display, through a cached 17³ LUT. //! * **Separations Preview** recolours the *document* before rendering: each paint colour is split //! into process inks (colour-managed), or a spot ink at the colour's tint when it's linked to a //! spot swatch (solid colours and gradient stops alike). With one plate visible the plate //! renders as greyscale ink coverage (black = 100%); with several, the visible inks are //! composited through the proof profile, spots multiplied on top. Colours linked to the //! Registration swatch print their tint on every plate, spot plates included. //! Placed raster images are not separated (limitation). //! * **Overprint Preview** (and Separations Preview, which implies it): fills and strokes that //! overprint ([`vectorcraft_doc::FillLayer::overprint`], characters' too) are drawn with //! Multiply, which approximates their inks printing over the inks below: a zero ink lets the //! inks below show through, where a knockout would replace them. //! //! The app's current view state lives in [`view`] / [`set_view`]; the canvas copies it into //! [`crate::RenderOptions`] with [`active_proof`] and [`overprint_preview_on`]. use std::borrow::Cow; use std::sync::{Arc, RwLock}; use vectorcraft_color::cms::{self, Cms, PROCESS_PLATES}; pub use vectorcraft_color::cms::{Intent, ProofSetup, ProofTarget}; use vectorcraft_color::swatch::REGISTRATION; use vectorcraft_color::{BlendMode, Color, Paint}; use vectorcraft_doc::{AppearanceItem, Document, Node, NodeKind}; use crate::RenderOptions; /// Ink coverage of one paint colour. #[derive(Clone, Debug, PartialEq)] pub struct Inks { /// Process inks C, M, Y, K (0..1). pub cmyk: [f32; 4], /// Spot ink (swatch name, tint 0..1). pub spot: Option<(String, f32)>, /// Registration: the tint (every process ink's) prints on every spot plate too. pub registration: bool, } impl Inks { /// The tint this colour prints on spot plate `name`. pub fn spot_tint(&self, name: &str) -> f32 { match &self.spot { _ if self.registration => self.cmyk[0], Some((n, t)) if n == name => *t, _ => 0.0, } } } /// A printing plate. #[derive(Clone, Debug, PartialEq)] pub struct Plate { pub name: String, pub spot: bool, /// Display colour of the ink (for the panel swatch). pub rgb: [f32; 3], } /// The plates of `doc`: the four process plates plus one per spot swatch. pub fn plates(doc: &Document) -> Vec { let c = cms::active(); let mut v: Vec = PROCESS_PLATES .iter() .enumerate() .map(|(i, n)| { let mut ink = [0.0; 4]; ink[i] = 1.0; Plate { name: (*n).into(), spot: false, rgb: c.cmyk_to_srgb(ink, false) } }) .collect(); for sw in doc.swatches_iter() { if let (true, Paint::Solid { color, .. }) = (sw.spot, &sw.paint) && !v.iter().any(|p| p.name == sw.name) { v.push(Plate { name: sw.name.clone(), spot: true, rgb: c.display_rgb(&doc.linked_color(*color, true)) }); } } v } /// The ink colour of spot swatch `name` as it shows ([`Document::linked_color`]). fn spot_color(doc: &Document, name: &str) -> Option { let s = doc.swatch(name).filter(|s| s.spot)?; s.paint.color().map(|c| doc.linked_color(c, true)) } /// A colour's swatch link and tint (`(swatch name, tint 0..1)`), as the colour visitors pass it. pub type Link<'a> = Option<(&'a str, f32)>; /// Separate one colour into inks. A colour linked to a spot swatch prints on that plate only, at /// its tint; one linked to the Registration swatch prints its tint on every plate. pub fn inks(doc: &Document, c: &Cms, color: &Color, link: Link, intent: Intent) -> Inks { if let Some((REGISTRATION, tint)) = link { return Inks { cmyk: [tint.clamp(0.0, 1.0); 4], spot: None, registration: true }; } if let Some((name, tint)) = link && doc.swatch(name).is_some_and(|s| s.spot && s.paint.color().is_some()) { return Inks { cmyk: [0.0; 4], spot: Some((name.to_string(), tint.clamp(0.0, 1.0))), registration: false }; } Inks { cmyk: c.to_cmyk(color, intent), spot: None, registration: false } } fn map_paint(p: &mut Paint, f: &mut dyn FnMut(&Color, Link) -> Color) { match p { Paint::Solid { color, swatch, tint } => *color = f(color, swatch.as_deref().map(|s| (s, *tint))), Paint::Gradient(g) => { for s in &mut g.gradient.stops { s.color = f(&s.color, s.swatch.as_deref().map(|n| (n, s.tint))); } } _ => {} } } /// Apply `f` to every colour of a node tree (fills, strokes, gradient stops, text runs, mesh /// points) given its swatch link and tint, keeping the links. pub fn map_node_colors(n: &mut Node, f: &mut dyn FnMut(&Color, Link) -> Color) { for it in &mut n.appearance.items { match it { AppearanceItem::Fill(l) => map_paint(&mut l.paint, f), AppearanceItem::Stroke(l) => map_paint(&mut l.paint, f), } } match &mut n.kind { NodeKind::Text(t) => { for r in &mut t.runs { map_paint(&mut r.style.fill, f); map_paint(&mut r.style.stroke, f); } } NodeKind::Mesh(m) => { for p in &mut m.points { p.color = f(&p.color, None); } } _ => {} } if let Some(ch) = n.children_mut() { for c in ch.iter_mut() { map_node_colors(Arc::make_mut(c), f); } } } /// Apply `f` to every colour in the document's art and symbol definitions. pub fn map_document_colors(doc: &mut Document, f: &mut dyn FnMut(&Color, Link) -> Color) { for l in &mut doc.layers { map_node_colors(Arc::make_mut(l), f); } for s in &mut doc.symbols { map_node_colors(Arc::make_mut(&mut s.art), f); } } /// Whether overprinting shows: Overprint Preview, or Separations Preview (which implies it). pub(crate) fn overprints(opts: &RenderOptions) -> bool { opts.overprint_preview || opts.proof.as_ref().is_some_and(|p| p.separations.is_some()) } /// Draw the overprinting fills and strokes of `a`'s subtree with Multiply (those with a blend /// mode of their own keep it), copying only the nodes on the way to them. With `discard_white` /// (Document Setup → Discard White Overprint) white fills and strokes knock out instead. fn multiply_overprints(a: &mut Arc, discard_white: bool) { if !a.has_overprint() { return; } let n = Arc::make_mut(a); let multiplies = |overprint: bool, blend: BlendMode, paint: &Paint| overprint && blend == BlendMode::Normal && !(discard_white && is_white(paint)); for it in &mut n.appearance.items { match it { AppearanceItem::Fill(l) if multiplies(l.overprint, l.blend, &l.paint) => l.blend = BlendMode::Multiply, AppearanceItem::Stroke(l) if multiplies(l.overprint, l.blend, &l.paint) => l.blend = BlendMode::Multiply, _ => {} } } for c in n.children_mut().into_iter().flatten() { multiply_overprints(c, discard_white); } } /// A solid paint of paper white (no ink in any colour model). fn is_white(p: &Paint) -> bool { let Paint::Solid { color, .. } = p else { return false }; match *color { Color::Rgb { r, g, b } => r.min(g).min(b) >= 1.0, Color::Cmyk { c, m, y, k } => c.max(m).max(y).max(k) <= 0.0, Color::Gray { k } => k <= 0.0, Color::Lab { l, a, b } => l >= 100.0 && a == 0.0 && b == 0.0, } } fn plate_color(doc: &Document, c: &Cms, proof: &ProofSetup, visible: &[String], color: &Color, link: Link) -> Color { let ink = inks(doc, c, color, link, proof.intent); let rgb = if visible.len() == 1 { let name = &visible[0]; let v = match PROCESS_PLATES.iter().position(|p| p == name) { Some(i) => ink.cmyk[i], None => ink.spot_tint(name), }; [1.0 - v; 3] } else { let mut cmyk = ink.cmyk; for (i, p) in PROCESS_PLATES.iter().enumerate() { if !visible.iter().any(|v| v == p) { cmyk[i] = 0.0; } } let mut rgb = c.proof_cmyk_to_srgb(cmyk, proof); for name in visible.iter().filter(|v| !PROCESS_PLATES.contains(&v.as_str())) { let t = ink.spot_tint(name); if t > 0.0 && let Some(sc) = spot_color(doc, name) { let s = c.display_rgb(&sc); for i in 0..3 { rgb[i] *= 1.0 - t * (1.0 - s[i]); } } } rgb }; let [r, g, b] = c.srgb_to_rgb(rgb); Color::Rgb { r, g, b } } /// The document as it should be drawn for these options (overprints, separations). pub(crate) fn prepare<'a>(doc: &'a Document, opts: &RenderOptions) -> Cow<'a, Document> { let seps = opts.proof.as_ref().and_then(|p| p.separations.as_ref().map(|s| (p, s))); let overprint = overprints(opts) && (doc.layers.iter().any(|l| l.has_overprint()) || doc.symbols.iter().any(|s| s.art.has_overprint())); if !overprint && seps.is_none() { return Cow::Borrowed(doc); } let mut d = doc.clone(); if overprint { let discard_white = doc.setup.discard_white_overprint; for l in &mut d.layers { multiply_overprints(l, discard_white); } for s in &mut d.symbols { multiply_overprints(&mut s.art, discard_white); } } if let Some((proof, visible)) = seps { let c = cms::active(); let src = doc.clone(); map_document_colors(&mut d, &mut |col, sw| plate_color(&src, &c, proof, visible, col, sw)); } Cow::Owned(d) } /// Soft-proof the rendered (premultiplied RGBA8) pixels in place. pub(crate) fn post(pixels: &mut [u8], opts: &RenderOptions) { let Some(proof) = opts.proof.as_ref() else { return }; if proof.separations.is_some() || matches!(proof.target, ProofTarget::MonitorRgb | ProofTarget::Srgb) { return; } let lut = cms::active().proof_lut(proof); let mut last_in = [0u8; 4]; let mut last_out = [0u8; 4]; for px in pixels.as_chunks_mut::<4>().0 { let a = px[3]; if a == 0 { continue; } if *px == last_in { px.copy_from_slice(&last_out); continue; } last_in.copy_from_slice(px); let un = |c: u8| if a == 255 { c } else { ((c as u32 * 255 + a as u32 / 2) / a as u32).min(255) as u8 }; let out = lut.apply8([un(px[0]), un(px[1]), un(px[2])]); for i in 0..3 { px[i] = if a == 255 { out[i] } else { ((out[i] as u32 * a as u32 + 127) / 255) as u8 }; } last_out.copy_from_slice(px); } } /// View-level proof state (View → Proof Setup / Proof Colors / Overprint Preview, Separations /// Preview panel). Process-wide: the app has one proof state for all windows. #[derive(Clone, Debug, Default, PartialEq)] pub struct ProofView { /// View → Proof Setup. pub setup: ProofSetup, /// View → Proof Colors. pub proof_colors: bool, /// View → Overprint Preview. pub overprint: bool, /// Separations Preview: `Some(visible plates)` when on. pub separations: Option>, } static VIEW: RwLock> = RwLock::new(None); pub fn view() -> ProofView { VIEW.read().unwrap_or_else(|e| e.into_inner()).clone().unwrap_or_default() } pub fn set_view(v: ProofView) { *VIEW.write().unwrap_or_else(|e| e.into_inner()) = Some(v); } /// The proof to pass in [`RenderOptions::proof`] for the current view state. pub fn active_proof() -> Option { let v = view(); match v.separations { Some(s) => Some(ProofSetup { separations: Some(s), ..v.setup }), None if v.proof_colors => Some(ProofSetup { separations: None, ..v.setup }), None => None, } } /// Whether [`RenderOptions::overprint_preview`] should be on (Separations Preview implies it). pub fn overprint_preview_on() -> bool { let v = view(); v.overprint || v.separations.is_some() }