diff --git a/crates/doc/src/overprint.rs b/crates/doc/src/overprint.rs index 50b54d3bc..a822728a3 100644 --- a/crates/doc/src/overprint.rs +++ b/crates/doc/src/overprint.rs @@ -94,26 +94,27 @@ impl Node { } } -/// Draw the overprinting fills and strokes of `a`'s subtree with Multiply (those with a blend +/// Draw the overprinting fills and strokes of `a`'s subtree with Darken (those with a blend /// mode of their own keep it), copying only the nodes on the way to them: on screen and on a -/// printed plate this approximates their inks printing over the inks below. With `discard_white` +/// printed plate this approximates their inks printing over the inks below. Darken keeps the +/// stronger ink of each plate (and each additive channel), so a colour overprinting itself stays +/// that colour, where Multiply would darken it. With `discard_white` /// (Document Setup → Discard White Overprint) white fills and strokes knock out instead. -pub fn multiply_overprints(a: &mut Arc, discard_white: bool) { +pub fn darken_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)); + let darkens = |overprint: bool, blend: BlendMode, paint: &Paint| overprint && blend == BlendMode::Normal && !(discard_white && is_white(paint)); for it in &mut n.appearance.items { match it { - crate::AppearanceItem::Fill(l) if multiplies(l.overprint, l.blend, &l.paint) => l.blend = BlendMode::Multiply, - crate::AppearanceItem::Stroke(l) if multiplies(l.overprint, l.blend, &l.paint) => l.blend = BlendMode::Multiply, + crate::AppearanceItem::Fill(l) if darkens(l.overprint, l.blend, &l.paint) => l.blend = BlendMode::Darken, + crate::AppearanceItem::Stroke(l) if darkens(l.overprint, l.blend, &l.paint) => l.blend = BlendMode::Darken, _ => {} } } for c in n.children_mut().into_iter().flatten() { - multiply_overprints(c, discard_white); + darken_overprints(c, discard_white); } } diff --git a/crates/pdf/src/print/mod.rs b/crates/pdf/src/print/mod.rs index 440231032..34762a2e0 100644 --- a/crates/pdf/src/print/mod.rs +++ b/crates/pdf/src/print/mod.rs @@ -36,7 +36,7 @@ use kurbo::{Affine, Rect}; use serde::Serialize; use vectorcraft_color::cms::{self, ProfileKind}; use vectorcraft_doc::marks::PrinterMarks; -use vectorcraft_doc::overprint::{clear_overprints, multiply_overprints}; +use vectorcraft_doc::overprint::{clear_overprints, darken_overprints}; use vectorcraft_doc::{ColorMode, Document, Node, NodeKind}; pub use layout::{MAX_TILES, TileGrid}; @@ -167,7 +167,7 @@ pub fn printed_document<'a>(doc: &'a Document, set: &PrintSettings) -> Cow<'a, D } /// Overprinting fills and strokes in composite output: discarded (they knock out) or simulated -/// (drawn with Multiply, as Overprint Preview shows them), after which nothing overprints any +/// (drawn with Darken, as Overprint Preview shows them), after which nothing overprints any /// more; or preserved. Separations always honour them. fn composite_overprints(doc: &mut Document, set: &PrintSettings) { let how = set.advanced.overprints; @@ -179,7 +179,7 @@ fn composite_overprints(doc: &mut Document, set: &PrintSettings) { let nodes = doc.layers.iter_mut().chain(symbols).chain(doc.patterns.iter_mut().flat_map(|p| p.art.iter_mut())); for n in nodes { if how == PrintOverprints::Simulate { - multiply_overprints(n, discard_white); + darken_overprints(n, discard_white); } clear_overprints(n); } @@ -411,7 +411,7 @@ fn print_inner(doc: &Document, opts: &PrintOptions) -> Result Separator<'a> { } /// The document as plate `plate`: art, symbols, pattern tiles and images in the grey of the - /// ink's coverage. Overprinting fills and strokes multiply, so where they have none of this + /// ink's coverage. Overprinting fills and strokes darken, so where they have none of this /// ink the plate below shows through instead of being knocked out. pub fn plate(&self, plate: &str) -> Document { let mut d = self.doc.clone(); let discard_white = d.setup.discard_white_overprint; let nodes = d.layers.iter_mut().chain(d.symbols.iter_mut().map(|s| &mut s.art)).chain(d.patterns.iter_mut().flat_map(|p| p.art.iter_mut())); for n in nodes { - multiply_overprints(n, discard_white); + darken_overprints(n, discard_white); self.node(plate, Arc::make_mut(n)); } for blob in d.images.values_mut() { diff --git a/crates/pdf/src/tests_printadvanced.rs b/crates/pdf/src/tests_printadvanced.rs index ca0abc07c..d1ae7c2e3 100644 --- a/crates/pdf/src/tests_printadvanced.rs +++ b/crates/pdf/src/tests_printadvanced.rs @@ -98,13 +98,13 @@ fn discarded_and_simulated_overprints_write_no_overprint() { assert!(preserve.warnings.is_empty(), "{:?}", preserve.warnings); assert!(String::from_utf8_lossy(&preserve.bytes).contains("/OP true/op true/OPM 1"), "preserved overprints overprint in the file"); assert!(overprints(&printed_document(&d, &settings(json!({})))), "preserved"); - for (how, multiplies) in [("discard", false), ("simulate", true)] { + for (how, darkens) in [("discard", false), ("simulate", true)] { let set = json!({"advanced": {"overprints": how}}); assert!(!overprints(&printed_document(&d, &settings(set.clone()))), "{how}: nothing overprints"); let r = job(&d, set); let text = String::from_utf8_lossy(&r.bytes); assert!(!text.contains("/OP"), "{how}: no overprint in the file"); - assert_eq!(text.contains("/Multiply"), multiplies, "{how}"); + assert_eq!(text.contains("/Darken"), darkens, "{how}"); assert!(!r.warnings.iter().any(|w| w.contains("knockouts")), "{how}: {:?}", r.warnings); } // Separations honour overprints whatever the option says. diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 5791bd49b..24bf5a0e0 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1773,12 +1773,12 @@ impl Renderer { return; } let tb = t.xf.transform_rect_bbox(g.bounds); - // Overprinting characters multiply per draw, like overprinting fills and strokes + // Overprinting characters darken per draw, like overprinting fills and strokes // ([`proof`]); the object blend mode is Normal here or applied by an enclosing layer. let overprints = proof::overprints(f.opts); let overprint = |ctx: &mut RenderContext, on: bool| { if overprints { - ctx.set_blend_mode(if on { blend_mode(vectorcraft_color::BlendMode::Multiply) } else { BlendMode::default() }); + ctx.set_blend_mode(if on { blend_mode(vectorcraft_color::BlendMode::Darken) } else { BlendMode::default() }); } }; // The object's own fills and strokes paint the whole outline: those below the Characters diff --git a/crates/render/src/proof.rs b/crates/render/src/proof.rs index 10db44cb6..e308e2b4f 100644 --- a/crates/render/src/proof.rs +++ b/crates/render/src/proof.rs @@ -11,7 +11,7 @@ //! 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 +//! Darken, 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 @@ -25,7 +25,7 @@ use vectorcraft_color::cms::{self, Cms, PROCESS_PLATES}; pub use vectorcraft_color::cms::{Intent, ProofSetup, ProofTarget}; use vectorcraft_color::{Color, Swatch, SwatchGroup}; pub use vectorcraft_doc::inks::{Inks, Link, Plate, inks, map_document_colors, map_node_colors, plates, spot_color}; -use vectorcraft_doc::overprint::multiply_overprints; +use vectorcraft_doc::overprint::darken_overprints; use vectorcraft_doc::{Document, Node, NodeId, Shared}; use crate::RenderOptions; @@ -103,8 +103,8 @@ impl PartialEq for Plates { } } -/// What [`prepare`] does to the fills and strokes of each node: overprinting ones multiply -/// ([`multiply_overprints`]), and with Separations Preview every color shows as the visible plates +/// What [`prepare`] does to the fills and strokes of each node: overprinting ones darken +/// ([`darken_overprints`]), and with Separations Preview every color shows as the visible plates /// print it ([`plate_color`]). struct How<'a> { doc: &'a Document, @@ -122,7 +122,7 @@ impl How<'_> { /// Change the fills, strokes and other colors of node `a`, which holds no members. fn apply(&self, a: &mut Arc) { - multiply_overprints(a, self.discard_white); + darken_overprints(a, self.discard_white); if let Some((c, proof, visible)) = self.plates { map_node_colors(Arc::make_mut(a), &mut |col, sw| plate_color(self.doc, c, proof, visible, col, sw)); } diff --git a/crates/render/src/tests_cmykblend.rs b/crates/render/src/tests_cmykblend.rs index 3ab3b19e8..9d3f1d933 100644 --- a/crates/render/src/tests_cmykblend.rs +++ b/crates/render/src/tests_cmykblend.rs @@ -261,3 +261,22 @@ fn rich_black_shows_black_ink_alone_as_rgb_black() { add(&mut d, n); assert_eq!(centre_with(&d, true), centre_with(&d, false), "an image shows as it is"); } + +/// An overprinting stroke in the colour of the fill under it shows no darker band in Overprint +/// Preview: the same ink printed over itself is that ink (#2415). Multiply would square the colour. +#[test] +fn overprint_of_the_same_colour_shows_no_darker_band() { + for (mode, c) in [(ColorMode::Rgb, Color::rgb(0.0, 0.55, 0.29)), (ColorMode::Cmyk, Color::cmyk(0.85, 0.0, 1.0, 0.0))] { + let mut d = Document::new_with_mode(40.0, 40.0, mode); + let mut n = + Node::path(d.alloc_id(), shapes::rectangle(Rect::new(10.0, 10.0, 30.0, 30.0)), Appearance::basic(Paint::solid(c), Paint::solid(c), 8.0)); + for it in &mut n.appearance.items { + *it.overprint_mut() = true; + } + add(&mut d, n); + let opts = RenderOptions { background: Some([255, 255, 255, 255]), overprint_preview: true, ..Default::default() }; + let r = Renderer::new().render_region_with(&d, d.artboards[0].rect, 1.0, &opts); + // (20, 20) is fill only; (12, 20) lies in the stroke's inner half, over the fill. + assert!(near(r.pixel(12, 20), r.pixel(20, 20), 2), "{mode:?}: {:?} vs {:?}", r.pixel(12, 20), r.pixel(20, 20)); + } +} diff --git a/docs/mcp.md b/docs/mcp.md index 8ba2017d5..bed84a08a 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1107,7 +1107,7 @@ Each fill and stroke (and each character's fill and stroke) can overprint: its i of knocking them out. `object.setOverprint {fill?, stroke?, item?, ids?}` sets it (groups set their contents, type its characters too; `item` aims at one appearance item) and `attributes.info {ids?}` reads it back (`null` where the objects differ). Overprint Preview (`view.overprintPreview`) and Separations Preview show it; rendering approximates -it by multiplying. Older files that listed Overprint Black objects in the document get the flags on load. +it by darkening (the stronger ink of each plate wins, so a colour overprinting itself stays that colour). Older files that listed Overprint Black objects in the document get the flags on load. PDF export writes them with `advanced.overprint: "preserve"` (the default): each overprinting fill and stroke sets a graphics state with `/OP true /op true /OPM 1` (`/OPM 0` in PDF/A). With `discardWhiteOverprint` (`document.setup`, on by default) white ones knock out instead; `"discard"` writes none. @@ -3049,7 +3049,7 @@ The Print dialog's Advanced section and the printer profile are print settings t `file.print`): `advanced: {printAsBitmap, overprints: preserve|discard|simulate, flattenerPreset}` and `color: {profile}`. In composite output, `preserve` writes overprinting fills and strokes with an overprinting graphics state (`/OP`, as PDF export does), `discard` makes overprinting fills and strokes knock out and `simulate` prints them -as Overprint Preview shows them (multiplied); either way the file has no overprint left. Separations always keep +as Overprint Preview shows them (darkened); either way the file has no overprint left. Separations always keep overprints. `flattenerPreset` (High Resolution, Medium Resolution, Low Resolution or a saved preset, `flattener.presets.list`) flattens transparency before printing, as Object → Flatten Transparency does; empty, a PDF keeps it live (PostScript, which has none, uses `file.print`'s `flattenerPreset`, else this one, else medium).