mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 17:44:08 +00:00
.ai import: legacy files without layers open through the format reader
Squashed from #5209 onto main.
This commit is contained in:
@@ -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> {
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user