mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
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:
@@ -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.
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user