//! Encapsulated PostScript: a small PostScript interpreter (PostScript Language Reference, 3rd //! edition, and the EPSF 3.0 specification) covering what exported vector EPS files use: the //! operand and dictionary stacks, procedures and `def`, arithmetic, control flow, arrays and //! dictionaries, the graphics state, path construction (including arcs) and painting, clipping //! and device colours. Text is skipped, as are images (their inline data is stepped over) and //! Level 3 smooth shading (`shfill`). use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use effectcraft_svg::{Affine, BezPath, Cap, FillRule, Join, Paint, Stroke}; use kurbo::{PathEl, Point}; use crate::build::Builder; use crate::object::{ascii85, is_white, parse_num}; type DictRef = Rc>>; #[derive(Clone)] enum V { Num(f64), Bool(bool), /// An executable name. Name(Rc), /// A literal name (`/name`). Lit(Rc), Str(Rc>>), Proc(Rc>), Array(Rc>>), Dict(DictRef), Mark, Null, /// `currentfile`. File, } impl V { fn num(&self) -> Option { match self { V::Num(n) => Some(*n), _ => None, } } fn key(&self) -> String { match self { V::Name(n) | V::Lit(n) => n.to_string(), V::Str(s) => String::from_utf8_lossy(&s.borrow()).into_owned(), V::Num(n) => format!("{n}"), V::Bool(b) => format!("{b}"), _ => String::new(), } } fn items(&self) -> Option> { match self { V::Array(a) => Some(a.borrow().clone()), V::Proc(p) => Some(p.as_ref().clone()), _ => None, } } } enum Flow { Exit, Stop, Quit, } #[derive(Clone)] struct GState { ctm: Affine, color: [f64; 3], lw: f64, cap: Cap, join: Join, miter: f64, dash: Option<(Vec, f64)>, path: BezPath, cur: Option, start: Option, depth: usize, } /// Source tokenizer (the data source for `currentfile`). struct Src<'a> { d: &'a [u8], pos: usize, } impl Src<'_> { fn skip_ws(&mut self) { while self.pos < self.d.len() { let c = self.d[self.pos]; if is_white(c) { self.pos += 1; } else if c == b'%' { let s = self.pos; while self.pos < self.d.len() && !matches!(self.d[self.pos], b'\n' | b'\r') { self.pos += 1; } // DSC: skip embedded binary / data sections. let line = &self.d[s..self.pos]; if line.starts_with(b"%%BeginData:") || line.starts_with(b"%%BeginBinary:") || line.starts_with(b"%%BeginPreview") { let end: &[u8] = if line.starts_with(b"%%BeginData:") { b"%%EndData" } else if line.starts_with(b"%%BeginBinary:") { b"%%EndBinary" } else { b"%%EndPreview" }; match self.d[self.pos..].windows(end.len()).position(|w| w == end) { Some(p) => self.pos += p, None => self.pos = self.d.len(), } } } else { break; } } } fn word(&mut self) -> &[u8] { let s = self.pos; while self.pos < self.d.len() { let c = self.d[self.pos]; if is_white(c) || matches!(c, b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%') { break; } self.pos += 1; } &self.d[s..self.pos] } /// The next token; `{ … }` comes back as a whole procedure. fn token(&mut self, depth: usize) -> Option { self.skip_ws(); let c = *self.d.get(self.pos)?; Some(match c { b'{' => { self.pos += 1; let mut body = vec![]; loop { self.skip_ws(); match self.d.get(self.pos) { None => break, Some(b'}') => { self.pos += 1; break; } _ => { if depth > 100 { return None; } match self.token(depth + 1) { Some(t) => body.push(t), None => break, } } } } V::Proc(Rc::new(body)) } b'}' => { self.pos += 1; V::Null } b'/' => { self.pos += 1; let imm = self.d.get(self.pos) == Some(&b'/'); if imm { self.pos += 1; } let w = String::from_utf8_lossy(self.word()).into_owned(); // `//name` (immediately evaluated) is treated like a plain name. if imm { V::Name(w.into()) } else { V::Lit(w.into()) } } b'(' => { self.pos += 1; let mut out = vec![]; let mut depth = 1; while self.pos < self.d.len() { let c = self.d[self.pos]; self.pos += 1; match c { b'\\' => { let e = self.d.get(self.pos).copied().unwrap_or(b'\\'); self.pos += 1; match e { b'n' => out.push(b'\n'), b'r' => out.push(b'\r'), b't' => out.push(b'\t'), b'0'..=b'7' => { let mut v = (e - b'0') as u32; for _ in 0..2 { match self.d.get(self.pos) { Some(d @ b'0'..=b'7') => { v = v * 8 + (d - b'0') as u32; self.pos += 1; } _ => break, } } out.push(v as u8); } b'\n' | b'\r' => {} o => out.push(o), } } b'(' => { depth += 1; out.push(c); } b')' => { depth -= 1; if depth == 0 { break; } out.push(c); } _ => out.push(c), } } V::Str(Rc::new(RefCell::new(out))) } b'<' if self.d.get(self.pos + 1) == Some(&b'<') => { self.pos += 2; V::Name("<<".into()) } b'>' if self.d.get(self.pos + 1) == Some(&b'>') => { self.pos += 2; V::Name(">>".into()) } b'<' if self.d.get(self.pos + 1) == Some(&b'~') => { let s = self.pos; while self.pos + 1 < self.d.len() && !(self.d[self.pos] == b'~' && self.d[self.pos + 1] == b'>') { self.pos += 1; } self.pos = (self.pos + 2).min(self.d.len()); V::Str(Rc::new(RefCell::new(ascii85(&self.d[s..self.pos])))) } b'<' => { self.pos += 1; let mut hex = vec![]; while self.pos < self.d.len() && self.d[self.pos] != b'>' { if self.d[self.pos].is_ascii_hexdigit() { hex.push(self.d[self.pos]); } self.pos += 1; } self.pos += 1; if hex.len() % 2 == 1 { hex.push(b'0'); } let h = |c: u8| (c as char).to_digit(16).unwrap_or(0) as u8; V::Str(Rc::new(RefCell::new(hex.chunks(2).map(|p| (h(p[0]) << 4) | h(p[1])).collect()))) } b'[' | b']' => { self.pos += 1; V::Name(if c == b'[' { "[" } else { "]" }.into()) } b')' | b'>' => { self.pos += 1; V::Null } _ => { let w = self.word(); if w.is_empty() { self.pos += 1; return Some(V::Null); } match parse_num(w) { Some(n) => V::Num(n), None => { // Radix numbers (16#FF). let s = String::from_utf8_lossy(w).into_owned(); match s.split_once('#').and_then(|(r, v)| i64::from_str_radix(v, r.parse().ok()?).ok()) { Some(n) => V::Num(n as f64), None => V::Name(s.into()), } } } } }) } /// Step over inline image data that follows an `image` operator reading `currentfile`. fn skip_image_data(&mut self) { self.skip_ws(); if self.d[self.pos..].starts_with(b"<~") || self.d.get(self.pos).is_some_and(|c| (b'!'..=b'u').contains(c) && !c.is_ascii_hexdigit()) { match self.d[self.pos..].windows(2).position(|w| w == b"~>") { Some(p) => self.pos += p + 2, None => self.pos = self.d.len(), } return; } while self.pos < self.d.len() && (self.d[self.pos].is_ascii_hexdigit() || is_white(self.d[self.pos])) { self.pos += 1; } if self.d.get(self.pos) == Some(&b'>') { self.pos += 1; } } } pub(crate) struct Interp { pub b: Builder, st: Vec, dicts: Vec, gs: GState, saved: Vec, budget: usize, page: [f64; 4], pub unknown: Vec, } fn new_dict() -> DictRef { Rc::new(RefCell::new(HashMap::new())) } fn array(v: Vec) -> V { V::Array(Rc::new(RefCell::new(v))) } fn matrix_of(v: &V) -> Option { let a: Vec = v.items()?.iter().filter_map(V::num).collect(); (a.len() == 6).then(|| Affine::new([a[0], a[1], a[2], a[3], a[4], a[5]])) } fn matrix_val(m: Affine) -> V { array(m.as_coeffs().iter().map(|c| V::Num(*c)).collect()) } impl Interp { pub fn new(page: [f64; 4]) -> Interp { Interp { b: Builder::new(), st: vec![], dicts: vec![new_dict()], gs: GState { ctm: Affine::IDENTITY, color: [0.0; 3], lw: 1.0, cap: Cap::Butt, join: Join::Miter, miter: 10.0, dash: None, path: BezPath::new(), cur: None, start: None, depth: 1, }, saved: vec![], budget: 8_000_000, page, unknown: vec![], } } pub fn run(&mut self, data: &[u8]) { let mut src = Src { d: data, pos: 0 }; while let Some(t) = src.token(0) { if let Err(Flow::Quit) = self.exec_token(t, &mut src) { break; } } } fn pop(&mut self) -> V { self.st.pop().unwrap_or(V::Null) } fn popn(&mut self) -> f64 { self.pop().num().unwrap_or(0.0) } fn push(&mut self, v: V) { if self.st.len() < 100_000 { self.st.push(v); } } fn lookup(&self, k: &str) -> Option { self.dicts.iter().rev().find_map(|d| d.borrow().get(k).cloned()) } /// A token read from the file: procedures are pushed, names executed. fn exec_token(&mut self, t: V, src: &mut Src) -> Result<(), Flow> { match t { V::Name(n) => self.exec_name(&n, src), other => { self.push(other); Ok(()) } } } /// Execute a value as an object (procedures run). fn exec_obj(&mut self, v: V, src: &mut Src) -> Result<(), Flow> { match v { V::Proc(p) => { for x in p.iter() { match x { V::Name(n) => self.exec_name(n, src)?, other => self.push(other.clone()), } } Ok(()) } V::Name(n) => self.exec_name(&n, src), other => { self.push(other); Ok(()) } } } fn exec_name(&mut self, n: &str, src: &mut Src) -> Result<(), Flow> { if self.budget == 0 { self.b.skip("PostScript (operation limit)"); return Err(Flow::Quit); } self.budget -= 1; if let Some(v) = self.lookup(n) { return match v { V::Proc(_) => self.exec_obj(v, src), other => { self.push(other); Ok(()) } }; } self.builtin(n, src) } fn device_scale(&self) -> f64 { self.gs.ctm.determinant().abs().sqrt() } fn user_point(&mut self) -> Point { let y = self.popn(); let x = self.popn(); Point::new(x, y) } fn move_to(&mut self, p: Point) { let q = self.gs.ctm * p; self.gs.path.move_to(q); self.gs.cur = Some(q); self.gs.start = Some(q); } fn line_to(&mut self, p: Point) { let q = self.gs.ctm * p; if self.gs.cur.is_none() { self.gs.path.move_to(q); self.gs.start = Some(q); } self.gs.path.line_to(q); self.gs.cur = Some(q); } fn curve_to(&mut self, a: Point, b: Point, c: Point) { let m = self.gs.ctm; if self.gs.cur.is_none() { self.gs.path.move_to(m * a); } self.gs.path.curve_to(m * a, m * b, m * c); self.gs.cur = Some(m * c); } fn current_user(&self) -> Point { self.gs.ctm.inverse() * self.gs.cur.unwrap_or_default() } fn arc(&mut self, neg: bool) { let a2 = self.popn().to_radians(); let a1 = self.popn().to_radians(); let r = self.popn(); let c = self.user_point(); let mut sweep = a2 - a1; if neg { while sweep > 0.0 { sweep -= std::f64::consts::TAU; } } else { while sweep < 0.0 { sweep += std::f64::consts::TAU; } } let start = Point::new(c.x + r * a1.cos(), c.y + r * a1.sin()); if self.gs.cur.is_some() { self.line_to(start); } else { self.move_to(start); } let arc = kurbo::Arc::new(c, (r, r), a1, sweep, 0.0); let m = self.gs.ctm; arc.to_cubic_beziers(0.1, |p1, p2, p3| { self.gs.path.curve_to(m * p1, m * p2, m * p3); self.gs.cur = Some(m * p3); }); } fn paint(&mut self, fill: Option, stroke: bool) { let path = std::mem::take(&mut self.gs.path); self.gs.cur = None; self.gs.start = None; let s = self.device_scale(); let st = stroke.then(|| Stroke { paint: Paint::Color(self.gs.color), opacity: 1.0, width: if self.gs.lw <= 0.0 { 1.0 } else { self.gs.lw * s }, cap: self.gs.cap, join: self.gs.join, miter: self.gs.miter.max(1.0), dash: self.gs.dash.as_ref().map(|(a, o)| (a.iter().map(|v| v * s).collect(), o * s)), }); self.b.fill_stroke(path, Affine::IDENTITY, fill.map(|r| (Paint::Color(self.gs.color), 1.0, r)), st); } fn rect_path(&mut self) -> Vec { // x y w h, or an array / string of numbers. let top = self.pop(); let nums: Vec = match &top { V::Array(_) | V::Proc(_) => top.items().unwrap_or_default().iter().filter_map(V::num).collect(), V::Num(h) => { let w = self.popn(); let y = self.popn(); let x = self.popn(); vec![x, y, w, *h] } _ => vec![], }; let m = self.gs.ctm; nums.chunks_exact(4) .map(|r| { let mut p = BezPath::new(); p.move_to(m * Point::new(r[0], r[1])); p.line_to(m * Point::new(r[0] + r[2], r[1])); p.line_to(m * Point::new(r[0] + r[2], r[1] + r[3])); p.line_to(m * Point::new(r[0], r[1] + r[3])); p.close_path(); p }) .collect() } fn grestore(&mut self) { if let Some(g) = self.saved.pop() { let d = g.depth; self.gs = g; self.b.close_to(d); } } fn builtin(&mut self, n: &str, src: &mut Src) -> Result<(), Flow> { match n { // Stack. "pop" => { self.pop(); } "exch" => { let b = self.pop(); let a = self.pop(); self.push(b); self.push(a); } "dup" => { let a = self.st.last().cloned().unwrap_or(V::Null); self.push(a); } "copy" => match self.pop() { V::Num(k) => { let k = (k.max(0.0) as usize).min(self.st.len()); let tail: Vec = self.st[self.st.len() - k..].to_vec(); self.st.extend(tail); } V::Array(dst) => { let s = self.pop().items().unwrap_or_default(); let mut d = dst.borrow_mut(); for (i, v) in s.into_iter().enumerate() { if i < d.len() { d[i] = v; } } drop(d); self.push(V::Array(dst)); } other => self.push(other), }, "index" => { let k = self.popn().max(0.0) as usize; let v = if k < self.st.len() { self.st[self.st.len() - 1 - k].clone() } else { V::Null }; self.push(v); } "roll" => { let j = self.popn() as i64; let k = (self.popn().max(0.0) as usize).min(self.st.len()); if k > 0 { let s = self.st.len() - k; let r = j.rem_euclid(k as i64) as usize; self.st[s..].rotate_right(r); } } "clear" => self.st.clear(), "count" => self.push(V::Num(self.st.len() as f64)), "mark" | "[" | "<<" => self.push(V::Mark), "cleartomark" => { while let Some(v) = self.st.pop() { if matches!(v, V::Mark) { break; } } } "counttomark" => { let k = self.st.iter().rev().position(|v| matches!(v, V::Mark)).unwrap_or(self.st.len()); self.push(V::Num(k as f64)); } "]" => { let k = self.st.iter().rev().position(|v| matches!(v, V::Mark)).unwrap_or(self.st.len()); let items = self.st.split_off(self.st.len() - k); self.st.pop(); self.push(array(items)); } ">>" => { let k = self.st.iter().rev().position(|v| matches!(v, V::Mark)).unwrap_or(self.st.len()); let items = self.st.split_off(self.st.len() - k); self.st.pop(); let d = new_dict(); for kv in items.chunks_exact(2) { d.borrow_mut().insert(kv[0].key(), kv[1].clone()); } self.push(V::Dict(d)); } // Arithmetic. "add" | "sub" | "mul" | "div" | "idiv" | "mod" | "atan" | "exp" | "max" | "min" => { let b = self.popn(); let a = self.popn(); let v = match n { "add" => a + b, "sub" => a - b, "mul" => a * b, "div" => { if b == 0.0 { 0.0 } else { a / b } } "idiv" => { if b as i64 == 0 { 0.0 } else { ((a as i64) / (b as i64)) as f64 } } "mod" => { if b as i64 == 0 { 0.0 } else { ((a as i64) % (b as i64)) as f64 } } "atan" => a.atan2(b).to_degrees().rem_euclid(360.0), "exp" => a.powf(b), "max" => a.max(b), _ => a.min(b), }; self.push(V::Num(if v.is_finite() { v } else { 0.0 })); } "neg" | "abs" | "sqrt" | "sin" | "cos" | "ln" | "log" | "cvi" | "cvr" | "round" | "floor" | "ceiling" | "truncate" => { let a = match self.pop() { V::Str(s) => parse_num(&s.borrow()).unwrap_or(0.0), v => v.num().unwrap_or(0.0), }; let v = match n { "neg" => -a, "abs" => a.abs(), "sqrt" => a.max(0.0).sqrt(), "sin" => a.to_radians().sin(), "cos" => a.to_radians().cos(), "ln" => a.ln(), "log" => a.log10(), "cvi" | "truncate" => a.trunc(), "round" => (a + 0.5).floor(), "floor" => a.floor(), "ceiling" => a.ceil(), _ => a, }; self.push(V::Num(if v.is_finite() { v } else { 0.0 })); } // Comparison and logic. "eq" | "ne" => { let b = self.pop(); let a = self.pop(); let eq = match (&a, &b) { (V::Num(x), V::Num(y)) => x == y, (V::Bool(x), V::Bool(y)) => x == y, (V::Null, V::Null) => true, (V::Array(x), V::Array(y)) => Rc::ptr_eq(x, y), (V::Dict(x), V::Dict(y)) => Rc::ptr_eq(x, y), (x, y) => !x.key().is_empty() && x.key() == y.key(), }; self.push(V::Bool(eq == (n == "eq"))); } "gt" | "ge" | "lt" | "le" => { let b = self.popn(); let a = self.popn(); self.push(V::Bool(match n { "gt" => a > b, "ge" => a >= b, "lt" => a < b, _ => a <= b, })); } "not" => match self.pop() { V::Bool(b) => self.push(V::Bool(!b)), v => self.push(V::Num(!(v.num().unwrap_or(0.0) as i64) as f64)), }, "and" | "or" | "xor" => { let b = self.pop(); let a = self.pop(); match (a, b) { (V::Bool(x), V::Bool(y)) => self.push(V::Bool(match n { "and" => x && y, "or" => x || y, _ => x ^ y, })), (x, y) => { let (x, y) = (x.num().unwrap_or(0.0) as i64, y.num().unwrap_or(0.0) as i64); self.push(V::Num(match n { "and" => x & y, "or" => x | y, _ => x ^ y, } as f64)); } } } "true" => self.push(V::Bool(true)), "false" => self.push(V::Bool(false)), "null" => self.push(V::Null), // Control. "exec" => { let v = self.pop(); self.exec_obj(v, src)?; } "if" => { let p = self.pop(); if let V::Bool(true) = self.pop() { self.exec_obj(p, src)?; } } "ifelse" => { let f = self.pop(); let t = self.pop(); let c = matches!(self.pop(), V::Bool(true)); self.exec_obj(if c { t } else { f }, src)?; } "repeat" => { let p = self.pop(); let k = self.popn().clamp(0.0, 1e6) as usize; for _ in 0..k { match self.exec_obj(p.clone(), src) { Err(Flow::Exit) => break, r => r?, } } } "for" => { let p = self.pop(); let lim = self.popn(); let inc = self.popn(); let mut i = self.popn(); let mut guard = 0; while inc != 0.0 && ((inc > 0.0 && i <= lim) || (inc < 0.0 && i >= lim)) && guard < 1_000_000 { self.push(V::Num(i)); match self.exec_obj(p.clone(), src) { Err(Flow::Exit) => break, r => r?, } i += inc; guard += 1; } } "loop" => { let p = self.pop(); for _ in 0..1_000_000 { match self.exec_obj(p.clone(), src) { Err(Flow::Exit) => break, r => r?, } } } "forall" => { let p = self.pop(); match self.pop() { V::Dict(d) => { let items: Vec<(String, V)> = d.borrow().iter().map(|(k, v)| (k.clone(), v.clone())).collect(); for (k, v) in items { self.push(V::Lit(k.into())); self.push(v); match self.exec_obj(p.clone(), src) { Err(Flow::Exit) => break, r => r?, } } } V::Str(s) => { let bytes = s.borrow().clone(); for c in bytes { self.push(V::Num(c as f64)); match self.exec_obj(p.clone(), src) { Err(Flow::Exit) => break, r => r?, } } } a => { for v in a.items().unwrap_or_default() { self.push(v); match self.exec_obj(p.clone(), src) { Err(Flow::Exit) => break, r => r?, } } } } } "exit" => return Err(Flow::Exit), "stop" => return Err(Flow::Stop), "stopped" => { let p = self.pop(); let stopped = match self.exec_obj(p, src) { Err(Flow::Quit) => return Err(Flow::Quit), Err(_) => true, Ok(()) => false, }; self.push(V::Bool(stopped)); } "quit" => return Err(Flow::Quit), "showpage" | "copypage" => {} // Dictionaries. "def" => { let v = self.pop(); let k = self.pop().key(); if let Some(d) = self.dicts.last() { d.borrow_mut().insert(k, v); } } "store" => { let v = self.pop(); let k = self.pop().key(); let d = self.dicts.iter().rev().find(|d| d.borrow().contains_key(&k)).or(self.dicts.last()).cloned(); if let Some(d) = d { d.borrow_mut().insert(k, v); } } "load" => { let k = self.pop().key(); let v = self.lookup(&k).unwrap_or(V::Name(k.into())); self.push(v); } "undef" => { let k = self.pop().key(); if let V::Dict(d) = self.pop() { d.borrow_mut().remove(&k); } } "dict" => { self.pop(); self.push(V::Dict(new_dict())); } "begin" => { if let V::Dict(d) = self.pop() && self.dicts.len() < 64 { self.dicts.push(d); } } "end" => { if self.dicts.len() > 1 { self.dicts.pop(); } } "currentdict" => { let d = self.dicts.last().cloned().unwrap_or_else(new_dict); self.push(V::Dict(d)); } "userdict" | "globaldict" | "systemdict" | "statusdict" | "errordict" | "$error" => { let d = self.dicts[0].clone(); self.push(V::Dict(d)); } "where" => { let k = self.pop().key(); match self.dicts.iter().rev().find(|d| d.borrow().contains_key(&k)).cloned() { Some(d) => { self.push(V::Dict(d)); self.push(V::Bool(true)); } None => self.push(V::Bool(false)), } } "known" => { let k = self.pop().key(); let v = match self.pop() { V::Dict(d) => d.borrow().contains_key(&k), _ => false, }; self.push(V::Bool(v)); } "countdictstack" => self.push(V::Num(self.dicts.len() as f64)), "cleardictstack" => self.dicts.truncate(1), "findresource" | "findfont" | "findencoding" => { if n == "findresource" { self.pop(); } self.pop(); self.push(V::Dict(new_dict())); } "defineresource" => { self.pop(); let v = self.pop(); self.pop(); self.push(v); } "resourcestatus" => { self.pop(); self.pop(); self.push(V::Bool(false)); } // Arrays and strings. "array" => { let k = self.popn().clamp(0.0, 1e6) as usize; self.push(array(vec![V::Null; k])); } "string" => { let k = self.popn().clamp(0.0, 1e7) as usize; self.push(V::Str(Rc::new(RefCell::new(vec![0; k])))); } "length" => { let l = match self.pop() { V::Str(s) => s.borrow().len(), V::Dict(d) => d.borrow().len(), V::Name(s) | V::Lit(s) => s.len(), v => v.items().map_or(0, |i| i.len()), }; self.push(V::Num(l as f64)); } "get" => { let k = self.pop(); let v = match self.pop() { V::Dict(d) => d.borrow().get(&k.key()).cloned().unwrap_or(V::Null), V::Str(s) => V::Num(s.borrow().get(k.num().unwrap_or(0.0) as usize).copied().unwrap_or(0) as f64), a => a.items().and_then(|i| i.get(k.num().unwrap_or(0.0) as usize).cloned()).unwrap_or(V::Null), }; self.push(v); } "put" => { let v = self.pop(); let k = self.pop(); match self.pop() { V::Dict(d) => { d.borrow_mut().insert(k.key(), v); } V::Array(a) => { let i = k.num().unwrap_or(0.0) as usize; if let Some(slot) = a.borrow_mut().get_mut(i) { *slot = v; } } V::Str(s) => { let i = k.num().unwrap_or(0.0) as usize; if let Some(slot) = s.borrow_mut().get_mut(i) { *slot = v.num().unwrap_or(0.0) as u8; } } _ => {} } } "getinterval" => { let c = self.popn().max(0.0) as usize; let i = self.popn().max(0.0) as usize; match self.pop() { V::Str(s) => { let b = s.borrow(); let part = b.get(i.min(b.len())..(i + c).min(b.len())).unwrap_or(&[]).to_vec(); drop(b); self.push(V::Str(Rc::new(RefCell::new(part)))); } a => { let items = a.items().unwrap_or_default(); let part = items.get(i.min(items.len())..(i + c).min(items.len())).unwrap_or(&[]).to_vec(); self.push(array(part)); } } } "putinterval" => { self.pop(); self.pop(); self.pop(); } "aload" => { let a = self.pop(); for v in a.items().unwrap_or_default() { self.push(v); } self.push(a); } "astore" => { let a = self.pop(); if let V::Array(arr) = &a { let k = arr.borrow().len().min(self.st.len()); let items = self.st.split_off(self.st.len() - k); *arr.borrow_mut() = items; } self.push(a); } "cvx" => match self.pop() { V::Array(a) => self.push(V::Proc(Rc::new(a.borrow().clone()))), V::Lit(s) => self.push(V::Name(s)), V::Str(s) => { // Executable string: tokenize it as a procedure. let bytes = s.borrow().clone(); let mut sub = Src { d: &bytes, pos: 0 }; let mut body = vec![]; while let Some(t) = sub.token(0) { body.push(t); } self.push(V::Proc(Rc::new(body))); } v => self.push(v), }, "cvlit" => match self.pop() { V::Proc(p) => self.push(array(p.as_ref().clone())), V::Name(s) => self.push(V::Lit(s)), v => self.push(v), }, "cvn" => { let k = self.pop().key(); self.push(V::Lit(k.into())); } "cvs" => { self.pop(); let k = self.pop().key(); self.push(V::Str(Rc::new(RefCell::new(k.into_bytes())))); } "xcheck" => { let v = self.pop(); self.push(V::Bool(matches!(v, V::Proc(_) | V::Name(_)))); } "type" => { let t = match self.pop() { V::Num(_) => "realtype", V::Bool(_) => "booleantype", V::Name(_) | V::Lit(_) => "nametype", V::Str(_) => "stringtype", V::Proc(_) | V::Array(_) => "arraytype", V::Dict(_) => "dicttype", V::Mark => "marktype", V::Null => "nulltype", V::File => "filetype", }; self.push(V::Name(t.into())); } "bind" | "readonly" | "executeonly" | "noaccess" => {} "languagelevel" => self.push(V::Num(3.0)), "version" | "product" => self.push(V::Str(Rc::new(RefCell::new(b"3010".to_vec())))), "save" => { self.saved.push(self.gs.clone()); self.gs.depth = self.b.depth(); self.push(V::Null); } "restore" => { self.pop(); self.grestore(); } "currentfile" => self.push(V::File), "readstring" | "readhexstring" | "readline" => { self.pop(); self.pop(); self.push(V::Str(Rc::new(RefCell::new(vec![])))); self.push(V::Bool(false)); } "flushfile" | "closefile" | "print" | "==" | "=" | "pstack" | "setpagedevice" | "setglobal" | "setpacking" | "setshared" | "setobjectformat" | "setsmoothness" => { if !matches!(n, "pstack") { self.pop(); } } "currentglobal" | "currentpacking" | "currentshared" => self.push(V::Bool(false)), "vmstatus" => { for _ in 0..3 { self.push(V::Num(0.0)); } } // Graphics state. "gsave" => { let mut g = self.gs.clone(); g.depth = self.b.depth(); self.saved.push(g); } "grestore" => self.grestore(), "grestoreall" => { while !self.saved.is_empty() { self.grestore(); } } "initgraphics" => { self.gs.ctm = Affine::IDENTITY; self.gs.lw = 1.0; self.gs.color = [0.0; 3]; } "setlinewidth" => self.gs.lw = self.popn(), "currentlinewidth" => self.push(V::Num(self.gs.lw)), "setlinecap" => self.gs.cap = [Cap::Butt, Cap::Round, Cap::Square][(self.popn().max(0.0) as usize).min(2)], "setlinejoin" => self.gs.join = [Join::Miter, Join::Round, Join::Bevel][(self.popn().max(0.0) as usize).min(2)], "setmiterlimit" => self.gs.miter = self.popn(), "setdash" => { let off = self.popn(); let a: Vec = self.pop().items().unwrap_or_default().iter().filter_map(V::num).collect(); self.gs.dash = (!a.is_empty() && a.iter().any(|v| *v > 0.0)).then_some((a, off)); } "setflat" | "setstrokeadjust" | "setoverprint" | "setrenderingintent" | "setcolorrendering" | "sethalftone" | "setscreen" | "settransfer" => { self.pop(); } "setgray" => { let g = self.popn().clamp(0.0, 1.0); self.gs.color = [g; 3]; } "setrgbcolor" => { let b = self.popn(); let g = self.popn(); let r = self.popn(); self.gs.color = [r, g, b].map(|v| v.clamp(0.0, 1.0)); } "sethsbcolor" => { let v = self.popn().clamp(0.0, 1.0); let s = self.popn().clamp(0.0, 1.0); let h = self.popn().rem_euclid(1.0) * 6.0; let (i, f) = (h.floor() as i32, h.fract()); let (p, q, t) = (v * (1.0 - s), v * (1.0 - s * f), v * (1.0 - s * (1.0 - f))); self.gs.color = match i { 0 => [v, t, p], 1 => [q, v, p], 2 => [p, v, t], 3 => [p, q, v], 4 => [t, p, v], _ => [v, p, q], }; } "setcmykcolor" => { let k = self.popn().clamp(0.0, 1.0); let y = self.popn().clamp(0.0, 1.0); let m = self.popn().clamp(0.0, 1.0); let c = self.popn().clamp(0.0, 1.0); self.gs.color = [(1.0 - c) * (1.0 - k), (1.0 - m) * (1.0 - k), (1.0 - y) * (1.0 - k)]; } "setcolorspace" => { self.pop(); } "setcolor" => { // Device colour by operand count (patterns are not drawn). let k = self.st.iter().rev().take_while(|v| matches!(v, V::Num(_))).count().min(4); let c: Vec = self.st.split_off(self.st.len() - k).iter().filter_map(V::num).collect(); self.gs.color = match c.len() { 1 => [c[0]; 3], 3 => [c[0], c[1], c[2]], 4 => [(1.0 - c[0]) * (1.0 - c[3]), (1.0 - c[1]) * (1.0 - c[3]), (1.0 - c[2]) * (1.0 - c[3])], _ => self.gs.color, }; } "currentgray" => self.push(V::Num((self.gs.color[0] + self.gs.color[1] + self.gs.color[2]) / 3.0)), "currentrgbcolor" => { for c in self.gs.color { self.push(V::Num(c)); } } // Coordinates. "matrix" => self.push(matrix_val(Affine::IDENTITY)), "identmatrix" | "defaultmatrix" => { self.pop(); self.push(matrix_val(Affine::IDENTITY)); } "currentmatrix" => { self.pop(); self.push(matrix_val(self.gs.ctm)); } "setmatrix" => { let m = self.pop(); self.gs.ctm = matrix_of(&m).unwrap_or(Affine::IDENTITY); } "concat" => { let m = self.pop(); if let Some(m) = matrix_of(&m) { self.gs.ctm *= m; } } "concatmatrix" => { self.pop(); let b = self.pop(); let a = self.pop(); let (a, b) = (matrix_of(&a).unwrap_or(Affine::IDENTITY), matrix_of(&b).unwrap_or(Affine::IDENTITY)); self.push(matrix_val(b * a)); } "invertmatrix" => { self.pop(); let a = self.pop(); self.push(matrix_val(matrix_of(&a).unwrap_or(Affine::IDENTITY).inverse())); } "translate" | "scale" => { let top = self.pop(); let (m_out, y) = match top { V::Array(_) => (Some(top), self.popn()), v => (None, v.num().unwrap_or(0.0)), }; let x = self.popn(); let t = if n == "translate" { Affine::translate((x, y)) } else { Affine::scale_non_uniform(x, y) }; match m_out { Some(_) => self.push(matrix_val(t)), None => self.gs.ctm *= t, } } "rotate" => { let top = self.pop(); match top { V::Array(_) => { let a = self.popn(); self.push(matrix_val(Affine::rotate(a.to_radians()))); } v => self.gs.ctm *= Affine::rotate(v.num().unwrap_or(0.0).to_radians()), } } "transform" | "itransform" | "dtransform" | "idtransform" => { let top = self.pop(); let (m, y) = match top { V::Array(_) | V::Proc(_) => (matrix_of(&top).unwrap_or(Affine::IDENTITY), self.popn()), v => (self.gs.ctm, v.num().unwrap_or(0.0)), }; let x = self.popn(); let m = if n.starts_with('i') { m.inverse() } else { m }; let p = if n.contains('d') { let c = m.as_coeffs(); Point::new(c[0] * x + c[2] * y, c[1] * x + c[3] * y) } else { m * Point::new(x, y) }; self.push(V::Num(p.x)); self.push(V::Num(p.y)); } // Paths. "newpath" => { self.gs.path = BezPath::new(); self.gs.cur = None; self.gs.start = None; } "moveto" => { let p = self.user_point(); self.move_to(p); } "rmoveto" => { let d = self.user_point(); let c = self.current_user(); self.move_to(Point::new(c.x + d.x, c.y + d.y)); } "lineto" => { let p = self.user_point(); self.line_to(p); } "rlineto" => { let d = self.user_point(); let c = self.current_user(); self.line_to(Point::new(c.x + d.x, c.y + d.y)); } "curveto" | "rcurveto" => { let p3 = self.user_point(); let p2 = self.user_point(); let p1 = self.user_point(); if n == "rcurveto" { let c = self.current_user(); let o = |p: Point| Point::new(c.x + p.x, c.y + p.y); self.curve_to(o(p1), o(p2), o(p3)); } else { self.curve_to(p1, p2, p3); } } "closepath" => { if self.gs.cur.is_some() { self.gs.path.close_path(); self.gs.cur = self.gs.start; } } "arc" => self.arc(false), "arcn" => self.arc(true), "currentpoint" => { let c = self.current_user(); self.push(V::Num(c.x)); self.push(V::Num(c.y)); } "pathbbox" => { let r = kurbo::Shape::bounding_box(&self.gs.path); let inv = self.gs.ctm.inverse(); let (a, b) = (inv * Point::new(r.x0, r.y0), inv * Point::new(r.x1, r.y1)); for v in [a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y)] { self.push(V::Num(v)); } } "flattenpath" | "reversepath" | "strokepath" | "initclip" => {} "clippath" => { let p = self.page; self.gs.path = kurbo::Shape::to_path(&kurbo::Rect::new(p[0], p[1], p[2], p[3]), 0.1); } // Painting. "fill" => self.paint(Some(FillRule::NonZero), false), "eofill" => self.paint(Some(FillRule::EvenOdd), false), "stroke" => self.paint(None, true), "clip" | "eoclip" => { let rule = if n == "clip" { FillRule::NonZero } else { FillRule::EvenOdd }; let p = self.gs.path.clone(); if !p.elements().iter().all(|e| matches!(e, PathEl::MoveTo(_))) { self.b.clip(p, rule); } } "rectfill" | "rectstroke" | "rectclip" => { let paths = self.rect_path(); if n == "rectstroke" && matches!(self.st.last(), Some(V::Array(_))) { self.pop(); } let mut all = BezPath::new(); for p in paths { all.extend(p); } match n { "rectclip" => self.b.clip(all, FillRule::NonZero), _ => { let saved = std::mem::replace(&mut self.gs.path, all); self.paint(if n == "rectfill" { Some(FillRule::NonZero) } else { None }, n == "rectstroke"); self.gs.path = saved; } } } "shfill" => { self.pop(); self.b.skip("smooth shading (shfill)"); } // Text (skipped). "scalefont" | "makefont" | "selectfont" | "setfont" | "show" | "charpath" | "glyphshow" | "rootfont" | "setcachedevice" | "setcharwidth" => { match n { "scalefont" | "makefont" => { self.pop(); } "selectfont" => { self.pop(); self.pop(); } "charpath" => { self.pop(); self.pop(); } "setcachedevice" => { for _ in 0..6 { self.pop(); } } "setcharwidth" => { self.pop(); self.pop(); } "rootfont" => self.push(V::Dict(new_dict())), _ => { self.pop(); } } if matches!(n, "show" | "charpath" | "glyphshow") { self.b.skip("text"); } } "currentfont" => self.push(V::Dict(new_dict())), "ashow" | "widthshow" | "awidthshow" | "xshow" | "yshow" | "xyshow" | "kshow" | "cshow" => { let k = match n { "ashow" | "xshow" | "yshow" | "xyshow" | "kshow" | "cshow" => 3, "widthshow" => 4, _ => 6, }; for _ in 0..k.min(self.st.len()) { self.pop(); } self.b.skip("text"); } "stringwidth" => { self.pop(); self.push(V::Num(0.0)); self.push(V::Num(0.0)); } // Images: operands popped, inline data skipped. "image" | "imagemask" | "colorimage" => { let mut reads_file = false; let check = |v: &V, rf: &mut bool| { if matches!(v, V::File) || matches!(v, V::Proc(p) if p.iter().any(|x| matches!(x, V::Name(n) if &**n == "currentfile"))) { *rf = true; } }; if n == "colorimage" { let ncomp = self.popn().max(1.0) as usize; let multi = matches!(self.pop(), V::Bool(true)); for _ in 0..if multi { ncomp.min(4) } else { 1 } { let v = self.pop(); check(&v, &mut reads_file); } for _ in 0..4 { self.pop(); } } else { match self.pop() { V::Dict(d) => { if let Some(ds) = d.borrow().get("DataSource") { check(ds, &mut reads_file); } } top => { check(&top, &mut reads_file); for _ in 0..4 { self.pop(); } } } } if reads_file { src.skip_image_data(); } self.b.skip("image"); } _ => { if self.unknown.len() < 32 && !self.unknown.iter().any(|u| u == n) { self.unknown.push(n.to_string()); } } } Ok(()) } } /// The PostScript section of an EPS file (DOS EPS binary header handled) and its bounding box /// (`%%HiResBoundingBox` preferred). pub fn eps_parts(bytes: &[u8]) -> Option<(&[u8], [f64; 4])> { let ps = if bytes.starts_with(&[0xC5, 0xD0, 0xD3, 0xC6]) && bytes.len() >= 12 { let off = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize; let len = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]) as usize; bytes.get(off..off.checked_add(len)?.min(bytes.len()))? } else { bytes }; let find_box = |key: &[u8]| -> Option<[f64; 4]> { let mut from = 0; while let Some(p) = ps[from..].windows(key.len()).position(|w| w == key) { let s = from + p + key.len(); let e = ps[s..].iter().position(|c| matches!(c, b'\n' | b'\r')).map_or(ps.len(), |q| s + q); let v: Vec = String::from_utf8_lossy(&ps[s..e]).split_whitespace().filter_map(|t| t.parse().ok()).collect(); if v.len() == 4 && v[2] > v[0] && v[3] > v[1] { return Some([v[0], v[1], v[2], v[3]]); } from = s; } None }; let bbox = find_box(b"%%HiResBoundingBox:").or_else(|| find_box(b"%%BoundingBox:"))?; Some((ps, bbox)) }