Merge pull request #7743 from dotCooCoo/fix/legacy-ai-artboard

.ai import: legacy PostScript .ai files open on their own artboard
This commit is contained in:
BFlat
2026-11-15 13:50:00 +07:00
committed by GitHub
12 changed files with 234 additions and 41 deletions
+3 -1
View File
@@ -393,8 +393,10 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
return Err(err(format!("can't open `{}`: {}", file_name(name), crate::cmd::place::dcs::LINK_ONLY)));
}
let editing = vectorcraft_eps::has_native(bytes).then_some((true, || vectorcraft_eps::native(bytes)));
// A `.ai` keeps the artboard its header gives; an EPS comes in on its page.
let import = if matches!(format.id, "ai" | "ait") { vectorcraft_eps::import_ai } else { vectorcraft_eps::import_names };
restore_or_import(editing, || {
let r = vectorcraft_eps::import_names(bytes, opts.editing_data, opts.trust_editing_data, opts.legacy_names)
let r = import(bytes, opts.editing_data, opts.trust_editing_data, opts.legacy_names)
.map_err(|e| err(format!("can't open `{}`: {e}", file_name(name))))?;
Ok((r.document, r.warnings))
})?
@@ -277,3 +277,17 @@ fn ps_and_epsf_files_open_and_place() {
let e = writable_format(None, Some("out.ps")).unwrap_err();
assert_eq!(e, "PostScript files can be opened but not written (see document.formats)");
}
/// A program in the legacy `.ai` format with a template box and no art size (version 3) opens as a
/// `.ai` or `.ait` on a US Letter artboard around the box's center, and as an EPS on its bounding
/// box.
#[test]
fn a_legacy_ai_program_opens_on_its_artboard_and_an_eps_on_its_page() {
let program = b"%!PS-Adobe-2.0 EPSF-1.2\n%%Creator: a script\n%%BoundingBox: 10 10 30 30\n%AI3_TemplateBox: 100 100 100 100\n\
%%EndComments\n%%BeginProlog\n/m {moveto} def /L {lineto} def /f {closepath fill} def /k {setcmykcolor} def\n\
%%EndProlog\n%%BeginSetup\n%%EndSetup\n0 0 0 1 k\n10 10 m\n30 10 L\n30 30 L\n10 30 L\nf\n%%PageTrailer\nshowpage\n%%Trailer\n%%EOF\n";
for (name, size) in [("old.ai", (612.0, 792.0)), ("old.ait", (612.0, 792.0)), ("old.eps", (20.0, 20.0))] {
let r = load(name, program).unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(r.doc.artboards[0].rect.size(), vectorcraft_geom::Size::new(size.0, size.1), "{name}: {:?}", r.warnings);
}
}
+22 -7
View File
@@ -24,7 +24,9 @@
//! from those layers alone, without its page.
//!
//! The page is the file's `%%BoundingBox` (`%%HiResBoundingBox` when it has one), the first page
//! of a PostScript file without one. A file whose program can't be read (an operator the
//! of a PostScript file without one. A `.ai` in the legacy format ([`import_ai`]) that comes in
//! through its layers keeps the artboard its header gives, with its art outside it; an EPS comes in
//! on its page. A file whose program can't be read (an operator the
//! interpreter doesn't know, an error, a limit reached) or that draws nothing comes in from its
//! layers when its program has them (a `.ai` in PostScript form whose prolog is named but not
//! included, as other apps write it), or with its art on one layer when it is in the legacy format
@@ -279,7 +281,20 @@ pub fn import_trusting(bytes: &[u8], editing_data: bool, trust: bool) -> Result<
/// and data sets) read in `names`. The type of the document it gives that is set in families that
/// aren't available shows in the fallback font, and a warning names them.
pub fn import_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding) -> Result<Imported, String> {
let mut r = read_names(bytes, editing_data, trust, names)?;
read_checked(bytes, editing_data, trust, names, false)
}
/// [`import_names`] for a `.ai` (or `.ait`) in PostScript form: a program in the legacy format
/// that comes in through its layers keeps the artboard its header gives, with its art outside it,
/// where an EPS comes in on its page, its art's box. A `.ai` of versions 3 and 4, which doesn't
/// give its artboard's size, has a US Letter artboard around its template box's center.
pub fn import_ai(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding) -> Result<Imported, String> {
read_checked(bytes, editing_data, trust, names, true)
}
/// [`import_names`] (a `.ai` when `ai_file`, see [`import_ai`]).
fn read_checked(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding, ai_file: bool) -> Result<Imported, String> {
let mut r = read_names(bytes, editing_data, trust, names, ai_file)?;
let missing = missing_fonts(&r.document);
if !missing.is_empty() {
r.warnings.push(format!("{MISSING_FONTS_NOTE}: {}", missing.join(", ")));
@@ -314,8 +329,8 @@ fn missing_fonts(d: &Document) -> Vec<String> {
missing
}
/// [`import_names`] before the type's fonts are checked.
fn read_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding) -> Result<Imported, String> {
/// [`import_names`] before the type's fonts are checked (a `.ai` when `ai_file`, see [`import_ai`]).
fn read_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding, ai_file: bool) -> Result<Imported, String> {
let ps = program(bytes)?;
let dsc = Dsc::read(ps);
// A legacy `.ai` VectorCraft wrote: its prolog prints the art plainly (gradients flat, no
@@ -323,7 +338,7 @@ fn read_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding
// printed).
if editing_data
&& dsc.own_legacy
&& let Some(Ok((document, warnings))) = native::program_alone(ps, dsc.illustrator, names)
&& let Some(Ok((document, warnings))) = native::program_alone(ps, dsc.illustrator, ai_file, names)
{
return Ok(Imported { document, warnings, preview: false });
}
@@ -343,7 +358,7 @@ fn read_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding
}
let drew = !out.drawn.is_empty();
let why = match (&result, drew) {
(Ok(()), true) if editing_data => return Ok(native::layered(ps, finish(out), trust, dsc.illustrator, names)),
(Ok(()), true) if editing_data => return Ok(native::layered(ps, finish(out), trust, dsc.illustrator, ai_file, names)),
(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()),
@@ -351,7 +366,7 @@ fn read_names(bytes: &[u8], editing_data: bool, trust: bool, names: NameEncoding
// A file in the legacy Illustrator format whose program can't be run (a `.ai` that names the
// prolog defining its operators without including it): its layers, which the program has, or
// its art on one layer when it has none (Illustrator 3).
let why = match editing_data.then(|| native::program_alone(ps, dsc.illustrator, names)).flatten() {
let why = match editing_data.then(|| native::program_alone(ps, dsc.illustrator, ai_file, names)).flatten() {
Some(Ok((document, warnings))) => return Ok(Imported { document, warnings, preview: false }),
Some(Err(e)) => format!("{why}; its layers couldn't be read either ({e})"),
None => why,
+2 -2
View File
@@ -55,8 +55,8 @@ pub use read::{
palette, read_beside, read_in, symbols, variables,
};
pub(crate) use read::{
MAX_NODES as MAX_EDITING_OBJECTS, MAX_SYMBOLS, join_and, read_art_style, read_over_page, read_style_list, slot_frame, slot_of, style_moves_with,
symbol_art_member,
MAX_NODES as MAX_EDITING_OBJECTS, MAX_SYMBOLS, join_and, read_art_style, read_over_page, read_program, read_style_list, slot_frame, slot_of,
style_moves_with, symbol_art_member,
};
pub(crate) use read::is_look_slot;
+32 -8
View File
@@ -21,9 +21,10 @@
//! - State: hidden `Xw`, locked `A`, transparency `mode opacity isolate knockout shape Xy`.
//! - Names: an `/ArtDictionary` after an object, its `AI10_ArtUID` the name's XML id.
//! - Artboards: the `ArtboardArray` of the `/Document` dictionary, with each one's display
//! options, else `%AI3_Cropmarks`, else the art size around the template box's centre
//! (`%AI3_TemplateBox`, `%%TemplateBox` in older files), else (a file in the legacy format
//! another app wrote) the bounding box.
//! options, else `%AI3_Cropmarks`, else the art size (`%AI5_ArtSize`; VectorCraft's own default,
//! US Letter, in a `.ai` of versions 3 and 4, which don't give it) around the template box's center (`%AI3_TemplateBox`,
//! `%%TemplateBox` in older files), which the version 7 specification (§2.2) puts at the
//! artboard's center, else (a file in the legacy format another app wrote) the bounding box.
//! - Document settings: Document Raster Effects Settings from the `/Document` dictionary's
//! `AI Auto Rasterize`; the author from the header's `%%For`; swatches for the custom colors of
//! `%%CMYKCustomColor` and `%%RGBCustomColor` that the document has none of.
@@ -273,12 +274,28 @@ pub(crate) fn read_over_page(data: &[u8], large_canvas: bool, names: NameEncodin
read_with(data, large_canvas, true, names)
}
fn read_with(data: &[u8], large_canvas: bool, page_art: bool, names: NameEncoding) -> Result<Structure, String> {
/// [`read_in`] (or [`read_over_page`] when `page_art`) for the program of a `.ai` in the legacy
/// format: one of versions 3 and 4 that has a template box but no art size has a US Letter
/// artboard ([`Reader::sized_board`]).
pub(crate) fn read_program(data: &[u8], large_canvas: bool, page_art: bool, names: NameEncoding) -> Result<Structure, String> {
let mut r = Reader::with_names(data, names);
r.large_canvas = large_canvas.then_some(1.0);
r.page_art = page_art;
r.run()?;
r.finish()
r.legacy_ai = true;
r.read_as(large_canvas, page_art)
}
fn read_with(data: &[u8], large_canvas: bool, page_art: bool, names: NameEncoding) -> Result<Structure, String> {
Reader::with_names(data, names).read_as(large_canvas, page_art)
}
impl Reader<'_> {
/// The structure read, the declared large canvas scale applied when `large_canvas`, the page's
/// art replacing the objects not read when `page_art` ([`read_over_page`]).
fn read_as(mut self, large_canvas: bool, page_art: bool) -> Result<Structure, String> {
self.large_canvas = large_canvas.then_some(1.0);
self.page_art = page_art;
self.run()?;
self.finish()
}
}
/// How many objects [`read`] counts against [`MAX_NODES`] in the editing data `data`.
@@ -645,6 +662,11 @@ struct Reader<'a> {
/// The artboard's size and centre in files without artboards or crop marks.
art_size: Option<(f64, f64)>,
template_center: Option<(f64, f64)>,
/// The data is the program of a `.ai` in the legacy format ([`read_program`]), not an EPS's:
/// without an art size, its artboard can be US Letter ([`Reader::sized_board`]).
legacy_ai: bool,
/// The header has `%AI5_FileFormat`, which files of version 5 and later write.
file_format: bool,
/// `%AI24_LargeCanvasScale`: a Large Canvas document's editing data is written at 1/N scale
/// (see [`Reader::ensure_space`]); 1 (no scaling) when the file doesn't declare one, `None`
/// when the declared factor is not applied ([`read`]).
@@ -827,6 +849,8 @@ impl<'a> Reader<'a> {
xml_id_bytes: 0,
art_size: None,
template_center: None,
legacy_ai: false,
file_format: false,
large_canvas: None,
page_art: false,
canvas_scale: 1.0,
+4 -1
View File
@@ -242,10 +242,13 @@ impl<'a> Reader<'a> {
&& let Ok(n) = v.trim().parse::<u8>()
{
self.ruler_units = Some(n);
} else if self.frames.is_empty() && line.starts_with("AI5_FileFormat") {
self.file_format = true;
} else if let Some(v) = line.strip_prefix("AI5_ArtSize:") {
self.art_size = two(v);
} else if let Some(v) = line.strip_prefix("AI3_TemplateBox:").or_else(|| line.strip_prefix("%TemplateBox:")) {
self.template_center = two(v);
// The center of the box (a file without a template writes it with no width or height).
self.template_center = four(v).map(|[x0, y0, x1, y1]| ((x0 + x1) / 2.0, (y0 + y1) / 2.0)).or_else(|| two(v));
self.template = four(v).or(self.template);
} else if let Some(v) = line.strip_prefix("AI24_LargeCanvasScale:") {
self.large_canvas_scale(v);
+8 -2
View File
@@ -871,9 +871,15 @@ impl<'a> Reader<'a> {
// ---- the document ----------------------------------------------------------------------
/// The artboard of a file without artboards or crop marks: its size around the template
/// box's centre (half a point off the grid).
/// box's center (half a point off the grid). The program of a `.ai` of versions 3 and 4
/// ([`Reader::legacy_ai`], no `%AI5_FileFormat`), which don't record the artboard's size, has
/// VectorCraft's own default there, a US Letter artboard (612 × 792 pt); an EPS, or a newer
/// file without `%AI5_ArtSize`, has none (its page is its bounding box).
fn sized_board(&self) -> Option<Rect> {
let ((w, h), (cx, cy)) = (self.art_size?, self.template_center?);
let (cx, cy) = self.template_center?;
let [_, _, letter_w, letter_h] = crate::import::LETTER;
let letter = self.legacy_ai && !self.file_format;
let (w, h) = self.art_size.or(letter.then_some((letter_w, letter_h)))?;
// Rounded out, but not off a whole point the decimals in the file only just miss.
let near = |v: f64| (v - v.round()).abs() < 1e-6;
let (left, top) = (cx - w / 2.0, cy + h / 2.0);
+22 -7
View File
@@ -19,6 +19,16 @@ pub(super) enum Page {
Art,
/// A `.ai` is read on its (first and only) artboard.
Artboard,
/// A `.ai` in the legacy format (PostScript, not an EPS): its page is its art's bounding box, as
/// an EPS's is, and its artboard is the one its header gives.
Program,
}
impl Page {
/// Is the page the art's bounding box?
fn of_art(self) -> bool {
matches!(self, Page::Art | Page::Program)
}
}
/// How art moved from a page to its place in the layers' document transforms: its pattern tiles
@@ -62,8 +72,12 @@ pub(super) fn build(
) -> Result<(Document, Vec<String>), String> {
// An EPS (its page is its art) draws a Large Canvas page at full size; a `.ai` keeps its art
// space, as its PDF part may (#2031).
let large_canvas = matches!(page, Page::Art);
let structure = if visible.is_some() { ai::read_over_page(data, large_canvas, names)? } else { ai::read_in(data, large_canvas, names)? };
let large_canvas = page.of_art();
let structure = match page {
Page::Program => ai::read_program(data, large_canvas, visible.is_some(), names)?,
_ if visible.is_some() => ai::read_over_page(data, large_canvas, names)?,
_ => ai::read_in(data, large_canvas, names)?,
};
build_from(structure, data, visible, page, outlined, trust, unit)
}
@@ -184,7 +198,7 @@ fn page_scale_alone(structure: &Structure, pages: &[(f64, f64)], unit: f64) -> O
/// page, which draws nothing for them, the linked images' `placeholders`. A PDF part draws those
/// images' pixels, which the placeholders don't match.
fn left_out_of_comparisons(page: Page, mut restyled: Vec<Rect>, placeholders: Vec<Rect>) -> Vec<Rect> {
if matches!(page, Page::Art) {
if page.of_art() {
restyled.extend(placeholders);
}
restyled
@@ -295,7 +309,7 @@ pub(super) fn build_weighing(
let mut frame = None;
if let Some(visible) = visible {
let art_box = match page {
Page::Art => bbox.map(|[a, b, c, d]| Rect::new(a, b, c, d)).ok_or("its editing data doesn't say where its art is")?,
Page::Art | Page::Program => bbox.map(|[a, b, c, d]| Rect::new(a, b, c, d)).ok_or("its editing data doesn't say where its art is")?,
Page::Artboard => artboard.ok_or("its editing data doesn't say where its artboard is")?,
};
let page_rect = visible.artboards.first().map(|a| a.rect).ok_or("it has no page")?;
@@ -393,8 +407,9 @@ pub(super) fn build_weighing(
// An EPS saved as its artboard has the art outside it left out of its page.
let artboard_page =
artboard.is_some_and(|a| [a.x0 - art_box.x0, a.y0 - art_box.y0, a.x1 - art_box.x1, a.y1 - art_box.y1].iter().all(|d| d.abs() <= 1.0));
let whole = matches!(page, Page::Art) && !artboard_page;
if whole {
let whole = page.of_art() && !artboard_page;
// A `.ai` keeps the artboard its editing copy gives, with the art outside it.
if whole && matches!(page, Page::Art) {
// Its page is the file's own (its bounding box), as when it comes in as its page: art
// reaching past the editing data's artboard stays on it (#1102).
doc.artboards.truncate(1);
@@ -1815,7 +1830,7 @@ fn last_boards(boards: &[vectorcraft_doc::Artboard], pages: &[Rect]) -> Option<V
/// the one whose art draws most like the page, the first of them on a tie. Else (and for an EPS,
/// whose page is its art) the first artboard (0).
fn shown_artboard(doc: &Document, visible: &Document, page: Page, page_rect: Rect, fits: impl Fn(&Rect) -> bool) -> usize {
if matches!(page, Page::Art) || visible.artboards.len() >= doc.artboards.len() {
if page.of_art() || visible.artboards.len() >= doc.artboards.len() {
return 0;
}
let pages: Vec<Rect> = visible.artboards.iter().map(|a| a.rect).collect();
+28 -11
View File
@@ -13,12 +13,16 @@ use super::{Affine, Arc, Cow, Document, Imported, Node, NodeKind, PathData, Rect
/// ([`use_symbols`]) and its variable library ([`use_variables`]) when it could be read, and with
/// its layer tree, empty, 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. `names`: the encoding of names
/// that aren't UTF-8.
pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illustrator: bool, names: ai::NameEncoding) -> Imported {
/// layer; when that can't be used, the file is `visible` as it was. The program of a `.ai`
/// (`ai_file`) in the legacy format keeps the artboard its header gives ([`Page::Program`]).
/// `names`: the encoding of names that aren't UTF-8.
pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illustrator: bool, ai_file: bool, names: ai::NameEncoding) -> Imported {
let program_page = if ai_file { Page::Program } else { Page::Art };
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, names).ok());
let read = illustrator
.then(|| one_layer(ps))
.flatten()
.and_then(|p| build(&p, Some(&visible.document), program_page, false, trust, 1.0, names).ok());
return match read {
Some((document, warnings)) => with_page_notes(document, warnings, &visible),
None => visible,
@@ -28,11 +32,17 @@ pub(in super::super) fn layered(ps: &[u8], visible: Imported, trust: bool, illus
// can't be used.
// Private data kept apart from the program: the program's own header gives the author and
// custom colors the private data's header may lack.
let read =
|d: &[u8]| if matches!(data, Ok(Cow::Owned(_))) { ai::read_beside(d, true, ps, true, names) } else { ai::read_over_page(d, true, names) };
let private = matches!(data, Ok(Cow::Owned(_)));
let read = |d: &[u8]| match (private, program_page) {
(true, _) => ai::read_beside(d, true, ps, true, names),
(false, Page::Program) => ai::read_program(d, true, true, names),
(false, _) => ai::read_over_page(d, true, names),
};
// Private data is an EPS's editing copy, whatever the file is named.
let page = if private { Page::Art } else { program_page };
let (tree, result) = match data.as_deref() {
Ok(d) => match read(d) {
Ok(s) => (layer_tree(&s.doc.layers), build_from(s, d, Some(&visible.document), Page::Art, false, trust, 1.0)),
Ok(s) => (layer_tree(&s.doc.layers), build_from(s, d, Some(&visible.document), page, false, trust, 1.0)),
Err(why) => (vec![], Err(why)),
},
Err(why) => (vec![], Err(why.clone())),
@@ -607,16 +617,23 @@ fn editing_copy(ps: &[u8]) -> Option<Result<Cow<'_, [u8]>, String>> {
/// but whose program has no layers (Illustrator 3 had none; image editors still export their
/// paths so, storytold/photocraft#6036) is read with its art on one layer. `None` when the program
/// has no layers and isn't in the format: other PostScript isn't read as the format's operators.
/// The program of a `.ai` (`ai_file`) has the artboard its header gives ([`Page::Program`]).
/// `names`: the encoding of names that aren't UTF-8.
pub(in super::super) fn program_alone(ps: &[u8], illustrator: bool, names: ai::NameEncoding) -> Option<Result<(Document, Vec<String>), String>> {
pub(in super::super) fn program_alone(
ps: &[u8],
illustrator: bool,
ai_file: bool,
names: ai::NameEncoding,
) -> Option<Result<(Document, Vec<String>), String>> {
if ai::has_eps_data(ps) {
return None;
}
let page = if ai_file { Page::Program } else { Page::Art };
if has_layers(ps) {
return Some(build(ps, None, Page::Art, false, false, 1.0, names));
return Some(build(ps, None, page, false, false, 1.0, names));
}
let program = illustrator.then(|| one_layer(ps)).flatten()?;
Some(build(&program, None, Page::Art, false, false, 1.0, names))
Some(build(&program, None, page, false, false, 1.0, names))
}
/// `ps` with its art (from its `%%EndSetup` to its `%%PageTrailer`, else its `%%Trailer`, else its
+95 -1
View File
@@ -9,7 +9,7 @@ use vectorcraft_doc::clipnest::MAX_NEST;
use vectorcraft_doc::{LayerColor, LineCap, Node, NodeKind};
use vectorcraft_geom::Rect;
use crate::import::import;
use crate::import::{import, import_ai};
/// A minimal file in Illustrator's format: its header comments, a prolog of our own defining the
/// operators `body` uses (`u` and `U` as `ugroup` and `Ugroup`), and `body`.
@@ -329,6 +329,100 @@ fn a_legacy_file_with_its_prolog_comes_in_through_its_layers() {
assert_eq!(page.document.layers[0].name.as_deref(), Some("Layer 1"));
}
/// A `.ai` in the legacy format (version 8, `%AI5_FileFormat 4.0`) whose prolog is included: a
/// square on its 200-point artboard and one far to its right. Its bounding box is the box of that
/// art; its artboard is its art size around the template box's center (version 7 specification
/// §2.2). Its first line names the EPSF version, as the format's files may.
const LEGACY_AI_ART_OFF_ITS_ARTBOARD: &str = "%!PS-Adobe-2.0 EPSF-1.2\n%%Creator: a script\n%%BoundingBox: 10 10 320 30\n\
%%HiResBoundingBox: 10 10 320 30\n%AI5_FileFormat 4.0\n%AI3_TemplateBox: 100.5 99.5 100.5 99.5\n%AI5_ArtSize: 200 200\n\
%%EndComments\n%%BeginProlog\n/m {moveto} def /L {lineto} def /f {closepath fill} def\n\
/Lb {10 {pop} repeat} def /Ln {pop} def /LB {} def /A {pop} def /k {setcmykcolor} def\n%%EndProlog\n%%BeginSetup\n%%EndSetup\n\
%AI5_BeginLayer\n1 1 1 1 0 0 0 79 128 255 Lb\n(Art) Ln\n0 A\n0 0 0 1 k\n\
10 10 m\n30 10 L\n30 30 L\n10 30 L\nf\n300 10 m\n320 10 L\n320 30 L\n300 30 L\nf\nLB\n%AI5_EndLayer--\n\
%%PageTrailer\nshowpage\n%%Trailer\n%%EOF\n";
/// `bytes` opened as a `.ai`.
fn open_ai(bytes: &[u8]) -> crate::Imported {
import_ai(bytes, true, false, crate::NameEncoding::default()).unwrap()
}
/// Such a `.ai` keeps the artboard its header gives, with the square outside it, when it comes in
/// through its layers. The same program opened as an EPS comes in on its page, its art's box
/// (#1102).
#[test]
fn a_legacy_ai_keeps_its_artboard_and_the_art_outside_it() {
let r = open_ai(LEGACY_AI_ART_OFF_ITS_ARTBOARD.as_bytes());
assert!(r.warnings.is_empty() && !r.preview, "{:?}", r.warnings);
let d = &r.document;
let boards: Vec<_> = d.artboards.iter().map(|a| a.rect).collect();
assert_eq!(boards, [Rect::new(0.0, 0.0, 200.0, 200.0)]);
let art: Vec<_> = d.layers[0].children().unwrap().iter().map(|n| n.geometric_bounds().unwrap()).collect();
// Art space has y up from the artboard's bottom: (10, 10) is 10 above it.
assert_eq!(art, [Rect::new(10.0, 170.0, 30.0, 190.0), Rect::new(300.0, 170.0, 320.0, 190.0)]);
let eps = import(LEGACY_AI_ART_OFF_ITS_ARTBOARD.as_bytes()).unwrap();
let boards: Vec<_> = eps.document.artboards.iter().map(|a| a.rect.size()).collect();
assert_eq!(boards, [kurbo::Size::new(310.0, 20.0)], "{:?}", eps.warnings);
}
/// A version 3 `.ai` doesn't give its artboard's size (`%AI5_ArtSize` came with version 5): its
/// artboard is VectorCraft's own default, a US Letter page, around the template box's center, the
/// artboard's center (version 7 specification §2.2). Without a template box it is the art's box,
/// and so is the page of the same file opened as an EPS.
#[test]
fn a_version_3_ai_has_a_letter_artboard_around_its_template_boxs_center() {
let v3 = PATHS_ONLY.replace("%AI3_Cropmarks: 0 0 200 100\n", "%AI3_TemplateBox: 100 100 100 100\n");
let r = open_ai(v3.as_bytes());
assert!(r.warnings.is_empty() && !r.preview, "{:?}", r.warnings);
let d = &r.document;
let boards: Vec<_> = d.artboards.iter().map(|a| a.rect).collect();
assert_eq!(boards, [Rect::new(0.0, 0.0, 612.0, 792.0)]);
// The artboard runs from -206 to 406 across and from -296 to 496 up in art space.
let art = d.layers[0].children().unwrap();
assert_eq!(art[1].geometric_bounds(), Some(Rect::new(226.0, 446.0, 266.0, 476.0)));
// The center of a template box with a size (a file with a template image).
let r = open_ai(PATHS_ONLY.replace("%AI3_Cropmarks: 0 0 200 100\n", "%AI3_TemplateBox: 50 50 150 150\n").as_bytes());
assert_eq!(r.document.layers[0].children().unwrap()[1].geometric_bounds(), Some(Rect::new(226.0, 446.0, 266.0, 476.0)));
let r = open_ai(PATHS_ONLY.replace("%AI3_Cropmarks: 0 0 200 100\n", "%AI3_ColorUsage: Color\n").as_bytes());
assert_eq!(r.document.artboards[0].rect, Rect::new(0.0, 0.0, 200.0, 100.0), "{:?}", r.warnings);
let r = import(v3.as_bytes()).unwrap();
assert_eq!(r.document.artboards[0].rect, Rect::new(0.0, 0.0, 200.0, 100.0), "{:?}", r.warnings);
// A newer file (`%AI5_FileFormat`, version 5 on) without an art size keeps the art's box too.
let r = open_ai(v3.replace("%%EndComments", "%AI5_FileFormat 1.0\n%%EndComments").as_bytes());
assert_eq!(r.document.artboards[0].rect, Rect::new(0.0, 0.0, 200.0, 100.0), "{:?}", r.warnings);
}
/// A version 3 `.ai` whose prolog is included and whose program has no layers: its page draws, and
/// its art comes in on one layer around a US Letter artboard, the square far to the right outside
/// it.
#[test]
fn a_version_3_ai_that_draws_keeps_its_letter_artboard_and_the_art_outside_it() {
let v3 = "%!PS-Adobe-2.0 EPSF-1.2\n%%Creator: a script\n%%BoundingBox: 10 10 720 30\n%AI3_TemplateBox: 100 100 100 100\n\
%%EndComments\n%%BeginProlog\n/m {moveto} def /L {lineto} def /f {closepath fill} def /k {setcmykcolor} def\n\
%%EndProlog\n%%BeginSetup\n%%EndSetup\n0 0 0 1 k\n10 10 m\n30 10 L\n30 30 L\n10 30 L\nf\n\
700 10 m\n720 10 L\n720 30 L\n700 30 L\nf\n%%PageTrailer\nshowpage\n%%Trailer\n%%EOF\n";
let r = open_ai(v3.as_bytes());
assert!(r.warnings.is_empty() && !r.preview, "{:?}", r.warnings);
let boards: Vec<_> = r.document.artboards.iter().map(|a| a.rect).collect();
assert_eq!(boards, [Rect::new(0.0, 0.0, 612.0, 792.0)]);
assert_eq!(r.document.layers.len(), 1);
let art: Vec<_> = r.document.layers[0].children().unwrap().iter().map(|n| n.geometric_bounds().unwrap()).collect();
// The artboard runs from -206 to 406 across and from -296 to 496 up in art space.
assert_eq!(art, [Rect::new(216.0, 466.0, 236.0, 486.0), Rect::new(906.0, 466.0, 926.0, 486.0)]);
let eps = import(v3.as_bytes()).unwrap();
assert_eq!(eps.document.artboards[0].rect, Rect::new(0.0, 0.0, 710.0, 20.0), "{:?}", eps.warnings);
}
/// A `.ai` another app wrote (#1027) that gives a template box but no art size opens on a US Letter
/// artboard around the box's center, from -18 to 594 across and -12 to 780 up in art space (as an
/// EPS it opens on its bounding box, see above).
#[test]
fn a_postscript_ai_with_a_template_box_and_no_art_size_opens_on_a_letter_artboard() {
let r = open_ai(NAMED_PROLOG.as_bytes());
assert_eq!(r.document.artboards[0].rect, Rect::new(0.0, 0.0, 612.0, 792.0), "{:?}", r.warnings);
let square = &r.document.layers[0].children().unwrap()[0];
assert_eq!(square.geometric_bounds(), Some(Rect::new(28.0, 690.0, 108.0, 770.0)));
}
/// A placed EPS in the legacy format (version 7 specification §16, no `%AI5_BeginPlace`), its file
/// included whole: the layers come in, the placed file as the art the page draws in its box.
#[test]
+3
View File
@@ -4791,6 +4791,9 @@ fn an_eps_page_of_its_art_stays_its_page_through_the_layers() {
assert_eq!(boards.len(), 1);
assert!((boards[0].width() - 4.0).abs() < 1e-6 && (boards[0].height() - 4.0).abs() < 1e-6, "{boards:?}");
assert!(boards[0].inflate(1e-6, 1e-6).contains_rect(art), "{boards:?} round {art:?}");
// Its private data is an EPS's editing copy, whatever the file is named.
let named_ai = crate::import::import_ai(&file, true, false, crate::NameEncoding::default()).unwrap();
assert_eq!(named_ai.document.artboards.iter().map(|a| a.rect).collect::<Vec<_>>(), boards, "{:?}", named_ai.warnings);
}
#[test]
+1 -1
View File
@@ -49,7 +49,7 @@ mod tiff;
pub use ai_editing::{AiEditingOptions, AiEditingOutput, CREATOR_VERSION, export_ai_editing, private_data as ai_private_data};
pub use import::{
AiLayers, Imported, NameEncoding, POSTSCRIPT_ERRORS, ai_alone, ai_alone_on_page, ai_alone_with, ai_layers_alone, ai_opened_artboard,
family_style, import, import_names, import_trusting, import_with, is_loss, layered_ai, layered_ai_from, page,
family_style, import, import_ai, import_names, import_trusting, import_with, is_loss, layered_ai, layered_ai_from, page,
};
pub use legacy_ai::{HiddenObjects, LegacyAiOptions, LegacyAiOutput, export_legacy_ai, writes_image_mask};
pub use print::{PrintJob, PrintPage, print};