From 201188c3a85b830bf81980ca36f36ff9417dcce5 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 18:06:57 -0800 Subject: [PATCH] Print: a spot or Registration tint that is not a finite number prints no ink Separations Preview and printed separations clamped a linked spot or Registration tint to 0..1, but a NaN tint passed through the clamp and reached the plate's coverage as NaN. A native file can store an infinite tint (a number such as 1e39 read into an f32), and a blend between two such paints yields NaN. A tint that is not a finite number now prints no ink, as a gradient stop's process plate amounts already did; finite tints are still clamped, and a stop's process plates still print beside it. --- crates/doc/src/inks.rs | 38 +++++++++++++++++++++++++++++++---- crates/pdf/src/tests/print.rs | 11 ++++++++++ docs/target-app-parity.md | 1 + 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/crates/doc/src/inks.rs b/crates/doc/src/inks.rs index a9fa5f3f80..ed920fbd41 100644 --- a/crates/doc/src/inks.rs +++ b/crates/doc/src/inks.rs @@ -80,16 +80,22 @@ pub fn spot_color(doc: &Document, name: &str) -> Option { /// 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)>; +/// An ink amount clamped to 0..1; an amount that is not a finite number prints no ink. +fn ink_amount(v: f32) -> f32 { + if v.is_finite() { v.clamp(0.0, 1.0) } else { 0.0 } +} + /// 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. +/// its tint; one linked to the Registration swatch prints its tint on every plate. A tint that is +/// not a finite number prints no ink. 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 }; + return Inks { cmyk: [ink_amount(tint); 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 }; + return Inks { cmyk: [0.0; 4], spot: Some((name.to_string(), ink_amount(tint))), registration: false }; } Inks { cmyk: c.to_cmyk(color, intent), spot: None, registration: false } } @@ -104,7 +110,7 @@ pub type StopPlates = Option<[f32; 4]>; pub fn stop_inks(doc: &Document, c: &Cms, color: &Color, link: Link, plates: StopPlates, intent: Intent) -> Inks { let mut ink = inks(doc, c, color, link, intent); if let (Some(p), Some(_)) = (plates, &ink.spot) { - ink.cmyk = p.map(|v| if v.is_finite() { v.clamp(0.0, 1.0) } else { 0.0 }); + ink.cmyk = p.map(ink_amount); } ink } @@ -227,6 +233,30 @@ mod tests { } } + #[test] + fn a_spot_or_registration_tint_that_is_not_a_finite_number_prints_no_ink() { + let d = gold_doc(); + let c = vectorcraft_color::cms::active(); + let look = Color::cmyk(0.25, 0.1, 0.8, 0.05); + let ink = |link: Link| inks(&d, &c, &look, link, Intent::RelativeColorimetric); + for t in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] { + let spot = ink(Some(("Gold", t))); + assert_eq!(spot, Inks { cmyk: [0.0; 4], spot: Some(("Gold".into(), 0.0)), registration: false }, "{t}"); + assert_eq!((spot.on_plate("Gold"), spot.on_plate("Cyan")), (0.0, 0.0), "{t}"); + let reg = ink(Some((REGISTRATION, t))); + assert_eq!(reg, Inks { cmyk: [0.0; 4], spot: None, registration: true }, "{t}"); + assert_eq!((reg.on_plate("Gold"), reg.on_plate("Black")), (0.0, 0.0), "{t}"); + // A stop's process plates still print beside a spot tint that prints nothing. + let stop = stop_inks(&d, &c, &look, Some(("Gold", t)), Some([0.25, 0.0, 0.4, 0.0]), Intent::RelativeColorimetric); + assert_eq!((stop.cmyk, stop.on_plate("Gold")), ([0.25, 0.0, 0.4, 0.0], 0.0), "{t}"); + } + // Finite tints outside 0..1 are clamped. + assert_eq!(ink(Some(("Gold", 2.0))).on_plate("Gold"), 1.0); + assert_eq!(ink(Some(("Gold", -1.0))).on_plate("Gold"), 0.0); + assert_eq!(ink(Some((REGISTRATION, 2.0))).cmyk, [1.0; 4]); + assert_eq!(ink(Some((REGISTRATION, -1.0))).cmyk, [0.0; 4]); + } + #[test] fn gradient_stops_pass_their_plates_and_freeform_points_are_visited() { let mut stop = GradientStop::new(0.0, Color::cmyk(0.25, 0.1, 0.8, 0.05)); diff --git a/crates/pdf/src/tests/print.rs b/crates/pdf/src/tests/print.rs index ecd1f25ce1..7fa887a90f 100644 --- a/crates/pdf/src/tests/print.rs +++ b/crates/pdf/src/tests/print.rs @@ -662,6 +662,17 @@ fn a_spot_gradient_stop_prints_its_process_plates() { assert_eq!(first(json!({"output": {"mode": "separations", "spotsToProcess": true}})), [25.0, 10.0, 64.0, 5.0]); } +#[test] +fn a_spot_gradient_stop_whose_tint_is_not_a_number_prints_no_spot_ink() { + let mut mixed = GradientStop::new(0.0, Color::cmyk(0.25, 0.1, 0.8, 0.05)); + (mixed.swatch, mixed.tint, mixed.plates) = (Some("Gold".into()), f32::NAN, Some([0.25, 0.0, 0.4, 0.0])); + let g = GradientPaint::new(Gradient::new(GradientKind::Linear, vec![mixed, GradientStop::new(1.0, Color::cmyk(0.0, 0.0, 0.0, 0.0))])); + let d = gold_gradient_doc(g); + let first = |v| gradient_plates(&d, v).into_iter().map(|(s, _)| s).collect::>(); + assert_eq!(first(json!({"output": {"mode": "separations"}})), [25.0, 0.0, 40.0, 0.0, 0.0], "Cyan, Magenta, Yellow, Black, Gold"); + assert_eq!(first(json!({"output": {"mode": "separations", "spotsToProcess": true}})), [25.0, 0.0, 40.0, 0.0]); +} + #[test] fn a_freeform_gradients_points_separate_into_plates() { let mut g = GradientPaint::new(Gradient::new(GradientKind::Freeform, vec![GradientStop::new(0.0, Color::cmyk(0.0, 0.0, 0.0, 1.0))])); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index cad5851e98..fcdfa36303 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1703,6 +1703,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Invisible text in a PDF (rendering mode 3, which carries a scanned page's recognized text over the scan) opens as type with no fill or stroke, a run of its own beside visible text on the same line; text that can't come in as type (opened as outlines, Type 3 fonts) is left out with a warning. PDF export, and printing, write characters with no fill or stroke as real text in rendering mode 3, so they draw nothing but stay selectable and searchable, also in PDF/X-1a and PDF/A-2b (#2675, `pdf/src/tests/import_fidelity.rs`, `pdf/src/tests/realtext.rs`). A PDF, or a `.ai` read through its PDF part, whose spot inks have the same values but different names opens with a spot swatch for each ink. The solid fills and strokes of paths and type, and the stops of gradients on paths, link to the swatch of the ink they are painted in, the points of a gradient mesh in one of these inks keep the ink's color, and PDF export writes each of these inks as its own separation (`pdf/src/tests/import_color.rs`). Paint in these inks inside pattern cells, soft masks and type 3 glyphs, and gradients on type in these inks, still import as RGB. Type in one of these inks can also import as RGB when type in another Separation or DeviceN ink is shown with no path drawn between them. The stops of an axial or radial shading in a DeviceN space that mixes one spot ink with process plates (a spot ink over a constant cyan, for example) link to the ink's swatch at its tint and keep the process plates, and PDF and EPS (Level 3) export write those stops in a DeviceN space of the ink and those plates, so the spot plate is not lost (#3235, `import_color::a_device_n_gradient_of_a_spot_ink_over_a_process_plate_keeps_both_inks`). Solid fills that mix a spot ink with process plates, and stops that mix several spot inks, still import as RGB, with the spot inks kept as swatches. Print separations and Separations Preview print such a stop's process plates on the Cyan, Magenta, Yellow and Black plates beside its tint on the spot plate; with spots printed as process, each process plate prints the stop's plate layered with the spot tint's process equivalent. A freeform gradient's points separate into plates as its stops do, and converting the document's color mode converts them (`print::a_spot_gradient_stop_prints_its_process_plates`, `print::a_freeform_gradients_points_separate_into_plates`, `colorize::separations_preview_shows_a_spot_gradient_stops_process_plates`, `colormgmt::rgb_cmyk_converts_each_freeform_point`). + Print separations and Separations Preview clamp the tint of a fill, stroke or gradient stop linked to a spot swatch or to Registration to 0–1, and a tint that is not a finite number prints no ink, so the plate's coverage stays a number; a stop's process plates still print beside it. This is VectorCraft's own rule (`inks::tests::a_spot_or_registration_tint_that_is_not_a_finite_number_prints_no_ink`, `print::a_spot_gradient_stop_whose_tint_is_not_a_number_prints_no_spot_ink`). A `.ai` or EPS opened from its editing data keeps both swatches when a pattern's name, or a named color's, is also another swatch's: a pattern's swatch named as a default swatch, a named color or a palette color becomes "Name 2", and a named color named as a gradient or pattern swatch of the palette becomes "Name 2", with the fills, strokes and gradient stops linked to it following it (`ai::tests::a_pattern_swatch_named_as_another_swatch_is_numbered`, `ai::tests::a_color_named_as_a_palette_gradient_or_pattern_is_numbered`). PDF export writes a linear or radial gradient that mixes a spot color with process colors, or two spot colors, as a DeviceN shading of the spot inks and the process inks its stops use, so each spot ink prints on its own plate; an off-center midpoint is written as a stop with the inks halfway between its two stops. A gradient that mixes a spot color with RGB or Lab colors is still written without the spot ink, with a warning. A CMYK document, or an export that converts colors to a CMYK destination profile, writes RGB and Lab stops in CMYK first (unless the export converts colors to an RGB destination profile), so there such a gradient, Classic or Perceptual, is written as a DeviceN shading that keeps the spot ink. A Perceptual gradient of spot inks is written as a Separation or DeviceN shading of the stops on its curve; a dithered gradient fill, and a Perceptual gradient with RGB or Lab stops other than white or more than 4,096 stops on its curve, are written as images in process colors, with a warning. A Lab spot color in such a shading is named by the same Separation space, with the Lab alternate, as its flat fills (`engine/src/tests/linked_stops.rs`, `engine/src/tests/labspots.rs`). PDF and EPS export name a spot ink in a gradient's Separation or DeviceN space, and EPS in `%%DocumentCustomColors`, only when a stop linked to it at a tint above 0, or with process plates, colors part of the gradient (VectorCraft's own rule): a stop out of order whose neighboring stretches have no width, and in PDF a stop whose stretches are fully transparent, adds no ink and no empty plate. EPS writes every stop opaque, so there a transparent spot stop keeps its ink (`crates/pdf/src/tests/output.rs`, `crates/eps/src/tests.rs`).