prepress: decode spot names from the PDF once, so A#41 and AA get their own plates

This commit is contained in:
aiongg committed 2026-10-24 02:29:33 +00:00
1 parent 07f1250d6c
commit f7b71a80a1
3 files changed
+43 -10

No files matched your search

+14 -5
View File
@@ -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<InkId> {
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<InkId> {
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<InkId> {
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)?;
+5 -5
View File
@@ -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<Plate
continue;
}
let alternate = Alternate::Lab(srgb8_to_lab(swatch).unwrap_or([0.0; 3]));
let spot = Ink::Spot(Spot { name: ink.name().to_string(), alternate, solidity: None });
// A name that normalizes to one already listed (`A#2341` and `AA`) has no plate of its
// own.
match inks.add(spot) {
let spot = Spot { name: ink.name().to_string(), alternate, solidity: None };
// hayro hands over names with their `#xx` escapes decoded, so `#` here is a literal
// character. Two names that read the same after lossy UTF-8 decoding share one plate.
match inks.add_decoded(spot) {
Some(id) if id.index() == separated.len() => separated.push(ink),
_ => unseparated.push(ink.name().to_string()),
}
+24
View File
@@ -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] >>] >>";