.ai/EPS import: gradient mesh corners in several inks take their tints last ink first

Under an `/NChannel` color space a mesh corner gives one tint for each
ink. The reader now pairs the tints with the inks in the opposite order
to the listing, for process and spot inks alike: under
`[[Yellow] [Magenta] [Cyan] 3 /NChannel]`, `0 0 1` is full yellow. No
public document describes these records. This is VectorCraft's reading
of them, and under it the meshes match the flattened page that the same
file carries.

The reader gave the first tint to the first ink listed, so mesh areas
came in with the wrong inks, yellow as cyan. Where enough of the art
changed, the layers no longer matched the file's page, and the file
opened as its page instead of its objects.
This commit is contained in:
dotCooCoo
2026-11-11 16:24:48 -08:00
parent 7f3a046b18
commit d262c65b14
+53 -4
View File
@@ -55,7 +55,9 @@ pub(super) enum NotOne {
/// it applies to, even within a patch; a mesh starts grey. Besides the device spaces it may be a
/// named colour, `[c m y k /DeviceCMYK (name) /Process]` (or `/Spot`), or several process or spot
/// inks, `[[ink] [ink] … n /NChannel]` (each ink `[c m y k /DeviceCMYK (name) /Separation]`). A
/// corner gives a named colour's tint (1 is the full colour), or each ink's in order.
/// corner gives a named color's tint (1 is the full color), or each ink's, process and spot inks
/// alike, the last ink listed first: under `[[Yellow] [Magenta] [Cyan] 3 /NChannel]`, `[0 0 1 …]`
/// is full yellow.
#[derive(Debug, Clone, Default, PartialEq)]
enum Space {
#[default]
@@ -113,10 +115,11 @@ impl Space {
Space::Cmyk => Color::cmyk(u(0)?, u(1)?, u(2)?, u(3)?),
Space::Inks(inks) => match inks.as_slice() {
[one] => one.tinted(u(0)?),
// Several inks overprint: their CMYK at their tints, added up.
// Several inks overprint: their CMYK at their tints, added up. The tints come in
// the opposite order to the inks.
many => {
let mut sum = [0.0f32; 4];
for (i, ink) in many.iter().enumerate() {
for (i, ink) in many.iter().rev().enumerate() {
let Color::Cmyk { c, m, y, k } = *ink else { return None };
let t = u(i)?;
for (s, v) in sum.iter_mut().zip([c, m, y, k]) {
@@ -911,7 +914,8 @@ mod tests {
let mut reader = MeshReader::default();
square(&mut reader, [&[0.25, 1.0], &[0.0, 0.0], &[1.0, 1.0], &[0.0, 0.0]], [vec![(channels(2.0), "CS")], vec![], vec![], vec![]]);
assert!(!reader.unread);
assert_color(reader.patches[0].points[0].color, Color::cmyk(0.0, 1.0, 0.25, 0.0));
// The tints come last ink first: 0.25 Magenta, 1 Yellow.
assert_color(reader.patches[0].points[0].color, Color::cmyk(0.0, 0.25, 1.0, 0.0));
assert_color(reader.patches[0].points[3].color, Color::cmyk(0.0, 1.0, 1.0, 0.0));
// An ink count that doesn't match its inks, or an ink in a space not read.
@@ -923,6 +927,51 @@ mod tests {
}
}
/// `/NChannel` inks take a corner's tints in the opposite order to their listing, whatever
/// order the inks are listed in, spot inks included: under Yellow, Magenta, Cyan `0 0 1` is
/// full yellow and `1 0.5 0` full cyan with half magenta; under Cyan, Magenta, Yellow `0 0 1`
/// is full cyan; under a spot ink and Cyan, `0.5 1` is the full spot ink with half cyan.
#[test]
fn inks_take_their_tints_last_listed_first() {
let ink = |c: [f64; 4], name: &str| {
let mut v: Vec<V> = c.iter().map(|x| V::Num(*x)).collect();
v.extend([V::Name("DeviceCMYK".into()), V::Str(name.as_bytes().to_vec()), V::Name("Separation".into())]);
V::Arr(v)
};
let channels = |inks: Vec<V>| {
let n = inks.len() as f64;
let mut a = inks;
a.extend([V::Num(n), V::Name("NChannel".into())]);
V::Arr(a)
};
let (yellow, magenta, cyan) = (ink([0.0, 0.0, 1.0, 0.0], "Yellow"), ink([0.0, 1.0, 0.0, 0.0], "Magenta"), ink([1.0, 0.0, 0.0, 0.0], "Cyan"));
let rose = ink([0.1, 0.6, 0.4, 0.2], "Rose");
// Two corners' tints (then opacity) under the inks, and the colors they give.
let check = |inks: Vec<V>, a: &[f64], b: &[f64], want_a: Color, want_b: Color| {
let mut reader = MeshReader::default();
square(&mut reader, [a, b, a, b], [vec![(channels(inks), "CS")], vec![], vec![], vec![]]);
assert!(!reader.unread);
let points = &reader.patches[0].points;
assert_color(points[0].color, want_a);
assert_color(points[2].color, want_b);
};
check(
vec![yellow.clone(), magenta.clone(), cyan.clone()],
&[0.0, 0.0, 1.0, 1.0],
&[1.0, 0.5, 0.0, 1.0],
Color::cmyk(0.0, 0.0, 1.0, 0.0),
Color::cmyk(1.0, 0.5, 0.0, 0.0),
);
check(
vec![cyan.clone(), magenta, yellow],
&[0.0, 0.0, 1.0, 1.0],
&[1.0, 0.5, 0.0, 1.0],
Color::cmyk(1.0, 0.0, 0.0, 0.0),
Color::cmyk(0.0, 0.5, 1.0, 0.0),
);
check(vec![rose, cyan], &[0.5, 1.0, 1.0], &[1.0, 0.0, 1.0], Color::cmyk(0.6, 0.6, 0.4, 0.2), Color::cmyk(1.0, 0.0, 0.0, 0.0));
}
/// A corner holding anything but numbers is unread: skipping it would shift the numbers after
/// it into its colour and opacity (two inks' tints `0.25 /bad 0.5`, read as `0.25 0.5` and no
/// opacity).