From 76c9aa579de6588a54393947da4f200bc412ef24 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 24 Oct 2026 08:38:29 +0700 Subject: [PATCH] .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) --- crates/eps/src/import/ai/mod.rs | 2 +- crates/eps/src/import/ai/read.rs | 9 ++++++++- crates/eps/src/import/native.rs | 21 ++++++++++++++------- crates/eps/src/import/tests_native.rs | 23 +++++++++++++++++++++++ docs/file-format-parity.md | 1 + 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/crates/eps/src/import/ai/mod.rs b/crates/eps/src/import/ai/mod.rs index 901c37085..eb0785553 100644 --- a/crates/eps/src/import/ai/mod.rs +++ b/crates/eps/src/import/ai/mod.rs @@ -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)] diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index e4e8fa76b..99b0eac77 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -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(()); } diff --git a/crates/eps/src/import/native.rs b/crates/eps/src/import/native.rs index aaf666244..99164aaa8 100644 --- a/crates/eps/src/import/native.rs +++ b/crates/eps/src/import/native.rs @@ -1401,20 +1401,27 @@ pub fn layered_ai(private: &[u8], visible: Document, warnings: Vec, 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, 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::>()); (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), } } diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index 3fe8bc9c9..4087b2fd9 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -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). diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 4c5459d83..55c503f57 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -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 |