From f7b71a80a1562fc62dda81c48e362df0b8a57e53 Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 24 Oct 2026 02:29:33 +0000 Subject: [PATCH] prepress: decode spot names from the PDF once, so A#41 and AA get their own plates --- crates/prepress/src/ink.rs | 19 ++++++++++++++----- crates/prepress/src/plates.rs | 10 +++++----- crates/prepress/tests/plates.rs | 24 ++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/crates/prepress/src/ink.rs b/crates/prepress/src/ink.rs index a6863006..90ab94a1 100644 --- a/crates/prepress/src/ink.rs +++ b/crates/prepress/src/ink.rs @@ -147,22 +147,31 @@ impl InkSet { Ink::Spot(spot) => spot, }; let name = normalize_name(&spot.name).into_owned(); - if name == "All" || name == "None" { + self.add_decoded(Spot { name, ..spot }) + } + + /// [`add`](Self::add) for a spot whose name is already decoded, as a PDF reader hands it + /// over: `#` in the name is a literal character. + pub(crate) fn add_decoded(&mut self, spot: Spot) -> Option { + if spot.name == "All" || spot.name == "None" { return None; } - if let Some(id) = self.lookup(&name) { + if let Some(id) = self.lookup_decoded(&spot.name) { return Some(id); } let id = InkId(u16::try_from(self.inks.len()).ok()?); - self.inks.push(Ink::Spot(Spot { name, ..spot })); + self.inks.push(Ink::Spot(spot)); Some(id) } /// The ink named `name`, compared after [`normalize_name`]: `"/Gold#20Foil"` finds /// `Gold Foil`. pub fn lookup(&self, name: &str) -> Option { - let name = normalize_name(name); - if let Some(p) = Process::from_name(&name) { + self.lookup_decoded(&normalize_name(name)) + } + + fn lookup_decoded(&self, name: &str) -> Option { + if let Some(p) = Process::from_name(name) { return Some(InkId::process(p)); } let at = self.inks.iter().skip(4).position(|ink| ink.name() == name)?; diff --git a/crates/prepress/src/plates.rs b/crates/prepress/src/plates.rs index 739b2829..4e2ce33f 100644 --- a/crates/prepress/src/plates.rs +++ b/crates/prepress/src/plates.rs @@ -24,7 +24,7 @@ use craft_pdf::syntax::object::{Array, Dict, Name, Stream, String as PdfText}; use craft_pdf::{OpenError, open_slice}; use crate::cms::{CmsConversion, ProfileError, srgb8_to_lab}; -use crate::ink::{Alternate, Ink, InkId, InkSet, Spot}; +use crate::ink::{Alternate, InkId, InkSet, Spot}; /// Spot inks separated per page, after the four process inks. More than any press runs: a /// `DeviceN` space has at most 32 colorants, and every plate costs a render and a byte per @@ -178,10 +178,10 @@ pub fn plates(pdf: &[u8], page: usize, dpi: f32, opts: &Options) -> Result separated.push(ink), _ => unseparated.push(ink.name().to_string()), } diff --git a/crates/prepress/tests/plates.rs b/crates/prepress/tests/plates.rs index 54197a28..da3dff3c 100644 --- a/crates/prepress/tests/plates.rs +++ b/crates/prepress/tests/plates.rs @@ -144,6 +144,30 @@ fn spot_inks_past_the_cap_are_listed_not_separated_and_bad_input_is_refused() { assert!(matches!(plates(b"not a pdf", 0, 72.0, &Options::default()), Err(Error::Open(_)))); } +#[test] +fn escaped_spot_names_are_decoded_once_and_each_gets_its_own_plate() { + // `/A#2341` is the spot `A#41`, not `AA`; `/Spot#20#2312` is `Spot #12`. + let names = ["/A#2341", "/AA", "/Spot#20#2312"]; + let spaces: String = names + .iter() + .enumerate() + .map(|(i, name)| format!("/S{i} [/Separation {name} /DeviceGray << /FunctionType 2 /Domain [0 1] /C0 [1] /C1 [0] /N 1 >>] ")) + .collect(); + let content: String = (0..names.len()).map(|i| format!("/S{i} cs 1 scn {} 0 10 10 re f ", i * 10)).collect(); + let pdf = page_pdf(&content, &format!("/ColorSpace << {spaces} >>"), "", &[]); + let p = plates(&pdf, 0, 72.0, &Options::default()).unwrap(); + let spots: Vec<&str> = p.inks.spots().map(|(_, s)| s.name.as_str()).collect(); + assert_eq!(spots, ["A#41", "AA", "Spot #12"]); + assert!(p.unseparated.is_empty()); + assert_eq!(p.planes.len(), 7); + for (i, (id, _)) in p.inks.spots().enumerate() { + for x in [5u32, 15, 25] { + let expected = if (x / 10) as usize == i { 255 } else { 0 }; + assert_eq!(p.tint(id, x, 35), Some(expected), "spot {i} at x = {x}"); + } + } +} + #[test] fn lab_colors_convert_through_the_profile_and_lab_white_is_paper() { let resources = "/ColorSpace << /D50 [/Lab << /WhitePoint [0.9642 1 0.8249] >>] /D65 [/Lab << /WhitePoint [0.9505 1 1.089] >>] >>";