diff --git a/crates/eps/src/import/graphics.rs b/crates/eps/src/import/graphics.rs index 5d4f7b9fa..bd6f25287 100644 --- a/crates/eps/src/import/graphics.rs +++ b/crates/eps/src/import/graphics.rs @@ -17,6 +17,14 @@ 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, clamp_between, ps_err}; +mod ops; +mod paint; +mod path; +mod state; + +pub(crate) use paint::collapse; +pub(crate) use state::{GState, Out, Space, process}; + /// 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 @@ -61,1775 +69,3 @@ const NESTED_PATTERNS: &str = "patterns nested too deeply are filled with mid-gr 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>, - alt: Rc, - tint: Obj, - }, - Indexed { - base: Rc, - table: Rc>, - }, - /// Patterns; an uncoloured one paints in a colour of the underlying space. - Pattern(Option>), -} - -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 { - 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 { - 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, - /// The `setcolorspace` operand, for `currentcolorspace`. - pub space_obj: Obj, - pub comps: Vec, - 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, - pub start: Option, - pub clips: Vec>, - /// The clips `clipsave` saved, for `cliprestore`. - pub saved_clips: Vec]>>, - pub overprint: bool, - pub font: Option, - /// `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, - /// 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>, - 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, - /// 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, Color>, - /// Rectangles painted with a shading (`shfill`): a clipping group of one is the clip path - /// filled with it. - pub shadings: HashSet, - /// 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>, - /// 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, Rect, Option, 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, 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]) -> Vec> { - 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]) -> Option { - 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]) { - 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]) { - 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> { - // 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 { - 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) -> 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 { - let nums: Vec = 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> = 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 { - 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 = 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)>> { - 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 = 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 = [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::())?; - 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 = 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 { - 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 { - 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> = 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 = 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) -> 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) -> 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 { - 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 { - 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 = 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>) -> 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) -> Res { - 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, m: Affine) -> Res { - let get = |k: &str| d.borrow().get(&Key::name(k)).cloned(); - let bbox: Vec = 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>> { - 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> { - 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| -> Vec { - 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 = 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::new(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> { - 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> { - *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| -> Vec { - 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, ..] => clamp_between(t, *a, *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::() / 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.alloc(std::mem::size_of_val(self.g.path.elements()))?; - self.push(Obj::GState(Rc::new(self.g.clone())))?; - } - CurrentGState => { - self.pop()?; - self.spend_elements(self.g.path.elements().len())?; - self.alloc(std::mem::size_of_val(self.g.path.elements()))?; - 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> = 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::())?; - 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 ] 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> { - *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], shadings: &HashSet) { - 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); - } - } -} diff --git a/crates/eps/src/import/graphics/ops.rs b/crates/eps/src/import/graphics/ops.rs new file mode 100644 index 000000000..608a62413 --- /dev/null +++ b/crates/eps/src/import/graphics/ops.rs @@ -0,0 +1,533 @@ +use super::paint::is_mesh; +use super::state::{GState, Space}; +use super::*; + +impl Interp<'_> { + // ---------- 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 = crate::import::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.alloc(std::mem::size_of_val(self.g.path.elements()))?; + self.push(Obj::GState(Rc::new(self.g.clone())))?; + } + CurrentGState => { + self.pop()?; + self.spend_elements(self.g.path.elements().len())?; + self.alloc(std::mem::size_of_val(self.g.path.elements()))?; + 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> = 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::())?; + 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 ] 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 = crate::import::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 = crate::import::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: crate::import::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(()) + } +} diff --git a/crates/eps/src/import/graphics/paint.rs b/crates/eps/src/import/graphics/paint.rs new file mode 100644 index 000000000..f2e343858 --- /dev/null +++ b/crates/eps/src/import/graphics/paint.rs @@ -0,0 +1,495 @@ +use super::state::{GState, Space, process, tiling_hash}; +use super::*; + +impl Interp<'_> { + // ---------- colour ---------- + + /// A colour space operand. + pub(in super::super) fn space_of(&mut self, o: &Obj, depth: u32) -> Res { + 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> = 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 = 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), + }) + } + + pub(super) 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. + pub(super) fn set_comps(&mut self, comps: Vec) -> 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. + pub(super) fn set_process(&mut self, space: Space, name: &str, comps: Vec) -> 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 { + 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 { + 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 = 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` …). + pub(super) 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. + pub(super) fn set_pattern(&mut self, base: Option>) -> 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) -> Res { + 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| crate::import::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, m: Affine) -> Res { + let get = |k: &str| d.borrow().get(&Key::name(k)).cloned(); + let bbox: Vec = 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>> { + 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. + pub(super) fn gradient(&mut self, sh: &DictRef, m: Affine) -> Res> { + 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| -> Vec { + 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 = 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::new(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> { + 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> { + *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| -> Vec { + 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, ..] => clamp_between(t, *a, *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::() / 2.0).collect()) + } + } + } +} + +/// 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? +pub(super) 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> { + *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], shadings: &HashSet) { + 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); + } + } +} diff --git a/crates/eps/src/import/graphics/path.rs b/crates/eps/src/import/graphics/path.rs new file mode 100644 index 000000000..3403e9add --- /dev/null +++ b/crates/eps/src/import/graphics/path.rs @@ -0,0 +1,428 @@ +use super::state::{ccw_turn, turn}; +use super::*; + +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 { + 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). + pub(super) fn spend_elements(&mut self, n: usize) -> Res { + self.spend(n as u64) + } + + pub(super) 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(()) + } + + pub(super) 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) -> 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). + pub(super) 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). + pub(super) 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. + pub(super) fn rects(&mut self) -> Res { + let nums: Vec = 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> = 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 ---------- + + pub(super) 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(()) + } + + pub(super) 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. + pub(super) fn stroke_layer(&mut self) -> Res { + 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 = 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)>> { + 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. + pub(super) 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). + pub(super) 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 = 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. + pub(super) 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). + pub(super) 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 = [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::())?; + self.push(Obj::proc(items)) + } + + /// `execform`: the form's `PaintProc` run with its matrix, clipped to its box. + pub(super) 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| crate::import::interp::matrix_of(&i.borrow()))).unwrap_or(Affine::IDENTITY); + let bbox: Vec = 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(in super::super) fn take_path(&mut self) -> BezPath { + self.g.cur = None; + self.g.start = None; + std::mem::take(&mut self.g.path) + } + + pub(super) 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. + pub(super) fn clip_bounds(&mut self) -> Res { + 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) + } +} diff --git a/crates/eps/src/import/graphics/state.rs b/crates/eps/src/import/graphics/state.rs new file mode 100644 index 000000000..6aa25d5a9 --- /dev/null +++ b/crates/eps/src/import/graphics/state.rs @@ -0,0 +1,328 @@ +use super::*; + +/// 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>, + alt: Rc, + tint: Obj, + }, + Indexed { + base: Rc, + table: Rc>, + }, + /// Patterns; an uncoloured one paints in a colour of the underlying space. + Pattern(Option>), +} + +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. + pub(super) fn initial(&self) -> Vec { + 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 { + 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, + /// The `setcolorspace` operand, for `currentcolorspace`. + pub space_obj: Obj, + pub comps: Vec, + 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, + pub start: Option, + pub clips: Vec>, + /// The clips `clipsave` saved, for `cliprestore`. + pub saved_clips: Vec]>>, + pub overprint: bool, + pub font: Option, + /// `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, + /// 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). + pub(super) groups: Vec>, + pub(super) 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. + pub(super) merge: Option, + /// 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. + pub(super) spot_colors: HashMap, Color>, + /// Rectangles painted with a shading (`shfill`): a clipping group of one is the clip path + /// filled with it. + pub shadings: HashSet, + /// 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. + pub(super) tilings: HashMap>, + /// 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. +pub(super) type Tiling = (Shared, Rect, Option, 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. +pub(super) fn tiling_hash(proc: &Shared, 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]) -> Vec> { + 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]) -> Option { + 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`. + pub(super) fn fill(&mut self, bp: BezPath, rule: FillRule, paint: Paint, overprint: bool, clips: &[Arc]) { + 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`). + pub(super) fn stroke(&mut self, bp: BezPath, st: StrokeLayer, clips: &[Arc]) { + 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. + pub(super) fn clip(&mut self, path: BezPath, rule: FillRule) -> Option> { + // 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 π). +pub(super) 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. +pub(super) fn ccw_turn(a: f64, b: f64) -> f64 { + (b % 360.0 - a % 360.0).rem_euclid(360.0) +}