Merge pull request #5184 from dotCooCoo/fix/eps-spot-reserved-colorant-names

EPS: spot inks named All or None print as spot inks
This commit is contained in:
Brandon Thomas
2026-11-01 23:45:30 -05:00
committed by GitHub
3 changed files with 90 additions and 20 deletions
+45 -17
View File
@@ -142,8 +142,9 @@ pub(crate) struct Scene<'a> {
cms: Arc<vectorcraft_color::cms::Cms>,
body: String,
setup: String,
/// Spot colour spaces defined in the setup (`VCsp<i>`): swatch name and CMYK equivalent.
spots: Vec<(String, [f32; 4])>,
/// Spot colour spaces defined in the setup (`VCsp<i>`): swatch name, colorant name
/// ([`Scene::colorant_name`]) and CMYK equivalent.
spots: Vec<(String, String, [f32; 4])>,
/// DeviceN color spaces defined in the setup (`VCdn<i>`), as their definitions.
device_n: Vec<String>,
warnings: Vec<String>,
@@ -191,7 +192,12 @@ impl<'a> Scene<'a> {
}
fn page(self) -> Page {
let custom = self.spots.iter().filter(|(n, _)| n != vectorcraft_color::swatch::REGISTRATION).cloned().collect();
let custom = self
.spots
.iter()
.filter(|(n, ..)| n != vectorcraft_color::swatch::REGISTRATION)
.map(|(_, colorant, cmyk)| (colorant.clone(), *cmyk))
.collect();
Page { setup: self.setup, body: self.body, custom, warnings: self.warnings }
}
@@ -235,7 +241,7 @@ impl<'a> Scene<'a> {
/// The index of the Separation colour space of spot swatch (or Registration) `name`, defined
/// in the setup the first time; `None` when `name` is neither.
fn spot_space(&mut self, name: &str) -> Option<usize> {
if let Some(i) = self.spots.iter().position(|(n, _)| n == name) {
if let Some(i) = self.spots.iter().position(|(n, ..)| n == name) {
return Some(i);
}
let registration = name == vectorcraft_color::swatch::REGISTRATION;
@@ -249,17 +255,37 @@ impl<'a> Scene<'a> {
};
let i = self.spots.len();
let _ = write!(self.setup, "/VCsp{i} [/Separation ");
if registration {
let colorant = if registration {
self.setup.push_str("/All /DeviceCMYK {dup dup dup}");
"All".to_string()
} else {
let colorant = self.colorant_name(name);
let [c, m, y, k] = cmyk.map(|v| ps::num(f64::from(v)));
let _ = write!(self.setup, "{} cvn /DeviceCMYK {{dup {c} mul exch dup {m} mul exch dup {y} mul exch {k} mul}}", ps::string(name));
}
let _ = write!(self.setup, "{} cvn /DeviceCMYK {{dup {c} mul exch dup {m} mul exch dup {y} mul exch {k} mul}}", ps::string(&colorant));
colorant
};
self.setup.push_str("] def\n");
self.spots.push((name.to_string(), cmyk));
self.spots.push((name.to_string(), colorant, cmyk));
Some(i)
}
/// The colorant name spot swatch `name` is written under: its own name, except that `All`
/// (every plate in PostScript) and `None` (no plate) become `All Spot` and `None Spot`,
/// numbered `All Spot 2`, `All Spot 3`, … past the names of the document's spot swatches.
fn colorant_name(&self, name: &str) -> String {
if name != "All" && name != "None" {
return name.to_string();
}
let taken = |c: &str| self.doc.swatches_iter().any(|s| s.spot && s.name == c);
let mut colorant = format!("{name} Spot");
let mut n = 2;
while taken(&colorant) {
colorant = format!("{name} Spot {n}");
n += 1;
}
colorant
}
fn set_ink(&mut self, ink: Ink) {
match ink {
Ink::Process(space, v) => {
@@ -819,26 +845,28 @@ impl<'a> Scene<'a> {
}
/// A DeviceN color space of process inks `process` (indices into CMYK) and spot inks `names`,
/// defined in the setup the first time, each spot ink with the CMYK equivalent its Separation
/// space has. `None` when a spot ink has no Separation space, two colorants share a name, a
/// spot ink has the name of a process ink, `All` or `None`, or there are more than
/// [`MAX_DEVICE_N`] colorants.
/// defined in the setup the first time, each spot ink under the colorant name and with the
/// CMYK equivalent its Separation space has. `None` when a spot ink has no Separation space,
/// two colorants share a name, a spot ink's colorant has the name of a process ink, `All` or
/// `None`, or there are more than [`MAX_DEVICE_N`] colorants.
fn device_n_space(&mut self, process: &[usize], names: &[String]) -> Option<GradientSpace> {
const PROCESS: [&str; 4] = ["Cyan", "Magenta", "Yellow", "Black"];
if process.len() + names.len() > MAX_DEVICE_N {
return None;
}
let mut colorants: Vec<&str> = process.iter().map(|&i| PROCESS[i]).collect();
let mut colorants: Vec<String> = process.iter().map(|&i| PROCESS[i].to_string()).collect();
let mut weights: Vec<[f32; 4]> = process.iter().map(|&i| [0, 1, 2, 3].map(|j| if i == j { 1.0 } else { 0.0 })).collect();
for (i, name) in names.iter().enumerate() {
if PROCESS.contains(&name.as_str()) || name == "All" || name == "None" || names[..i].contains(name) {
let spots: Vec<String> = names.iter().map(|n| self.colorant_name(n)).collect();
for (i, name) in spots.iter().enumerate() {
if PROCESS.contains(&name.as_str()) || name == "All" || name == "None" || spots[..i].contains(name) {
return None;
}
}
for name in names {
colorants.push(name);
let i = self.spot_space(name)?;
weights.push(self.spots.get(i)?.1);
let (_, colorant, cmyk) = self.spots.get(i)?;
colorants.push(colorant.clone());
weights.push(*cmyk);
}
let mut def = String::from("[/DeviceN [");
for (i, c) in colorants.iter().enumerate() {
+44 -2
View File
@@ -381,8 +381,50 @@ fn a_gradient_mixing_a_spot_ink_with_rgb_is_written_without_the_ink() {
}
#[test]
fn spot_inks_named_like_process_inks_all_or_none_are_not_device_n_colorants() {
for name in ["Cyan", "Black", "All", "None"] {
fn spot_inks_named_all_or_none_are_written_under_names_of_their_own() {
let rect = |d: &mut Document, fill: &str, stroke: &str| {
let solid = |name: &str| Paint::Solid { color: GOLD, swatch: Some(name.into()), tint: 1.0 };
path(d, shapes::rectangle(Rect::new(50.0, 60.0, 150.0, 100.0)), solid(fill), solid(stroke), 2.0)
};
for (name, colorant) in [("All", "All Spot"), ("None", "None Spot")] {
// A fill of the spot ink and a Registration stroke.
let mut d = doc_with(|_| vec![]);
d.swatches.push(Swatch { name: name.into(), paint: Paint::solid(GOLD), global: true, spot: true });
let n = rect(&mut d, name, vectorcraft_color::swatch::REGISTRATION);
d.insert(Some(d.layers[0].id), 0, n).unwrap();
let ps = text(&export(&d, &opts(&d), None, None).unwrap());
assert_eq!(dsc(&ps, "DocumentCustomColors"), Some(format!("({colorant})")), "{ps}");
assert_eq!(dsc(&ps, "CMYKCustomColor"), Some(format!("0 0.2 0.8 0.1 ({colorant})")), "{ps}");
assert!(ps.contains(&format!("[/Separation ({colorant}) cvn /DeviceCMYK {{dup 0 mul")), "{name}: {ps}");
assert!(ps.contains("[/Separation /All /DeviceCMYK {dup dup dup}]"), "Registration prints on every plate: {ps}");
assert!(!ps.contains(&format!("({name}) cvn")), "{name}: {ps}");
checked_page(&ps);
// In a gradient with another spot ink, the DeviceN space has the same colorant.
let mut d = spot_gradient_doc(vec![spot_stop(0.0, "Gold Ink", GOLD, 1.0), spot_stop(1.0, name, LEAF, 1.0)]);
d.swatches.push(Swatch { name: name.into(), paint: Paint::solid(LEAF), global: true, spot: true });
let out = export(&d, &opts(&d), None, None).unwrap();
let ps = text(&out);
assert!(ps.contains(&format!("/VCdn0 [/DeviceN [(Gold Ink) cvn ({colorant}) cvn ] /DeviceCMYK")), "{name}: {ps}");
assert!(ps.contains(&format!("%%DocumentCustomColors: (Gold Ink)\n%%+ ({colorant})\n")), "{name}: {ps}");
assert!(!out.warnings.iter().any(|w| w.contains("spot")), "{:?}", out.warnings);
}
// A spot swatch already named All Spot keeps its name, and All is numbered past it.
let mut d = doc_with(|_| vec![]);
for name in ["All", "All Spot"] {
d.swatches.push(Swatch { name: name.into(), paint: Paint::solid(GOLD), global: true, spot: true });
}
let n = rect(&mut d, "All", "All Spot");
d.insert(Some(d.layers[0].id), 0, n).unwrap();
let ps = text(&export(&d, &opts(&d), None, None).unwrap());
assert!(ps.contains("[/Separation (All Spot 2) cvn ") && ps.contains("[/Separation (All Spot) cvn "), "{ps}");
assert!(ps.contains("%%DocumentCustomColors: (All Spot 2)\n%%+ (All Spot)\n"), "{ps}");
}
#[test]
fn spot_inks_named_like_process_inks_are_not_device_n_colorants() {
for name in ["Cyan", "Black"] {
// Gold Ink to the spot ink, with no process ink in the stops.
let mut d = spot_gradient_doc(vec![spot_stop(0.0, "Gold Ink", GOLD, 1.0), spot_stop(1.0, name, LEAF, 1.0)]);
d.swatches.retain(|s| s.name != name);
+1 -1
View File
@@ -1082,7 +1082,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- The Open and Place dialogs list PostScript `.ps` files and `.epsf` files. A `.ps` file opens and places through the EPS reader (its first page), also when its header is only `%!`, as the `ps` format, which exports don't write (Print to PostScript does); an `.epsf` file opens and places as EPS. Copied in a file manager, these files paste as EPS, and `clipboard.importEps` also reads PostScript whose header is only `%!` (`crates/engine/src/cmd/fileio/tests_epsimport.rs`).
`document.pdfInfo` reads an EPS, a `.ps` file or a `.ai` in PostScript form as one page the size of its bounding box (a US Letter page without one), and its `thumbnail: 1` is a PNG of what that page prints (`crates/engine/src/cmd/fileio/tests_pdfimport.rs`).
- EPS export with Use Artboards writes a central EPS file of the whole document, bounded by its visible art, beside the file of each artboard when it writes two or more artboards; a document with no visible art gets only the artboard files (`crates/engine/src/cmd/fileio/tests_eps.rs`).
EPS export writes a linear or radial gradient of tints of one spot color (and paper white) in that ink's Separation space, at Level 2 and 3, and at Level 3 one that mixes a spot color with process or gray colors, or two spot colors, in a DeviceN space 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. At Level 2, such a mix is written in process colors with a warning. In an RGB export, a gradient that mixes a spot color with RGB or Lab colors is written without the spot ink, with a warning; a CMYK export converts those stops to CMYK first, and the gradient follows the rules above. A Perceptual gradient of spot colors, and a mix whose DeviceN space would need more than 32 inks or a spot color named Cyan, Magenta, Yellow, Black, All or None, are also written in process colors with a warning (`crates/eps/src/tests.rs`).
EPS export writes a linear or radial gradient of tints of one spot color (and paper white) in that ink's Separation space, at Level 2 and 3, and at Level 3 one that mixes a spot color with process or gray colors, or two spot colors, in a DeviceN space 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. At Level 2, such a mix is written in process colors with a warning. In an RGB export, a gradient that mixes a spot color with RGB or Lab colors is written without the spot ink, with a warning; a CMYK export converts those stops to CMYK first, and the gradient follows the rules above. A Perceptual gradient of spot colors, and a mix whose DeviceN space would need more than 32 inks or a spot color named Cyan, Magenta, Yellow or Black, are also written in process colors with a warning (`crates/eps/src/tests.rs`). A spot color named All or None (in a Separation space, the names of every plate and of no plate: [ISO 32000-1:2008](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf), 8.6.6.4), is written as the ink All Spot or None Spot (All Spot 2 and on when a spot swatch already has that name) in its fills, strokes, gradients and DSC comments; Registration still prints on every plate (`tests::spot_inks_named_all_or_none_are_written_under_names_of_their_own`).
- Targa files (`.tga`, `.vda`, `.icb`, `.vst`) open, place linked or embedded, relink and update at their pixel size: color-mapped, true-color and black-and-white, raw or run-length encoded, in any of the four row orders. The image descriptor's attribute bits and the extension area's Attributes Type decide the transparency: a file with no attribute bits, or with attribute data the Attributes Type calls undefined, opens opaque; with one bit, a 16-bit true-color pixel takes that bit as its alpha, and a color-mapped pixel takes the bit of the 16-bit color map entry it indexes; premultiplied colors are divided by their alpha. Open and Place recognize a Targa file by its extension, and `clipboard.importImage` and Glass's Load Texture… by its header; a Targa file copied in a file manager pastes as a bitmap (`crates/engine/src/cmd/fileio/tests_tiffbmp.rs`, `crates/engine/src/tests/links.rs`).
- PCX files (`.pcx`) open and place, from the ZSoft PCX Technical Reference Manual: 1 bit in the header palette's first two colors (black and white when the version has no palette or both colors are the same), 1 bit in two to four planes and 4 bits with up to 16 colors from the header's palette (the EGA colors for versions 0 and 3), 2 bits in the CGA colors that the header's CGA palette settings pick (background, color burst, palette and intensity), 8 bits with the 256-color palette after the image data (gray levels without one) and 24-bit color in three planes, run-length encoded. The header's HDpi and VDpi size the image. A file of another depth or plane count, one cut short, and one whose pixels as RGBA pass the pixel allowance are refused with a message before its pixels are decoded. Open and Place recognize a PCX file by its extension, and `clipboard.importImage` and Glass's Load Texture… by its header; a PCX file copied in a file manager pastes as a bitmap (`crates/engine/src/cmd/fileio/tests_pcxread.rs`).
- JPEG 2000 JP2 and raw codestream images open and place from `.jpf`, `.jpx`, `.jp2`, `.j2k`, `.j2c` and `.jpc`. The last valid `resc` or `resd` resolution value wins on each axis; an axis with none uses 72 ppi. Multiple codestreams, JPX composition or layers, and unsupported color spaces are refused with a message. The decoder checks image size before allocating (`crates/engine/src/cmd/fileio/tests_jp2read.rs`).