From 69ff3fc00e88cda78d2ed717ba5d88a0a00daf70 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 27 Oct 2026 20:05:46 -0700 Subject: [PATCH] Part of #3235: PDF import keeps the spot swatch of a DeviceN color that mixes inks A DeviceN color with a spot ink at a tint above 0 together with a process plate or a second ink imports as RGB. The importer returned before marking those inks as used, and only used inks become spot swatches, so the spot ink was missing from the document and from every later export. Shading stops take the same path, so a DeviceN gradient lost its ink the same way. The art still imports as RGB with the existing warning; each ink it paints with now becomes a spot swatch with its 100% color. --- crates/pdf/src/import_color.rs | 10 ++++++++++ crates/pdf/src/tests/import_color.rs | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/crates/pdf/src/import_color.rs b/crates/pdf/src/import_color.rs index ac5b1580e..ff63f9493 100644 --- a/crates/pdf/src/import_color.rs +++ b/crates/pdf/src/import_color.rs @@ -301,6 +301,16 @@ impl<'p> Colors<'p> { let process = cmyk.iter().any(|v| *v > 0.0); let (c, t) = match (ink, first_ink) { (Some(_), _) if several || process => { + // The color can't link to one swatch, but each ink it paints with is still a + // spot swatch. + for (c, t) in colorants.iter().zip(comps) { + if let Colorant::Ink(k) = *c + && round3(t.clamp(0.0, 1.0)) > 0.0 + && let Some(ink) = self.inks.get_mut(k) + { + ink.used = true; + } + } self.mixed = true; return None; } diff --git a/crates/pdf/src/tests/import_color.rs b/crates/pdf/src/tests/import_color.rs index 8b98f7703..e2a7283df 100644 --- a/crates/pdf/src/tests/import_color.rs +++ b/crates/pdf/src/tests/import_color.rs @@ -164,6 +164,29 @@ fn device_n_inks_become_spot_swatches_and_process_plates_cmyk() { assert_eq!(d.swatches.iter().filter(|s| s.spot).count(), 2); } +#[test] +fn a_spot_ink_mixed_with_a_process_plate_keeps_its_swatch() { + // Gold and Cyan → CMYK (cyan, 0, gold, 0): a fill, then an axial shading of Gold 0 to 1 over + // Cyan at 0.3. + let space = format!("[/DeviceN [/Gold /Cyan] /DeviceCMYK {} 0 R]", first_extra(1)); + let func = "<< /FunctionType 4 /Domain [0 1 0 1] /Range [0 1 0 1 0 1 0 1] /Length 17 >>\nstream\n{ exch 0 exch 0 }\nendstream"; + let shading = format!( + "<< /ShadingType 2 /ColorSpace {space} /Coords [0 0 50 0] /Function << /FunctionType 2 /Domain [0 1] /C0 [0 0.3] /C1 [1 0.3] /N 1 >> /Extend [true true] >>" + ); + let fill = page_with(&format!("/CS0 {space}"), &[("CS0", "0.5 0.3")]); + let sh = PdfPage { resources: format!("/Shading << /Sh0 {shading} >>"), ..PdfPage::new(100.0, 50.0, "q 0 0 50 50 re W n /Sh0 sh Q") }; + for (what, page) in [("fill", fill), ("shading", sh)] { + let r = import_with_report(&pdf_with(&[page], &[func], None), &ImportOptions::default()).unwrap(); + let d = &r.document; + assert!(r.warnings.iter().any(|w| w.contains("several inks")), "{what}: {:?}", r.warnings); + assert!(links(d).iter().all(Option::is_none), "{what}: the art stays RGB"); + let gold = d.swatch("Gold").unwrap_or_else(|| panic!("{what}: no Gold swatch")); + assert!(gold.spot && gold.global, "{what}: Gold is a spot swatch"); + assert!(near(gold.paint.color().unwrap(), Color::cmyk(0.0, 0.0, 1.0, 0.0)), "{what}: {gold:?}"); + assert_eq!(d.swatches.iter().filter(|s| s.spot).count(), 1, "{what}: Cyan is a process plate"); + } +} + /// The spaces `/CS1` and `/CS2` of two inks (`[/{kind} {a} …]`, `[/{kind} {b} …]`) with the same /// alternate and tint transform, blue at 100 %: they look the same to the interpreter. fn blues(kind: &str, a: &str, b: &str) -> String {