Files
vectorcraft/crates/eps/src/import/ai/read.rs
T

335 lines
15 KiB
Rust

//! The reader: runs the editing data's operators into a document.
//!
//! - Art space has y up; the first artboard's top left corner is the document's origin.
//! - Layers: `%AI5_BeginLayer`, `visible preview unlocked printing dimmed … colorIndex r g b …
//! dimPercent … Lb` (ten operands in the legacy format, without the one before the colour
//! index), `(name) Ln`, its art, `LB`.
//! - Groups `u` … `U`, compound paths `*u` … `*U`, clipping groups `q` … `Q` (the path painted
//! with `W` clips); a group's transparency and name follow its end.
//! - Paths: `m`, `l`/`L`, `c`/`C`, `v`/`V`, `y`/`Y`, painted by `N n F f S s B b` (lower case
//! closes), `(op) *` for a guide. Stroke `w J j M d`, fill rule `XR`, overprint `O`/`R`.
//! - 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, else `%AI3_Cropmarks`, else
//! the art size around the template box's centre, else (a file in the legacy format another
//! app wrote) the bounding box.
//! - Variables: the variable library, XML nodes of the `/Document` dictionary, isn't read; a note
//! gives how many variables and data sets it has ([`VariableLibrary`]).
//! - Images: `%AI5_BeginRaster`, the colour space `XN`, `[matrix] bounds w h bits type alpha …`
//! and the samples after `XI`. 8-bit CMYK samples keep their ink amounts in a CMYK TIFF, and
//! an alpha channel that hides part of such an image becomes its opacity mask; other samples
//! become an RGBA PNG.
//! - Placed (linked) files, as written in files users sent: `%AI5_BeginPlace`, `[matrix] llx lly urx
//! ury … (path)` and the operator `` ` ``, the file's preview between `%AI26_BeginPlacedObjectPreview`
//! and its end (`w h rowBytes 32 n `, then rows of premultiplied ARGB, the top one first), `~`. The
//! box is as large as the file has pixels; the matrix takes them into art space from the canvas
//! (y down, its centre the template box's), where they start at the point of art space's origin,
//! the first row the top one. Each is a linked image showing the preview until its file is found.
//! - Art with an appearance the format can't write as plain art (several fills or strokes,
//! effects, brushes) comes first as that look, drawn (a group, or an image where an effect
//! rasterizes it), then the object itself on `%_`
//! lines and `1 (style) XW`: the drawn look is kept, named after the object.
//! - Patterns: `%AI3_BeginPattern: (name)`, `(name) llx lly urx ury` (the tile, in the pattern's
//! space, y up), the tile's art (on `%_` lines in newer files), `E`, `%AI3_EndPattern`; each is
//! read by a reader of its own and becomes a pattern swatch. A fill or stroke with one:
//! `(name) x y scaleX scaleY angle reflect axis shear shearAxis [matrix] p` (`P`), the matrix
//! taking the pattern's space to art space (the other operands are what it is made of).
//!
//! - Type in the legacy format (version 7 specification §15): `type To` … `TO`, read by
//! [`legacy_text`].
//!
//! - Swatches: the palette (`%AI5_BeginPalette`), read by [`super::palette`], replaces the
//! default swatches.
//!
//! Sections the reader doesn't need (brushes, symbols, styles, filters) are skipped;
//! type, symbols, fills with a pattern it can't read, placed files without a preview and operators
//! it doesn't know are errors.
use std::collections::{BTreeSet, HashMap, HashSet};
use std::rc::Rc;
use std::sync::Arc;
use kurbo::{Affine, BezPath, PathEl, Point, Rect};
use vectorcraft_color::gradient::Gradient;
use vectorcraft_color::swatch::Swatch;
use vectorcraft_color::{BlendMode, Paint};
use vectorcraft_doc::cmyk::Inks;
use vectorcraft_doc::{
Appearance, AppearanceItem, Artboard, ColorMode, Dash, Document, FillLayer, ImageBlob, ImageObject, Knockout, LayerColor, LineCap, LineJoin,
LinkInfo, Node, NodeId, NodeKind, OpacityMask, PatternDef, StrokeLayer, Unit,
};
use vectorcraft_geom::{FillRule, PathData};
use super::super::ate::CANVAS_CENTRE;
use super::lex::{Lexer, Tok};
use super::obj::{self, Obj, V};
use super::paint::{self, GradientDef, Instance, Named};
use crate::ps;
mod art;
mod containers;
mod directives;
mod document;
mod ops;
mod types;
use art::preview_png;
pub use document::{VariableLibrary, artboards, variables};
pub(crate) use document::{slot_frame, slot_of};
pub use types::IMAGE_TOO_LARGE;
use types::{BLEND_BUDGET, FLIP, Frame, GState, Kind, PatternHead, Place, Raster};
#[cfg(test)]
pub use types::{NON_NATIVE_ART, NON_NATIVE_ART_UNREAD};
pub(super) use types::{blend_mode, blend_name};
#[path = "legacy_text.rs"]
mod legacy_text;
/// What the editing data holds: the document of its art, and where its art space is.
#[derive(Debug)]
pub struct Structure {
/// The layers, artboards, swatches and colour mode. A text object is an empty group named
/// [`TEXT_SLOT`] and its story's number, for the importer to fill.
pub doc: Document,
/// Art space (y up) → the document.
pub to_doc: Affine,
/// The art's bounding box in art space (`%%HiResBoundingBox`, else `%%BoundingBox`).
pub bbox: Option<[f64; 4]>,
/// The first artboard in art space.
pub artboard: Option<Rect>,
/// `%AI3_TemplateBox`, whose centre is the canvas's (the text document's origin).
pub template: Option<[f64; 4]>,
/// What layers that don't show have that isn't read (operators, objects): left out of them.
pub hidden_unread: BTreeSet<String>,
/// The names of text objects, by their slot's id.
pub slot_names: HashMap<NodeId, String>,
/// Where the drawn looks lay that objects read with their art style took the place of (the
/// document's coordinates): the page draws Illustrator's rendering of those styles there.
pub restyled: Vec<Rect>,
pub warnings: Vec<String>,
}
/// Read the structure of the editing data `data`, applying the file's `%AI24_LargeCanvasScale` only
/// when `large_canvas` is set: an EPS's page is drawn at the full size, but a `.ai`'s PDF part may
/// keep the reduced one, which the PDF import reads as it is (#2031).
pub fn read(data: &[u8], large_canvas: bool) -> Result<Structure, String> {
let mut r = Reader::new(data);
r.large_canvas = large_canvas.then_some(1.0);
r.run()?;
r.finish()
}
struct Reader<'a> {
lex: Lexer<'a>,
stack: Vec<V>,
/// Where the `[` marks are on the stack, innermost last (some may have gone with operands).
marks: Vec<usize>,
gs: GState,
frames: Vec<Frame>,
path: BezPath,
points: usize,
/// The path is a clipping path (`W`).
clip_next: bool,
doc: Document,
to_doc: Option<Affine>,
cropmarks: Option<Rect>,
artboards: Vec<(String, Rect)>,
/// The variables and data sets of the `/Document` dictionary's variable library.
variables: VariableLibrary,
gradients: HashMap<String, Gradient>,
gradient_def: Option<GradientDef>,
instance: Option<Instance>,
pending_fill: Option<Paint>,
pending_stroke: Option<Paint>,
/// The named colours the colour operators define (`x`, `Xx`, `Xk`…).
spots: Vec<Named>,
/// The named colours gradient stops name. A stop doesn't say whether its colour is an ink or
/// a global process colour, so these make swatches only for names no colour operator defines.
stop_spots: Vec<Named>,
raster: Option<Raster>,
/// The placed file being read (`%AI5_BeginPlace` … `~`).
place: Option<Place>,
/// The last object read is the last child of the innermost frame, and nothing started since.
after_object: bool,
nodes: usize,
/// What layers that show have that this reader doesn't read (errors).
unsupported: BTreeSet<String>,
/// Operators the reader doesn't know on layers that show.
unknown: BTreeSet<String>,
/// What layers that don't show have that this reader doesn't read.
hidden_unread: BTreeSet<String>,
/// The header's bounding boxes and template box.
bbox: Option<[f64; 4]>,
hires: Option<[f64; 4]>,
template: Option<[f64; 4]>,
/// Colours set in each model, when the header doesn't say the document's.
cmyk_colors: usize,
rgb_colors: usize,
color_model: Option<bool>,
warnings: Vec<String>,
/// Objects read as their drawn look (several fills or strokes, effects, brushes).
drawn_looks: usize,
/// Nodes read from `%_` lines.
hidden_ids: HashSet<NodeId>,
/// The blend part each object's dictionary names (`ShapeBlendPathType`, `ShapeBlendPathID`).
blend_roles: super::blend::Roles,
/// A blend's record being read from the `%` lines after its `XP`: its length and hex digits.
blend_record: Option<(usize, Vec<u8>)>,
/// How many more objects blends may lay out in this import ([`super::blend::live_blend`]).
blend_budget: usize,
/// A group just ended, its state (`Xy`, `Xd`, `XW`) not yet read.
group_ended: bool,
/// A layer's own state (`Xy` after its art, before `LB`), until something says the art moves on.
layer_state: Option<(f32, BlendMode, bool, Knockout)>,
/// Painted copies of clipping paths that have a fill (drawn under what they clip).
clip_fills: HashSet<NodeId>,
/// Groups closed as an object's drawn look (`9 () XW`).
looks: HashSet<NodeId>,
/// Legacy compound member groups, even when followed by the generic `9 () XW` marker.
compound_groups: HashSet<NodeId>,
/// Groups of a text object and its look (see [`Self::style_marker`]), which aren't a look.
text_looks: HashSet<NodeId>,
/// The gradient mesh being read (`/Mesh X!` … `/End X!`), and whether it started on a `%_` line.
mesh: Option<(super::mesh::MeshReader, bool)>,
/// The art styles (`%AI9_BeginArtStyles`), by name.
styles: HashMap<String, super::style::ArtStyle>,
/// The swatches (`%AI5_BeginPalette`).
palette: super::palette::Palette,
/// See [`Structure::restyled`].
restyled: Vec<Rect>,
/// CMYK samples converted, by value.
cmyk_cache: HashMap<[u8; 4], [u8; 3]>,
/// A `/ForeignObject`'s `/Data`: its PDF as ASCII85 in comment lines, until the object closes.
foreign_data: Option<Vec<u8>>,
/// The page ended: what follows isn't art.
done: bool,
/// The dictionary closed last was opened on a `%_` line.
obj_hidden: bool,
/// The names of text objects, by their slot's id.
slot_names: HashMap<NodeId, String>,
/// The artboard's size and centre in files without artboards or crop marks.
art_size: Option<(f64, f64)>,
template_center: Option<(f64, f64)>,
/// `%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`]).
large_canvas: Option<f64>,
/// `%AI5_RulerUnits` from the header (Illustrator 7 spec table 3, plus centimeters and pixels).
ruler_units: Option<u8>,
/// This reader reads a pattern's definition (see [`Reader::define_pattern`]).
pattern: Option<PatternHead>,
/// The tile (y down) of each pattern read, by name.
pattern_tiles: HashMap<String, Rect>,
/// The legacy text object being read (`To` … `TO`), and the modal type attributes.
text: Option<legacy_text::LegacyText>,
type_state: legacy_text::TypeState,
}
/// A section comment's marker without its `AI<version>_` prefix and value (`AI14_BeginSymbol` →
/// `BeginSymbol`); DSC comments (`%%BeginProlog`) keep their `%`.
fn marker(c: &str) -> &str {
let word = c.split(|ch: char| ch == ':' || ch.is_whitespace()).next().unwrap_or(c);
word.strip_prefix("AI").map(|r| r.trim_start_matches(|ch: char| ch.is_ascii_digit())).and_then(|r| r.strip_prefix('_')).unwrap_or(word)
}
impl<'a> Reader<'a> {
fn new(data: &'a [u8]) -> Self {
let mut doc = Document::new(1.0, 1.0);
doc.layers.clear();
Self {
lex: Lexer::new(data),
stack: Vec::new(),
marks: Vec::new(),
gs: GState::default(),
frames: Vec::new(),
path: BezPath::new(),
points: 0,
clip_next: false,
doc,
to_doc: None,
cropmarks: None,
artboards: Vec::new(),
variables: VariableLibrary::default(),
gradients: HashMap::new(),
gradient_def: None,
instance: None,
pending_fill: None,
pending_stroke: None,
spots: Vec::new(),
stop_spots: Vec::new(),
raster: None,
place: None,
foreign_data: None,
after_object: false,
nodes: 0,
unsupported: BTreeSet::new(),
unknown: BTreeSet::new(),
hidden_unread: BTreeSet::new(),
bbox: None,
hires: None,
template: None,
cmyk_colors: 0,
rgb_colors: 0,
color_model: None,
warnings: Vec::new(),
drawn_looks: 0,
hidden_ids: HashSet::new(),
blend_roles: HashMap::new(),
blend_record: None,
blend_budget: BLEND_BUDGET,
clip_fills: HashSet::new(),
looks: HashSet::new(),
compound_groups: HashSet::new(),
text_looks: HashSet::new(),
group_ended: false,
layer_state: None,
styles: HashMap::new(),
palette: super::palette::Palette::default(),
restyled: vec![],
mesh: None,
cmyk_cache: HashMap::new(),
done: false,
obj_hidden: false,
slot_names: HashMap::new(),
art_size: None,
template_center: None,
large_canvas: None,
ruler_units: None,
pattern: None,
pattern_tiles: HashMap::new(),
text: None,
type_state: legacy_text::TypeState::default(),
}
}
fn run(&mut self) -> Result<(), String> {
while let Some(t) = self.lex.next_token() {
if self.done {
break;
}
let hidden = t.hidden;
match t.tok {
Tok::Num(v) => self.push(V::Num(v))?,
Tok::Str(s) | Tok::Hex(s) => self.push(V::Str(s))?,
Tok::Name(n) => self.push(V::Name(obj::text(n)))?,
Tok::Word(w) => {
// Operators are ASCII; others are unknown words all the same.
let w = std::str::from_utf8(w).unwrap_or("?");
self.op(w, hidden)?;
}
Tok::Comment(c) => self.comment(c)?,
Tok::Data(d) => self.image(d)?,
Tok::Preview(d) => {
if let Some(p) = self.place.as_mut() {
p.preview = preview_png(d);
}
}
}
}
if self.frames.iter().any(|f| matches!(f.kind, Kind::Layer(_))) {
return Err("it ends inside a layer".into());
}
Ok(())
}
}