Merge pull request #2514 from vangel76/fix/2415-overprint-same-colour

Overprint Preview: a colour overprinting itself stays that colour (#2415)
This commit is contained in:
BFlat
2026-10-17 13:38:34 +07:00
committed by GitHub
8 changed files with 46 additions and 26 deletions
+9 -8
View File
@@ -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<Node>, discard_white: bool) {
pub fn darken_overprints(a: &mut Arc<Node>, 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);
}
}
+4 -4
View File
@@ -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<PrintReport, PdfEr
ex.intent = opts.settings.color.intent;
// The writer's colour output: the printer profile's conversion.
ex.out = w.out.clone();
// Composite output keeps overprints as the Advanced option says (plates multiply them).
// Composite output keeps overprints as the Advanced option says (plates darken them).
ex.overprint = !separations && pdf.advanced.overprint == Overprint::Preserve;
}
for &i in &plan.order {
+3 -3
View File
@@ -7,7 +7,7 @@ use vectorcraft_color::Color;
use vectorcraft_color::cms::{self, Cms, Model, ProfileKind};
use vectorcraft_color::swatch::REGISTRATION;
use vectorcraft_doc::inks::{Link, inks, plates, visit_node_colors};
use vectorcraft_doc::overprint::multiply_overprints;
use vectorcraft_doc::overprint::darken_overprints;
use vectorcraft_doc::{ColorMode, Document, Node};
use super::{DEFAULT_FREQUENCY, PrintInk, PrintSettings};
@@ -93,14 +93,14 @@ impl<'a> 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() {
+2 -2
View File
@@ -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.
+2 -2
View File
@@ -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
+5 -5
View File
@@ -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<Node>) {
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));
}
+19
View File
@@ -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));
}
}
+2 -2
View File
@@ -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).