.ai import: an image too large for the editing data's reader opens the file as its PDF part, not with a blank layer (#1479)

This commit is contained in:
bflatastic
2026-10-24 08:39:00 +07:00
parent 8cc69ed45c
commit 76c9aa579d
5 changed files with 47 additions and 9 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ use super::lex::find;
use crate::ps;
pub(crate) use frames::text_frames;
pub use read::{Structure, artboards, read, variables};
pub use read::{IMAGE_TOO_LARGE, Structure, artboards, read, variables};
pub(crate) use read::{slot_frame, slot_of};
#[cfg(test)]
+8 -1
View File
@@ -76,6 +76,9 @@ const MAX_NODES: usize = 4_000_000;
const MAX_POINTS: usize = 4_000_000;
/// Largest image read (pixels).
const MAX_PIXELS: u64 = 1 << 25;
/// The note for an image past [`MAX_PIXELS`]: the file then opens from its PDF part, which reads
/// larger images (#1479).
pub const IMAGE_TOO_LARGE: &str = "an image too large to read from the editing data was left out";
/// Most hex digits of an image's samples read from comment lines (CMYK and alpha at the largest size).
const MAX_HEX: usize = (MAX_PIXELS as usize) * 10;
/// Largest side of the document and its artboards (points).
@@ -2096,7 +2099,11 @@ impl<'a> Reader<'a> {
return Ok(());
};
let (w, h) = (*w as u64, *h as u64);
if w == 0 || h == 0 || w.saturating_mul(h) > MAX_PIXELS || ![1.0, 8.0].contains(bits) {
if w.saturating_mul(h) > MAX_PIXELS {
self.warn(IMAGE_TOO_LARGE);
return Ok(());
}
if w == 0 || h == 0 || ![1.0, 8.0].contains(bits) {
self.warn("an image VectorCraft can't read was left out");
return Ok(());
}
+14 -7
View File
@@ -1401,20 +1401,27 @@ pub fn layered_ai(private: &[u8], visible: Document, warnings: Vec<String>, outl
if !has_layers(&data) {
return (visible, warnings, false);
}
// The PDF part, its pages placed and named as the editing data's artboards are, when the
// layers can't be used (`why`).
let from_pdf_part = |mut visible: Document, warnings: Vec<String>, why: &str| {
place_pages(&ai::artboards(&data), &mut visible, unit);
let mut all = unread(warnings, why);
all.extend(ai::variables(&data).note());
(visible, all, false)
};
match build(&data, Some(&visible), Page::Artboard, outlined, trust, unit) {
// An image too large for the editing data's reader would be missing from the layers, while
// the PDF part has it: the file comes in as its PDF part instead (#1479).
Ok((_, notes)) if notes.iter().any(|n| n == ai::IMAGE_TOO_LARGE) => {
from_pdf_part(visible, warnings, "an image is too large to read from them")
}
Ok((mut done, notes)) => {
done.setup.bleed = visible.setup.bleed;
let mut all = warnings;
all.extend(notes.into_iter().filter(|n| !all.contains(n)).collect::<Vec<_>>());
(done, all, true)
}
Err(why) => {
let mut visible = visible;
place_pages(&ai::artboards(&data), &mut visible, unit);
let mut all = unread(warnings, &why);
all.extend(ai::variables(&data).note());
(visible, all, false)
}
Err(why) => from_pdf_part(visible, warnings, &why),
}
}
+23
View File
@@ -243,6 +243,29 @@ fn in_points(mut d: Document, k: f64) -> Document {
d
}
/// #1479: an image larger than the editing data's reader takes would be missing from the layers,
/// while the PDF part has it (it reads larger images): the file comes in as its PDF part, with a
/// note saying why, rather than without the image.
#[test]
fn an_ai_file_with_an_image_too_large_for_its_editing_data_comes_in_as_its_pdf_part() {
let raster = |w: u32, h: u32| {
format!(
"%AI5_File:\n%AI5_BeginRaster\n() 1 XG\n/DeviceGray XN\n[ 1 0 0 1 0 100 ] {w} {h} 0 Xh\n\
[ 1 0 0 1 0 100 ] 0 0 {w} {h} {w} {h} 8 1 0 0 1 0 4 4 0 0\n%%BeginData: 1\rXI\n\u{0}%%EndData\r\nXH\r\n%AI5_EndRaster\r\nN"
)
};
// 6,000 × 6,000 pixels: past the reader's 2^25.
let art = native(&layer("Texture", true, &format!("{}\n{}", square(10, 10), raster(6000, 6000))));
let (d, warnings, read) = crate::layered_ai(&private(&art), pdf_part(10.0, 86.0), vec![], false, true, 1.0);
assert!(!read, "the PDF part is used: {warnings:?}");
assert!(warnings.iter().any(|w| w.contains("an image is too large to read from them")), "{warnings:?}");
assert_eq!(d.layers[0].children().map_or(0, |c| c.len()), 1, "the PDF part's art");
// An image the reader takes keeps the layers.
let art = native(&layer("Texture", true, &format!("{}\n{}", square(10, 10), raster(1, 1))));
let (_, warnings, read) = crate::layered_ai(&private(&art), pdf_part(10.0, 86.0), vec![], false, true, 1.0);
assert!(read, "{warnings:?}");
}
#[test]
fn an_ai_file_gets_its_layers_and_the_art_outside_its_artboard() {
// A square on the artboard, and one above it (the artboard is y 0 to 100 here).
+1
View File
@@ -130,6 +130,7 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri
| Date | Change | Summary |
|---|---|---|
| 2026-10-24 | minor | `.ai` import: an image larger than the editing data's reader takes (2^25 pixels, a halftone texture at print resolution) no longer leaves its layer blank: the file comes in as its PDF part, which reads larger images, with a note saying why (#1479). Own-fixture test; scores and hours unchanged |
| 2026-10-24 | minor | PDF export: a pattern fill or stroke is written as a PDF tiling pattern (ISO 32000-1, 8.7.3.1) of one period of the tiling, as the canvas repeats it, so grid, brick and hex tilings and overlapping tiles keep their look in a few kilobytes, and PDF import reads it back as one object with a pattern swatch instead of hundreds of tile paths (#3849). Patterns nested more than four deep, or with tiles beyond a billion points, are still written as their tiles, now with a warning. Tests: the issue's hatch round trip, a brick tiling read back against the canvas, the nesting warning; scores and hours unchanged |
| 2026-10-23 | minor | SVG: type written as outlines stacks each run's colour glyphs (bitmap images, COLR and SVG art) under the run's paint, as the canvas does, so a plain character set over a colour one stays on top; scores unchanged |
| 2026-10-23 | minor | PDF import: a line's spacing is measured from the file's own glyph widths, so type in a font the file doesn't carry, drawn with an installed font whose advances its widths round off (Helvetica on macOS: 2048 units an em), keeps one tracking value on its letters instead of a run per character (#722 follow-up); test with built-in Inter, independent of the fonts installed; scores and hours unchanged |