Open .ai files whose non-native art has ASCII85 lines starting with %_

A letterhead saved by Illustrator 28 without PDF compatibility wouldn't
open: "its editing data can't be read (it ends inside a layer)". Its
layer starts with a /ForeignObject (non-native art: the content of a
placed PDF), whose /Data entry is the PDF as ASCII85 in comment lines.
ASCII85 includes `_`, so about one line in 85 starts with `%_`, which
the lexer reads as hidden tokens; a `(` in such a line opened a string
that swallowed the dictionary's end, the rest of the art and the
layer's LB.

The reader now skips to the data's `~>` after `/Data ,` in a
/ForeignObject, as it already did for a /Binary dictionary's ASCII85,
and leaves the non-native art out with a note (nothing draws its PDF
from the editing data yet).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ZwZjADUZP4VzvrFGpiZch
This commit is contained in:
Reinhard Schuerer 2026-10-10 01:50:56 +02:00 committed by bflatastic
parent 57932f1204
commit f2ba41d1c1
3 changed files with 27 additions and 3 deletions

View File

@ -311,6 +311,9 @@ const SKIPPED: &[(&str, &str)] = &[
("Alternate_Content", "End_Versioned_Content"),
];
/// The note for a `/ForeignObject`, art the editor shows but doesn't edit.
pub const NON_NATIVE_ART: &str = "non-native art (a placed PDF's content) is left out: VectorCraft doesn't draw it from the editing data yet";
/// A section comment's marker without its `AI<version>_` prefix and value (`AI14_BeginSymbol` →
/// `BeginSymbol`); DSC comments (`%%BeginProlog`) keep their `%`.
fn marker(c: &str) -> &str {
@ -645,8 +648,13 @@ impl<'a> Reader<'a> {
"," => {
if let Some(Frame { kind: Kind::Obj(o), base, .. }) = self.frames.last_mut() {
obj::add_entry(o, &mut self.stack, *base);
// Binary data in ASCII85 follows, up to its `~>`.
if o.ty == "Binary" && o.entries.last().is_some_and(|(k, _)| k.as_deref() == Some("ASCII85Decode")) {
// Binary data in ASCII85 follows, up to its `~>`: a `/Binary` dictionary's
// (`/ASCII85Decode ,`) or a non-native art object's (`/ForeignObject`'s
// `/Data ,`, the PDF it keeps), in comment lines. ASCII85 has `_`, so a line
// of it may start with `%_` and read as hidden tokens: a `(` there would open
// a string that swallows the rest of the layer.
let key = o.entries.last().and_then(|(k, _)| k.as_deref());
if (o.ty == "Binary" && key == Some("ASCII85Decode")) || (o.ty == "ForeignObject" && key == Some("Data")) {
self.lex.skip_past(b"~>");
}
}
@ -826,6 +834,9 @@ impl<'a> Reader<'a> {
"Document" => self.document(&o),
"AI11Text" => self.text_slot(&o)?,
"SymbolInstance" => self.unreadable("symbols"),
// Art Illustrator shows but doesn't edit (a placed PDF's content): its PDF is kept in
// the dictionary, which nothing draws from yet, so the file opens without it.
"ForeignObject" => self.warn(NON_NATIVE_ART),
_ => {}
}
Ok(())

View File

@ -329,6 +329,18 @@ fn sections_it_skips() {
assert_eq!(art(&doc).len(), 1);
}
/// Non-native art (a placed PDF's content) keeps its PDF as ASCII85 in comment lines after
/// `/Data ,`. ASCII85 has `_`, so a line may start with `%_`, which otherwise reads as hidden
/// tokens, and a `(` in it would swallow the rest of the layer (a letterhead saved by Illustrator
/// 28 wouldn't open: "it ends inside a layer"). The art is left out with a note.
#[test]
fn non_native_art_is_skipped_with_its_ascii85_data() {
let foreign = "/ForeignObject :\n1 /Version ,\n2 0 0 2 10 20 /RTransform ,\n/Data ,\n%,u@!!/MSk8%41#ocdN=10d&\n%_O%*mGn(RduU.]4`[u[.f\n%_;(]<[%\n%1H@<P2`V<S,pbuU7L]\\~>\n;\n0 0 Xd\n6 () XW\n0 Ae\n";
let s = read_ok(&layer(&format!("{foreign}{}", rect_path(0.0, 0.0, 1.0, 1.0) + "f\n")));
assert_eq!(art(&s.doc).len(), 1);
assert!(s.warnings.iter().any(|w| w == read::NON_NATIVE_ART), "{:?}", s.warnings);
}
#[test]
fn cmyk_documents() {
let doc = read_ok(&format!("%AI9_ColorModel: 2\n{}", layer(""))).doc;

View File

@ -450,7 +450,8 @@ CMYK too.
PostScript files (`.eps`, and `.ai` files saved in older formats or without PDF compatibility) open through the EPS
reader (see EPS and PostScript import). An `.ai` saved without PDF compatibility (its PDF part is only a placeholder
page) opens from its editing data alone: its type is made from the file's text document where it can be (point type), and
what can't be is left out with a warning; without editing data it says it can't be opened.
what can't be is left out with a warning; so is non-native art (the content of a placed PDF, which Illustrator shows but
doesn't edit and keeps as a PDF inside the editing data); without editing data it says it can't be opened.
What a PDF holds comes in as editable art: soft masks become opacity masks (an alpha mask as a white copy of its art;
the backdrop colour gives Clip, an inverting transfer function Invert), transparency groups keep isolation and knockout,