Merge pull request #2293 from dotCooCoo/fix/ai-one-bit-color-images

.ai import: 1-bit RGB and CMYK images in the editing data open in their colors
This commit is contained in:
BFlat
2026-10-16 06:33:36 +07:00
committed by GitHub
4 changed files with 63 additions and 15 deletions
@@ -82,6 +82,27 @@ fn an_illustrator_ai_file_opens_from_its_editing_data() {
assert_ne!(names(&s.doc().unwrap().doc.layers), ["Art", "Hidden"]);
}
/// A 1-bit RGB image opens from the editing data in the colors its PDF part shows.
#[test]
fn a_one_bit_rgb_image_opens_from_the_editing_data_in_its_colors() {
// 3 × 1: red, green, blue (100 010 001), as hex samples in both parts.
let raster = "%AI5_File:\n%AI5_BeginRaster\n() 1 XG\n/DeviceRGB XN\n[ 60 0 0 60 10 90 ] 3 1 0 Xh\n\
[ 60 0 0 60 10 90 ] 0 0 3 1 3 1 1 3 0 0 0 0 4 4 0 0\n%%BeginData: 6\rXI\n8880\n%%EndData\r\nXH\r\n%AI5_EndRaster\r\nN\r\n";
let data = ai::editing_data(200.0, 100.0, &ai::layer("Art", raster));
let page = "q 180 0 0 60 10 30 cm BI /W 3 /H 1 /CS /RGB /BPC 1 /F /AHx ID 8880> EI Q";
let file = ai::ai(&compressed(data.as_bytes()), page);
let mut s = Session::new();
let r = open(&mut s, "bits.ai", &file, json!({})).unwrap();
let doc = &s.doc().unwrap().doc;
assert_eq!(names(&doc.layers), ["Art"], "{r}");
let mut found = vec![];
doc.visit_images(|_, im| {
let img = image::load_from_memory(&doc.images[&im.key].bytes).unwrap().to_rgb8();
found.push(img.pixels().map(|p| p.0).collect::<Vec<_>>());
});
assert_eq!(found, [[[255, 0, 0], [0, 255, 0], [0, 0, 255]]], "{r}");
}
/// A PDF saved with the editing data opens from it, as a `.ai` does (#868).
#[test]
fn a_pdf_saved_with_the_editing_data_opens_from_it() {
+1
View File
@@ -2239,6 +2239,7 @@ fn arb_ai_token() -> impl Strategy<Value = String> {
"/DeviceCMYK XN",
"/DeviceGray XN",
"[ 1 0 0 1 0 0 ] 0 0 2 2 2 2 8 3 1 0 1 0",
"[ 1 0 0 1 0 0 ] 0 0 3 2 3 2 1 4 1 0 1 0",
"[ 1 0 0 1 0 0 ] 0 0 99999 99999 99999 99999 1 1 0 0 0 0",
"[",
"]",
+14 -15
View File
@@ -1841,23 +1841,22 @@ impl<'a> Reader<'a> {
for y in 0..h as usize {
let line = samples.get(y * row..(y + 1) * row).unwrap_or_default();
for x in 0..w as usize {
let px = if *bits == 1.0 {
let bit = line.get(x / 8).map_or(0, |b| (b >> (7 - x % 8)) & 1);
let v = if bit == 1 { 255 } else { 0 };
[v, v, v]
} else {
let s = |c: usize| line.get(x * channels + c).copied().unwrap_or(0);
match channels {
1 => [s(0); 3],
4 => {
let key = [s(0), s(1), s(2), s(3)];
if self.cmyk_cache.len() > 1 << 16 {
self.cmyk_cache.clear();
}
*self.cmyk_cache.entry(key).or_insert_with(|| cmyk_rgb(key))
// A 1-bit row holds each pixel's channels in turn, a bit each, padded to a whole
// byte (`row`); a set bit is the channel at full strength (255).
let s = |c: usize| {
let i = x * channels + c;
if *bits == 1.0 { line.get(i / 8).map_or(0, |b| ((b >> (7 - i % 8)) & 1) * 255) } else { line.get(i).copied().unwrap_or(0) }
};
let px = match channels {
1 => [s(0); 3],
4 => {
let key = [s(0), s(1), s(2), s(3)];
if self.cmyk_cache.len() > 1 << 16 {
self.cmyk_cache.clear();
}
_ => [s(0), s(1), s(2)],
*self.cmyk_cache.entry(key).or_insert_with(|| cmyk_rgb(key))
}
_ => [s(0), s(1), s(2)],
};
let a = opacity.and_then(|o| o.get(y * w as usize + x)).copied().unwrap_or(255);
rgba.extend_from_slice(&[px[0], px[1], px[2], a]);
+27
View File
@@ -584,6 +584,33 @@ fn pixels(doc: &Document) -> (image::RgbaImage, kurbo::Affine) {
(image::load_from_memory(&blob.bytes).unwrap().to_rgba8(), im.xf)
}
/// A 1-bit image's row holds each pixel's channels in turn, one bit each, padded to whole bytes;
/// a set bit is the channel at full strength.
#[test]
fn one_bit_images_read_each_channel() {
let one_bit = |src: Vec<u8>, kind: u32| {
let text = String::from_utf8(src).unwrap();
text.replacen(&format!(" 8 {kind} 0 0 1 0 "), &format!(" 1 {kind} 0 0 0 0 "), 1).into_bytes()
};
// 3 × 2 RGB in hex: red, green, blue (100 010 001), then white, black, yellow (111 000 110).
let doc = read(&one_bit(image("[ 1 0 0 1 0 100 ]", (3, 2), ("DeviceRGB", 3, 0), b"8880e300", 11), 3)).unwrap().doc;
let img = pixels(&doc).0;
let row = |y: u32| (0..3).map(|x| img.get_pixel(x, y).0).collect::<Vec<_>>();
assert_eq!(row(0), [[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255]]);
assert_eq!(row(1), [[255, 255, 255, 255], [0, 0, 0, 255], [255, 255, 0, 255]]);
// 2 × 1 CMYK: cyan, then black (1000 0001).
let doc = read(&one_bit(image("[ 1 0 0 1 0 100 ]", (2, 1), ("DeviceCMYK", 4, 0), b"81", 5), 4)).unwrap().doc;
let img = pixels(&doc).0;
for (x, ink) in [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]].into_iter().enumerate() {
let [r, g, b, _] = Color::cmyk(ink[0], ink[1], ink[2], ink[3]).to_rgba8(1.0);
assert_eq!(img.get_pixel(x as u32, 0).0, [r, g, b, 255], "pixel {x}");
}
// 3 × 1 gray: a bit a pixel (101).
let doc = read(&one_bit(image("[ 1 0 0 1 0 100 ]", (3, 1), ("DeviceGray", 1, 0), b"a0", 5), 1)).unwrap().doc;
let img = pixels(&doc).0;
assert_eq!((0..3).map(|x| img.get_pixel(x, 0).0[0]).collect::<Vec<_>>(), [255, 0, 255]);
}
#[test]
fn images_with_alpha_channels() {
// RGB samples, then the alpha channel (not counted in the data's size).