.ai import: legacy files without layers open through the format reader

Squashed from #5209 onto main.
This commit is contained in:
dotCooCoo
2026-11-02 14:32:58 +07:00
committed by bflatastic
parent ec0a98a77b
commit f69f7976b7
5 changed files with 102 additions and 16 deletions
+32 -2
View File
@@ -2070,10 +2070,11 @@ proptest! {
/// Hostile PostScript programs: operators in any order with any operands, opened and placed;
/// as files in the legacy Illustrator format too (the creator line is what turns on its
/// documented `u` … `U` groups), the groups unbalanced, deep and among clips; in a layer, which
/// the editing data reader reads from the program too.
/// the editing data reader reads from the program too, or after a setup without a layer, which
/// it reads with the art on one layer.
#[test]
fn eps_hostile_programs_never_panic(tokens in prop::collection::vec(arb_ps_token(), 0..60), illustrator in any::<bool>(), layers in any::<bool>()) {
let head = if illustrator { "%%Creator: Adobe Illustrator(R) 8.0\n%%EndComments\n/u {} def /U {} def" } else { "%%EndComments" };
let head = if illustrator { "%%Creator: Adobe Illustrator(R) 8.0\n%%EndComments\n/u {} def /U {} def\n%%BeginSetup\n%%EndSetup" } else { "%%EndComments" };
let (open, close) = if layers {
("/Lb {10 {pop} repeat} def /Ln {pop} def /LB {} def\n%AI5_BeginLayer\n1 1 1 1 0 0 -1 0 0 0 Lb\n(L) Ln\n", "\nLB\n%AI5_EndLayer--")
} else {
@@ -3441,6 +3442,35 @@ proptest! {
}
}
/// A `.ai` in the legacy format without layers (Illustrator 3) whose prolog is included: its
/// program runs, and its art is read on one layer.
const LAYERLESS_AI: &str = "%!PS-Adobe-3.0 EPSF-3.0\n%%Creator: a script\n%%BoundingBox: 0 0 200 100\n%AI3_Cropmarks: 0 0 200 100\n%%EndComments\n\
%%BeginProlog\n/m {moveto} def /L {lineto} def /f {closepath fill} def /S {stroke} def /A {pop} def /k {setcmykcolor} def /u {} def /U {} def\n\
%%EndProlog\n%%BeginSetup\n%%EndSetup\n1 A\n0 0 0 1 k\n10 10 m\n90 10 L\n90 90 L\nf\n0 A\nu\n110 10 m\n190 90 L\n150 90 L\nf\nU\n\
%%PageTrailer\nshowpage\n%%Trailer\n%%EOF\n";
/// The fixture, as it is, opens through the reader: its first path keeps its lock.
#[test]
fn the_layerless_fixture_opens_through_the_reader() {
let l = vectorcraft_engine::cmd::fileio::load("x.ai", LAYERLESS_AI.as_bytes()).unwrap();
assert_eq!(l.doc.layers.len(), 1, "{:?}", l.warnings);
let art = l.doc.layers[0].children().unwrap();
assert_eq!(art.len(), 2, "{:?}", l.warnings);
assert!(art[0].locked && !art[1].locked);
}
proptest! {
#![proptest_config(config())]
/// A `.ai` in the legacy format without layers, damaged: read through the reader, as its page,
/// or refused.
#[test]
fn layerless_ai_never_panics(cut in 0usize..800, edits in prop::collection::vec((0usize..800, prop::sample::select(vec!['0', '9', '-', '.', ' ', '\n', '(', ')', '/', '{', '}', '%', 'u', 'U', 'L', 'f', 'A', 'k'])), 0..12)) {
let text = mutate_text(LAYERLESS_AI, cut, &edits);
survive("mutated layerless .ai", || vectorcraft_engine::cmd::fileio::load("x.ai", text.as_bytes()).ok().map(|l| l.doc))?;
}
}
/// An operand of a layer's options (`Lb`): a flag, a colour index or component, a percentage, or
/// anything else.
fn arb_layer_operand() -> impl Strategy<Value = String> {
+3 -2
View File
@@ -18,7 +18,8 @@
//! `%%Creator`) and these operators: Adobe Illustrator File Format Specification, version 7.0
//! (1998), listed by PRONOM at https://www.nationalarchives.gov.uk/PRONOM/fmt/423. A file in that
//! format whose program has its layers (`%AI5_BeginLayer`) comes in through them, as `native` reads
//! an Illustrator EPS's editing copy (#1027). One VectorCraft wrote (its prolog the procedure set
//! an Illustrator EPS's editing copy (#1027), and one without layers (Illustrator 3) comes in
//! through its art put on one layer, as its page when that can't be read. One VectorCraft wrote (its prolog the procedure set
//! [`crate::legacy_ai::PROCSET`], which prints its gradients flat and leaves its type out) comes in
//! from those layers alone, without its page.
//!
@@ -266,7 +267,7 @@ pub fn import_trusting(bytes: &[u8], editing_data: bool, trust: bool) -> Result<
}
let drew = !out.drawn.is_empty();
let why = match (&result, drew) {
(Ok(()), true) if editing_data => return Ok(native::layered(ps, finish(out), trust)),
(Ok(()), true) if editing_data => return Ok(native::layered(ps, finish(out), trust, dsc.illustrator)),
(Ok(()), true) => return Ok(finish(out)),
(Ok(()), false) => "it draws nothing VectorCraft's PostScript reader can show".to_string(),
(Err(e), _) => reason(e, fault.as_ref()),
+24 -12
View File
@@ -8,9 +8,17 @@ use super::{Affine, Arc, Cow, Document, Imported, Node, NodeKind, PathData, Rect
/// strokes around it of the page. When the file has no editing copy, `visible` as it was; when it
/// has one that can't be used, `visible` with a warning that says why, with the swatches of the
/// editing copy's palette ([`use_palette`]) when it could be read, and with its layer tree, empty,
/// below the page's layer when that could be read (#2033).
pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool) -> Imported {
let Some(data) = editing_copy(ps) else { return visible };
/// below the page's layer when that could be read (#2033). A program in the legacy format without
/// layers (`illustrator`, see [`program_alone`]) is read with its art on one layer; when that
/// can't be used, the file is `visible` as it was.
pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illustrator: bool) -> Imported {
let Some(data) = editing_copy(ps) else {
let read = illustrator.then(|| one_layer(ps)).flatten().and_then(|p| build(&p, Some(&visible.document), Page::Art, false, trust, 1.0).ok());
return match read {
Some((document, warnings)) => with_page_notes(document, warnings, &visible),
None => visible,
};
};
// The layer tree is kept from the structure read, for the page to come in on when the layers
// can't be used.
// Private data kept apart from the program: the program's own header gives the author and
@@ -24,14 +32,7 @@ pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool) -> Im
Err(why) => (vec![], Err(why.clone())),
};
match result {
Ok((document, mut warnings)) => {
for w in &visible.warnings {
if !warnings.contains(w) {
warnings.push(w.clone());
}
}
Imported { document, warnings, preview: false }
}
Ok((document, warnings)) => with_page_notes(document, warnings, &visible),
Err(why) => {
let mut v = visible;
if let Ok(data) = &data {
@@ -61,6 +62,17 @@ pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool) -> Im
}
}
/// `document`, read from the editing copy with the notes `warnings`, followed by the notes of
/// reading `visible` that it doesn't already have.
fn with_page_notes(document: Document, mut warnings: Vec<String>, visible: &Imported) -> Imported {
for w in &visible.warnings {
if !warnings.contains(w) {
warnings.push(w.clone());
}
}
Imported { document, warnings, preview: false }
}
/// The layers in `nodes` with only their sublayers in them: names, order and options without the art.
fn layer_tree(nodes: &[Arc<Node>]) -> Vec<Arc<Node>> {
let layers = nodes.iter().filter(|n| matches!(n.kind, NodeKind::Layer { .. }));
@@ -256,7 +268,7 @@ pub(in super::super) fn program_alone(ps: &[u8], illustrator: bool) -> Option<Re
if has_layers(ps) {
return Some(build(ps, None, Page::Art, false, false, 1.0));
}
let program = one_layer(ps).filter(|_| illustrator)?;
let program = illustrator.then(|| one_layer(ps)).flatten()?;
Some(build(&program, None, Page::Art, false, false, 1.0))
}
@@ -324,6 +324,47 @@ fn a_legacy_file_with_a_placed_eps_comes_in_through_its_layers() {
assert!(r.warnings.iter().any(|w| w.contains("placed files")), "{:?}", r.warnings);
}
/// A file in the legacy format without layers (Illustrator 3) whose prolog is included: its prolog,
/// a setup, and `art`.
fn layerless(art: &str) -> Vec<u8> {
format!(
"%!PS-Adobe-3.0 EPSF-3.0\n%%Creator: a script\n%%BoundingBox: 0 0 100 100\n%AI3_Cropmarks: 0 0 100 100\n%%EndComments\n\
%%BeginProlog\n/m {{moveto}} def /L {{lineto}} def /f {{closepath fill}} def /A {{pop}} def /k {{setcmykcolor}} def\n\
%%EndProlog\n%%BeginSetup\n%%EndSetup\n{art}\n%%PageTrailer\nshowpage\n%%Trailer\n%%EOF\n"
)
.into_bytes()
}
/// A file in the legacy format without layers whose prolog is included runs as its page, and comes
/// in through its art on one layer when that looks like the page: what only the format keeps (here
/// a lock) stays.
#[test]
fn a_legacy_file_without_layers_comes_in_through_its_art_on_one_layer() {
let file = layerless(&format!("1 A\n0 0 0 1 k\n{}", tri(10)));
let r = import(&file).unwrap();
assert!(r.warnings.is_empty() && !r.preview, "{:?}", r.warnings);
let d = &r.document;
assert_eq!(d.layers.len(), 1);
let art = d.layers[0].children().unwrap();
assert_eq!(tree(art), "p");
assert!(art[0].locked, "the lock is kept");
// Without the editing data, it is its page, which has no locks.
let page = crate::import::import_with(&file, false).unwrap();
assert!(!page.document.layers[0].children().unwrap()[0].locked);
}
/// When its art can't be read as the format (here a procedure of its own draws it), the file comes
/// in as its page, as before, without a note.
#[test]
fn a_legacy_file_without_layers_whose_art_cannot_be_read_comes_in_as_its_page() {
let file = layerless("/x {20 20 moveto 80 20 lineto 80 80 lineto closepath fill} def x");
let r = import(&file).unwrap();
assert!(r.warnings.is_empty() && !r.preview, "{:?}", r.warnings);
let page = crate::import::import_with(&file, false).unwrap();
assert_eq!(tree(r.document.layers[0].children().unwrap()), tree(page.document.layers[0].children().unwrap()));
assert_eq!(r.document.layers[0].children().unwrap()[0].visual_bounds(), page.document.layers[0].children().unwrap()[0].visual_bounds());
}
/// A version 7 palette writes no swatch names (the AI7 spec's §11 example): its swatches come in
/// named by their paint, in its order, in place of the defaults.
#[test]
+2
View File
@@ -43,6 +43,8 @@ private editing data. VectorCraft:
write) is its own editing data: it opens through the layers its program has, compared with its page
when the program runs, from the layers alone when it names its prolog without including it (#1027), and with its art on one
layer when it has no layers (Illustrator 3, and image editors' exported paths: storytold/photocraft#6036).
A file without layers whose program runs opens through that layer too, compared with its page, so its
objects keep what only the format records (locks, for example); it opens as its page when the layer can't be used.
A multi-layer mask (`Mb` … `Md` … `MB`) comes in as a clipping group in each layer it covers, each
clipped by a copy of the mask's path.
RGB in the PDF page's `ICCBased` spaces keeps the file's numbers in a `.ai` (the `uncalibrated_rgb`