Files
vectorcraft/crates/eps/src/import/graphics.rs
T
Gert Pellin 3412d3a073 EPS import: a stitched shading keeps its hard colour changes (#1653)
A stitching function's bound became one stop, evaluated at the bound:
the right piece's colour. Where the pieces don't meet, the left piece's
end colour was lost. The colour just before the bound is now a stop at
the same offset too, ahead of the one after it, when the two differ;
continuous stitches keep one stop per bound as before.

Fixes #1653
2026-10-14 02:09:09 +07:00

1834 lines
77 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The graphics state and the operators that draw: coordinates, paths, colours, painting,
//! clipping and shadings. Painted paths become document objects as they are drawn ([`Out`]).
use std::collections::{HashMap, HashSet};
use std::hash::{DefaultHasher, Hash, Hasher};
use std::rc::Rc;
use std::sync::Arc;
use kurbo::{PathEl, Shape};
use vectorcraft_color::swatch::REGISTRATION;
use vectorcraft_color::{Color, Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop, Paint};
use vectorcraft_doc::clipnest::{Clip, Drawn, MAX_NEST, deep_clips};
use vectorcraft_doc::pattern::PatternDef;
use vectorcraft_doc::{Appearance, AppearanceItem, Dash, Document, FillLayer, LineCap, LineJoin, Node, NodeId, NodeKind, StrokeLayer};
use vectorcraft_geom::{Affine, BezPath, FillRule, PathData, Point, Rect, Vec2};
use super::interp::{Interp, MAX_GSAVE, matrix_obj};
use super::obj::{DictRef, Key, Obj, Op, PsError, Res, Shared, ps_err};
/// Most objects one file makes.
const MAX_NODES: usize = 1 << 20;
/// Objects reaching further than this from the page (points) come from damaged coordinates and
/// are left out.
const LIMIT: f64 = 1e6;
/// Width of a zero-width line (the thinnest a device draws), points.
const HAIRLINE: f64 = 0.25;
/// Most inks a `DeviceN` space has (PostScript's own limit).
const MAX_INKS: usize = 32;
/// Most dashes `strokepath` makes of a path (more and it outlines the path undashed).
const MAX_DASHES: f64 = 1e5;
/// Most entries a dash pattern has: the most elements an array has in a typical PostScript
/// implementation (PLRM 3rd ed., Table B.1).
const MAX_DASH_ENTRIES: usize = 65_535;
/// Most entries an indexed colour space's table has.
const MAX_HIVAL: usize = 4095;
/// Most elements the current path may have.
const MAX_PATH: usize = 1 << 20;
/// Most path elements drawn in all (each object's clips and groups counting as elements too).
const MAX_DRAWN: usize = 1 << 24;
/// Samples taken of a shading function that isn't a plain interpolation.
const SHADING_SAMPLES: usize = 32;
/// Most bounds of a stitched linear function that become a gradient's stops (two where the pieces
/// don't meet): past it, the function is sampled like any other (each stop evaluates the whole
/// function).
const MAX_STITCH_STOPS: usize = 1024;
/// Most functions one evaluation of a shading's function visits: the function itself, each entry
/// of an array of functions and, in each stitching function, the piece that applies to the input.
/// For every shading type, the `Function` entry is one function dictionary or an array of one for
/// each color component (PLRM 3rd ed., Tables 4.12 to 4.17; a type 7 shading has the entries of
/// type 6). The reader allows at most [`MAX_INKS`] components, and the depth limit allows each
/// entry a chain of at most 8 functions.
const MAX_FUNCTION_VISITS: usize = 1 + MAX_INKS * 8;
/// Work one function costs against the operation budget each time it is evaluated: about as much
/// as 8 operations (measured).
const FUNCTION_OPS: u64 = 8;
pub(super) const FAR_AWAY: &str = "objects far outside the page were left out";
pub(super) const TOO_MUCH: &str = "the file draws more than VectorCraft reads: the rest was left out";
const NESTED_PATTERNS: &str = "patterns nested too deeply are filled with mid-grey";
/// Deepest patterns and glyph procedures drawn inside each other's art.
pub(crate) const MAX_APART: u32 = 8;
const UNKNOWN_FUNCTIONS: &str = "shadings whose functions are of an unknown type are filled with their middle colour";
const UNKNOWN_SHADINGS: &str = "shadings of an unknown type were left out";
/// A colour space.
#[derive(Clone, Debug)]
pub(crate) enum Space {
Gray,
Rgb,
Cmyk,
/// Separation and DeviceN: the ink names, the alternate space and the tint transform.
Inks {
names: Vec<Rc<str>>,
alt: Rc<Space>,
tint: Obj,
},
Indexed {
base: Rc<Space>,
table: Rc<Vec<f64>>,
},
/// Patterns; an uncoloured one paints in a colour of the underlying space.
Pattern(Option<Rc<Space>>),
}
impl Space {
/// Components a colour has in it.
pub fn n(&self) -> usize {
match self {
Self::Gray | Self::Indexed { .. } | Self::Pattern(_) => 1,
Self::Rgb => 3,
Self::Cmyk => 4,
Self::Inks { names, .. } => names.len().max(1),
}
}
/// The colour `setcolorspace` starts with.
fn initial(&self) -> Vec<f64> {
match self {
Self::Cmyk => vec![0.0, 0.0, 0.0, 1.0],
Self::Inks { names, .. } => vec![1.0; names.len().max(1)],
s => vec![0.0; s.n()],
}
}
}
/// A process colour from components in `space` (grey, RGB or CMYK), each clamped to 0–1.
pub(crate) fn process(space: &Space, c: &[f64]) -> Option<Color> {
let v = |i: usize| c.get(i).map_or(0.0, |x| if x.is_finite() { x.clamp(0.0, 1.0) as f32 } else { 0.0 });
Some(match space {
Space::Gray => Color::gray(1.0 - v(0)),
Space::Rgb => Color::rgb(v(0), v(1), v(2)),
Space::Cmyk => Color::cmyk(v(0), v(1), v(2), v(3)),
_ => return None,
})
}
#[derive(Clone, Debug)]
pub(crate) struct GState {
/// User space → PostScript's default space.
pub ctm: Affine,
pub space: Rc<Space>,
/// The `setcolorspace` operand, for `currentcolorspace`.
pub space_obj: Obj,
pub comps: Vec<f64>,
pub paint: Paint,
pub width: f64,
pub cap: LineCap,
pub join: LineJoin,
pub miter: f64,
/// The dash pattern, shared by the states saved or copied from this one.
pub dash: Rc<[f64]>,
pub dash_offset: f64,
/// The current path, in document space.
pub path: BezPath,
pub cur: Option<Point>,
pub start: Option<Point>,
pub clips: Vec<Arc<Clip>>,
/// The clips `clipsave` saved, for `cliprestore`.
pub saved_clips: Vec<Rc<[Arc<Clip>]>>,
pub overprint: bool,
pub font: Option<DictRef>,
/// `nulldevice`: painting draws nothing.
pub null: bool,
/// The colour is a shading pattern whose shading is a mesh: a fill paints it (the shading
/// and pattern space → the document).
pub mesh: Option<(DictRef, Affine)>,
}
impl Default for GState {
fn default() -> Self {
Self {
ctm: Affine::IDENTITY,
space: Rc::new(Space::Gray),
space_obj: Obj::name("DeviceGray"),
comps: vec![0.0],
paint: Paint::solid(Color::gray(1.0)),
width: 1.0,
cap: LineCap::Butt,
join: LineJoin::Miter,
miter: 10.0,
dash: Rc::from([]),
dash_offset: 0.0,
path: BezPath::new(),
cur: None,
start: None,
clips: vec![],
saved_clips: vec![],
overprint: false,
font: None,
null: false,
mesh: None,
}
}
}
/// What the program draws.
pub(crate) struct Out {
/// The document being made: ids, images and swatches.
pub doc: Document,
pub drawn: Drawn,
pub warnings: Vec<String>,
/// PostScript's default space → document space.
pub page: Affine,
/// The page (the bounding box) in document space.
pub frame: Rect,
/// Clip and group ids are given in the order they open, so a chain of both sorts by id.
next_clip: u32,
/// The groups open (outermost first), and how many more opened past [`MAX_NEST`] (their art
/// goes in the innermost group kept).
groups: Vec<Arc<Clip>>,
too_deep: usize,
/// Path elements (and the clips and groups of each object) drawn so far (see [`MAX_DRAWN`]).
elements: usize,
/// The path the last object was filled with: a stroke of the same path right after joins it.
merge: Option<BezPath>,
/// Process colours painted, by model (the document opens in CMYK when most are).
pub cmyk: usize,
pub rgb: usize,
/// Spot inks used: name and colour at full tint.
pub spots: Vec<(String, Color)>,
/// The color at full tint of each spot ink in [`Self::spots`], by name.
spot_colors: HashMap<Rc<str>, Color>,
/// Rectangles painted with a shading (`shfill`): a clipping group of one is the clip path
/// filled with it.
pub shadings: HashSet<NodeId>,
/// Tiling patterns made into pattern swatches: the pattern's `PaintProc` (a pattern made
/// again with `makepattern` shares it) and cell, the colour an uncoloured one painted in, and
/// the swatch's name. Each list holds the patterns of one [`tiling_hash`]; a pattern whose
/// cell has a NaN corner is left out.
tilings: HashMap<u64, Vec<Tiling>>,
/// How deep art drawn apart (a pattern's cell, a Type 3 glyph) is nested.
pub apart: u32,
}
fn sane(r: Rect) -> bool {
[r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite() && v.abs() <= LIMIT)
}
/// A tiling pattern made into a pattern swatch, as [`Out::tilings`] lists it.
type Tiling = (Shared<Obj>, Rect, Option<Paint>, String);
/// A hash of what tells tiling patterns apart in [`Out::tilings`]. Patterns that compare equal
/// hash alike: each number goes in by its bits, and −0 goes in as 0.
fn tiling_hash(proc: &Shared<Obj>, tile: Rect, under: Option<&Paint>) -> u64 {
let mut h = DefaultHasher::new();
proc.hash(&mut h);
[tile.x0, tile.y0, tile.x1, tile.y1].map(|v| (v + 0.0).to_bits()).hash(&mut h);
if let Some(Paint::Solid { color, swatch, tint }) = under {
let c = match *color {
Color::Rgb { r, g, b } => [r, g, b, 0.0],
Color::Cmyk { c, m, y, k } => [c, m, y, k],
Color::Gray { k } => [k, 0.0, 0.0, 0.0],
Color::Lab { l, a, b } => [l, a, b, 0.0],
};
c.map(|v| (v + 0.0).to_bits()).hash(&mut h);
swatch.hash(&mut h);
(tint + 0.0).to_bits().hash(&mut h);
}
h.finish()
}
impl Out {
pub fn new(doc: Document, page: Affine, frame: Rect) -> Self {
Self {
doc,
drawn: vec![],
warnings: vec![],
page,
frame,
next_clip: 0,
groups: vec![],
too_deep: 0,
elements: 0,
merge: None,
cmyk: 0,
rgb: 0,
spots: vec![],
spot_colors: HashMap::new(),
shadings: HashSet::new(),
tilings: HashMap::new(),
apart: 0,
}
}
pub fn warn(&mut self, w: &str) {
if !self.warnings.iter().any(|x| x == w) {
self.warnings.push(w.to_string());
}
}
/// Open a group: what is drawn until it ends ([`Self::end_group`]) goes in it.
pub fn begin_group(&mut self) {
if self.groups.len() >= MAX_NEST {
self.too_deep += 1;
self.warn(&format!("groups nested more than {MAX_NEST} deep were read as part of the group around them"));
return;
}
self.next_clip += 1;
self.groups.push(Arc::new(Clip { id: self.next_clip, region: None }));
}
/// End the innermost group open (an end without a beginning ends nothing).
pub fn end_group(&mut self) {
if self.too_deep > 0 {
self.too_deep -= 1;
} else {
self.groups.pop();
}
}
/// The chain an object drawn under `clips` is drawn in: the clips and the groups open, in the
/// order they opened.
pub fn chain(&self, clips: &[Arc<Clip>]) -> Vec<Arc<Clip>> {
if self.groups.is_empty() {
return clips.to_vec();
}
let mut chain = Vec::with_capacity(clips.len() + self.groups.len());
let (mut a, mut b) = (clips.iter().peekable(), self.groups.iter().peekable());
while let (Some(x), Some(y)) = (a.peek(), b.peek()) {
let next = if x.id < y.id { a.next() } else { b.next() };
chain.extend(next.cloned());
}
chain.extend(a.chain(b).cloned());
chain
}
/// Add `node` under `clips` (in the groups open); `None` when it was left out.
pub fn push(&mut self, mut node: Node, clips: &[Arc<Clip>]) -> Option<NodeId> {
self.merge = None;
let chain = self.chain(clips);
let size = node.path_data().map_or(1, |p| p.subpaths.iter().map(|s| s.anchors.len()).sum()) + chain.len();
self.elements = self.elements.saturating_add(size);
if self.drawn.len() >= MAX_NODES || self.elements > MAX_DRAWN {
self.warn(TOO_MUCH);
return None;
}
if !node.visual_bounds().is_none_or(sane) {
self.warn(FAR_AWAY);
return None;
}
node.id = self.doc.alloc_id();
let id = node.id;
self.drawn.push((chain, node));
Some(id)
}
/// Count a paint's colour model.
fn count(&mut self, p: &Paint) {
match p {
Paint::Solid { color: Color::Cmyk { .. }, swatch: None, .. } => self.cmyk += 1,
Paint::Solid { color: Color::Rgb { .. }, swatch: None, .. } => self.rgb += 1,
_ => {}
}
}
/// Fill `bp` with `paint`.
fn fill(&mut self, bp: BezPath, rule: FillRule, paint: Paint, overprint: bool, clips: &[Arc<Clip>]) {
self.count(&paint);
let item = AppearanceItem::Fill(FillLayer { overprint, ..FillLayer::new(paint) });
let mut n = Node::path(NodeId(0), PathData::from_bezpath(&bp), Appearance { items: vec![item], ..Appearance::default() });
if let NodeKind::Path { rule: r, .. } = &mut n.kind {
*r = rule;
}
if self.push(n, clips).is_some() {
self.merge = Some(bp);
}
}
/// Stroke `bp` with `st`: joins the object just filled with the same path, under the same
/// clips (`gsave fill grestore stroke`).
fn stroke(&mut self, bp: BezPath, st: StrokeLayer, clips: &[Arc<Clip>]) {
self.count(&st.paint);
if self.merge.as_ref() == Some(&bp) {
let here = self.chain(clips);
if let Some((chain, last)) = self.drawn.last_mut()
&& chain.iter().map(|c| c.id).eq(here.iter().map(|c| c.id))
{
last.appearance.items.push(AppearanceItem::Stroke(st));
self.merge = None;
return;
}
}
let n = Node::path(NodeId(0), PathData::from_bezpath(&bp), Appearance { items: vec![AppearanceItem::Stroke(st)], ..Appearance::default() });
self.push(n, clips);
}
/// A new clip of `path` inside `clips`; `None` for a rectangle around the whole page, which
/// clips nothing.
fn clip(&mut self, path: BezPath, rule: FillRule) -> Option<Arc<Clip>> {
// Only a polygon that really covers the page: a diamond touching its edges has the page as
// its bounding box but cuts its corners away (#1542).
if path.elements().len() <= 6 && vectorcraft_geom::hit::polygon_covers_rect(&path, self.frame, 0.5) {
return None;
}
self.next_clip += 1;
Some(Arc::new(Clip { id: self.next_clip, region: Some((path, rule)) }))
}
}
/// The smallest angle from `a` to `b` (radians, −π to π).
fn turn(a: f64, b: f64) -> f64 {
let d = (b - a).rem_euclid(std::f64::consts::TAU);
if d > std::f64::consts::PI { d - std::f64::consts::TAU } else { d }
}
/// The angle from `a` counterclockwise to `b` (degrees, 0 to 360), worked out from what is left
/// of each after whole turns.
fn ccw_turn(a: f64, b: f64) -> f64 {
(b % 360.0 - a % 360.0).rem_euclid(360.0)
}
impl Interp<'_> {
/// User space → document space.
pub fn xf(&self) -> Affine {
self.out.page * self.g.ctm
}
/// How much user-space lengths grow in the document.
pub fn scale(&self) -> f64 {
let s = self.xf().determinant().abs().sqrt();
if s.is_finite() { s } else { 0.0 }
}
/// The current point in user space.
pub fn user_point(&self) -> Res<Point> {
let p = self.g.cur.ok_or(PsError::Ps("nocurrentpoint", String::new()))?;
let inv = self.xf();
if inv.determinant().abs() < 1e-12 {
return ps_err("undefinedresult", "");
}
Ok(inv.inverse() * p)
}
pub fn gsave(&mut self) -> Res {
if self.saved.len() >= MAX_GSAVE {
return Err(PsError::Limit("too many saved graphics states"));
}
self.alloc(std::mem::size_of_val(self.g.path.elements()))?;
self.saved.push(self.g.clone());
Ok(())
}
pub fn grestore(&mut self) {
if let Some(g) = self.saved.pop() {
self.g = g;
}
}
// ---------- paths ----------
/// Room for one more element in the current path.
pub fn grow(&self) -> Res {
if self.g.path.elements().len() >= MAX_PATH {
return Err(PsError::Limit("a path has too many points"));
}
Ok(())
}
/// Count `n` path elements an operator makes, reads or copies against the operation budget,
/// an operation each: making or reading an element takes up to about as long as an operation,
/// and copying one takes less (measured).
fn spend_elements(&mut self, n: usize) -> Res {
self.spend(n as u64)
}
fn move_to(&mut self, p: Point) -> Res {
self.grow()?;
// Consecutive movetos: the last one counts.
if let Some(PathEl::MoveTo(_)) = self.g.path.elements().last() {
self.g.path.pop();
}
self.g.path.move_to(p);
self.g.cur = Some(p);
self.g.start = Some(p);
Ok(())
}
fn line_to(&mut self, p: Point) -> Res {
self.grow()?;
if self.g.cur.is_none() {
return ps_err("nocurrentpoint", "");
}
self.g.path.line_to(p);
self.g.cur = Some(p);
Ok(())
}
/// Path elements in user space appended (the first move a line when there is a current point),
/// each counted against the operation budget.
fn append_user(&mut self, els: impl Iterator<Item = PathEl>) -> Res {
let xf = self.xf();
for el in els {
self.spend_elements(1)?;
match el {
PathEl::MoveTo(p) => {
let p = xf * p;
if self.g.cur.is_some() { self.line_to(p)? } else { self.move_to(p)? }
}
PathEl::LineTo(p) => self.line_to(xf * p)?,
PathEl::QuadTo(a, p) => {
self.grow()?;
self.g.path.quad_to(xf * a, xf * p);
self.g.cur = Some(xf * p);
}
PathEl::CurveTo(a, b, p) => {
self.grow()?;
self.g.path.curve_to(xf * a, xf * b, xf * p);
self.g.cur = Some(xf * p);
}
PathEl::ClosePath => {
self.grow()?;
self.g.path.close_path();
}
}
}
Ok(())
}
/// `arc` / `arcn`: from angle `a1` to `a2` (degrees, counter-clockwise or clockwise).
fn arc(&mut self, c: Point, r: f64, a1: f64, a2: f64, ccw: bool) -> Res {
// `arc` raises `a2` by multiples of 360 until it is at least `a1`, and `arcn` lowers it
// until it is at most `a1` (PLRM 3rd ed., `arc` and `arcn` in chapter 8). Those sweeps,
// and the first endpoint, are worked out from what is left of each angle after whole turns.
let sweep = if ccw && a2 < a1 {
ccw_turn(a1, a2)
} else if !ccw && a2 > a1 {
-ccw_turn(a2, a1)
} else {
a2 - a1
};
let tau = std::f64::consts::TAU;
let sweep = sweep.to_radians().clamp(-tau * 4.0, tau * 4.0);
let a1 = (a1 % 360.0).to_radians();
let start = c + Vec2::new(r * a1.cos(), r * a1.sin());
self.append_user(std::iter::once(PathEl::MoveTo(start)))?;
let arc = kurbo::Arc::new(c, Vec2::new(r, r), a1, sweep, 0.0);
let tol = (0.01 / self.scale().max(1e-9)).max(1e-9);
self.append_user(arc.append_iter(tol))
}
/// `arct` / `arcto`: a line towards `p1` rounded with radius `r` into the line to `p2`; the
/// tangent points (user space).
fn arct(&mut self, p1: Point, p2: Point, r: f64) -> Res<[Point; 2]> {
let p0 = self.user_point()?;
let (u, v) = (p0 - p1, p2 - p1);
let (lu, lv) = (u.hypot(), v.hypot());
let cross = u.cross(v);
if lu < 1e-12 || lv < 1e-12 || cross.abs() < 1e-12 || r <= 0.0 {
self.append_user(std::iter::once(PathEl::LineTo(p1)))?;
return Ok([p1, p1]);
}
let (u, v) = (u / lu, v / lv);
let half = u.dot(v).clamp(-1.0, 1.0).acos() / 2.0;
let dist = r / half.tan();
let (t1, t2) = (p1 + u * dist, p1 + v * dist);
let bis = (u + v).normalize();
let c = p1 + bis * (r / half.sin());
let (a1, a2) = ((t1 - c).atan2(), (t2 - c).atan2());
self.append_user(std::iter::once(PathEl::LineTo(t1)))?;
let tol = (0.01 / self.scale().max(1e-9)).max(1e-9);
let arc = kurbo::Arc::new(c, Vec2::new(r, r), a1, turn(a1, a2), 0.0);
self.append_user(arc.append_iter(tol))?;
Ok([t1, t2])
}
fn rect_path(&self, x: f64, y: f64, w: f64, h: f64) -> BezPath {
let xf = self.xf();
let mut bp = BezPath::new();
bp.move_to(xf * Point::new(x, y));
bp.line_to(xf * Point::new(x + w, y));
bp.line_to(xf * Point::new(x + w, y + h));
bp.line_to(xf * Point::new(x, y + h));
bp.close_path();
bp
}
/// The rectangles of `rectfill`, `rectstroke` and `rectclip`: `x y w h` or an array of them.
fn rects(&mut self) -> Res<BezPath> {
let nums: Vec<f64> = match self.stack.last() {
Some(Obj::Array { .. }) => {
let items = self.pop_array()?;
// Five elements a rectangle, counted before the array is read.
self.spend_elements(items.len() / 4 * 5)?;
let v: Option<Vec<f64>> = items.borrow().iter().map(Obj::as_num).collect();
v.ok_or(PsError::Ps("typecheck", String::new()))?
}
_ => self.nums::<4>()?.to_vec(),
};
if nums.len() / 4 * 5 >= MAX_PATH {
return Err(PsError::Limit("a path has too many points"));
}
let mut bp = BezPath::new();
for [x, y, w, h] in nums.as_chunks::<4>().0 {
bp.extend(self.rect_path(*x, *y, *w, *h));
}
Ok(bp)
}
// ---------- painting ----------
fn fill_path(&mut self, bp: BezPath, rule: FillRule) -> Res {
if bp.elements().is_empty() || self.g.null {
return Ok(());
}
// A mesh pattern paints its shading inside the path.
if let Some((sh, m)) = self.g.mesh.clone() {
let clips = self.g.clips.clone();
self.clip_with(bp, rule);
let r = self.mesh_fill(&sh, m);
self.g.clips = clips;
return r;
}
if self.g.paint.is_none() {
return Ok(());
}
let (paint, overprint) = (self.g.paint.clone(), self.g.overprint);
self.out.fill(bp, rule, paint, overprint, &self.g.clips);
Ok(())
}
fn stroke_path(&mut self, bp: BezPath) -> Res {
if bp.elements().is_empty() || self.g.paint.is_none() || self.g.null {
return Ok(());
}
let st = self.stroke_layer()?;
self.out.stroke(bp, st, &self.g.clips);
Ok(())
}
/// The stroke the graphics state draws, in document space. Each entry of the dash pattern it
/// copies counts as an operation.
fn stroke_layer(&mut self) -> Res<StrokeLayer> {
self.spend(self.g.dash.len() as u64)?;
let k = self.scale();
let w = self.g.width * k;
let mut st = StrokeLayer::new(self.g.paint.clone(), if w > 0.0 && w.is_finite() { w } else { HAIRLINE });
st.cap = self.g.cap;
st.join = self.g.join;
st.miter_limit = self.g.miter.clamp(1.0, 500.0);
st.overprint = self.g.overprint;
let pattern: Vec<f64> = self.g.dash.iter().map(|d| d * k).collect();
let dash = Dash { pattern, offset: self.g.dash_offset * k, align_corners: false };
st.dash = dash.is_dashed().then_some(dash);
Ok(st)
}
/// The current path in user space, each element as its operator and points (quadratic
/// segments as cubic ones).
fn user_elements(&self) -> Res<Vec<(Op, Vec<Point>)>> {
let xf = self.xf();
if xf.determinant().abs() < 1e-12 {
return ps_err("undefinedresult", "");
}
let inv = xf.inverse();
let mut last = Point::ZERO;
let mut out = Vec::with_capacity(self.g.path.elements().len());
for el in self.g.path.elements() {
let (op, pts) = match *el {
PathEl::MoveTo(p) => (Op::MoveTo, vec![p]),
PathEl::LineTo(p) => (Op::LineTo, vec![p]),
PathEl::QuadTo(a, p) => (Op::CurveTo, vec![last + (a - last) * (2.0 / 3.0), p + (a - p) * (2.0 / 3.0), p]),
PathEl::CurveTo(a, b, p) => (Op::CurveTo, vec![a, b, p]),
PathEl::ClosePath => (Op::ClosePath, vec![]),
};
last = pts.last().copied().unwrap_or(last);
out.push((op, pts.into_iter().map(|p| inv * p).collect()));
}
Ok(out)
}
/// `pathforall`: the current path in user space, each element through its procedure.
fn path_for_all(&mut self) -> Res {
let close = self.pop_proc()?;
let curve = self.pop_proc()?;
let line = self.pop_proc()?;
let mv = self.pop_proc()?;
self.spend_elements(self.g.path.elements().len())?;
for (op, pts) in self.user_elements()? {
for p in pts {
self.push_num(p.x)?;
self.push_num(p.y)?;
}
let proc = match op {
Op::MoveTo => &mv,
Op::LineTo => &line,
Op::CurveTo => &curve,
_ => &close,
};
if !self.body(proc)? {
break;
}
}
Ok(())
}
/// Append user path `up` to the current path: a procedure of path operators, or the encoded
/// form (numbers and a string of operator codes).
fn user_path(&mut self, up: Obj) -> Res {
let items = up.items().cloned().ok_or(PsError::Ps("typecheck", "a user path".into()))?;
let encoded = match (items.get(0), items.get(1), items.len()) {
(Some(nums @ (Obj::Array { .. } | Obj::Str(_))), Some(Obj::Str(ops)), 2) => Some((nums, ops.to_vec())),
_ => None,
};
let Some((nums, ops)) = encoded else { return self.call(Obj::Array { items, exec: true }) };
// The operators by code, with how many operands each takes.
const OPS: [(Op, usize); 12] = [
(Op::SetBBox, 4),
(Op::MoveTo, 2),
(Op::RMoveTo, 2),
(Op::LineTo, 2),
(Op::RLineTo, 2),
(Op::CurveTo, 6),
(Op::RCurveTo, 6),
(Op::Arc, 5),
(Op::Arcn, 5),
(Op::Arct, 5),
(Op::ClosePath, 0),
(Op::UCache, 0),
];
let nums: Vec<f64> = match nums {
Obj::Array { items, .. } => {
// Each entry of the data array and each byte of the operator string counts as an
// element read.
self.spend_elements(items.len().saturating_add(ops.len()))?;
items.borrow().iter().filter_map(Obj::as_num).collect()
}
_ => return ps_err("typecheck", "an encoded user path"),
};
let mut next = nums.into_iter();
let mut repeat = 1usize;
for code in ops {
if code >= 32 {
repeat = usize::from(code - 32);
continue;
}
let &(op, n) = OPS.get(usize::from(code)).ok_or(PsError::Ps("rangecheck", "a user path".into()))?;
for _ in 0..std::mem::replace(&mut repeat, 1) {
for _ in 0..n {
let v = next.next().ok_or(PsError::Ps("rangecheck", "a user path".into()))?;
self.push_num(v)?;
}
// Each operator run counts as an operation.
self.spend(1)?;
self.op(op)?;
}
}
Ok(())
}
/// `ufill`, `ueofill`, `ustroke` (with an optional matrix): paint a user path, keeping the
/// current path.
fn paint_user_path(&mut self, op: Op) -> Res {
let m = match (op, self.stack.last()) {
(Op::UStroke, Some(Obj::Array { items, exec: false })) if items.len() == 6 && self.stack.len() >= 2 => Some(self.pop_matrix()?),
_ => None,
};
let up = self.pop()?;
self.gsave()?;
let depth = self.saved.len();
self.take_path();
let r = self.user_path(up).and_then(|()| {
if let Some(m) = m {
self.g.ctm *= m;
}
let bp = self.take_path();
match op {
Op::UStroke => self.stroke_path(bp),
_ => self.fill_path(bp, if op == Op::UEoFill { FillRule::EvenOdd } else { FillRule::NonZero }),
}
});
self.saved.truncate(depth);
self.grestore();
r
}
/// `upath`: the current path as a user path (a procedure, in user space).
fn upath(&mut self) -> Res {
self.pop_bool()?;
let elements = self.user_elements()?;
let b = self.xf().inverse().transform_rect_bbox(self.g.path.bounding_box());
let mut items: Vec<Obj> = [b.x0, b.y0, b.x1, b.y1].into_iter().map(Obj::Real).collect();
items.push(Obj::Op(Op::SetBBox));
for (op, pts) in elements {
items.extend(pts.iter().flat_map(|p| [Obj::Real(p.x), Obj::Real(p.y)]));
items.push(Obj::Op(op));
}
self.alloc(items.len() * std::mem::size_of::<Obj>())?;
self.push(Obj::proc(items))
}
/// `execform`: the form's `PaintProc` run with its matrix, clipped to its box.
fn exec_form(&mut self) -> Res {
let d = self.pop_dict()?;
let get = |k: &str| d.borrow().get(&Key::name(k)).cloned();
let m = get("Matrix").and_then(|o| o.items().and_then(|i| super::interp::matrix_of(&i.borrow()))).unwrap_or(Affine::IDENTITY);
let bbox: Vec<f64> = get("BBox").and_then(|o| o.items().map(|i| i.borrow().iter().filter_map(Obj::as_num).collect())).unwrap_or_default();
let paint = get("PaintProc").ok_or(PsError::Ps("undefined", "PaintProc".into()))?;
self.gsave()?;
let depth = self.saved.len();
self.g.ctm *= m;
if let [x0, y0, x1, y1] = bbox[..] {
let bp = self.rect_path(x0, y0, x1 - x0, y1 - y0);
self.take_path();
self.clip_with(bp, FillRule::NonZero);
}
self.push(Obj::Dict(d))?;
let r = self.call(paint);
self.saved.truncate(depth);
self.grestore();
r
}
pub(super) fn take_path(&mut self) -> BezPath {
self.g.cur = None;
self.g.start = None;
std::mem::take(&mut self.g.path)
}
fn clip_with(&mut self, path: BezPath, rule: FillRule) {
let Some(c) = self.out.clip(path, rule) else { return };
if self.g.clips.len() < MAX_NEST {
self.g.clips.push(c);
} else {
self.out.warn(&deep_clips());
}
}
/// The area the clip leaves, in document space (the page without a clip). The clip path read
/// counts against the operation budget.
fn clip_bounds(&mut self) -> Res<Rect> {
let (bounds, len) =
self.g.clips.iter().rev().find_map(|c| c.region.as_ref()).map_or((self.out.frame, 0), |(p, _)| (p.bounding_box(), p.elements().len()));
self.spend_elements(len)?;
Ok(bounds)
}
// ---------- colour ----------
/// A colour space operand.
pub(super) fn space_of(&mut self, o: &Obj, depth: u32) -> Res<Space> {
if depth > 4 {
return ps_err("limitcheck", "setcolorspace");
}
let (family, items) = match o {
Obj::Name(n) | Obj::Exec(n) => (n.clone(), vec![]),
Obj::Array { items, .. } => {
let v = items.to_vec();
let n = v.first().and_then(Obj::text).ok_or(PsError::Ps("typecheck", "setcolorspace".into()))?;
(n, v)
}
_ => return ps_err("typecheck", "setcolorspace"),
};
let arg = |i: usize| items.get(i).cloned().ok_or(PsError::Ps("rangecheck", "setcolorspace".into()));
Ok(match &*family {
"DeviceGray" | "CIEBasedA" | "CalGray" => Space::Gray,
"DeviceRGB" | "CIEBasedABC" | "CalRGB" | "Lab" | "CIEBasedDEF" => Space::Rgb,
"DeviceCMYK" | "CIEBasedDEFG" => Space::Cmyk,
"Pattern" => Space::Pattern(match items.get(1) {
Some(base) => Some(Rc::new(self.space_of(base, depth + 1)?)),
None => None,
}),
"ICCBased" => match arg(1)? {
Obj::Dict(d) => match d.borrow().get(&Key::name("N")).and_then(Obj::as_num) {
Some(1.0) => Space::Gray,
Some(4.0) => Space::Cmyk,
_ => Space::Rgb,
},
_ => Space::Rgb,
},
"Separation" | "DeviceN" => {
let names: Vec<Rc<str>> = match arg(1)? {
Obj::Array { items, .. } => items.borrow().iter().filter_map(Obj::text).collect(),
o => o.text().into_iter().collect(),
};
if names.len() > MAX_INKS {
return ps_err("limitcheck", "DeviceN");
}
let alt = self.space_of(&arg(2)?, depth + 1)?;
Space::Inks { names, alt: Rc::new(alt), tint: arg(3)? }
}
"Indexed" => {
let base = self.space_of(&arg(1)?, depth + 1)?;
let hival =
arg(2)?.as_num().filter(|v| (0.0..=MAX_HIVAL as f64).contains(v)).ok_or(PsError::Ps("rangecheck", "Indexed".into()))? as usize;
let n = base.n();
let table: Vec<f64> = match arg(3)? {
Obj::Str(s) => s.borrow().iter().map(|b| f64::from(*b) / 255.0).collect(),
p @ Obj::Array { .. } => {
let mut t = Vec::with_capacity((hival + 1) * n);
for i in 0..=hival {
self.push(Obj::Int(i as i64))?;
self.call(p.clone())?;
let mut c = vec![0.0; n];
for slot in c.iter_mut().rev() {
*slot = self.pop_num()?;
}
t.extend(c);
}
t
}
_ => return ps_err("typecheck", "Indexed"),
};
Space::Indexed { base: Rc::new(base), table: Rc::new(table) }
}
_ => return ps_err("undefined", &family),
})
}
fn set_space(&mut self, o: Obj) -> Res {
let space = self.space_of(&o, 0)?;
let comps = space.initial();
self.g.space = Rc::new(space);
self.g.space_obj = o;
self.set_comps(comps)
}
/// Set the colour to `comps` in the current space.
fn set_comps(&mut self, comps: Vec<f64>) -> Res {
let space = self.g.space.clone();
self.g.mesh = None;
self.g.paint = self.paint_of(&space, &comps)?;
self.g.comps = comps;
Ok(())
}
/// Set a process colour, its space with it.
fn set_process(&mut self, space: Space, name: &str, comps: Vec<f64>) -> Res {
self.g.space = Rc::new(space);
self.g.space_obj = Obj::name(name);
self.set_comps(comps)
}
/// The paint of colour `comps` in `space`: a spot ink links to its swatch at the tint.
pub fn paint_of(&mut self, space: &Space, comps: &[f64]) -> Res<Paint> {
Ok(match space {
Space::Inks { names, alt, tint } => {
let t = comps.first().copied().unwrap_or(1.0).clamp(0.0, 1.0) as f32;
match names.as_slice() {
[one] if &**one == "None" => Paint::None,
[one] if &**one == "All" => {
Paint::Solid { color: Color::cmyk(1.0, 1.0, 1.0, 1.0).tinted(t), swatch: Some(REGISTRATION.into()), tint: t }
}
[one] => {
let full = match self.out.spot_colors.get(one) {
Some(c) => *c,
None => {
let c = self.tint_color(alt, tint, &[1.0])?;
self.out.spots.push((one.to_string(), c));
// The tint transform may have listed this ink already: the color
// listed first stays.
self.out.spot_colors.entry(one.clone()).or_insert(c);
c
}
};
Paint::Solid { color: full.tinted(t), swatch: Some(one.to_string()), tint: t }
}
_ => Paint::solid(self.tint_color(alt, tint, comps)?),
}
}
Space::Indexed { base, table } => {
let n = base.n();
let i = comps.first().copied().unwrap_or(0.0).round().max(0.0) as usize;
let c = table.get(i * n..i * n + n).ok_or(PsError::Ps("rangecheck", "setcolor".into()))?.to_vec();
Paint::solid(process(base, &c).unwrap_or(Color::BLACK))
}
Space::Pattern(_) => Paint::None,
s => Paint::solid(process(s, comps).unwrap_or(Color::BLACK)),
})
}
/// The alternate colour of inks at `comps`, through the tint transform.
pub fn tint_color(&mut self, alt: &Space, tint: &Obj, comps: &[f64]) -> Res<Color> {
let base = self.stack.len();
for c in comps {
self.push_num(*c)?;
}
self.call(tint.clone())?;
let n = alt.n();
let at = self.stack.len().checked_sub(n).filter(|a| *a >= base).ok_or(PsError::Ps("stackunderflow", "tint transform".into()))?;
let v: Vec<f64> = self.stack.split_off(at).iter().map(|o| o.as_num().unwrap_or(0.0)).collect();
self.stack.truncate(base);
Ok(process(alt, &v).unwrap_or(Color::BLACK))
}
/// The current colour as grey, RGB or CMYK components (`currentgray` …).
fn current_rgb(&self) -> [f32; 3] {
self.g.paint.color().map_or([0.0; 3], |c| c.to_rgb_uncalibrated())
}
/// `setcolor` in a pattern space: the pattern operand, and before it an uncoloured
/// pattern's colour in the underlying space.
fn set_pattern(&mut self, base: Option<Rc<Space>>) -> Res {
let Obj::Dict(d) = self.pop()? else { return ps_err("typecheck", "") };
let uncoloured = d.borrow().get(&Key::name("PaintType")).and_then(Obj::as_num) == Some(2.0);
let under = match base.filter(|_| uncoloured) {
Some(base) => {
let mut c = vec![0.0; base.n()];
for slot in c.iter_mut().rev() {
*slot = self.pop_num()?;
}
Some(self.paint_of(&base, &c)?)
}
None => None,
};
self.g.mesh = None;
self.g.paint = self.pattern_paint(&d, under)?;
Ok(())
}
/// A pattern colour: a shading pattern's gradient, a tiling pattern's swatch (`under`: the
/// colour an uncoloured one paints in).
fn pattern_paint(&mut self, d: &DictRef, under: Option<Paint>) -> Res<Paint> {
let kind = d.borrow().get(&Key::name("PatternType")).and_then(Obj::as_num);
let sh = d.borrow().get(&Key::name("Shading")).cloned();
let m = d.borrow().get(&Key::name("VCMatrix")).and_then(|o| o.items().and_then(|i| super::interp::matrix_of(&i.borrow())));
// Pattern space → the document.
let m = self.out.page * m.unwrap_or(self.g.ctm);
match (kind, sh) {
(Some(2.0), Some(Obj::Dict(sh))) if is_mesh(&sh) => {
self.g.mesh = Some((sh, m));
Ok(Paint::None)
}
(Some(2.0), Some(Obj::Dict(sh))) => Ok(self.gradient(&sh, m)?.map_or(Paint::None, |g| Paint::Gradient(Box::new(g)))),
(Some(2.0), _) => ps_err("typecheck", "Shading"),
_ => self.tiling(d, under, m),
}
}
/// A tiling pattern as a pattern swatch (made the first time): its `PaintProc` drawn in
/// pattern space, one cell of `XStep` × `YStep` from the corner of its `BBox`; `m` maps pattern
/// space onto the document.
fn tiling(&mut self, d: &DictRef, under: Option<Paint>, m: Affine) -> Res<Paint> {
let get = |k: &str| d.borrow().get(&Key::name(k)).cloned();
let bbox: Vec<f64> = get("BBox").and_then(|o| o.items().map(|i| i.borrow().iter().filter_map(Obj::as_num).collect())).unwrap_or_default();
let [x0, y0, x1, y1] = bbox[..] else { return ps_err("rangecheck", "BBox") };
let step = |k: &str| get(k).and_then(|o| o.as_num()).map(f64::abs).filter(|v| v.is_finite() && *v > 1e-9 && *v < LIMIT);
let (Some(xs), Some(ys)) = (step("XStep"), step("YStep")) else { return ps_err("rangecheck", "XStep") };
let tile = Rect::from_origin_size((x0.min(x1), y0.min(y1)), (xs, ys));
let xf = m * Affine::translate(tile.origin().to_vec2());
let proc = get("PaintProc").ok_or(PsError::Ps("undefined", "PaintProc".into()))?;
let key = proc.items().cloned().ok_or(PsError::Ps("typecheck", "PaintProc".into()))?;
let hash = tiling_hash(&key, tile, under.as_ref());
let made = self.out.tilings.get(&hash).and_then(|list| list.iter().find(|(p, t, u, _)| p.same(&key) && *t == tile && *u == under));
if let Some((.., name)) = made {
return Ok(Paint::Pattern { pattern: name.clone(), xf });
}
if self.out.apart >= MAX_APART {
self.out.warn(NESTED_PATTERNS);
return Ok(Paint::solid(Color::gray(0.5)));
}
let paint = under.clone();
let art = self.draw_apart(Rect::new(x0, y0, x1, y1).abs(), |it| {
if let Some(p) = paint {
it.g.paint = p;
}
it.push(Obj::Dict(d.clone()))?;
it.call(proc)
})?;
if art.is_empty() {
return Ok(Paint::None);
}
// The document gets its patterns only here, so the n made so far are Pattern 1 to
// Pattern n.
let name = format!("Pattern {}", self.out.doc.patterns.len() + 1);
let mut def = PatternDef::new(&name, art);
def.tile = tile;
self.out.doc.patterns.push(def);
// A cell with a NaN corner equals no cell, so the lookup above could never find it.
if !tile.is_nan() {
self.out.tilings.entry(hash).or_insert_with(|| Vec::with_capacity(1)).push((key, tile, under, name.clone()));
}
Ok(Paint::Pattern { pattern: name, xf })
}
/// The art `draw` draws on its own, in its own space (user space is the document's, the page
/// `frame`), from a fresh graphics state with the current font: a pattern's cell, a glyph.
pub fn draw_apart(&mut self, frame: Rect, draw: impl FnOnce(&mut Self) -> Res) -> Res<Vec<Arc<Node>>> {
let fresh = GState { font: self.g.font.clone(), ..GState::default() };
let g = std::mem::replace(&mut self.g, fresh);
let saved = std::mem::take(&mut self.saved);
let drawn = std::mem::take(&mut self.out.drawn);
let page = std::mem::replace(&mut self.out.page, Affine::IDENTITY);
let frame = std::mem::replace(&mut self.out.frame, frame);
let merge = self.out.merge.take();
// The art isn't in the groups open around it.
let groups = std::mem::take(&mut self.out.groups);
let too_deep = std::mem::take(&mut self.out.too_deep);
self.out.apart += 1;
let r = draw(self);
self.out.apart -= 1;
let art = std::mem::replace(&mut self.out.drawn, drawn);
(self.out.page, self.out.frame, self.out.merge) = (page, frame, merge);
(self.out.groups, self.out.too_deep) = (groups, too_deep);
(self.g, self.saved) = (g, saved);
r?;
let mut nodes = vectorcraft_doc::clipnest::nest(&mut self.out.doc, art);
if !self.out.shadings.is_empty() {
collapse(&mut nodes, &self.out.shadings);
}
Ok(nodes)
}
/// An axial or radial shading as a gradient, `m` mapping its space onto the document.
fn gradient(&mut self, sh: &DictRef, m: Affine) -> Res<Option<GradientPaint>> {
let get = |k: &str| sh.borrow().get(&Key::name(k)).cloned();
let kind = get("ShadingType").and_then(|o| o.as_num()).unwrap_or(0.0);
if !is_gradient(sh) {
self.out.warn(UNKNOWN_SHADINGS);
return Ok(None);
}
let space = self.space_of(&get("ColorSpace").ok_or(PsError::Ps("undefined", "ColorSpace".into()))?, 0)?;
let nums = |o: Option<Obj>| -> Vec<f64> {
o.and_then(|o| o.items().map(|i| i.borrow().iter().filter_map(Obj::as_num).collect())).unwrap_or_default()
};
let coords = nums(get("Coords"));
let domain = nums(get("Domain"));
let (d0, d1) = (domain.first().copied().unwrap_or(0.0), domain.get(1).copied().unwrap_or(1.0));
let f = get("Function").ok_or(PsError::Ps("undefined", "Function".into()))?;
// Where the colour changes: the ends, and the bounds of stitched linear functions (unless
// there are too many).
let mut visits = 0;
let points = linear_points(&f, 0, &mut visits).filter(|p| p.len() <= MAX_STITCH_STOPS);
let stitched = points.is_some();
let ts: Vec<f64> = match points {
Some(mut pts) => {
pts.retain(|t| (d0.min(d1)..=d0.max(d1)).contains(t));
pts.iter().map(|t| if d1 != d0 { (t - d0) / (d1 - d0) } else { 0.0 }).chain([0.0, 1.0]).collect()
}
None => (0..=SHADING_SAMPLES).map(|i| i as f64 / SHADING_SAMPLES as f64).collect(),
};
let mut ts = ts;
ts.sort_by(f64::total_cmp);
ts.dedup_by(|a, b| (*a - *b).abs() < 1e-9);
let radial = kind == 3.0;
let c = |i: usize| coords.get(i).copied().unwrap_or(0.0);
let (r0, r1) = (c(2).max(0.0), c(5).max(1e-6));
let mut stops = Vec::with_capacity(ts.len());
let stop = |paint: Paint, offset: f64| match paint {
Paint::Solid { color, swatch, tint } => GradientStop { swatch, tint, ..GradientStop::new(offset as f32, color) },
_ => GradientStop::new(offset as f32, Color::BLACK),
};
for t in ts {
let comps = self.eval(&f, &[d0 + (d1 - d0) * t], 0)?;
let paint = self.paint_of(&space, &comps)?;
let offset = if radial { (r0 + (r1 - r0) * t) / r1 } else { t };
// At a bound of a stitched function the pieces need not meet (a hard colour change):
// the colour just before the bound is a stop at the same offset, ahead of the one
// after it.
if stitched && t > 0.0 && t < 1.0 {
let comps = self.eval(&f, &[d0 + (d1 - d0) * (t - 1e-9)], 0)?;
let before = stop(self.paint_of(&space, &comps)?, offset);
if before.color != stop(paint.clone(), offset).color {
stops.push(before);
}
}
stops.push(stop(paint, offset));
}
let (gk, geom) = if radial {
let centre = Point::new(c(3), c(4));
let mut geom = GradientGeom { start: centre, end: centre + Vec2::new(r1, 0.0), aspect: 1.0, focal: None };
geom.set_focal(Some(Point::new(c(0), c(1))));
geom.transform(m, GradientKind::Radial);
(GradientKind::Radial, geom)
} else {
(GradientKind::Linear, GradientGeom { start: m * Point::new(c(0), c(1)), end: m * Point::new(c(2), c(3)), aspect: 1.0, focal: None })
};
let mut g = GradientPaint::new(Gradient { kind: gk, stops });
g.geom = Some(geom);
Ok(Some(g))
}
/// A function (types 0, 2 and 3; an array of them, one per component) at `x` (types 2 and 3
/// take one input).
pub fn eval(&mut self, f: &Obj, x: &[f64], depth: u32) -> Res<Vec<f64>> {
self.eval_inner(f, x, depth, &mut 0)
}
/// [`Self::eval`], with `visits` (shared by the whole recursion, incremented on every call)
/// counting the functions visited against [`MAX_FUNCTION_VISITS`], as [`linear_points`] counts
/// its nodes. The depth limit alone stops a function that refers to itself, but not one that
/// fans out through shared arrays: when four levels of arrays each have 100 entries that all
/// refer to the same array of the level below (at the last level, the same function), one
/// evaluation would visit 101,010,101 functions. Each function visited also counts
/// [`FUNCTION_OPS`] against the operation budget.
fn eval_inner(&mut self, f: &Obj, x: &[f64], depth: u32, visits: &mut usize) -> Res<Vec<f64>> {
*visits += 1;
if depth > 8 || *visits > MAX_FUNCTION_VISITS {
return ps_err("limitcheck", "Function");
}
self.spend(FUNCTION_OPS)?;
let d = match f {
Obj::Dict(d) => d.clone(),
Obj::Array { items, .. } => {
// Each entry is at least one more function: an array with more entries than the
// visits left raises `limitcheck` before it is copied.
if items.len() > MAX_FUNCTION_VISITS - *visits {
return ps_err("limitcheck", "Function");
}
let fs = items.to_vec();
let mut out = vec![];
for g in &fs {
out.extend(self.eval_inner(g, x, depth + 1, visits)?.first().copied());
}
return Ok(out);
}
_ => return ps_err("typecheck", "Function"),
};
let get = |k: &str| d.borrow().get(&Key::name(k)).cloned();
let nums = |o: Option<Obj>| -> Vec<f64> {
o.and_then(|o| o.items().map(|i| i.borrow().iter().filter_map(Obj::as_num).collect())).unwrap_or_default()
};
let domain = nums(get("Domain"));
let t = x.first().copied().unwrap_or(0.0);
let t = match domain.as_slice() {
[a, b, ..] => t.clamp(a.min(*b), a.max(*b)),
_ => t,
};
match get("FunctionType").and_then(|o| o.as_num()) {
Some(0.0) => self.sampled(&d, x),
Some(2.0) => {
let c0 = nums(get("C0"));
let c1 = nums(get("C1"));
let (c0, c1) = (if c0.is_empty() { vec![0.0] } else { c0 }, if c1.is_empty() { vec![1.0] } else { c1 });
let n = get("N").and_then(|o| o.as_num()).unwrap_or(1.0);
let x = t.max(0.0).powf(n);
Ok(c0.iter().zip(&c1).map(|(a, b)| a + x * (b - a)).collect())
}
Some(3.0) => {
// Read the pieces through the borrow and clone only the one we recurse into: a
// `Functions` array of millions (shared, or reached again through a cycle) must
// not be copied on every call and at every live recursion level.
let functions = get("Functions");
let fs = functions.as_ref().and_then(Obj::items);
let n = fs.map_or(0, Shared::len);
let bounds = nums(get("Bounds"));
let encode = nums(get("Encode"));
let (lo, hi) = (domain.first().copied().unwrap_or(0.0), domain.get(1).copied().unwrap_or(1.0));
let k = bounds.iter().take_while(|b| t >= **b).count().min(n.saturating_sub(1));
let a = if k == 0 { lo } else { bounds.get(k - 1).copied().unwrap_or(lo) };
let b = bounds.get(k).copied().unwrap_or(hi);
let (e0, e1) = (encode.get(2 * k).copied().unwrap_or(0.0), encode.get(2 * k + 1).copied().unwrap_or(1.0));
let u = if b != a { e0 + (t - a) / (b - a) * (e1 - e0) } else { e0 };
let g = fs.and_then(|fs| fs.get(k)).ok_or(PsError::Ps("rangecheck", "Functions".into()))?;
self.eval_inner(&g, &[u], depth + 1, visits)
}
_ => {
self.out.warn(UNKNOWN_FUNCTIONS);
let r = nums(get("Range"));
Ok(r.chunks(2).map(|p| p.iter().sum::<f64>() / 2.0).collect())
}
}
}
// ---------- the operators ----------
pub fn graphics_op(&mut self, op: Op) -> Res {
use Op::*;
match op {
Gsave => self.gsave()?,
Grestore => self.grestore(),
GrestoreAll => {
if let Some(g) = self.saved.first().cloned() {
self.saved.clear();
self.g = g;
}
}
InitGraphics => {
let font = self.g.font.take();
self.g = GState { font, null: self.g.null, ..GState::default() };
}
NullDevice => {
self.g.null = true;
self.g.ctm = Affine::IDENTITY;
self.g.clips.clear();
}
ExecForm => self.exec_form()?,
StrokePath => {
let bp = self.take_path();
let mut st = self.stroke_layer()?;
// A dash pattern far finer than the path would make countless pieces.
let period: f64 = st.dash.as_ref().map_or(0.0, |d| d.pattern.iter().sum());
let periods = if st.dash.is_some() && period > 0.0 { bp.perimeter(1e-3) / period } else { 0.0 };
if st.dash.is_some() && !(period > 0.0 && periods < MAX_DASHES) {
st.dash = None;
}
let tol = (0.01 / self.scale().max(1e-9)).clamp(1e-4, 1.0);
let outline = vectorcraft_effects::stroke::line_outline(&bp, &st, st.width, tol);
// The path, each entry of the dash pattern in each period, and the outline count
// as elements.
let dashes = st.dash.as_ref().map_or(0.0, |d| periods * d.pattern.len() as f64) as usize;
self.spend_elements(bp.elements().len().saturating_add(dashes).saturating_add(outline.elements().len()))?;
if outline.elements().len() >= MAX_PATH {
return Err(PsError::Limit("a path has too many points"));
}
self.g.cur = outline.elements().iter().rev().find_map(|e| e.end_point());
self.g.start = self.g.cur;
self.g.path = outline;
}
PathForAll => self.path_for_all()?,
UFill | UEoFill | UStroke => self.paint_user_path(op)?,
UAppend => {
let up = self.pop()?;
self.user_path(up)?;
}
UPath => self.upath()?,
SetBBox => {
self.nums::<4>()?;
}
CurrentHsbColor => {
let [r, g, b] = self.current_rgb();
let (max, min) = (r.max(g).max(b), r.min(g).min(b));
let d = max - min;
let h = if d <= 0.0 {
0.0
} else if max == r {
((g - b) / d).rem_euclid(6.0) / 6.0
} else if max == g {
((b - r) / d + 2.0) / 6.0
} else {
((r - g) / d + 4.0) / 6.0
};
let s = if max > 0.0 { d / max } else { 0.0 };
for v in [h, s, max] {
self.push_num(f64::from(v))?;
}
}
CurrentColorRendering => {
let d = super::obj::Dict::from([(Key::name("ColorRenderingType"), Obj::Int(1))]);
self.push(Obj::dict(d))?;
}
FindColorRendering => {
self.pop()?;
self.push(Obj::name("DefaultColorRendering"))?;
self.push(Obj::Bool(false))?;
}
CurrentColorScreen => {
for _ in 0..4 {
self.push(Obj::Real(60.0))?;
self.push(Obj::Real(45.0))?;
self.push(Obj::proc(vec![]))?;
}
}
CurrentSmoothness => self.push(Obj::Real(0.02))?,
SetHalftonePhase => {
self.nums::<2>()?;
}
CurrentHalftonePhase => {
self.push(Obj::Int(0))?;
self.push(Obj::Int(0))?;
}
GStateNew => {
self.spend_elements(self.g.path.elements().len())?;
self.push(Obj::GState(Rc::new(self.g.clone())))?;
}
CurrentGState => {
self.pop()?;
self.spend_elements(self.g.path.elements().len())?;
self.push(Obj::GState(Rc::new(self.g.clone())))?;
}
SetGState => match self.pop()? {
Obj::GState(g) => {
self.spend_elements(g.path.elements().len())?;
self.g = (*g).clone();
}
_ => return ps_err("typecheck", ""),
},
SetLineWidth => self.g.width = self.pop_num()?.abs(),
CurrentLineWidth => self.push_num(self.g.width)?,
SetLineCap => {
self.g.cap = match self.pop_int()? {
1 => LineCap::Round,
2 => LineCap::Square,
_ => LineCap::Butt,
}
}
CurrentLineCap => self.push(Obj::Int(self.g.cap as i64))?,
SetLineJoin => {
self.g.join = match self.pop_int()? {
1 => LineJoin::Round,
2 => LineJoin::Bevel,
_ => LineJoin::Miter,
}
}
CurrentLineJoin => self.push(Obj::Int(self.g.join as i64))?,
SetMiterLimit => self.g.miter = self.pop_num()?,
CurrentMiterLimit => self.push_num(self.g.miter)?,
SetDash => {
let offset = self.pop_num()?;
let items = self.pop_array()?;
if items.len() > MAX_DASH_ENTRIES {
return ps_err("limitcheck", "");
}
// Each entry read counts as an operation.
self.spend(items.len() as u64)?;
let v: Option<Vec<f64>> = items.borrow().iter().map(|o| o.as_num().filter(|v| v.is_finite() && *v >= 0.0)).collect();
self.g.dash = v.ok_or(PsError::Ps("typecheck", String::new()))?.into();
self.g.dash_offset = offset;
}
CurrentDash => {
// The array made counts against the memory budget.
self.alloc(self.g.dash.len() * std::mem::size_of::<Obj>())?;
let d = self.g.dash.iter().map(|v| Obj::Real(*v)).collect();
self.push(Obj::array(d))?;
self.push_num(self.g.dash_offset)?;
}
SetGray => {
let [g] = self.nums()?;
self.set_process(Space::Gray, "DeviceGray", vec![g])?;
}
SetRgbColor => {
let c = self.nums::<3>()?;
self.set_process(Space::Rgb, "DeviceRGB", c.to_vec())?;
}
SetHsbColor => {
let [h, s, b] = self.nums()?;
let [r, g, bl] =
Color::from_hsb((h.rem_euclid(1.0) * 360.0) as f32, s.clamp(0.0, 1.0) as f32, b.clamp(0.0, 1.0) as f32).to_rgb_uncalibrated();
self.set_process(Space::Rgb, "DeviceRGB", vec![f64::from(r), f64::from(g), f64::from(bl)])?;
}
SetCmykColor => {
let c = self.nums::<4>()?;
self.set_process(Space::Cmyk, "DeviceCMYK", c.to_vec())?;
}
CurrentGray => {
let [r, g, b] = self.current_rgb();
self.push_num(f64::from(0.3 * r + 0.59 * g + 0.11 * b))?;
}
CurrentRgbColor => {
for v in self.current_rgb() {
self.push_num(f64::from(v))?;
}
}
CurrentCmykColor => {
let c = self.g.paint.color().map_or([0.0, 0.0, 0.0, 1.0], |c| c.to_cmyk());
for v in c {
self.push_num(f64::from(v))?;
}
}
SetColorSpace => {
let o = self.pop()?;
self.set_space(o)?;
}
CurrentColorSpace => {
let o = match &self.g.space_obj {
o @ Obj::Array { .. } => o.clone(),
o => Obj::array(vec![o.clone()]),
};
self.push(o)?;
}
SetColor => {
if let Space::Pattern(base) = &*self.g.space {
self.set_pattern(base.clone())?;
} else {
let n = self.g.space.n();
let mut c = vec![0.0; n];
for slot in c.iter_mut().rev() {
*slot = self.pop_num()?;
}
self.set_comps(c)?;
}
}
CurrentColor => {
for v in self.g.comps.clone() {
self.push_num(v)?;
}
}
// `[/Pattern <current space>] setcolorspace setcolor`, unless in a pattern space already.
SetPattern => {
if !matches!(*self.g.space, Space::Pattern(_)) {
self.g.space = Rc::new(Space::Pattern(Some(self.g.space.clone())));
self.g.space_obj = Obj::array(vec![Obj::name("Pattern"), self.g.space_obj.clone()]);
}
let Space::Pattern(base) = &*self.g.space else { return ps_err("typecheck", "") };
self.set_pattern(base.clone())?;
}
MakePattern => {
let m = self.pop_matrix()?;
let d = self.pop_dict()?;
let mut copy = d.borrow().clone();
copy.insert(Key::name("VCMatrix"), matrix_obj(self.g.ctm * m));
self.push(Obj::dict(copy))?;
}
SetOverprint => self.g.overprint = self.pop_bool()?,
CurrentOverprint => self.push(Obj::Bool(self.g.overprint))?,
SetFlat | SetHalftone | SetTransfer | SetBlackGeneration | SetUnderColorRemoval | SetColorRendering | SetSmoothness | SetStrokeAdjust
| SetPageDevice | SetUserParams | SetSystemParams => {
self.pop()?;
}
SetScreen => {
for _ in 0..3 {
self.pop()?;
}
}
SetColorTransfer => {
for _ in 0..4 {
self.pop()?;
}
}
SetColorScreen => {
for _ in 0..12 {
self.pop()?;
}
}
SetCacheLimit => {
self.pop()?;
}
SetUCacheParams => {
let at = self.stack.iter().rposition(|o| matches!(o, Obj::Mark)).ok_or(PsError::Ps("unmatchedmark", String::new()))?;
self.stack.truncate(at);
}
UCache => {}
CurrentScreen => {
self.push(Obj::Real(60.0))?;
self.push(Obj::Real(45.0))?;
self.push(Obj::proc(vec![]))?;
}
CurrentTransfer | CurrentBlackGeneration | CurrentUnderColorRemoval => self.push(Obj::proc(vec![]))?,
CurrentColorTransfer => {
for _ in 0..4 {
self.push(Obj::proc(vec![]))?;
}
}
CurrentHalftone => {
let mut d = super::obj::Dict::new();
d.insert(Key::name("HalftoneType"), Obj::Int(1));
d.insert(Key::name("Frequency"), Obj::Real(60.0));
d.insert(Key::name("Angle"), Obj::Real(45.0));
d.insert(Key::name("SpotFunction"), Obj::proc(vec![]));
self.push(Obj::dict(d))?;
}
FindEncoding => {
self.pop()?;
self.push(Obj::array(vec![Obj::name(".notdef"); 256]))?;
}
CurrentFlat => self.push(Obj::Real(1.0))?,
CurrentStrokeAdjust => self.push(Obj::Bool(false))?,
CurrentPageDevice => {
let mut d = super::obj::Dict::new();
let f = self.out.frame;
d.insert(Key::name("PageSize"), Obj::array(vec![Obj::Real(f.width()), Obj::Real(f.height())]));
self.push(Obj::dict(d))?;
}
// The cache and stack sizes programs read (they `get` them without asking first).
CurrentUserParams | CurrentSystemParams => {
let keys: &[&str] = if op == CurrentUserParams {
&["MaxFontItem", "MaxFormItem", "MaxPatternItem", "MaxUPathItem", "MaxOpStack", "MaxDictStack", "MaxExecStack"]
} else {
&["MaxFontCache", "MaxFormCache", "MaxPatternCache", "MaxUPathCache", "MaxScreenStorage", "MaxDisplayList"]
};
let d: super::obj::Dict = keys.iter().map(|k| (Key::name(k), Obj::Int(1 << 20))).collect();
self.push(Obj::dict(d))?;
}
ShowPage => return Err(PsError::Quit),
CopyPage | ErasePage => {}
Matrix => self.push(matrix_obj(Affine::IDENTITY))?,
IdentMatrix | DefaultMatrix => {
let items = self.pop_array()?;
self.put_matrix(items, Affine::IDENTITY)?;
}
CurrentMatrix => {
let items = self.pop_array()?;
self.put_matrix(items, self.g.ctm)?;
}
SetMatrix => self.g.ctm = self.pop_matrix()?,
InitMatrix => self.g.ctm = Affine::IDENTITY,
Concat => {
let m = self.pop_matrix()?;
self.g.ctm *= m;
}
ConcatMatrix => {
let out = self.pop_array()?;
let (m2, m1) = (self.pop_matrix()?, self.pop_matrix()?);
self.put_matrix(out, m2 * m1)?;
}
Translate | Scale | Rotate => {
let into = match self.stack.last() {
Some(Obj::Array { .. }) => Some(self.pop_array()?),
_ => None,
};
let m = match op {
Rotate => Affine::rotate(self.pop_num()?.to_radians()),
Translate => {
let [x, y] = self.nums()?;
Affine::translate((x, y))
}
_ => {
let [x, y] = self.nums()?;
Affine::scale_non_uniform(x, y)
}
};
match into {
Some(items) => self.put_matrix(items, m)?,
None => self.g.ctm *= m,
}
}
Transform | ITransform | DTransform | IDTransform => {
let m = match self.stack.last() {
Some(Obj::Array { .. }) => self.pop_matrix()?,
_ => self.g.ctm,
};
let [x, y] = self.nums()?;
let inverse = matches!(op, ITransform | IDTransform);
if inverse && m.determinant().abs() < 1e-12 {
return ps_err("undefinedresult", "");
}
let m = if inverse { m.inverse() } else { m };
let p = if matches!(op, DTransform | IDTransform) {
let v = m * Point::new(x, y) - m * Point::ZERO;
Point::new(v.x, v.y)
} else {
m * Point::new(x, y)
};
self.push_num(p.x)?;
self.push_num(p.y)?;
}
InvertMatrix => {
let out = self.pop_array()?;
let m = self.pop_matrix()?;
if m.determinant().abs() < 1e-12 {
return ps_err("undefinedresult", "");
}
self.put_matrix(out, m.inverse())?;
}
NewPath => {
self.take_path();
}
CurrentPoint => {
let p = self.user_point()?;
self.push_num(p.x)?;
self.push_num(p.y)?;
}
MoveTo => {
let [x, y] = self.nums()?;
let p = self.xf() * Point::new(x, y);
self.move_to(p)?;
}
RMoveTo => {
let [dx, dy] = self.nums()?;
let p = self.user_point()? + Vec2::new(dx, dy);
let p = self.xf() * p;
self.move_to(p)?;
}
LineTo => {
let [x, y] = self.nums()?;
self.line_to(self.xf() * Point::new(x, y))?;
}
RLineTo => {
let [dx, dy] = self.nums()?;
let p = self.user_point()? + Vec2::new(dx, dy);
self.line_to(self.xf() * p)?;
}
CurveTo | RCurveTo => {
let v = self.nums::<6>()?;
let o = if op == RCurveTo { self.user_point()?.to_vec2() } else { Vec2::ZERO };
if self.g.cur.is_none() {
return ps_err("nocurrentpoint", "");
}
let xf = self.xf();
let [a, b, p] = [0, 2, 4].map(|i| xf * (Point::new(v[i], v[i + 1]) + o));
self.grow()?;
self.g.path.curve_to(a, b, p);
self.g.cur = Some(p);
}
Arc | Arcn => {
let [x, y, r, a1, a2] = self.nums()?;
self.arc(Point::new(x, y), r, a1, a2, op == Arc)?;
}
Arct | Arcto => {
let [x1, y1, x2, y2, r] = self.nums()?;
let [t1, t2] = self.arct(Point::new(x1, y1), Point::new(x2, y2), r)?;
if op == Arcto {
for v in [t1.x, t1.y, t2.x, t2.y] {
self.push_num(v)?;
}
}
}
ClosePath => {
if self.g.cur.is_some() {
self.grow()?;
self.g.path.close_path();
self.g.cur = self.g.start;
}
}
FlattenPath | ReversePath => {}
PathBBox => {
if self.g.path.elements().is_empty() {
return ps_err("nocurrentpoint", "");
}
let xf = self.xf();
if xf.determinant().abs() < 1e-12 {
return ps_err("undefinedresult", "");
}
self.spend_elements(self.g.path.elements().len())?;
let b = xf.inverse().transform_rect_bbox(self.g.path.bounding_box());
for v in [b.x0, b.y0, b.x1, b.y1] {
self.push_num(v)?;
}
}
ClipPath => {
let b = self.clip_bounds()?;
self.take_path();
self.g.path = b.to_path(0.1);
self.g.cur = Some(b.origin());
self.g.start = self.g.cur;
}
InitClip => self.g.clips.clear(),
ClipSave => {
if self.g.saved_clips.len() >= MAX_GSAVE {
return Err(PsError::Limit("too many saved clips"));
}
self.alloc(std::mem::size_of_val(self.g.clips.as_slice()))?;
let clips = self.g.clips.as_slice().into();
self.g.saved_clips.push(clips);
}
ClipRestore => {
if let Some(clips) = self.g.saved_clips.pop() {
self.g.clips = clips.to_vec();
}
}
Clip | EoClip => {
self.spend_elements(self.g.path.elements().len())?;
let bp = self.g.path.clone();
self.clip_with(bp, if op == EoClip { FillRule::EvenOdd } else { FillRule::NonZero });
}
RectClip => {
let bp = self.rects()?;
self.take_path();
self.clip_with(bp, FillRule::NonZero);
}
Fill | EoFill => {
let bp = self.take_path();
self.fill_path(bp, if op == EoFill { FillRule::EvenOdd } else { FillRule::NonZero })?;
}
Stroke => {
let bp = self.take_path();
self.stroke_path(bp)?;
}
RectFill => {
let bp = self.rects()?;
self.fill_path(bp, FillRule::NonZero)?;
}
RectStroke => {
let m = match self.stack.last() {
Some(Obj::Array { items, .. }) if items.len() == 6 => Some(self.pop_matrix()?),
_ => None,
};
let mut bp = self.rects()?;
if let Some(m) = m {
let xf = self.xf();
bp.apply_affine(xf * m * xf.inverse());
}
self.stroke_path(bp)?;
}
ShFill => {
let d = self.pop_dict()?;
if self.g.null {
return Ok(());
}
if is_mesh(&d) {
let m = self.xf();
return self.mesh_fill(&d, m);
}
let m = self.xf();
if let Some(g) = self.gradient(&d, m)? {
let area = self.clip_bounds()?;
let paint = Paint::Gradient(Box::new(g));
let clips = self.g.clips.clone();
self.out.fill(area.to_path(0.1), FillRule::NonZero, paint, self.g.overprint, &clips);
if let Some((_, n)) = self.out.drawn.last() {
let id = n.id;
self.out.shadings.insert(id);
}
}
}
_ => self.data_op(op)?,
}
Ok(())
}
}
/// Is shading `sh` axial or radial (a gradient)?
fn is_gradient(sh: &DictRef) -> bool {
matches!(sh.borrow().get(&Key::name("ShadingType")).and_then(Obj::as_num), Some(2.0 | 3.0))
}
/// Is shading `sh` one [`Interp::mesh_fill`] paints?
fn is_mesh(sh: &DictRef) -> bool {
matches!(sh.borrow().get(&Key::name("ShadingType")).and_then(Obj::as_num), Some(1.0 | 4.0 | 5.0 | 6.0 | 7.0))
}
/// Where a stitched function of linear interpolations changes slope (its bounds), when it is
/// one; `None` for functions that must be sampled. `depth` bounds the nesting the same way
/// [`Interp::eval`] does (`> 8`) and `visits` (shared by the whole recursion, incremented on
/// every call) bounds the total nodes looked at. Both are needed: the depth limit alone stops a
/// function that refers to itself in a cycle, but not one that fans out through shared children
/// (1025 references to a child a few levels deep is 1025^levels nodes without the visit cap). A
/// well-formed stitch of linear pieces is one level, at most `MAX_STITCH_STOPS + 1` pieces, so it
/// costs at most `MAX_STITCH_STOPS + 2` visits and is never cut off.
fn linear_points(f: &Obj, depth: u32, visits: &mut usize) -> Option<Vec<f64>> {
*visits += 1;
if depth > 8 || *visits > 2 * (MAX_STITCH_STOPS + 1) {
return None;
}
let Obj::Dict(d) = f else { return None };
let d = d.borrow();
let num = |k: &str| d.get(&Key::name(k)).and_then(Obj::as_num);
match num("FunctionType")? {
2.0 if num("N").unwrap_or(1.0) == 1.0 => Some(vec![]),
3.0 => {
let items = d.get(&Key::name("Functions"))?.items()?;
// More pieces than the stop cap allows: sample the function instead (tested before
// `to_vec`, so a huge `Functions` array is not copied only to be rejected). Each stop
// runs [`Interp::eval`], which reads one piece, so a short `Bounds` with a huge
// `Functions` would still be O(stops * Functions) to find each piece without this.
if items.len() > MAX_STITCH_STOPS + 1 {
return None;
}
let fs = items.to_vec();
if !fs.iter().all(|g| linear_points(g, depth + 1, visits).is_some_and(|p| p.is_empty())) {
return None;
}
d.get(&Key::name("Bounds"))?.items().map(|i| i.borrow().iter().filter_map(Obj::as_num).collect())
}
_ => None,
}
}
/// The clip path a clipping group of one shading rectangle (`shfill` through a clip) can be
/// filled with instead.
pub(crate) fn collapse(nodes: &mut [Arc<Node>], shadings: &HashSet<NodeId>) {
for n in nodes.iter_mut() {
let NodeKind::Group { children, clip: true } = &n.kind else {
if let Some(c) = Arc::make_mut(n).children_mut() {
collapse(c, shadings);
}
continue;
};
if let [clip, fill] = children.as_slice()
&& shadings.contains(&fill.id)
&& let (NodeKind::Path { path, rule, clipping: true, .. }, NodeKind::Path { .. }) = (&clip.kind, &fill.kind)
{
let mut p = Node::path(fill.id, path.clone(), (*fill.appearance).clone());
if let NodeKind::Path { rule: r, .. } = &mut p.kind {
*r = *rule;
}
*n = Arc::new(p);
continue;
}
if let Some(c) = Arc::make_mut(n).children_mut() {
collapse(c, shadings);
}
}
}