mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 18:27:02 -06:00
Fix #498: EPS colour images whose inks are read in turn aren't banded
Illustrator CS and later write a colour image (in their own and in every CS-to-CC compatibility format) with one data source per ink: four procedures that each read one row of their ink from the same file, as `image` calls them in turn (C, M, Y, K, C, M, Y, K …). The reader drained each source on its own, so the cyan plane took the first rows of all four inks and the image came in as sheared horizontal bands (the map in #498). - Image data sources are read in turn: a procedure once a round, a file or string a row a round, until each plane is full or its source ends. Single sources, filters over procedures and masked images share the same reader (`read_sources`); a string source starts each round where it left off without walking it from its start. - Tests: CMYK procedures reading one file (dictionary form) and RGB ones (`colorimage` operand form), separate string sources, rows padded to a byte at 1, 2, 4, 12 and 16 bits with an odd width (one and several sources), PNG predictors of every row filter at an odd width. The first fails without the fix; the others pin down the stride rules the report suggested. The import fuzz vocabulary has several sources reading one file and sources of uneven lengths. - Checked against EPS saved here by Illustrator 2026 with an embedded 1001 × 667 RGB raster (and grey and RGBA ones, in RGB and CMYK documents, compatibility formats 3, 8, 10, CS6 and 2026): the 10, CS6 and 2026 ones came in banded and now match the source image; those files aren't committed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f12188ad15
commit
a303add775
+2
-2
File diff suppressed because one or more lines are too long
@@ -949,6 +949,10 @@ fn arb_ps_token() -> impl Strategy<Value = String> {
|
||||
"/T4 << /FontType 3 /FontMatrix [1 0 0 1 0 0] /BuildGlyph { pop pop (x) show } /Encoding [/a] >> definefont setfont",
|
||||
"glyphshow", "xshow", "xyshow", "awidthshow", "kshow", "/a", "[1 2 3]",
|
||||
// Executable strings and integer keys (Illustrator 8's procsets).
|
||||
// Several data sources reading one file in turn, and strings of other lengths.
|
||||
"/f currentfile /ASCIIHexDecode filter def", "{f 3 string readstring pop}", "{f 0 string readstring pop}", "true 3 colorimage",
|
||||
"3 2 8 [3 0 0 2 0 0] {f 5 string readstring pop} {(ab)} {f 1 string readstring pop} true 3 colorimage",
|
||||
"<< /ImageType 1 /Width 3 /Height 2 /BitsPerComponent 4 /Decode [0 1 0 1 0 1 0 1] /ImageMatrix [3 0 0 2 0 0] /MultipleDataSources true /DataSource [(a) (bc) {f 2 string readstring pop} (d)] >>",
|
||||
"(>>) cvx", "(1 2 add) cvx exec", "(x) cvx dup exec", "0 load", "1 { } def", "(mark) cvx cvlit"
|
||||
])
|
||||
.prop_map(str::to_string),
|
||||
|
||||
@@ -635,33 +635,50 @@ impl Interp<'_> {
|
||||
|
||||
/// The bytes a data procedure gives, called until it gives an empty string or `n` bytes.
|
||||
fn gather(&mut self, p: &Obj, n: usize) -> Res<Vec<u8>> {
|
||||
let mut out = vec![];
|
||||
while out.len() < n {
|
||||
self.call(p.clone())?;
|
||||
let s = self.pop_str()?;
|
||||
let s = s.borrow();
|
||||
if s.is_empty() {
|
||||
break;
|
||||
}
|
||||
self.alloc(s.len())?;
|
||||
out.extend_from_slice(&s);
|
||||
}
|
||||
out.truncate(n);
|
||||
Ok(out)
|
||||
Ok(self.read_sources(&[Source::Proc(p.clone())], n, n)?.pop().unwrap_or_default())
|
||||
}
|
||||
|
||||
/// `n` bytes of image data from `src` (zeros past its end).
|
||||
fn source_bytes(&mut self, src: &Source, n: usize) -> Res<Vec<u8>> {
|
||||
let mut v = match src {
|
||||
Source::File(f) => self.read(f, n)?,
|
||||
Source::Str(s) if !s.is_empty() => s.iter().copied().cycle().take(n).collect(),
|
||||
Source::Str(_) => vec![],
|
||||
Source::Proc(p) => self.gather(p, n)?,
|
||||
};
|
||||
let mut v = self.read_sources(std::slice::from_ref(src), n, n)?.pop().unwrap_or_default();
|
||||
v.resize(n, 0);
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// Up to `n` bytes from each of `sources`, read in turn as an image reads its data sources:
|
||||
/// each procedure called once a round (its string, whatever its length), each file and
|
||||
/// string giving `step` bytes a round, until each has given `n` bytes or ended. Procedures
|
||||
/// reading one file (the planes of an image, row by row) get their own rows that way.
|
||||
fn read_sources(&mut self, sources: &[Source], n: usize, step: usize) -> Res<Vec<Vec<u8>>> {
|
||||
let mut out: Vec<Vec<u8>> = vec![vec![]; sources.len()];
|
||||
let mut ended = vec![false; sources.len()];
|
||||
while out.iter().zip(&ended).any(|(o, e)| o.len() < n && !e) {
|
||||
for ((src, o), e) in sources.iter().zip(out.iter_mut()).zip(ended.iter_mut()) {
|
||||
if *e || o.len() >= n {
|
||||
continue;
|
||||
}
|
||||
let want = step.min(n - o.len());
|
||||
let chunk = match src {
|
||||
Source::File(f) => self.read(f, want)?,
|
||||
// A string gives its bytes over and over.
|
||||
Source::Str(s) => {
|
||||
let at = o.len() % s.len().max(1);
|
||||
s.get(at..).unwrap_or_default().iter().chain(s.iter().cycle()).take(want).copied().collect()
|
||||
}
|
||||
Source::Proc(p) => {
|
||||
self.call(p.clone())?;
|
||||
let s = self.pop_str()?.to_vec();
|
||||
self.alloc(s.len())?;
|
||||
s
|
||||
}
|
||||
};
|
||||
*e = chunk.is_empty();
|
||||
o.extend_from_slice(chunk.get(..chunk.len().min(n - o.len())).unwrap_or_default());
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `eexec`: an encrypted font program follows. It is skipped (type is drawn in the app's
|
||||
/// fonts, by name): the font dictionary it would complete is defined as it is.
|
||||
fn eexec(&mut self) -> Res {
|
||||
@@ -849,16 +866,12 @@ impl Interp<'_> {
|
||||
let multi = s.sources.len() > 1;
|
||||
let row_bits = if multi { s.w * bpc } else { s.w * n * bpc };
|
||||
let row = row_bits.div_ceil(8);
|
||||
let planes: Vec<Vec<u8>> = if multi {
|
||||
let mut v = vec![];
|
||||
for src in &s.sources {
|
||||
v.push(self.source_bytes(src, row * s.h)?);
|
||||
}
|
||||
v
|
||||
} else {
|
||||
let src = s.sources.first().ok_or(PsError::Ps("undefined", "DataSource".into()))?;
|
||||
vec![self.source_bytes(src, row * s.h)?]
|
||||
};
|
||||
// Several sources are read a row at a time each, in turn (zeros past their ends).
|
||||
let sources = if multi { &s.sources[..] } else { s.sources.get(..1).ok_or(PsError::Ps("undefined", "DataSource".into()))? };
|
||||
let mut planes = self.read_sources(sources, row * s.h, if multi { row } else { row * s.h })?;
|
||||
for p in &mut planes {
|
||||
p.resize(row * s.h, 0);
|
||||
}
|
||||
let max = ((1u32 << bpc.min(16)) - 1) as f64;
|
||||
let sample = |plane: &[u8], y: usize, i: usize| -> u32 {
|
||||
let bit = y * row * 8 + i * bpc;
|
||||
|
||||
@@ -39,6 +39,8 @@ mod tests_fontnames;
|
||||
mod tests_generators;
|
||||
#[cfg(test)]
|
||||
mod tests_illustrator;
|
||||
#[cfg(test)]
|
||||
mod tests_images;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ fn a85_flate(data: &[u8]) -> String {
|
||||
}
|
||||
|
||||
/// Pixel `(x, y)` (straight RGBA) of the first image in `d`.
|
||||
fn pixel(d: &Document, x: u32, y: u32) -> [u8; 4] {
|
||||
pub(super) fn pixel(d: &Document, x: u32, y: u32) -> [u8; 4] {
|
||||
let im = all(d)
|
||||
.into_iter()
|
||||
.find_map(|n| match &n.kind {
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
//! Image data read the way images read it: several data sources in turn, rows padded to a byte at
|
||||
//! every bit depth and odd widths, predictors undone row by row (#498).
|
||||
|
||||
use super::tests::read;
|
||||
use super::tests_generators::pixel;
|
||||
|
||||
/// Each pixel of the first image, row by row.
|
||||
fn pixels(r: &crate::Imported, w: u32, h: u32) -> Vec<[u8; 4]> {
|
||||
assert!(!r.preview && r.warnings.is_empty(), "{:?}", r.warnings);
|
||||
(0..h).flat_map(|y| (0..w).map(move |x| (x, y))).map(|(x, y)| pixel(&r.document, x, y)).collect()
|
||||
}
|
||||
|
||||
/// A CMYK image whose four data sources are procedures reading one file, a row of one ink each in
|
||||
/// turn (how Illustrator CS and later write colour images): each source gets its own rows. Read
|
||||
/// one after the other, the cyan source took the first four rows of all four inks and the image
|
||||
/// came out in horizontal bands (#498).
|
||||
#[test]
|
||||
fn procedures_reading_one_file_are_read_in_turn() {
|
||||
// 3 × 2 pixels; rows: C M Y K of row 0, then of row 1.
|
||||
let rows = ["ff0000", "00ff00", "000000", "000000", "000000", "000000", "ffff00", "0000ff"];
|
||||
let r = read(&format!(
|
||||
"/DeviceCMYK setcolorspace 30 20 scale /f currentfile /ASCIIHexDecode filter def \
|
||||
<< /ImageType 1 /Width 3 /Height 2 /BitsPerComponent 8 /Decode [0 1 0 1 0 1 0 1] /ImageMatrix [3 0 0 -2 0 2] \
|
||||
/MultipleDataSources true /DataSource [ {{f 3 string readstring pop}} {{f 3 string readstring pop}} \
|
||||
{{f 3 string readstring pop}} {{f 3 string readstring pop}} ] >> image\n{}>",
|
||||
rows.concat()
|
||||
));
|
||||
let cyan = [0, 174, 239, 255];
|
||||
let magenta = [236, 0, 140, 255];
|
||||
let yellow = [255, 242, 0, 255];
|
||||
let black = [35, 31, 32, 255];
|
||||
let white = [255, 255, 255, 255];
|
||||
let px = pixels(&r, 3, 2);
|
||||
let near = |a: [u8; 4], b: [u8; 4]| a.iter().zip(b).all(|(x, y)| x.abs_diff(y) <= 40);
|
||||
let want = [cyan, magenta, white, yellow, yellow, black];
|
||||
assert!(px.iter().zip(want).all(|(a, b)| near(*a, b)), "{px:?}");
|
||||
|
||||
// The operand form: `{r} {g} {b} true 3 colorimage`, procedures reading one file.
|
||||
let r = read(
|
||||
"30 20 scale /f currentfile /ASCIIHexDecode filter def \
|
||||
3 2 8 [3 0 0 -2 0 2] {f 3 string readstring pop} {f 3 string readstring pop} {f 3 string readstring pop} true 3 colorimage\n\
|
||||
ff0000 00ff00 0000ff 102030 405060 708090>",
|
||||
);
|
||||
assert_eq!(pixels(&r, 3, 2), [[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [16, 64, 112, 255], [32, 80, 128, 255], [48, 96, 144, 255]]);
|
||||
}
|
||||
|
||||
/// Separate strings and files as the sources: each is read for its own plane, whatever the row.
|
||||
#[test]
|
||||
fn separate_sources_keep_their_planes() {
|
||||
let r = read("30 20 scale 3 2 8 [3 0 0 -2 0 2] <ff0000102030> <00ff00405060> <0000ff708090> true 3 colorimage");
|
||||
assert_eq!(pixels(&r, 3, 2), [[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [16, 64, 112, 255], [32, 80, 128, 255], [48, 96, 144, 255]]);
|
||||
}
|
||||
|
||||
/// Rows of an odd width at 1, 2, 4, 12 and 16 bits a sample start at a byte (their last byte
|
||||
/// padded), in one source and in several.
|
||||
#[test]
|
||||
fn rows_of_every_depth_start_at_a_byte() {
|
||||
// 3 pixels a row, two rows: 0, max, mid / max, 0, 0 (grey), each row padded.
|
||||
let cases = [
|
||||
(1, "a0 80"), // 101(00000) 100(00000)
|
||||
(2, "cc c0"), // 11 00 11 (00), 11 00 00 (00)
|
||||
(4, "f0 80 f0 00"), // f 0 8 (0), f 0 0 (0)
|
||||
(12, "fff0008000 fff0000000"), // fff 000 800 (0), fff 000 000 (0)
|
||||
(16, "ffff00008000 ffff00000000"),
|
||||
];
|
||||
for (bpc, data) in cases {
|
||||
let data = data.replace(' ', "");
|
||||
let r = read(&format!("30 20 scale 3 2 {bpc} [3 0 0 -2 0 2] <{data}> image"));
|
||||
let px: Vec<u8> = pixels(&r, 3, 2).iter().map(|p| p[0]).collect();
|
||||
let want: &[u8] = match bpc {
|
||||
1 => &[255, 0, 255, 255, 0, 0],
|
||||
2 => &[255, 0, 255, 255, 0, 0],
|
||||
4 => &[255, 0, 136, 255, 0, 0],
|
||||
12 => &[255, 0, 128, 255, 0, 0],
|
||||
_ => &[255, 0, 128, 255, 0, 0],
|
||||
};
|
||||
assert_eq!(px, want, "{bpc} bits");
|
||||
}
|
||||
// Several 4-bit sources, odd width: each plane's rows padded on their own.
|
||||
let r = read("30 20 scale 3 2 4 [3 0 0 -2 0 2] <f0f000f0> <0f00f000> <00f00ff0> true 3 colorimage");
|
||||
let (on, off) = (255, 0);
|
||||
let want = [[on, off, off], [off, on, off], [on, off, on], [off, on, off], [off, off, on], [on, off, on]];
|
||||
assert_eq!(pixels(&r, 3, 2), want.map(|[r, g, b]| [r, g, b, 255]));
|
||||
}
|
||||
|
||||
/// A PNG-predicted Flate image of an odd width: every row filter (None, Sub, Up, Average, Paeth)
|
||||
/// undone on its own row.
|
||||
#[test]
|
||||
fn predicted_rows_of_an_odd_width() {
|
||||
// 3 RGB pixels a row, five rows; each row predicted with its own filter.
|
||||
let raw: Vec<[u8; 9]> = (0..5u8).map(|y| std::array::from_fn(|i| (i as u8).wrapping_mul(29).wrapping_add(y.wrapping_mul(53)))).collect();
|
||||
let mut encoded = vec![];
|
||||
let mut prev = [0u8; 9];
|
||||
for (kind, row) in raw.iter().enumerate() {
|
||||
encoded.push(kind as u8);
|
||||
for i in 0..9 {
|
||||
let a = if i >= 3 { row[i - 3] } else { 0 };
|
||||
let b = prev[i];
|
||||
let c = if i >= 3 { prev[i - 3] } else { 0 };
|
||||
let pred = match kind {
|
||||
1 => a,
|
||||
2 => b,
|
||||
3 => ((u16::from(a) + u16::from(b)) / 2) as u8,
|
||||
4 => {
|
||||
let p = i16::from(a) + i16::from(b) - i16::from(c);
|
||||
let (pa, pb, pc) = ((p - i16::from(a)).abs(), (p - i16::from(b)).abs(), (p - i16::from(c)).abs());
|
||||
if pa <= pb && pa <= pc {
|
||||
a
|
||||
} else if pb <= pc {
|
||||
b
|
||||
} else {
|
||||
c
|
||||
}
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
encoded.push(row[i].wrapping_sub(pred));
|
||||
}
|
||||
prev = *row;
|
||||
}
|
||||
let r = read(&format!(
|
||||
"/DeviceRGB setcolorspace 30 50 scale << /ImageType 1 /Width 3 /Height 5 /BitsPerComponent 8 /Decode [0 1 0 1 0 1] /ImageMatrix [3 0 0 -5 0 5] \
|
||||
/DataSource currentfile /ASCII85Decode filter << /Predictor 15 /Colors 3 /Columns 3 >> /FlateDecode filter >> image\n{}",
|
||||
crate::ps::ascii85(&crate::ps::deflate(&encoded))
|
||||
));
|
||||
let want: Vec<[u8; 4]> = raw.iter().flat_map(|row| (0..3).map(move |x| [row[3 * x], row[3 * x + 1], row[3 * x + 2], 255])).collect();
|
||||
assert_eq!(pixels(&r, 3, 5), want);
|
||||
}
|
||||
Reference in New Issue
Block a user