mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 18:27:02 -06:00
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.
2254 lines
97 KiB
Rust
2254 lines
97 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.
|
||
//! - 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(", ")),
|
||
}
|
||
}
|