Files
vectorcraft/crates/eps/src/import/ai/read.rs
T
dotCooCoo dc179c3671 .ai import: CMYK images in the editing data keep their ink amounts
When a .ai file, or an EPS with editing data, opened from its editing
data, the reader converted each embedded 8-bit CMYK image to RGB and
stored it as a PNG, so the document lost the image's ink amounts. When
the same .ai opens from its PDF part, the PDF importer keeps them in a
CMYK TIFF.

The reader now stores those samples with ImageBlob::cmyk_tiff, as the
PDF importer does (crates/pdf/src/import_image.rs). An alpha channel
that hides part of the image becomes the image's opacity mask, a
grayscale image over it, as in PDF import. Grayscale, RGB and 1-bit
images still open as RGBA PNGs.

Docs: docs/file-format-parity.md, docs/target-app-parity.md.
2026-10-16 03:36:20 +07:00

2254 lines
97 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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.
//! - 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`].
//!
//! Sections the reader doesn't need (brushes, symbols, styles, swatches, 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,
};
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;
#[path = "legacy_text.rs"]
mod legacy_text;
/// Deepest nesting of layers, groups and dictionaries.
const MAX_DEPTH: usize = 256;
/// Most operands on the stack.
const MAX_STACK: usize = 1 << 20;
/// Most objects read.
const MAX_NODES: usize = 4_000_000;
/// Most points in one path.
const MAX_POINTS: usize = 4_000_000;
/// Largest image read (pixels).
const MAX_PIXELS: u64 = 1 << 25;
/// Most hex digits of an image's samples read from comment lines (CMYK and alpha at the largest size).
const MAX_HEX: usize = (MAX_PIXELS as usize) * 10;
/// Largest side of the document and its artboards (points).
const MAX_SIDE: f64 = 1e6;
/// Art space (y up) → a pattern's space in the document's terms (y down).
const FLIP: Affine = Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, 0.0]);
/// Most patterns read (others can't fill art).
const MAX_PATTERNS: usize = 4096;
/// 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>,
pub warnings: Vec<String>,
}
/// The name of the empty group that stands for a text object, followed by the number of its
/// story in the text document when the file says.
pub(crate) const TEXT_SLOT: &str = "\u{0}text";
/// Is `name` that of a text slot, and which story is it of?
pub(crate) fn slot_of(name: Option<&str>) -> Option<Option<u32>> {
name?.strip_prefix(TEXT_SLOT).map(|rest| rest.split(':').next().and_then(|s| s.parse().ok()))
}
/// Which of its story's frames a text slot named `name` is (0 when it doesn't say).
pub(crate) fn slot_frame(name: Option<&str>) -> usize {
name.and_then(|n| n.strip_prefix(TEXT_SLOT)).and_then(|rest| rest.split_once(':')).and_then(|(_, f)| f.parse().ok()).unwrap_or(0)
}
/// 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()
}
/// The artboards (names, and rects in art space) of the editing data `data` that [`read`] can't
/// read as a whole, as far as it gets: where to put the pages of its PDF part instead, and what to
/// call them (#1068).
pub fn artboards(data: &[u8]) -> Vec<(String, Rect)> {
let found = artboards_read(data);
if !found.is_empty() {
return found;
}
// Reading can stop before the artboards (art kept in the document's data comes first): read
// their array on its own.
artboard_array(data).map_or(found, |array| {
let mut alone = b"%_/Document :\n%_/Dictionary :\n".to_vec();
alone.extend_from_slice(array);
alone.extend_from_slice(b"\n%_; /NotRecorded ,\n%_;\n");
artboards_read(&alone)
})
}
/// The `ArtboardArray` entry of the editing data `data`, from its `/Array :` to its key: found
/// from the key back, counting the dictionaries its artboards open and close.
fn artboard_array(data: &[u8]) -> Option<&[u8]> {
const KEY: &[u8] = b"%_; (ArtboardArray) ,";
let end = data.windows(KEY.len()).rposition(|w| w == KEY)? + KEY.len();
let head = data.get(..end)?;
let mut depth = 0usize;
let mut line_end = head.len();
loop {
let line_start = head.get(..line_end)?.iter().rposition(|&b| b == b'\n' || b == b'\r').map_or(0, |at| at + 1);
if let Some(text) = head.get(line_start..line_end)?.strip_prefix(b"%_") {
if text.starts_with(b";") || text.starts_with(b"/Def ;") {
depth += 1;
} else if text.starts_with(b"/") && text.windows(2).any(|w| w == b" :") {
depth = depth.checked_sub(1)?;
if depth == 0 {
return head.get(line_start..);
}
}
}
line_end = line_start.checked_sub(1)?;
}
}
/// [`artboards`] as far as reading `data` from its start gets: those read before an error, and
/// those of a `/Document` still open when it stopped (its art can be what stopped it).
fn artboards_read(data: &[u8]) -> Vec<(String, Rect)> {
let mut r = Reader::new(data);
let _ = r.run();
let open: Vec<Obj> = r.frames.iter().filter_map(|f| if let Kind::Obj(o) = &f.kind { Some((**o).clone()) } else { None }).collect();
for o in &open {
if r.artboards.is_empty() {
r.document(o);
}
}
r.artboards
}
#[derive(Clone, Debug)]
struct GState {
fill: Paint,
stroke: Paint,
width: f64,
cap: LineCap,
join: LineJoin,
miter: f64,
dash: Option<Dash>,
rule: FillRule,
opacity: f32,
blend: BlendMode,
isolate: bool,
knockout: Knockout,
knockout_shape: bool,
overprint_fill: bool,
overprint_stroke: bool,
locked: bool,
hidden: bool,
}
impl Default for GState {
fn default() -> Self {
let black = Paint::solid(vectorcraft_color::Color::BLACK);
Self {
fill: black.clone(),
stroke: black,
width: 1.0,
cap: LineCap::Butt,
join: LineJoin::Miter,
miter: 10.0,
dash: None,
rule: FillRule::NonZero,
opacity: 1.0,
blend: BlendMode::Normal,
isolate: false,
knockout: Knockout::Off,
knockout_shape: false,
overprint_fill: false,
overprint_stroke: false,
locked: false,
hidden: false,
}
}
}
/// The blend modes by their number in `Xy`.
const BLENDS: [BlendMode; 16] = [
BlendMode::Normal,
BlendMode::Multiply,
BlendMode::Screen,
BlendMode::Overlay,
BlendMode::SoftLight,
BlendMode::HardLight,
BlendMode::ColorDodge,
BlendMode::ColorBurn,
BlendMode::Darken,
BlendMode::Lighten,
BlendMode::Difference,
BlendMode::Exclusion,
BlendMode::Hue,
BlendMode::Saturation,
BlendMode::Color,
BlendMode::Luminosity,
];
/// The name effects take for blend mode `n` of `Xy` (and of effects' `blnd`).
pub(super) fn blend_name(n: usize) -> String {
format!("{:?}", BLENDS.get(n).copied().unwrap_or(BlendMode::Normal))
}
#[derive(Debug, Default)]
struct LayerAttrs {
name: String,
visible: bool,
preview: bool,
locked: bool,
printable: bool,
dim: Option<u8>,
color: Option<LayerColor>,
}
#[derive(Debug)]
enum Kind {
Layer(Box<LayerAttrs>),
Group,
Compound,
Clip,
Obj(Box<Obj>),
}
#[derive(Debug)]
struct Frame {
kind: Kind,
children: Vec<Node>,
/// Opened on a `%_` line.
hidden: bool,
/// [`Reader::after_object`] when it opened.
after: bool,
/// The state when it opened (a container's lock and visibility; a dictionary's state to go
/// back to).
gs: GState,
/// The operand stack's height when it opened.
base: usize,
/// The clipping paths' indexes among the children.
clips: Vec<usize>,
}
/// What a reader of a pattern's definition has of its head, `(name) llx lly urx ury`: the operands
/// before its first operator.
#[derive(Debug, Default)]
struct PatternHead {
taken: bool,
name: String,
/// The tile in the pattern's space (y up).
tile: Option<Rect>,
}
/// An image's operands, from `XN` and before `XI`.
#[derive(Debug, Default)]
struct Raster {
space: Option<String>,
/// `XI`'s operands when its samples follow as hex in comment lines (`%FF00…`) up to
/// `%AI5_EndRaster`, the form the version 7 specification documents (§8).
operands: Option<Vec<V>>,
/// Those hex digits.
hex: Vec<u8>,
}
/// A placed file's operands, from `` ` `` and its preview, until `~`.
#[derive(Debug, Default)]
struct Place {
/// The canvas, where the file's pixels are → art space.
matrix: Option<Affine>,
/// The file's box (`llx lly urx ury`): its size is the file's, in pixels.
bounds: Option<Rect>,
path: String,
/// The preview as a PNG, and its size in pixels.
preview: Option<(Vec<u8>, u32, u32)>,
}
/// What the open result says of placed files: they show the preview the file keeps of them.
const LINKED_PREVIEWS: &str =
"linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel";
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)>,
gradients: HashMap<String, Gradient>,
gradient_def: Option<GradientDef>,
instance: Option<Instance>,
pending_fill: Option<Paint>,
pending_stroke: Option<Paint>,
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>,
/// 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>,
/// 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>,
/// 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>,
/// 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,
}
/// Operators that change nothing the reader keeps.
const IGNORED: &[&str] = &[
"Ae",
"AE",
"Ap",
"As",
"Xd",
"Xr",
"XG",
"Xh",
"XH",
"XF",
"D",
"X=",
"X+",
"Bc",
"XP",
"Np",
// Flatness (`i`): how finely curves print.
"i",
// Path resolution (`Ar`, §12): how finely curves print.
"Ar",
"TZ",
"Xs",
"Xt",
// A brushed path's width at each anchor (`(…) Xt n XD`), on its hidden source path.
"XD",
"Xi",
"XI",
"`",
"LB2",
"Lc",
"Xv",
"XV",
"Xq",
"XQ",
"Xg",
"Xn",
"Bn",
"gsave",
"grestore",
"showpage",
"annotatepage",
];
/// Sections skipped whole, by their markers without the `AI<version>_` prefix: begin → end.
const SKIPPED: &[(&str, &str)] = &[
("BeginPattern", "EndPattern"),
("BeginBrushPattern", "EndBrushPattern"),
("BeginSVGFilter", "EndSVGFilter"),
("BeginSymbol", "EndSymbol"),
("BeginPluginObject", "EndPluginObject"),
("BeginArtStyles", "EndArtStyles"),
("BeginArtStyleList", "EndArtStyleList"),
("BeginPalette", "EndPalette"),
("BeginSymbolList", "EndSymbolList"),
("BeginTextDocument", "EndTextDocument"),
("BeginEncoding", "EndEncoding"),
("Alternate_Content", "End_Versioned_Content"),
];
/// The note for a `/ForeignObject` (non-native art, the content of a placed PDF, which the editor
/// shows but doesn't edit) whose PDF can't be read.
pub const NON_NATIVE_ART_UNREAD: &str = "non-native art (a placed PDF's content) that VectorCraft can't read is left out";
/// The name of the group a `/ForeignObject`'s art comes in as.
pub const NON_NATIVE_ART: &str = "Non-native art";
/// 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(),
gradients: HashMap::new(),
gradient_def: None,
instance: None,
pending_fill: None,
pending_stroke: None,
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(),
clip_fills: HashSet::new(),
looks: HashSet::new(),
group_ended: false,
layer_state: None,
styles: HashMap::new(),
mesh: None,
cmyk_cache: HashMap::new(),
done: false,
obj_hidden: false,
slot_names: HashMap::new(),
art_size: None,
template_center: None,
large_canvas: 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(())
}
fn push(&mut self, v: V) -> Result<(), String> {
if self.stack.len() >= MAX_STACK {
return Err("it has too many values in a row".into());
}
self.stack.push(v);
Ok(())
}
/// The operands of the operator being run (above the innermost dictionary's), taken off.
fn operands(&mut self) -> Vec<V> {
let base = self.frames.last().map_or(0, |f| f.base).min(self.stack.len());
self.stack.split_off(base)
}
fn nums(vals: &[V]) -> Vec<f64> {
vals.iter().filter_map(V::num).collect()
}
// ---- comments --------------------------------------------------------------------------
fn comment(&mut self, c: &[u8]) -> Result<(), String> {
let line = String::from_utf8_lossy(c);
let line = line.trim_end();
if let Some(r) = self.raster.as_mut()
&& r.operands.is_some()
&& !line.is_empty()
&& line.bytes().all(|b| b.is_ascii_hexdigit())
{
if r.hex.len() < MAX_HEX {
r.hex.extend_from_slice(line.as_bytes());
}
return Ok(());
}
let m = marker(line);
// A pattern's definition (one inside another's isn't read).
if m == "BeginPattern" && self.pattern.is_none() {
let section = self.section("EndPattern")?;
self.define_pattern(section);
return Ok(());
}
if m == "BeginArtStyles" {
let section = self.section("EndArtStyles")?;
self.styles = super::style::art_styles(section);
return Ok(());
}
if m == "BeginEncoding" {
self.type_state.encoding_comment(line);
}
if let Some((_, end)) = SKIPPED.iter().find(|(b, _)| *b == m) {
return self.skip_to(end);
}
match m {
"%BeginProlog" => return self.skip_to("%EndProlog"),
"%BeginResource" => return self.skip_to("%EndResource"),
// The art ends with the page.
"%PageTrailer" | "%Trailer" => {
self.done = true;
return Ok(());
}
_ => {}
}
if line.starts_with("AI5_BeginLayer") {
return self.open(Kind::Layer(Box::default()), false);
}
if line.starts_with("AI5_BeginRaster") {
self.raster = Some(Raster::default());
} else if line.starts_with("AI5_EndRaster") {
if let Some(Raster { operands: Some(vals), hex, .. }) = self.raster.take() {
return self.image_from(vals, &hex);
}
} else if line.starts_with("AI5_BeginPlace") {
self.place = Some(Place::default());
} else if line.starts_with("AI5_EndPlace") {
// One that `~` didn't end isn't read.
if self.place.take().is_some() {
self.unreadable("placed files");
}
} else if self.frames.is_empty()
&& let Some(v) = line.strip_prefix("%HiResBoundingBox:")
{
self.hires = four(v).or(self.hires);
} else if self.frames.is_empty()
&& let Some(v) = line.strip_prefix("%BoundingBox:")
{
self.bbox = four(v).or(self.bbox);
} else if let Some(v) = line.strip_prefix("AI3_Cropmarks:") {
let n: Vec<f64> = v.split_whitespace().filter_map(|w| w.parse().ok()).collect();
if let [x0, y0, x1, y1] = n.as_slice() {
let r = Rect::new(*x0, *y0, *x1, *y1);
if sane(r) {
self.cropmarks = Some(r);
}
}
} 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:") {
self.template_center = two(v);
self.template = four(v).or(self.template);
} else if let Some(v) = line.strip_prefix("AI24_LargeCanvasScale:") {
// Illustrator's Large Canvas writes the editing data (art and boxes) at 1/N scale, N
// being this; the printed page applies the same factor, so the art reaches the page's
// size once it is applied to the art space ([`Self::ensure_space`]). A value outside
// the range Illustrator writes is left alone.
if let (Some(scale), Ok(n)) = (&mut self.large_canvas, v.trim().parse::<f64>())
&& (1.0..=100.0).contains(&n)
{
*scale = n;
}
} else if let Some(v) = line.strip_prefix("AI9_ColorModel:") {
self.color_model = Some(v.trim() == "2");
}
Ok(())
}
/// Skip to the comment with marker `end` (sections of the same kind nested in it count, and
/// versioned content in an alternate).
fn skip_to(&mut self, end: &str) -> Result<(), String> {
self.section(end).map(|_| ())
}
/// [`Self::skip_to`] → what was skipped, up to the comment that ends it.
fn section(&mut self, end: &str) -> Result<&'a [u8], String> {
let begin = SKIPPED.iter().find(|(_, e)| *e == end).map_or("", |(b, _)| *b);
let begin = if end == "End_Versioned_Content" { "Begin_Content_if_version_gt" } else { begin };
let start = self.lex.offset();
let mut depth = 0usize;
loop {
let before = self.lex.offset();
let Some(t) = self.lex.next_token() else { break };
if let Tok::Comment(c) = t.tok {
let line = String::from_utf8_lossy(c);
let m = marker(&line);
if !begin.is_empty() && m == begin {
depth += 1;
} else if m == end {
if depth == 0 {
return Ok(self.lex.slice(start, before));
}
depth -= 1;
}
}
}
Err(format!("its section ending with `%{end}` doesn't end"))
}
/// A pattern's definition (`section`: what follows `%AI3_BeginPattern`), read by a reader of its
/// own into a pattern swatch, its art in the pattern's space (y down). A definition it can't
/// read whole (an operator it doesn't know, type, unbalanced groups, no tile) isn't kept: art
/// filled with it can't be read.
fn define_pattern(&mut self, section: &[u8]) {
if self.pattern_tiles.len() >= MAX_PATTERNS {
return;
}
let mut sub = Reader::new(section);
sub.pattern = Some(PatternHead::default());
// The gradients defined so far, lent (and those it defines kept).
sub.gradients = std::mem::take(&mut self.gradients);
sub.nodes = self.nodes;
sub.template = self.template;
let read = sub.open(Kind::Group, false).and_then(|()| sub.run());
self.nodes = sub.nodes;
self.gradients = std::mem::take(&mut sub.gradients);
let whole = read.is_ok() && sub.unsupported.is_empty() && sub.unknown.is_empty() && sub.frames.len() == 1;
let (Some(head), Some(frame)) = (sub.pattern.take(), sub.frames.pop()) else { return };
let Some(tile) = head.tile.filter(|t| whole && sane(*t) && t.width() > 0.0 && t.height() > 0.0 && !frame.children.is_empty()) else {
return;
};
let art: Vec<Arc<Node>> = frame.children.iter().map(|n| Arc::new(self.doc.reid(n))).collect();
let mut def = PatternDef::new(&head.name, art);
def.tile = FLIP.transform_rect_bbox(tile);
if self.pattern_tiles.insert(def.name.clone(), def.tile).is_some() {
self.doc.patterns.retain(|p| p.name != def.name);
}
self.doc.patterns.push(def);
for (key, blob) in sub.doc.images {
self.doc.images.entry(key).or_insert(blob);
}
self.spots.extend(sub.spots);
self.cmyk_colors += sub.cmyk_colors;
self.rgb_colors += sub.rgb_colors;
for w in &sub.warnings {
self.warn(w);
}
}
/// In a pattern's definition, the operands of its first operator start with its head,
/// `(name) llx lly urx ury`: taken off them.
fn take_head(&mut self, mut vals: Vec<V>) -> Vec<V> {
let Some(head) = self.pattern.as_mut().filter(|h| !h.taken) else { return vals };
head.taken = true;
let Some(at) = vals.iter().position(|v| matches!(v, V::Str(_))) else { return vals };
let tile = Self::nums(vals.get(at + 1..at + 5).unwrap_or_default());
let ([x0, y0, x1, y1], Some(name)) = (tile.as_slice(), vals.get(at).and_then(V::text)) else { return vals };
head.name = name;
head.tile = Some(Rect::new(*x0, *y0, *x1, *y1));
vals.split_off(at + 5)
}
// ---- frames ----------------------------------------------------------------------------
fn open(&mut self, kind: Kind, hidden: bool) -> Result<(), String> {
if self.frames.len() >= MAX_DEPTH {
return Err("its objects are nested too deeply".into());
}
if matches!(kind, Kind::Layer(_)) {
self.ensure_space();
}
let base = if matches!(kind, Kind::Obj(_)) { self.stack.len() } else { self.frames.last().map_or(0, |f| f.base) };
self.frames.push(Frame { kind, children: Vec::new(), hidden, after: self.after_object, gs: self.gs.clone(), base, clips: Vec::new() });
self.after_object = false;
Ok(())
}
/// Close the innermost frame, which must be of the kind `is` accepts.
fn close(&mut self, what: &str, is: impl Fn(&Kind) -> bool) -> Result<Frame, String> {
match self.frames.last() {
Some(f) if is(&f.kind) => self.frames.pop().ok_or_else(|| format!("`{what}` closes nothing")),
_ => Err(format!("`{what}` closes something it didn't open")),
}
}
/// Add a node to the innermost container.
fn add(&mut self, node: Node, hidden: bool) -> Result<(), String> {
self.nodes += 1;
if self.nodes > MAX_NODES {
return Err(format!("it has more than {MAX_NODES} objects"));
}
if hidden {
self.hidden_ids.insert(node.id);
}
match self.frames.last_mut() {
Some(f) => f.children.push(node),
None => return Err("it has art outside its layers".into()),
}
self.after_object = true;
Ok(())
}
/// The last object read, while nothing else started.
fn last_object(&mut self) -> Option<&mut Node> {
if !self.after_object {
return None;
}
self.frames.last_mut()?.children.last_mut()
}
fn new_node(&mut self, kind: NodeKind, gs: &GState) -> Node {
let mut n = Node::new(self.doc.alloc_id(), kind);
n.visible = !gs.hidden;
n.locked = gs.locked;
n
}
fn end_container(&mut self, what: &str, hidden: bool) -> Result<(), String> {
let f = match what {
"U" => self.close(what, |k| matches!(k, Kind::Group))?,
"*U" => self.close(what, |k| matches!(k, Kind::Compound))?,
_ => self.close(what, |k| matches!(k, Kind::Clip))?,
};
let Frame { kind, children: kids, gs, clips, hidden: opened_hidden, .. } = f;
// A group holding only clipping paths (`q … u <path> W n U … Q`) doesn't clip on its own: `u`/`U` don't save
// the graphics state as `q`/`Q` do, so its paths clip the clipping group around it, as the file's PDF part draws it.
if matches!(kind, Kind::Group)
&& !clips.is_empty()
&& clips.len() == kids.len()
&& matches!(self.frames.last(), Some(Frame { kind: Kind::Clip, .. }))
{
for kid in kids {
if let Some(parent) = self.frames.last_mut() {
parent.clips.push(parent.children.len());
}
self.add(kid, opened_hidden || hidden)?;
}
return Ok(());
}
// A compound path of clipping paths clips its clipping group.
let clipping = matches!(kind, Kind::Compound) && !clips.is_empty();
self.group_ended = !matches!(kind, Kind::Compound);
let mut painted = None;
let node = match kind {
// A compound path holding a group (its drawn look) is that look: its paths paint apart.
Kind::Compound if kids.iter().any(|k| self.looks.contains(&k.id)) => {
let (children, clip) = self.clip_first(kids, &clips);
self.new_node(NodeKind::Group { children, clip }, &gs)
}
Kind::Compound => {
// A group in a compound path (`*u u … U *U`) only holds its paths. Clipping subpaths'
// painted copies make a compound of their own, under what it clips.
let (fills, kids): (Vec<Node>, Vec<Node>) = ungrouped(kids).into_iter().partition(|n| self.clip_fills.contains(&n.id));
painted = self.compound(fills, &gs);
let Some(n) = self.compound(kids, &gs) else { return Ok(()) };
n
}
// A group with a clipping path (`q` … `Q`, or a `W` path in a group) is a clipping group.
_ => {
let (children, clip) = self.clip_first(kids, &clips);
self.new_node(NodeKind::Group { children, clip }, &gs)
}
};
if clipping && let Some(f) = self.frames.last_mut() {
f.clips.push(f.children.len());
}
self.add(node, opened_hidden || hidden)?;
if let Some(p) = painted {
self.clip_fills.insert(p.id);
self.add(p, opened_hidden || hidden)?;
}
Ok(())
}
/// The compound path of `kids`, painted and made transparent as its subpaths say.
fn compound(&mut self, mut kids: Vec<Node>, gs: &GState) -> Option<Node> {
// The compound path's paint is its last path's (a gradient's instance wraps only
// that one); the last painted one, should the last have none.
let shown = kids.iter().rev().find(|k| !k.appearance.items.is_empty()).or(kids.last())?;
let (appearance, opacity, blend, rule) = (shown.appearance.clone(), shown.opacity, shown.blend, path_rule(shown));
for k in &mut kids {
k.opacity = 1.0;
k.blend = BlendMode::Normal;
k.visible = true;
k.locked = false;
}
let mut n = self.new_node(NodeKind::Compound { children: kids.into_iter().map(Arc::new).collect(), rule }, gs);
n.appearance = appearance;
n.opacity = opacity;
n.blend = blend;
Some(n)
}
/// `kids` with the clipping paths among them (indexes `clips`) first, several as one compound
/// path → the children and whether they clip.
fn clip_first(&mut self, kids: Vec<Node>, clips: &[usize]) -> (Vec<Arc<Node>>, bool) {
let (mut clip, rest): (Vec<Indexed>, Vec<Indexed>) = kids.into_iter().enumerate().partition(|(i, _)| clips.contains(i));
let clip_node = match clip.len() {
0 => None,
1 => clip.pop().map(|(_, n)| n),
_ => {
let rule = clip.first().map_or(FillRule::NonZero, |(_, n)| path_rule(n));
let children = clip.into_iter().map(|(_, n)| Arc::new(n)).collect();
Some(Node::new(self.doc.alloc_id(), NodeKind::Compound { children, rule }))
}
};
let clips = clip_node.is_some();
// A clipping path's own fill paints under the art it clips.
let (fills, rest): (Vec<Indexed>, Vec<Indexed>) = rest.into_iter().partition(|(_, n)| self.clip_fills.contains(&n.id));
let children = clip_node.into_iter().chain(fills.into_iter().chain(rest).map(|(_, n)| n)).map(Arc::new).collect();
(children, clips)
}
fn end_layer(&mut self) -> Result<(), String> {
let f = self.close("LB", |k| matches!(k, Kind::Layer(_)))?;
let Kind::Layer(a) = f.kind else { return Ok(()) };
let (children, clip) = self.clip_first(f.children, &f.clips);
let color = a.color.unwrap_or(LayerColor::Preset(0));
let mut layer = Node::new(
self.doc.alloc_id(),
NodeKind::Layer { color, template: false, printable: a.printable, children, clip, preview: a.preview, dim_images: a.dim },
);
layer.name = Some(a.name.clone());
layer.visible = a.visible;
layer.locked = a.locked;
// The layer's own opacity, blend and knockout, written after its art (#2003).
if let Some((opacity, blend, isolate, knockout)) = self.layer_state.take() {
(layer.opacity, layer.blend, layer.isolate, layer.knockout) = (opacity, blend, isolate, knockout);
}
match self.frames.last_mut() {
Some(parent) => {
parent.children.push(layer);
self.after_object = true;
}
None => {
self.doc.layers.push(Arc::new(layer));
self.after_object = false;
}
}
Ok(())
}
// ---- operators -------------------------------------------------------------------------
fn op(&mut self, w: &str, hidden: bool) -> Result<(), String> {
match w {
"[" => {
self.marks.push(self.stack.len());
return self.push(V::Mark);
}
"]" => {
// The innermost `[` still on the stack (the stack is cut back by operators).
let mut at = self.stack.len();
while let Some(m) = self.marks.pop() {
if matches!(self.stack.get(m), Some(V::Mark)) {
at = m;
break;
}
}
let items: Vec<V> = self.stack.split_off(at).into_iter().skip(1).collect();
return self.push(V::Arr(items));
}
":" => return self.begin_obj(hidden),
"," => {
if let Some(Frame { kind: Kind::Obj(o), base, .. }) = self.frames.last_mut() {
obj::add_entry(o, &mut self.stack, *base);
// Binary data in ASCII85 follows, up to its `~>`: a `/Binary` dictionary's
// (`/ASCII85Decode ,`) or a non-native art object's (`/ForeignObject`'s
// `/Data ,`, the PDF it keeps), in comment lines. ASCII85 has `_`, so a line
// of it may start with `%_` and read as hidden tokens: a `(` there would open
// a string that swallows the rest of the layer.
let key = o.entries.last().and_then(|(k, _)| k.as_deref());
if o.ty == "Binary" && key == Some("ASCII85Decode") {
self.lex.skip_past(b"~>");
} else if o.ty == "ForeignObject" && key == Some("Data") {
self.foreign_data = Some(self.lex.skip_past(b"~>").to_vec());
}
}
return Ok(());
}
";" => return self.end_obj(),
_ => {}
}
// What follows an object (its name, transparency and style) ends with anything else. The
// state block of a layer whose art ended (`Xy`, `Xd`, `XW`, then `LB`) is read at the `LB`.
if !matches!(w, "Xy" | "Xd" | "XW" | "BB" | "LB") {
self.after_object = false;
self.layer_state = None;
}
let group_ended = std::mem::take(&mut self.group_ended);
let vals = self.operands();
let vals = self.take_head(vals);
if let Some(d) = self.gradient_def.as_mut() {
match w {
"Bs" => d.stop(&vals, &mut self.spots),
"BD" => {
if let Some((name, g)) = self.gradient_def.take().and_then(GradientDef::finish) {
self.gradients.insert(name, g);
}
}
_ => {}
}
return Ok(());
}
if w == "X!" || self.mesh.is_some() {
let name = vals.iter().rev().find_map(|v| if let V::Name(n) = v { Some(n.as_str()) } else { None });
match (w, name) {
("X!", Some("Mesh")) => {
self.end_path_quietly();
self.mesh = Some((super::mesh::MeshReader::default(), hidden));
return Ok(());
}
("X!", Some("End")) if self.mesh.is_some() => return self.end_mesh(),
("X#", _) => {
let to_doc = self.space();
if let Some((m, _)) = self.mesh.as_mut() {
m.entry(&vals, to_doc);
}
return Ok(());
}
_ if self.mesh.is_some() => return Ok(()),
_ => {}
}
}
match w {
// Containers.
"u" => {
self.end_path_quietly();
self.open(Kind::Group, hidden)?
}
"*u" => {
self.end_path_quietly();
self.open(Kind::Compound, hidden)?
}
"q" => {
self.end_path_quietly();
self.open(Kind::Clip, hidden)?
}
"U" | "*U" | "Q" => self.end_container(w, hidden)?,
"Lb" => self.layer_attrs(&vals),
"Ln" => {
if let Some(Frame { kind: Kind::Layer(a), .. }) = self.frames.last_mut() {
a.name = vals.iter().rev().find_map(V::text).unwrap_or_default();
}
}
"LB" => self.end_layer()?,
// Paths.
"m" | "l" | "L" | "c" | "C" | "v" | "V" | "y" | "Y" => self.path_op(w, &vals)?,
"h" => close(&mut self.path),
// Ends a path that stays open.
"H" => {}
"W" => self.clip_next = true,
// A text object's frame or path.
"N" | "n" | "F" | "f" | "S" | "s" | "B" | "b" if self.in_text_path() => self.text_path(),
"N" | "n" | "F" | "f" | "S" | "s" | "B" | "b" => self.paint(w, hidden, false)?,
"*" => {
let op = vals.iter().rev().find_map(V::text).unwrap_or_else(|| "N".into());
self.paint(&op, hidden, true)?;
}
// Colours.
"g" | "G" | "k" | "K" | "x" | "X" | "Xa" | "XA" | "Xx" | "XX" | "Xk" | "XK" => self.color(w, &vals),
"p" | "P" => self.pattern_paint(w, &vals),
// A pattern's definition ends.
"E" if self.pattern.is_some() => self.done = true,
"O" => self.gs.overprint_fill = Self::nums(&vals).last() == Some(&1.0),
"R" => self.gs.overprint_stroke = Self::nums(&vals).last() == Some(&1.0),
"XR" => self.gs.rule = if Self::nums(&vals).last() == Some(&1.0) { FillRule::EvenOdd } else { FillRule::NonZero },
"w" => {
if let Some(v) = Self::nums(&vals).last().filter(|v| v.is_finite() && **v >= 0.0) {
self.gs.width = *v;
}
}
"J" => {
self.gs.cap = match Self::nums(&vals).last().map(|v| *v as i64) {
Some(1) => LineCap::Round,
Some(2) => LineCap::Square,
_ => LineCap::Butt,
}
}
"j" => {
self.gs.join = match Self::nums(&vals).last().map(|v| *v as i64) {
Some(1) => LineJoin::Round,
Some(2) => LineJoin::Bevel,
_ => LineJoin::Miter,
}
}
"M" => {
if let Some(v) = Self::nums(&vals).last().filter(|v| v.is_finite() && **v >= 1.0) {
self.gs.miter = v.min(500.0);
}
}
"d" => self.dash(&vals),
// Transparency and state.
"Xy" => self.transparency(&vals),
// A group's own state (`Xd`, `6 () XW`) without an `Xy` keeps the current transparency, as
// the file's PDF part draws it. A look's group (`9 () XW`) has no state of its own.
"Xd" if group_ended => {
let gs = self.gs.clone();
if let Some(n) = self.last_object().filter(|n| matches!(n.kind, NodeKind::Group { .. })) {
(n.opacity, n.blend, n.isolate, n.knockout) = (gs.opacity, gs.blend, gs.isolate, gs.knockout);
}
}
"Xw" => self.gs.hidden = Self::nums(&vals).last() == Some(&1.0),
"A" => self.gs.locked = Self::nums(&vals).last() == Some(&1.0),
"XW" => self.style_marker(&vals, hidden),
// Gradients.
"Bd" => self.gradient_def = GradientDef::begin(&vals),
"Bs" | "BD" => {}
"Bb" => self.instance = Some(Instance::begin(&vals)),
"Bg" => {
if let Some(i) = self.instance.as_mut() {
i.bg(&vals);
}
}
"Bm" => {
if let Some(i) = self.instance.as_mut() {
i.bm(&vals);
}
}
"Xm" => {
if let Some(i) = self.instance.as_mut() {
i.xm(&vals);
}
}
"Bh" => {
if let Some(i) = self.instance.as_mut() {
i.bh(&vals);
}
}
"BB" => {
// An instance that closes with no path to paint is an object's own note of its
// gradient, written after it: it paints nothing (not the next path either).
if let Some(i) = self.instance.take()
&& !self.path.elements().is_empty()
{
self.set_instance(&i);
}
// `1 BB` strokes the path the gradient just filled, `2 BB` closes it first (§3.5.3).
if matches!(Self::nums(&vals).last(), Some(f) if *f == 1.0 || *f == 2.0) {
self.stroke_last();
}
}
// Images.
"XN" => {
if let Some(r) = self.raster.as_mut() {
r.space = vals.iter().rev().find_map(V::text);
}
}
// Placed files.
"`" if self.place.is_some() => {
let matrix = vals.iter().find_map(|v| if let V::Arr(a) = v { Some(Self::nums(a)) } else { None });
let path = vals.iter().rev().find_map(V::text).unwrap_or_default();
if let Some(p) = self.place.as_mut() {
p.matrix = matrix.and_then(|m| <[f64; 6]>::try_from(m).ok()).map(Affine::new);
p.bounds =
Self::nums(&vals).get(..4).and_then(|b| <[f64; 4]>::try_from(b).ok()).map(|[x0, y0, x1, y1]| Rect::new(x0, y0, x1, y1));
p.path = path;
}
}
"~" if self.place.is_some() => self.end_place(hidden)?,
// Type in the legacy format.
"To" => self.begin_text(&vals, hidden),
"Tp" => self.begin_text_path(&vals),
"TP" => self.end_text_path(),
"TO" => self.end_text(hidden)?,
"Tx" | "Tj" | "TX" => self.text_body(w, &vals),
"Tr" | "Tf" | "Ts" | "Tz" | "Tt" | "TA" | "Tl" | "Ta" | "Ti" => self.text_attr(w, &vals),
// Attributes the reader doesn't keep (kerning pairs, spacing, hanging punctuation,
// Far-Eastern type) and final-form positions (§15.3).
"Tk" | "TK" | "TC" | "TW" | "Tq" | "Tc" | "Tw" | "Tm" | "Td" | "T*" | "TR" | "T+" | "T-" | "TV" | "Tv" | "Ty" | "TY" | "TG" | "Tg"
| "Tu" | "Xu" => {}
"TE" => self.te(&vals),
// An image whose samples follow in comment lines (read at `%AI5_EndRaster`).
"XI" if self.raster.is_some() => {
if let Some(r) = self.raster.as_mut() {
r.operands = Some(vals);
}
}
_ if IGNORED.contains(&w) => {}
// Before the art, the setup runs procedures of the printing format.
_ if self.frames.is_empty() => {}
_ => {
let op: String = w.chars().take(24).collect();
if !self.shown() {
if self.hidden_unread.len() < 16 {
self.hidden_unread.insert(format!("`{op}`"));
}
} else if self.unknown.len() < 16 {
self.unknown.insert(op);
}
}
}
Ok(())
}
/// `:` after `/Type`: a dictionary opens.
fn begin_obj(&mut self, hidden: bool) -> Result<(), String> {
let ty = match self.stack.pop() {
Some(V::Name(n)) => n,
Some(other) => {
self.stack.push(other);
String::new()
}
None => String::new(),
};
self.open(Kind::Obj(Box::new(Obj { ty, ..Obj::default() })), hidden)?;
// A dictionary's state starts afresh (styles set theirs inside).
self.gs = GState::default();
Ok(())
}
/// `;`: the innermost dictionary closes.
fn end_obj(&mut self) -> Result<(), String> {
if !matches!(self.frames.last(), Some(Frame { kind: Kind::Obj(_), .. })) {
return Ok(());
}
let Some(f) = self.frames.pop() else { return Ok(()) };
let Kind::Obj(mut o) = f.kind else { return Ok(()) };
obj::close(&mut o, &mut self.stack, f.base);
self.obj_hidden = f.hidden;
// Art made inside a dictionary (type's paths) isn't kept.
self.gs = f.gs;
self.after_object = f.after;
if matches!(self.frames.last(), Some(Frame { kind: Kind::Obj(_), .. })) {
return self.push(V::Obj(Rc::new(*o)));
}
match o.ty.as_str() {
"ArtDictionary" => self.art_dictionary(&o),
"Document" => self.document(&o),
"AI11Text" => self.text_slot(&o)?,
"SymbolInstance" => self.unreadable("symbols"),
"ForeignObject" => self.foreign_art(&o)?,
_ => {}
}
Ok(())
}
/// A text object (`/AI11Text`): an empty group standing for it, named after its story.
fn text_slot(&mut self, o: &Obj) -> Result<(), String> {
if self.pattern.is_some() {
self.unreadable("type in a pattern");
}
// `1 /FreeUndo`: the type in the drawn look of a text object that has an appearance. Its
// story is one of another text document (`/AI11UndoFreeTextDocument`), which counts its
// stories on its own; the object itself follows the look, on `%_` lines, and is the type.
if o.nums("FreeUndo").first() == Some(&1.0) {
return Ok(());
}
let index = |key: &str| o.nums(key).first().copied().filter(|v| (0.0..f64::from(u32::MAX)).contains(v)).map(|v| v as u32);
let (story, frame) = (index("StoryIndex"), index("FrameIndex").unwrap_or(0));
let gs = self.gs.clone();
let mut n = self.new_node(NodeKind::Group { children: vec![], clip: false }, &gs);
n.name = Some(story.map_or_else(|| TEXT_SLOT.to_string(), |s| format!("{TEXT_SLOT}{s}:{frame}")));
let hidden = self.obj_hidden;
self.add(n, hidden)
}
/// Non-native art (`/ForeignObject`): the content of a placed PDF, which Illustrator shows but
/// doesn't edit. Its `/Data` is that PDF (ASCII85 in comment lines, kept by the `,` handler),
/// `/Bounds` the art's box in a space measured from `/Origin` with y down, and `/RTransform`
/// maps that space onto the art (both read off files: the box lands on the clipping paths
/// around it). The PDF importer reads the PDF, and its art is fitted to the box, as one
/// group. Without data, or with a PDF that can't be read, nothing is drawn.
fn foreign_art(&mut self, o: &Obj) -> Result<(), String> {
let Some(text) = self.foreign_data.take() else { return Ok(()) };
// One `%` starts each line; the rest, `_` included, is data.
let mut encoded = String::with_capacity(text.len());
for line in text.split(|b| matches!(b, b'\r' | b'\n')) {
encoded.push_str(&String::from_utf8_lossy(line.strip_prefix(b"%").unwrap_or(line)));
}
let Some(pdf) = ps::ascii85_decode(&encoded) else {
self.warn(NON_NATIVE_ART_UNREAD);
return Ok(());
};
let (rt, origin, bounds) = (o.get("RTransform").map(Self::nums), o.get("Origin").map(Self::nums), o.get("Bounds").map(Self::nums));
let (Some([a, b, c, d, tx, ty]), Some([ox, oy]), Some([x0, y0, x1, y1])) = (rt.as_deref(), origin.as_deref(), bounds.as_deref()) else {
self.warn(NON_NATIVE_ART_UNREAD);
return Ok(());
};
let place = Affine::new([*a, *b, *c, *d, *tx, *ty]) * Affine::new([1.0, 0.0, 0.0, -1.0, *ox, *oy]);
let art_box = place.transform_rect_bbox(Rect::new(*x0, *y0, *x1, *y1));
if !sane(art_box) || art_box.width() <= 0.0 || art_box.height() <= 0.0 {
self.warn(NON_NATIVE_ART_UNREAD);
return Ok(());
}
let target = self.space().transform_rect_bbox(art_box);
let opts = vectorcraft_pdf::ImportOptions { layers: false, ..Default::default() };
let read = match vectorcraft_pdf::import_with_report(&pdf, &opts) {
Ok(r) => r,
Err(e) => {
self.warn(&format!("{NON_NATIVE_ART_UNREAD} ({e})"));
return Ok(());
}
};
let mut art: Vec<Node> = read.document.layers.iter().filter_map(|l| l.children()).flatten().map(|n| n.as_ref().clone()).collect();
let Some(bbox) = art.iter().fold(None, |acc, n| vectorcraft_geom::union_opt(acc, n.geometric_bounds())) else { return Ok(()) };
if bbox.width() <= 0.0 || bbox.height() <= 0.0 {
return Ok(());
}
// The art's own box onto the object's box (its measure of the same art). The PDF importer
// hands the art back as the page shows it, and it keeps that orientation unless its
// placement mirrors it (a negative determinant): the usual `/RTransform` `1 0 0 -1 …` with
// the origin's flip doesn't, and the art then shows as the file's own PDF part draws it.
let fit = if place.determinant() < 0.0 {
Affine::translate((target.x0, target.y1)) * Affine::scale_non_uniform(target.width() / bbox.width(), -target.height() / bbox.height())
} else {
Affine::translate((target.x0, target.y0)) * Affine::scale_non_uniform(target.width() / bbox.width(), target.height() / bbox.height())
} * Affine::translate((-bbox.x0, -bbox.y0));
for n in &mut art {
n.transform(fit, true);
// The PDF's swatches stay with it: its colours keep their values, unlinked.
self.reid(n);
}
for (key, blob) in read.document.images {
self.doc.images.entry(key).or_insert(blob);
}
let gs = self.gs.clone();
let mut group = self.new_node(NodeKind::Group { children: art.into_iter().map(Arc::new).collect(), clip: false }, &gs);
group.name = Some(NON_NATIVE_ART.into());
self.add(group, self.obj_hidden)
}
/// Give `n` and its descendants ids of this document, and unlink their swatches.
fn reid(&mut self, n: &mut Node) {
n.id = self.doc.alloc_id();
for it in &mut n.appearance.items {
let p = match it {
AppearanceItem::Fill(f) => &mut f.paint,
AppearanceItem::Stroke(s) => &mut s.paint,
};
if let Paint::Gradient(g) = p {
g.swatch = None;
}
p.map_links(&mut |_, link, _| {
*link = None;
true
});
}
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) {
self.reid(Arc::make_mut(&mut m.art));
}
if let Some(children) = n.children_mut() {
for c in children {
self.reid(Arc::make_mut(c));
}
}
}
/// Note something the data has that isn't read: an error on a layer that shows, left out of a
/// layer that doesn't.
fn unreadable(&mut self, what: &str) {
if self.shown() {
self.unsupported.insert(what.into());
} else {
self.hidden_unread.insert(what.into());
}
}
/// Do the layers being read show?
fn shown(&self) -> bool {
self.frames.iter().all(|f| !matches!(&f.kind, Kind::Layer(a) if !a.visible))
}
/// An object's dictionary: its name (`AIArtName`, or the XML id `AI10_ArtUID`), and the
/// opacity mask it may carry.
fn art_dictionary(&mut self, o: &Obj) {
// A mask (`/Mask`, keyed `AI9_Mask`): its art is on `X=` … `X+` lines, and drawing the
// object without it shows what the mask hides, so it must be named (#2004). A mask that
// is disabled changes nothing.
if let Some(mask) = o.obj("AI9_Mask").or_else(|| o.obj("Mask"))
&& mask.nums("Disabled").first().copied() != Some(1.0)
{
self.unreadable("opacity masks");
}
let name = match o.text("AIArtName") {
Some(name) => name,
None => match o.obj("AI10_ArtUID").and_then(Obj::content_text) {
Some(id) => obj::xml_name(&id),
None => return,
},
};
let Some(n) = self.last_object().filter(|n| !matches!(n.kind, NodeKind::Layer { .. }) && !name.is_empty()) else { return };
if slot_of(n.name.as_deref()).is_some() {
let id = n.id;
self.slot_names.insert(id, name);
} else {
n.name = Some(name);
}
}
/// The `/Document` dictionary: the artboards.
fn document(&mut self, o: &Obj) {
let Some(list) = find_entry(o, "ArtboardArray", 0) else { return };
for (i, ab) in list.items().enumerate().take(1000) {
let (p1, p2) = (ab.nums("PositionPoint1"), ab.nums("PositionPoint2"));
let ([x0, y0, ..], [x1, y1, ..]) = (p1.as_slice(), p2.as_slice()) else { continue };
let r = Rect::new(*x0, *y0, *x1, *y1).abs();
if !sane(r) || r.area() <= 0.0 {
continue;
}
let name = ab.text("Name").unwrap_or_else(|| format!("Artboard {}", i + 1));
self.artboards.push((name, r));
}
}
/// `… Lb`: a layer's options. The legacy format's has ten operands, its colour index one place
/// earlier (`visible preview unlocked printing dimmed masks colorIndex r g b`).
fn layer_attrs(&mut self, vals: &[V]) {
let n = Self::nums(vals);
let Some(Frame { kind: Kind::Layer(a), .. }) = self.frames.last_mut() else { return };
let flag = |i: usize, default: bool| n.get(i).map_or(default, |v| *v != 0.0);
a.visible = flag(0, true);
a.preview = flag(1, true);
a.locked = !flag(2, true);
a.printable = flag(3, true);
let dim_pct = n.get(12).copied().filter(|v| v.is_finite()).unwrap_or(50.0).clamp(0.0, 100.0);
a.dim = flag(4, false).then_some(dim_pct.round() as u8);
let at = if n.len() <= 10 { 6 } else { 7 };
let index = n.get(at).copied().unwrap_or(0.0);
let rgb = |i: usize| n.get(at + i).map_or(0, |v| v.clamp(0.0, 255.0) as u8);
a.color = Some(if (0.0..vectorcraft_doc::LAYER_COLORS.len() as f64).contains(&index) && index.fract() == 0.0 {
LayerColor::Preset(index as u8)
} else {
LayerColor::Custom([rgb(1), rgb(2), rgb(3)])
});
}
/// `(name) x y scaleX scaleY angle reflect axis shear shearAxis [matrix] p` (`P`: the stroke's):
/// the pattern swatch `name`, its space taken to art space by the matrix. A pattern this hasn't
/// read can't be drawn.
fn pattern_paint(&mut self, w: &str, vals: &[V]) {
// In the setup it paints nothing (and the art's space isn't fixed yet).
if self.frames.is_empty() {
return;
}
let name = vals.iter().find_map(|v| if let V::Str(_) = v { v.text() } else { None });
let Some(tile) = name.as_deref().and_then(|n| self.pattern_tiles.get(n)).copied() else {
self.unreadable("pattern fills");
return;
};
let matrix = vals.iter().rev().find_map(|v| if let V::Arr(a) = v { paint::affine(&Self::nums(a)) } else { None });
// The tile's art (y down from the tile's top left) → the pattern's space → art space. The
// tile's lower left is the pattern's origin wherever the definition drew it, as the file's
// PDF part tiles it.
let xf = self.space() * matrix.unwrap_or(Affine::IDENTITY) * FLIP * Affine::translate((0.0, -tile.height()));
if xf.determinant().abs() < 1e-12 || !xf.as_coeffs().iter().all(|v| v.is_finite()) {
self.unreadable("pattern fills");
return;
}
let p = Paint::Pattern { pattern: name.unwrap_or_default(), xf };
if w == "p" {
self.gs.fill = p;
} else {
self.gs.stroke = p;
}
}
fn color(&mut self, w: &str, vals: &[V]) {
let Some((p, named)) = paint::color_op(w, vals) else { return };
match p.color() {
Some(vectorcraft_color::Color::Rgb { .. }) => self.rgb_colors += 1,
Some(vectorcraft_color::Color::Cmyk { .. }) => self.cmyk_colors += 1,
_ => {}
}
if let Some(n) = named {
self.spots.push(n);
}
// A colour set after a gradient instance that painted nothing (`Bb` … `0 BB` with no
// path) is the next object's paint, not the gradient.
if w.ends_with(|c: char| c.is_ascii_lowercase()) {
self.gs.fill = p;
self.pending_fill = None;
} else {
self.gs.stroke = p;
self.pending_stroke = None;
}
}
/// `[dashes] phase d`.
fn dash(&mut self, vals: &[V]) {
let pattern: Vec<f64> = vals.iter().rev().find_map(|v| if let V::Arr(a) = v { Some(Self::nums(a)) } else { None }).unwrap_or_default();
let offset = Self::nums(vals).last().copied().unwrap_or(0.0);
self.gs.dash = (!pattern.is_empty() && pattern.len() <= 64 && pattern.iter().all(|v| v.is_finite() && *v >= 0.0)).then(|| Dash {
pattern,
offset: if offset.is_finite() { offset } else { 0.0 },
align_corners: false,
});
}
/// `mode opacity isolate knockout shape Xy`; after a group's end, the group's too.
fn transparency(&mut self, vals: &[V]) {
let n = Self::nums(vals);
let [mode, opacity, rest @ ..] = n.as_slice() else { return };
let gs = &mut self.gs;
gs.blend = BLENDS.get(*mode as usize).copied().filter(|_| *mode >= 0.0).unwrap_or(BlendMode::Normal);
gs.opacity = if opacity.is_finite() { opacity.clamp(0.0, 1.0) as f32 } else { 1.0 };
gs.isolate = rest.first().is_some_and(|v| *v != 0.0);
gs.knockout = match rest.get(1).map(|v| *v as i64) {
Some(1) => Knockout::On,
Some(2) => Knockout::Neutral,
_ => Knockout::Off,
};
gs.knockout_shape = rest.get(2).is_some_and(|v| *v != 0.0);
let gs = self.gs.clone();
if self.last_object().is_some_and(|n| matches!(n.kind, NodeKind::Group { .. } | NodeKind::Compound { .. })) {
if let Some(n) = self.last_object() {
n.opacity = gs.opacity;
n.blend = gs.blend;
n.isolate = gs.isolate;
n.knockout = gs.knockout;
}
self.layer_state = None;
} else {
// After a layer's art and before its `LB`: the layer's own state (#2003). Kept until
// an operator that isn't part of the state block (or the `LB` that takes it) says the
// art moves on, so a path's state before its paint isn't read as its layer's.
self.layer_state =
matches!(self.frames.last(), Some(Frame { kind: Kind::Layer(_), .. })).then_some((gs.opacity, gs.blend, gs.isolate, gs.knockout));
}
}
/// `n (style) XW` after an object: `1 (style)` after the object written on `%_` lines, its
/// drawn look before it (all on `%_` lines inside another such object). Other markers end what
/// follows an object.
fn style_marker(&mut self, vals: &[V], hidden: bool) {
let code = Self::nums(vals).last().copied().unwrap_or(0.0);
let style = vals.iter().rev().find_map(V::text).unwrap_or_default();
let after = std::mem::take(&mut self.after_object);
if code == 9.0
&& let Some(look) = self.frames.last().and_then(|f| f.children.last()).filter(|n| matches!(n.kind, NodeKind::Group { .. }))
{
self.looks.insert(look.id);
}
if code != 1.0 || style.is_empty() || !after {
return;
}
let Some(f) = self.frames.last_mut() else { return };
let n = f.children.len();
let (Some(object), Some(look)) = (n.checked_sub(1).and_then(|i| f.children.get(i)), n.checked_sub(2).and_then(|i| f.children.get(i))) else {
return;
};
// The look is a group of paths, or one image where an effect (a blur, a shadow) rasterizes it.
if !self.hidden_ids.contains(&object.id)
|| self.hidden_ids.contains(&look.id) != hidden
|| !matches!(look.kind, NodeKind::Group { clip: false, .. } | NodeKind::Image(_))
{
return;
}
// A style read here: the object, painted as it says.
if let Some(appearance) = self.styles.get(&style).and_then(|st| styled(object, st)) {
let Some(mut object) = f.children.pop() else { return };
let Some(look) = f.children.pop() else { return };
object.appearance = appearance.into();
// The object's transparency is written on its look, after the look's group ends.
(object.opacity, object.blend, object.isolate, object.knockout) = (look.opacity, look.blend, look.isolate, look.knockout);
// Now the object itself, not a copy an enclosing look's `strip` would remove.
self.hidden_ids.remove(&object.id);
f.children.push(object);
return;
}
let is_text = slot_of(object.name.as_deref()).is_some();
let Some(object) = f.children.pop() else { return };
let Some(look) = f.children.last_mut() else { return };
// Inside a copy on `%_` lines everything is, so nothing tells the look's own copies apart.
if !hidden {
strip(look, &self.hidden_ids);
}
if is_text {
// A text object stays the type it is: over what else its look draws, or in place of a
// look that was only its type.
if look.children().is_some_and(|c| c.is_empty()) {
*look = object;
} else {
f.children.push(object);
}
return;
}
look.name = object.name.clone().or(look.name.take());
look.visible = object.visible;
look.locked = object.locked;
if !hidden {
self.drawn_looks += 1;
}
}
// ---- paths -----------------------------------------------------------------------------
fn path_op(&mut self, w: &str, vals: &[V]) -> Result<(), String> {
let n = Self::nums(vals);
let p = |i: usize| -> Option<Point> {
let at = n.len().checked_sub(i)?;
Some(Point::new(*n.get(at)?, *n.get(at + 1)?))
};
self.points += 1;
if self.points > MAX_POINTS {
return Err("a path has too many points".into());
}
let current = self.path.elements().last().and_then(|e| e.end_point());
match w {
"m" => {
let Some(a) = p(2) else { return Ok(()) };
self.path.move_to(a);
}
_ if current.is_none() => {}
"l" | "L" => {
if let Some(a) = p(2) {
self.path.line_to(a);
}
}
"c" | "C" => {
if let (Some(a), Some(b), Some(c)) = (p(6), p(4), p(2)) {
self.path.curve_to(a, b, c);
}
}
"v" | "V" => {
if let (Some(cur), Some(b), Some(c)) = (current, p(4), p(2)) {
self.path.curve_to(cur, b, c);
}
}
_ => {
if let (Some(a), Some(c)) = (p(4), p(2)) {
self.path.curve_to(a, c, c);
}
}
}
Ok(())
}
/// A path left unpainted before a container opens.
fn end_path_quietly(&mut self) {
self.path = BezPath::new();
self.points = 0;
self.clip_next = false;
}
/// Paint the current path (`op`), as a guide when `guide`.
fn paint(&mut self, op: &str, hidden: bool, guide: bool) -> Result<(), String> {
let mut bp = std::mem::take(&mut self.path);
self.points = 0;
let clip = std::mem::take(&mut self.clip_next);
if let Some(i) = self.instance.take() {
self.set_instance(&i);
}
let (fill_paint, stroke_paint) = (self.pending_fill.take(), self.pending_stroke.take());
if bp.elements().is_empty() {
return Ok(());
}
if op.chars().next().is_some_and(|c| c.is_ascii_lowercase()) {
close(&mut bp);
}
let to_doc = self.space();
let path = PathData::from_bezpath(&(to_doc * bp));
let gs = self.gs.clone();
let (fill, stroke) = match op.to_ascii_lowercase().as_str() {
"f" => (true, false),
"s" => (false, true),
"b" => (true, true),
_ => (false, false),
};
let mut items = Vec::new();
if fill && !clip {
items.push(AppearanceItem::Fill(FillLayer {
overprint: gs.overprint_fill,
..FillLayer::new(fill_paint.clone().unwrap_or(gs.fill.clone()))
}));
}
if stroke && !clip {
let mut st = StrokeLayer::new(stroke_paint.unwrap_or(gs.stroke.clone()), gs.width);
st.cap = gs.cap;
st.join = gs.join;
st.miter_limit = gs.miter;
st.dash = gs.dash.clone();
st.overprint = gs.overprint_stroke;
items.push(AppearanceItem::Stroke(st));
}
let kind = NodeKind::Path { path, rule: gs.rule, live: None, clipping: clip, guide };
let mut node = self.new_node(kind, &gs);
node.appearance = Appearance { items, ..Appearance::default() }.into();
node.opacity = gs.opacity;
node.blend = gs.blend;
node.knockout_shape = gs.knockout_shape;
if clip && let Some(f) = self.frames.last_mut() {
f.clips.push(f.children.len());
}
// A clipping path's fill is painted too, under the art it clips, as the file's PDF part draws it.
let painted = clip && fill && !matches!(gs.fill, Paint::None);
let copy = painted.then(|| {
let mut copy = node.clone();
copy.id = self.doc.alloc_id();
if let NodeKind::Path { clipping, .. } = &mut copy.kind {
*clipping = false;
}
let paint = fill_paint.clone().unwrap_or_else(|| gs.fill.clone());
copy.appearance = Appearance {
items: vec![AppearanceItem::Fill(FillLayer { overprint: gs.overprint_fill, ..FillLayer::new(paint) })],
..Appearance::default()
}
.into();
copy
});
self.add(node, hidden)?;
if let Some(copy) = copy {
self.clip_fills.insert(copy.id);
self.add(copy, hidden)?;
}
Ok(())
}
/// `/End X!`: the gradient mesh read, as one object.
fn end_mesh(&mut self) -> Result<(), String> {
let Some((m, hidden)) = self.mesh.take() else { return Ok(()) };
if m.unread || m.patches.is_empty() {
self.unreadable("gradient meshes it can't read");
return Ok(());
}
let gs = self.gs.clone();
let mut patches: Vec<Node> = m.patches.into_iter().map(|p| self.new_node(NodeKind::Mesh(p), &gs)).collect();
let mut node = match patches.len() {
1 => patches.pop().unwrap_or_else(|| self.new_node(NodeKind::Group { children: vec![], clip: false }, &gs)),
_ => self.new_node(NodeKind::Group { children: patches.into_iter().map(Arc::new).collect(), clip: false }, &gs),
};
node.opacity = gs.opacity;
node.blend = gs.blend;
self.add(node, hidden)
}
/// Stroke the path just painted (a gradient-filled one, `1 BB` or `2 BB`) with the stroke's state.
fn stroke_last(&mut self) {
let gs = self.gs.clone();
let Some(n) = self.last_object() else { return };
if !matches!(n.kind, NodeKind::Path { clipping: false, .. }) || n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Stroke(_))) {
return;
}
let mut st = StrokeLayer::new(gs.stroke.clone(), gs.width);
st.cap = gs.cap;
st.join = gs.join;
st.miter_limit = gs.miter;
st.dash = gs.dash.clone();
st.overprint = gs.overprint_stroke;
let mut a = (*n.appearance).clone();
a.items.push(AppearanceItem::Stroke(st));
n.appearance = a.into();
}
fn set_instance(&mut self, i: &Instance) {
let mut to_doc = self.space();
// In a pattern's definition a gradient's `Xm` and `Bm` are written y down about the canvas point of art space's
// origin, as the file's PDF part draws them.
if self.pattern.is_some()
&& i.has_matrix()
&& let Some(t) = self.template
{
let (cx, cy) = ((t[0] + t[2]) / 2.0, (t[1] + t[3]) / 2.0);
to_doc *= Affine::new([1.0, 0.0, 0.0, -1.0, CANVAS_CENTRE - cx, CANVAS_CENTRE + cy]);
}
match i.paint(&self.gradients, to_doc) {
Some(p) if i.stroke => self.pending_stroke = Some(p),
Some(p) => self.pending_fill = Some(p),
None => {
let name = i.name.clone().unwrap_or_default();
self.warn(&format!("gradient “{name}” isn't defined in the file: a plain colour stands in for it"));
}
}
}
// ---- images ----------------------------------------------------------------------------
/// The samples after `XI`, its operands on the stack.
fn image(&mut self, data: &[u8]) -> Result<(), String> {
let vals = self.operands();
// Operands that came before an `XI` on their own line.
let vals = match self.raster.as_mut().and_then(|r| r.operands.take()) {
Some(before) if !vals.iter().any(|v| matches!(v, V::Arr(_))) => before,
_ => vals,
};
self.image_from(vals, data)
}
/// An image of operands `vals` and samples `data`.
fn image_from(&mut self, vals: Vec<V>, data: &[u8]) -> Result<(), String> {
let space = self.raster.as_ref().and_then(|r| r.space.clone()).unwrap_or_default();
let Some(m) = vals.iter().find_map(|v| if let V::Arr(a) = v { Some(Self::nums(a)) } else { None }) else { return Ok(()) };
let n = Self::nums(&vals);
let [_, _, _, _, w, h, bits, kind, alpha, _, binary, ..] = n.as_slice() else { return Ok(()) };
let (w, h) = (*w as u64, *h as u64);
if w == 0 || h == 0 || w.saturating_mul(h) > MAX_PIXELS || ![1.0, 8.0].contains(bits) {
self.warn("an image VectorCraft can't read was left out");
return Ok(());
}
let channels: usize = match space.as_str() {
"DeviceGray" => 1,
"DeviceCMYK" => 4,
"DeviceRGB" => 3,
_ => match *kind as i64 {
1 => 1,
4 => 4,
_ => 3,
},
};
let decoded;
let data = if *binary == 0.0 {
let digits: Vec<u8> = data.iter().copied().filter(|b| b.is_ascii_hexdigit()).collect();
decoded = super::super::lex::hex_decode(&digits).unwrap_or_default();
decoded.as_slice()
} else {
data
};
let row = if *bits == 1.0 { (w as usize * channels).div_ceil(8) } else { w as usize * channels };
let need = row.saturating_mul(h as usize);
// An alpha channel follows the colour samples, one byte a pixel.
let alpha_len = if *alpha > 0.0 { (w * h) as usize } else { 0 };
let (Some(samples), opacity) = (data.get(..need), data.get(need..need.saturating_add(alpha_len))) else {
self.warn("an image whose data is cut short was left out");
return Ok(());
};
let opacity = opacity.filter(|_| alpha_len > 0);
// CMYK samples keep their ink amounts as a CMYK TIFF, as the PDF importer keeps a CMYK
// image's, and an alpha channel that hides part of the image becomes its opacity mask.
if channels == 4
&& *bits == 8.0
&& let Some(blob) = Inks::new(w as u32, h as u32, samples.to_vec()).and_then(|i| ImageBlob::cmyk_tiff(&i))
{
return self.add_raster(blob, opacity, &m, (w as u32, h as u32));
}
let mut rgba = Vec::with_capacity((w * h * 4) as usize);
for y in 0..h as usize {
let line = samples.get(y * row..(y + 1) * row).unwrap_or_default();
for x in 0..w as usize {
let px = if *bits == 1.0 {
let bit = line.get(x / 8).map_or(0, |b| (b >> (7 - x % 8)) & 1);
let v = if bit == 1 { 255 } else { 0 };
[v, v, v]
} else {
let s = |c: usize| line.get(x * channels + c).copied().unwrap_or(0);
match channels {
1 => [s(0); 3],
4 => {
let key = [s(0), s(1), s(2), s(3)];
if self.cmyk_cache.len() > 1 << 16 {
self.cmyk_cache.clear();
}
*self.cmyk_cache.entry(key).or_insert_with(|| cmyk_rgb(key))
}
_ => [s(0), s(1), s(2)],
}
};
let a = opacity.and_then(|o| o.get(y * w as usize + x)).copied().unwrap_or(255);
rgba.extend_from_slice(&[px[0], px[1], px[2], a]);
}
}
let Some(img) = image::RgbaImage::from_raw(w as u32, h as u32, rgba) else { return Ok(()) };
let mut png = vec![];
if img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).is_err() {
self.warn("an image VectorCraft can't read was left out");
return Ok(());
}
self.add_raster(ImageBlob::new("image/png", png), None, &m, (w as u32, h as u32))
}
/// Add the image `blob` (`w` × `h` pixels), which the matrix `m` maps into art space, with
/// `alpha` (its alpha channel, one byte a pixel) as its opacity mask when it hides part of the
/// image.
fn add_raster(&mut self, blob: ImageBlob, alpha: Option<&[u8]>, m: &[f64], (w, h): (u32, u32)) -> Result<(), String> {
// The matrix maps pixels (y down) into art space with its y flipped.
let [a, b, c, d, tx, ty] = m else { return Ok(()) };
let to_doc = self.space();
let xf = to_doc * FLIP * Affine::new([*a, *b, *c, *d, *tx, -*ty]);
if !xf.as_coeffs().iter().all(|v| v.is_finite()) || xf.determinant().abs() < 1e-12 {
return Ok(());
}
let mask = alpha.and_then(|a| self.alpha_mask(a, w, h, xf));
let key = blob.content_key();
self.doc.images.entry(key.clone()).or_insert(blob);
self.add_image_masked(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default() }, mask, false)
}
/// An image's alpha channel (`w` × `h` bytes) as a grayscale image over the image (`xf`): its
/// opacity mask, as in PDF import. `None` when it hides nothing.
fn alpha_mask(&mut self, alpha: &[u8], w: u32, h: u32, xf: Affine) -> Option<OpacityMask> {
if alpha.iter().all(|v| *v == 255) {
return None;
}
let mut png = Vec::new();
image::GrayImage::from_raw(w, h, alpha.to_vec())?.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).ok()?;
let blob = ImageBlob::new("image/png", png);
let key = blob.content_key();
self.doc.images.entry(key.clone()).or_insert(blob);
let art = Node::new(
self.doc.alloc_id(),
NodeKind::Image(Box::new(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default() })),
);
Some(OpacityMask::new(art, true))
}
/// Add an image object with the state's transparency.
fn add_image(&mut self, im: ImageObject, hidden: bool) -> Result<(), String> {
self.add_image_masked(im, None, hidden)
}
/// Add an image object with the state's transparency and `mask` as its opacity mask.
fn add_image_masked(&mut self, im: ImageObject, mask: Option<OpacityMask>, hidden: bool) -> Result<(), String> {
let gs = self.gs.clone();
let mut node = self.new_node(NodeKind::Image(Box::new(im)), &gs);
node.opacity = gs.opacity;
node.blend = gs.blend;
node.extras_mut().mask = mask.map(Box::new);
self.add(node, hidden)
}
/// `~`: the placed file read since `%AI5_BeginPlace` becomes a linked image in its place that
/// shows the preview the data keeps of it, as a linked image whose file is missing does. One
/// without a preview, a path or a matrix isn't read.
fn end_place(&mut self, hidden: bool) -> Result<(), String> {
let Some(Place { matrix: Some(m), bounds: Some(b), path, preview: Some((png, w, h)) }) = self.place.take().filter(|p| !p.path.is_empty())
else {
self.unreadable("placed files");
return Ok(());
};
// The file's pixels (y down) start at the canvas point of art space's origin (the box's own
// corner when the file doesn't say where the canvas is); the preview's are spread over them.
let centre = self.template.map(|t| ((t[0] + t[2]) / 2.0, (t[1] + t[3]) / 2.0));
let origin = centre.map_or(b.origin(), |(x, y)| Point::new(CANVAS_CENTRE - x, CANVAS_CENTRE + y));
let over_file = Affine::translate(origin.to_vec2()) * Affine::scale_non_uniform(b.width() / f64::from(w), b.height() / f64::from(h));
let xf = self.space() * m * over_file;
if !xf.as_coeffs().iter().all(|v| v.is_finite()) || xf.determinant().abs() < 1e-12 {
self.unreadable("placed files");
return Ok(());
}
// The preview is all the document has of the file: what a missing link shows.
let bytes = Arc::new(png);
let blob = ImageBlob { mime: "image/png".into(), bytes: bytes.clone(), proxy: Some(bytes) };
let key = blob.content_key();
self.doc.images.entry(key.clone()).or_insert(blob);
self.warn(LINKED_PREVIEWS);
self.add_image(ImageObject { key, width: w, height: h, xf, link: Some(LinkInfo::new(path)), placement: Default::default() }, hidden)
}
// ---- 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).
fn sized_board(&self) -> Option<Rect> {
let ((w, h), (cx, cy)) = (self.art_size?, self.template_center?);
// 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);
let x0 = if near(left) { left.round() } else { left.floor() };
let y1 = if near(top) { top.round() } else { top.ceil() };
let r = Rect::new(x0, y1 - h, x0 + w, y1);
(sane(r) && w > 0.0 && h > 0.0).then_some(r)
}
/// The first artboard: the `/Document`'s first, else the crop marks, else [`Self::sized_board`].
fn first_board(&self) -> Option<Rect> {
self.artboards.first().map(|(_, r)| *r).or(self.cropmarks).or_else(|| self.sized_board())
}
/// The page of a file that says nothing of its artboard (the legacy format as other apps write
/// it): its art's bounding box.
fn bbox_board(&self) -> Option<Rect> {
let [x0, y0, x1, y1] = self.hires.or(self.bbox)?;
Some(Rect::new(x0, y0, x1, y1)).filter(|r| r.width() > 0.0 && r.height() > 0.0)
}
/// Art space → the document (fixed when the art starts, from the artboards read by then). A
/// Large Canvas editing copy is written at 1/N scale ([`Self::large_canvas`]): the factor makes
/// its art and boxes reach the page's size, which is what the page it is opened beside holds.
fn ensure_space(&mut self) -> Affine {
if let Some(m) = self.to_doc {
return m;
}
let first = self.first_board().or_else(|| self.bbox_board());
let top_left = first.map_or(Point::new(0.0, 792.0), |r| Point::new(r.x0, r.y1));
let s = self.large_canvas.unwrap_or(1.0);
let m = Affine::new([s, 0.0, 0.0, -s, -s * top_left.x, s * top_left.y]);
self.to_doc = Some(m);
m
}
/// Art space → where the art being read goes: the document, or while a pattern's definition is
/// read, the pattern's space (y down).
fn space(&mut self) -> Affine {
if self.pattern.is_some() { FLIP } else { self.ensure_space() }
}
fn warn(&mut self, w: &str) {
if !self.warnings.iter().any(|x| x == w) && self.warnings.len() < 64 {
self.warnings.push(w.to_string());
}
}
fn finish(mut self) -> Result<Structure, String> {
if !self.unknown.is_empty() {
let ops: Vec<String> = self.unknown.iter().map(|o| format!("`{o}`")).collect();
self.unsupported.insert(format!("operators VectorCraft doesn't know ({})", ops.join(", ")));
}
if !self.unsupported.is_empty() {
let list: Vec<&str> = self.unsupported.iter().map(String::as_str).collect();
return Err(format!("it has {}, which VectorCraft doesn't read from it yet", join_and(&list)));
}
if self.doc.layers.is_empty() {
return Err("it has no layers".into());
}
let to_doc = self.ensure_space();
let artboard = self.first_board();
drop_commented_copies(&mut self.doc.layers, &self.hidden_ids);
let mut doc = std::mem::replace(&mut self.doc, Document::new(1.0, 1.0));
let boards: Vec<(String, Rect)> = if self.artboards.is_empty() {
artboard.or_else(|| self.bbox_board()).map(|r| vec![("Artboard 1".to_string(), r)]).unwrap_or_default()
} else {
self.artboards.clone()
};
doc.artboards = boards
.into_iter()
.enumerate()
.map(|(i, (name, r))| Artboard {
id: i as u32 + 1,
name,
rect: to_doc.transform_rect_bbox(r),
show_center_mark: false,
show_cross_hairs: false,
})
.collect();
if doc.artboards.is_empty() {
let rect = doc.art_bounds().filter(|r| sane(*r) && r.area() > 0.0).unwrap_or(Rect::new(0.0, 0.0, 612.0, 792.0));
doc.artboards = vec![Artboard { id: 1, name: "Artboard 1".into(), rect, show_center_mark: false, show_cross_hairs: false }];
}
if self.color_model.unwrap_or(self.cmyk_colors > self.rgb_colors) {
doc.color_mode = ColorMode::Cmyk;
let (swatches, groups) = vectorcraft_color::default_swatches(ColorMode::Cmyk.model());
(doc.swatches, doc.swatch_groups) = (swatches.into(), groups.into());
}
for n in &self.spots {
if doc.swatch(&n.name).is_none() {
doc.swatches.push(Swatch { name: n.name.clone(), paint: Paint::solid(n.color), global: true, spot: n.spot });
}
}
for name in doc.patterns.iter().map(|p| p.name.clone()).collect::<Vec<_>>() {
if doc.swatch(&name).is_none() {
doc.swatches.push(Swatch { paint: Paint::Pattern { pattern: name.clone(), xf: Affine::IDENTITY }, name, global: false, spot: false });
}
}
let mut warnings = self.warnings;
if self.drawn_looks > 0 {
warnings.push(format!(
"objects with several fills or strokes, effects or brushes came in as their drawn look ({}): their appearance isn't editable yet",
self.drawn_looks
));
}
Ok(Structure {
doc,
to_doc,
bbox: self.hires.or(self.bbox),
artboard,
template: self.template,
hidden_unread: self.hidden_unread,
slot_names: self.slot_names,
warnings,
})
}
}
/// The text objects written twice, plainly and on `%_` lines: the commented copy (`hidden`) of a
/// story that also has a plain one goes.
fn drop_commented_copies(layers: &mut [Arc<Node>], hidden: &HashSet<NodeId>) {
fn plain(nodes: &[Arc<Node>], hidden: &HashSet<NodeId>, out: &mut HashSet<u32>) {
for n in nodes {
match slot_of(n.name.as_deref()) {
Some(Some(story)) if !hidden.contains(&n.id) => {
out.insert(story);
}
_ => {
if let Some(c) = n.children() {
plain(c, hidden, out);
}
}
}
}
}
fn drop(nodes: &mut Vec<Arc<Node>>, hidden: &HashSet<NodeId>, plain: &HashSet<u32>) {
nodes.retain(|n| !(hidden.contains(&n.id) && slot_of(n.name.as_deref()).flatten().is_some_and(|s| plain.contains(&s))));
for n in nodes.iter_mut() {
if n.children().is_some_and(|c| !c.is_empty())
&& let Some(c) = Arc::make_mut(n).children_mut()
{
drop(c, hidden, plain);
}
}
}
let mut stories = HashSet::new();
plain(layers, hidden, &mut stories);
if stories.is_empty() {
return;
}
for l in layers.iter_mut() {
if let Some(c) = Arc::make_mut(l).children_mut() {
drop(c, hidden, &stories);
}
}
}
/// A node and its index among its siblings.
type Indexed = (usize, Node);
/// Close the subpath `bp` ends with, if it has one open.
fn close(bp: &mut BezPath) {
if bp.elements().last().is_some_and(|e| !matches!(e, PathEl::ClosePath)) {
bp.close_path();
}
}
/// The fill rule of a path or compound node.
/// `kids` with the members of each plain group among them in the group's place, at any depth.
fn ungrouped(kids: Vec<Node>) -> Vec<Node> {
let mut out = Vec::with_capacity(kids.len());
for k in kids {
match k.kind {
NodeKind::Group { children, clip: false } => out.extend(ungrouped(children.into_iter().map(Arc::unwrap_or_clone).collect())),
_ => out.push(k),
}
}
out
}
fn path_rule(n: &Node) -> FillRule {
match &n.kind {
NodeKind::Path { rule, .. } | NodeKind::Compound { rule, .. } => *rule,
_ => FillRule::NonZero,
}
}
/// `object`'s appearance with style `st`: `None` when the style isn't one this reads, or asks a
/// group for fills or strokes.
fn styled(object: &Node, st: &super::style::ArtStyle) -> Option<Appearance> {
use super::style::StyleItem;
if !st.plain || (object.is_container() && !matches!(object.kind, NodeKind::Compound { .. }) && !st.items.is_empty()) {
return None;
}
let mut items = vec![];
for item in &st.items {
items.push(match item {
StyleItem::Fill(paint) => AppearanceItem::Fill(FillLayer::new(paint.clone())),
StyleItem::ObjectFill => AppearanceItem::Fill(object.appearance.fill().filter(|f| matches!(f.paint, Paint::Gradient(_)))?.clone()),
StyleItem::Stroke { paint, width, cap, join, miter, dash, profile } => {
let mut layer = StrokeLayer::new(paint.clone(), *width);
(layer.cap, layer.join, layer.miter_limit, layer.dash, layer.profile) = (*cap, *join, *miter, dash.clone(), profile.clone());
AppearanceItem::Stroke(layer)
}
});
}
Some(Appearance { items, effects: st.effects.clone(), ..(*object.appearance).clone() })
}
/// Remove from `n` the children read from `%_` lines (`hidden`: the object itself, not its look).
fn strip(n: &mut Node, hidden: &HashSet<NodeId>) {
if let Some(children) = n.children_mut() {
children.retain(|c| !hidden.contains(&c.id));
for c in children.iter_mut() {
strip(Arc::make_mut(c), hidden);
}
}
}
/// The dictionary of entry `key` in `o` or the dictionaries it holds.
fn find_entry<'o>(o: &'o Obj, key: &str, depth: usize) -> Option<&'o Obj> {
if depth > 8 {
return None;
}
o.obj(key).or_else(|| o.entries.iter().flat_map(|(_, v)| v.iter()).filter_map(V::obj).find_map(|c| find_entry(c, key, depth + 1)))
}
/// A placed file's preview as a PNG, with its size: `w h rowBytes 32 n ` (each number followed by
/// a space), then `h` rows of `rowBytes` bytes, premultiplied ARGB. `None` when it isn't that, is
/// larger than [`MAX_PIXELS`] or is cut short.
fn preview_png(data: &[u8]) -> Option<(Vec<u8>, u32, u32)> {
let mut rest = data.trim_ascii_start();
let mut head = [0usize; 5];
for v in &mut head {
let n = rest.iter().position(|b| *b == b' ')?;
*v = std::str::from_utf8(rest.get(..n)?).ok()?.parse().ok()?;
rest = rest.get(n + 1..)?;
}
let [w, h, row, bits, _] = head;
let (width, height) = (u32::try_from(w).ok()?, u32::try_from(h).ok()?);
if w == 0 || h == 0 || u64::from(width) * u64::from(height) > MAX_PIXELS || bits != 32 || row < w * 4 {
return None;
}
// Every row is there before anything is allocated for them.
if rest.len() < (h - 1).checked_mul(row)?.checked_add(w * 4)? {
return None;
}
let mut rgba = Vec::with_capacity(w * h * 4);
for line in rest.chunks(row).take(h) {
for &[a, r, g, b] in line.get(..w * 4)?.as_chunks::<4>().0 {
let straight = |c: u8| if a == 0 { 0 } else { (u32::from(c) * 255 / u32::from(a)).min(255) as u8 };
rgba.extend_from_slice(&[straight(r), straight(g), straight(b), a]);
}
}
let mut png = vec![];
image::RgbaImage::from_raw(width, height, rgba)?.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).ok()?;
Some((png, width, height))
}
fn sane(r: Rect) -> bool {
[r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite() && v.abs() <= MAX_SIDE)
}
/// The four numbers of a comment's value (a box that may be a point).
fn four(v: &str) -> Option<[f64; 4]> {
let n: Vec<f64> = v.split_whitespace().map_while(|w| w.parse::<f64>().ok()).collect();
let [a, b, c, d] = n.as_slice() else { return None };
[a, b, c, d].iter().all(|v| v.is_finite() && v.abs() <= MAX_SIDE).then_some([*a, *b, *c, *d])
}
/// The first two numbers of a comment's value.
fn two(v: &str) -> Option<(f64, f64)> {
let n: Vec<f64> = v.split_whitespace().filter_map(|w| w.parse().ok()).filter(|v: &f64| v.is_finite()).collect();
match n.as_slice() {
[a, b, ..] => Some((*a, *b)),
_ => None,
}
}
/// A CMYK sample in RGB.
fn cmyk_rgb([c, m, y, k]: [u8; 4]) -> [u8; 3] {
let f = |v: u8| f32::from(v) / 255.0;
let rgb = vectorcraft_color::Color::cmyk(f(c), f(m), f(y), f(k)).to_rgba8(1.0);
[rgb[0], rgb[1], rgb[2]]
}
/// `a`, `a and b`, `a, b and c`.
fn join_and(items: &[&str]) -> String {
match items {
[] => String::new(),
[one] => (*one).to_string(),
[rest @ .., last] => format!("{} and {last}", rest.join(", ")),
}
}