mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
.ai import: legacy PostScript .ai files open on their own artboard
A .ai in the legacy PostScript format opened with the box around all of its art as the artboard, so art placed off the artboard came in on it. Files opened as .eps keep their page. - A .ai file read through its layers used the EPS rule from #1102, which makes the art's bounding box the artboard. It now keeps the artboard its header gives (%AI5_ArtSize around the center of %AI3_TemplateBox), with its art outside it. The engine opens a PostScript .ai or .ait through the new vectorcraft_eps::import_ai; an EPS's private data is still read as an EPS. - A .ai of versions 3 and 4 (no %AI5_FileFormat) does not record its artboard's size. When it has a template box, its artboard is now VectorCraft's own default, a US Letter page, centered on the template box's center, which the version 7 specification (section 2.2) places at the artboard's center. Before, it was the art's box. This also applies to .ai files other apps write with a template box and no art size (#1027). - The center of a template box with a size (a file with a template image) is now the middle of the box, not its lower-left corner. A .ai with neither an art size nor a template box keeps its art's box as the artboard.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
@@ -271,12 +272,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`.
|
||||
@@ -643,6 +660,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`]).
|
||||
@@ -825,6 +847,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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -4663,6 +4663,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]
|
||||
|
||||
@@ -47,7 +47,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};
|
||||
|
||||
Reference in New Issue
Block a user