refactor(eps): move import/graphics.rs items to import/graphics/{ops,paint,path,state}.rs

This commit is contained in:
aiongg
2026-10-26 21:45:32 +00:00
parent 6e6b0bb3eb
commit f8fe7075f4
5 changed files with 1792 additions and 1772 deletions
File diff suppressed because it is too large Load Diff
+533
View File
@@ -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<Vec<f64>> = items.borrow().iter().map(|o| o.as_num().filter(|v| v.is_finite() && *v >= 0.0)).collect();
self.g.dash = v.ok_or(PsError::Ps("typecheck", String::new()))?.into();
self.g.dash_offset = offset;
}
CurrentDash => {
// The array made counts against the memory budget.
self.alloc(self.g.dash.len() * std::mem::size_of::<Obj>())?;
let d = self.g.dash.iter().map(|v| Obj::Real(*v)).collect();
self.push(Obj::array(d))?;
self.push_num(self.g.dash_offset)?;
}
SetGray => {
let [g] = self.nums()?;
self.set_process(Space::Gray, "DeviceGray", vec![g])?;
}
SetRgbColor => {
let c = self.nums::<3>()?;
self.set_process(Space::Rgb, "DeviceRGB", c.to_vec())?;
}
SetHsbColor => {
let [h, s, b] = self.nums()?;
let [r, g, bl] =
Color::from_hsb((h.rem_euclid(1.0) * 360.0) as f32, s.clamp(0.0, 1.0) as f32, b.clamp(0.0, 1.0) as f32).to_rgb_uncalibrated();
self.set_process(Space::Rgb, "DeviceRGB", vec![f64::from(r), f64::from(g), f64::from(bl)])?;
}
SetCmykColor => {
let c = self.nums::<4>()?;
self.set_process(Space::Cmyk, "DeviceCMYK", c.to_vec())?;
}
CurrentGray => {
let [r, g, b] = self.current_rgb();
self.push_num(f64::from(0.3 * r + 0.59 * g + 0.11 * b))?;
}
CurrentRgbColor => {
for v in self.current_rgb() {
self.push_num(f64::from(v))?;
}
}
CurrentCmykColor => {
let c = self.g.paint.color().map_or([0.0, 0.0, 0.0, 1.0], |c| c.to_cmyk());
for v in c {
self.push_num(f64::from(v))?;
}
}
SetColorSpace => {
let o = self.pop()?;
self.set_space(o)?;
}
CurrentColorSpace => {
let o = match &self.g.space_obj {
o @ Obj::Array { .. } => o.clone(),
o => Obj::array(vec![o.clone()]),
};
self.push(o)?;
}
SetColor => {
if let Space::Pattern(base) = &*self.g.space {
self.set_pattern(base.clone())?;
} else {
let n = self.g.space.n();
let mut c = vec![0.0; n];
for slot in c.iter_mut().rev() {
*slot = self.pop_num()?;
}
self.set_comps(c)?;
}
}
CurrentColor => {
for v in self.g.comps.clone() {
self.push_num(v)?;
}
}
// `[/Pattern <current space>] setcolorspace setcolor`, unless in a pattern space already.
SetPattern => {
if !matches!(*self.g.space, Space::Pattern(_)) {
self.g.space = Rc::new(Space::Pattern(Some(self.g.space.clone())));
self.g.space_obj = Obj::array(vec![Obj::name("Pattern"), self.g.space_obj.clone()]);
}
let Space::Pattern(base) = &*self.g.space else { return ps_err("typecheck", "") };
self.set_pattern(base.clone())?;
}
MakePattern => {
let m = self.pop_matrix()?;
let d = self.pop_dict()?;
let mut copy = d.borrow().clone();
copy.insert(Key::name("VCMatrix"), matrix_obj(self.g.ctm * m));
self.push(Obj::dict(copy))?;
}
SetOverprint => self.g.overprint = self.pop_bool()?,
CurrentOverprint => self.push(Obj::Bool(self.g.overprint))?,
SetFlat | SetHalftone | SetTransfer | SetBlackGeneration | SetUnderColorRemoval | SetColorRendering | SetSmoothness | SetStrokeAdjust
| SetPageDevice | SetUserParams | SetSystemParams => {
self.pop()?;
}
SetScreen => {
for _ in 0..3 {
self.pop()?;
}
}
SetColorTransfer => {
for _ in 0..4 {
self.pop()?;
}
}
SetColorScreen => {
for _ in 0..12 {
self.pop()?;
}
}
SetCacheLimit => {
self.pop()?;
}
SetUCacheParams => {
let at = self.stack.iter().rposition(|o| matches!(o, Obj::Mark)).ok_or(PsError::Ps("unmatchedmark", String::new()))?;
self.stack.truncate(at);
}
UCache => {}
CurrentScreen => {
self.push(Obj::Real(60.0))?;
self.push(Obj::Real(45.0))?;
self.push(Obj::proc(vec![]))?;
}
CurrentTransfer | CurrentBlackGeneration | CurrentUnderColorRemoval => self.push(Obj::proc(vec![]))?,
CurrentColorTransfer => {
for _ in 0..4 {
self.push(Obj::proc(vec![]))?;
}
}
CurrentHalftone => {
let mut d = 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(())
}
}
+495
View File
@@ -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<Space> {
if depth > 4 {
return ps_err("limitcheck", "setcolorspace");
}
let (family, items) = match o {
Obj::Name(n) | Obj::Exec(n) => (n.clone(), vec![]),
Obj::Array { items, .. } => {
let v = items.to_vec();
let n = v.first().and_then(Obj::text).ok_or(PsError::Ps("typecheck", "setcolorspace".into()))?;
(n, v)
}
_ => return ps_err("typecheck", "setcolorspace"),
};
let arg = |i: usize| items.get(i).cloned().ok_or(PsError::Ps("rangecheck", "setcolorspace".into()));
Ok(match &*family {
"DeviceGray" | "CIEBasedA" | "CalGray" => Space::Gray,
"DeviceRGB" | "CIEBasedABC" | "CalRGB" | "Lab" | "CIEBasedDEF" => Space::Rgb,
"DeviceCMYK" | "CIEBasedDEFG" => Space::Cmyk,
"Pattern" => Space::Pattern(match items.get(1) {
Some(base) => Some(Rc::new(self.space_of(base, depth + 1)?)),
None => None,
}),
"ICCBased" => match arg(1)? {
Obj::Dict(d) => match d.borrow().get(&Key::name("N")).and_then(Obj::as_num) {
Some(1.0) => Space::Gray,
Some(4.0) => Space::Cmyk,
_ => Space::Rgb,
},
_ => Space::Rgb,
},
"Separation" | "DeviceN" => {
let names: Vec<Rc<str>> = match arg(1)? {
Obj::Array { items, .. } => items.borrow().iter().filter_map(Obj::text).collect(),
o => o.text().into_iter().collect(),
};
if names.len() > MAX_INKS {
return ps_err("limitcheck", "DeviceN");
}
let alt = self.space_of(&arg(2)?, depth + 1)?;
Space::Inks { names, alt: Rc::new(alt), tint: arg(3)? }
}
"Indexed" => {
let base = self.space_of(&arg(1)?, depth + 1)?;
let hival =
arg(2)?.as_num().filter(|v| (0.0..=MAX_HIVAL as f64).contains(v)).ok_or(PsError::Ps("rangecheck", "Indexed".into()))? as usize;
let n = base.n();
let table: Vec<f64> = match arg(3)? {
Obj::Str(s) => s.borrow().iter().map(|b| f64::from(*b) / 255.0).collect(),
p @ Obj::Array { .. } => {
let mut t = Vec::with_capacity((hival + 1) * n);
for i in 0..=hival {
self.push(Obj::Int(i as i64))?;
self.call(p.clone())?;
let mut c = vec![0.0; n];
for slot in c.iter_mut().rev() {
*slot = self.pop_num()?;
}
t.extend(c);
}
t
}
_ => return ps_err("typecheck", "Indexed"),
};
Space::Indexed { base: Rc::new(base), table: Rc::new(table) }
}
_ => return ps_err("undefined", &family),
})
}
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<f64>) -> Res {
let space = self.g.space.clone();
self.g.mesh = None;
self.g.paint = self.paint_of(&space, &comps)?;
self.g.comps = comps;
Ok(())
}
/// Set a process colour, its space with it.
pub(super) fn set_process(&mut self, space: Space, name: &str, comps: Vec<f64>) -> Res {
self.g.space = Rc::new(space);
self.g.space_obj = Obj::name(name);
self.set_comps(comps)
}
/// The paint of colour `comps` in `space`: a spot ink links to its swatch at the tint.
pub fn paint_of(&mut self, space: &Space, comps: &[f64]) -> Res<Paint> {
Ok(match space {
Space::Inks { names, alt, tint } => {
let t = comps.first().copied().unwrap_or(1.0).clamp(0.0, 1.0) as f32;
match names.as_slice() {
[one] if &**one == "None" => Paint::None,
[one] if &**one == "All" => {
Paint::Solid { color: Color::cmyk(1.0, 1.0, 1.0, 1.0).tinted(t), swatch: Some(REGISTRATION.into()), tint: t }
}
[one] => {
let full = match self.out.spot_colors.get(one) {
Some(c) => *c,
None => {
let c = self.tint_color(alt, tint, &[1.0])?;
self.out.spots.push((one.to_string(), c));
// The tint transform may have listed this ink already: the color
// listed first stays.
self.out.spot_colors.entry(one.clone()).or_insert(c);
c
}
};
Paint::Solid { color: full.tinted(t), swatch: Some(one.to_string()), tint: t }
}
_ => Paint::solid(self.tint_color(alt, tint, comps)?),
}
}
Space::Indexed { base, table } => {
let n = base.n();
let i = comps.first().copied().unwrap_or(0.0).round().max(0.0) as usize;
let c = table.get(i * n..i * n + n).ok_or(PsError::Ps("rangecheck", "setcolor".into()))?.to_vec();
Paint::solid(process(base, &c).unwrap_or(Color::BLACK))
}
Space::Pattern(_) => Paint::None,
s => Paint::solid(process(s, comps).unwrap_or(Color::BLACK)),
})
}
/// The alternate colour of inks at `comps`, through the tint transform.
pub fn tint_color(&mut self, alt: &Space, tint: &Obj, comps: &[f64]) -> Res<Color> {
let base = self.stack.len();
for c in comps {
self.push_num(*c)?;
}
self.call(tint.clone())?;
let n = alt.n();
let at = self.stack.len().checked_sub(n).filter(|a| *a >= base).ok_or(PsError::Ps("stackunderflow", "tint transform".into()))?;
let v: Vec<f64> = self.stack.split_off(at).iter().map(|o| o.as_num().unwrap_or(0.0)).collect();
self.stack.truncate(base);
Ok(process(alt, &v).unwrap_or(Color::BLACK))
}
/// The current colour as grey, RGB or CMYK components (`currentgray` …).
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<Rc<Space>>) -> Res {
let Obj::Dict(d) = self.pop()? else { return ps_err("typecheck", "") };
let uncoloured = d.borrow().get(&Key::name("PaintType")).and_then(Obj::as_num) == Some(2.0);
let under = match base.filter(|_| uncoloured) {
Some(base) => {
let mut c = vec![0.0; base.n()];
for slot in c.iter_mut().rev() {
*slot = self.pop_num()?;
}
Some(self.paint_of(&base, &c)?)
}
None => None,
};
self.g.mesh = None;
self.g.paint = self.pattern_paint(&d, under)?;
Ok(())
}
/// A pattern colour: a shading pattern's gradient, a tiling pattern's swatch (`under`: the
/// colour an uncoloured one paints in).
fn pattern_paint(&mut self, d: &DictRef, under: Option<Paint>) -> Res<Paint> {
let kind = d.borrow().get(&Key::name("PatternType")).and_then(Obj::as_num);
let sh = d.borrow().get(&Key::name("Shading")).cloned();
let m = d.borrow().get(&Key::name("VCMatrix")).and_then(|o| o.items().and_then(|i| 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<Paint>, m: Affine) -> Res<Paint> {
let get = |k: &str| d.borrow().get(&Key::name(k)).cloned();
let bbox: Vec<f64> = get("BBox").and_then(|o| o.items().map(|i| i.borrow().iter().filter_map(Obj::as_num).collect())).unwrap_or_default();
let [x0, y0, x1, y1] = bbox[..] else { return ps_err("rangecheck", "BBox") };
let step = |k: &str| get(k).and_then(|o| o.as_num()).map(f64::abs).filter(|v| v.is_finite() && *v > 1e-9 && *v < LIMIT);
let (Some(xs), Some(ys)) = (step("XStep"), step("YStep")) else { return ps_err("rangecheck", "XStep") };
let tile = Rect::from_origin_size((x0.min(x1), y0.min(y1)), (xs, ys));
let xf = m * Affine::translate(tile.origin().to_vec2());
let proc = get("PaintProc").ok_or(PsError::Ps("undefined", "PaintProc".into()))?;
let key = proc.items().cloned().ok_or(PsError::Ps("typecheck", "PaintProc".into()))?;
let hash = tiling_hash(&key, tile, under.as_ref());
let made = self.out.tilings.get(&hash).and_then(|list| list.iter().find(|(p, t, u, _)| p.same(&key) && *t == tile && *u == under));
if let Some((.., name)) = made {
return Ok(Paint::Pattern { pattern: name.clone(), xf });
}
if self.out.apart >= MAX_APART {
self.out.warn(NESTED_PATTERNS);
return Ok(Paint::solid(Color::gray(0.5)));
}
let paint = under.clone();
let art = self.draw_apart(Rect::new(x0, y0, x1, y1).abs(), |it| {
if let Some(p) = paint {
it.g.paint = p;
}
it.push(Obj::Dict(d.clone()))?;
it.call(proc)
})?;
if art.is_empty() {
return Ok(Paint::None);
}
// The document gets its patterns only here, so the n made so far are Pattern 1 to
// Pattern n.
let name = format!("Pattern {}", self.out.doc.patterns.len() + 1);
let mut def = PatternDef::new(&name, art);
def.tile = tile;
self.out.doc.patterns.push(def);
// A cell with a NaN corner equals no cell, so the lookup above could never find it.
if !tile.is_nan() {
self.out.tilings.entry(hash).or_insert_with(|| Vec::with_capacity(1)).push((key, tile, under, name.clone()));
}
Ok(Paint::Pattern { pattern: name, xf })
}
/// The art `draw` draws on its own, in its own space (user space is the document's, the page
/// `frame`), from a fresh graphics state with the current font: a pattern's cell, a glyph.
pub fn draw_apart(&mut self, frame: Rect, draw: impl FnOnce(&mut Self) -> Res) -> Res<Vec<Arc<Node>>> {
let fresh = GState { font: self.g.font.clone(), ..GState::default() };
let g = std::mem::replace(&mut self.g, fresh);
let saved = std::mem::take(&mut self.saved);
let drawn = std::mem::take(&mut self.out.drawn);
let page = std::mem::replace(&mut self.out.page, Affine::IDENTITY);
let frame = std::mem::replace(&mut self.out.frame, frame);
let merge = self.out.merge.take();
// The art isn't in the groups open around it.
let groups = std::mem::take(&mut self.out.groups);
let too_deep = std::mem::take(&mut self.out.too_deep);
self.out.apart += 1;
let r = draw(self);
self.out.apart -= 1;
let art = std::mem::replace(&mut self.out.drawn, drawn);
(self.out.page, self.out.frame, self.out.merge) = (page, frame, merge);
(self.out.groups, self.out.too_deep) = (groups, too_deep);
(self.g, self.saved) = (g, saved);
r?;
let mut nodes = vectorcraft_doc::clipnest::nest(&mut self.out.doc, art);
if !self.out.shadings.is_empty() {
collapse(&mut nodes, &self.out.shadings);
}
Ok(nodes)
}
/// An axial or radial shading as a gradient, `m` mapping its space onto the document.
pub(super) fn gradient(&mut self, sh: &DictRef, m: Affine) -> Res<Option<GradientPaint>> {
let get = |k: &str| sh.borrow().get(&Key::name(k)).cloned();
let kind = get("ShadingType").and_then(|o| o.as_num()).unwrap_or(0.0);
if !is_gradient(sh) {
self.out.warn(UNKNOWN_SHADINGS);
return Ok(None);
}
let space = self.space_of(&get("ColorSpace").ok_or(PsError::Ps("undefined", "ColorSpace".into()))?, 0)?;
let nums = |o: Option<Obj>| -> Vec<f64> {
o.and_then(|o| o.items().map(|i| i.borrow().iter().filter_map(Obj::as_num).collect())).unwrap_or_default()
};
let coords = nums(get("Coords"));
let domain = nums(get("Domain"));
let (d0, d1) = (domain.first().copied().unwrap_or(0.0), domain.get(1).copied().unwrap_or(1.0));
let f = get("Function").ok_or(PsError::Ps("undefined", "Function".into()))?;
// Where the colour changes: the ends, and the bounds of stitched linear functions (unless
// there are too many).
let mut visits = 0;
let points = linear_points(&f, 0, &mut visits).filter(|p| p.len() <= MAX_STITCH_STOPS);
let stitched = points.is_some();
let ts: Vec<f64> = match points {
Some(mut pts) => {
pts.retain(|t| (d0.min(d1)..=d0.max(d1)).contains(t));
pts.iter().map(|t| if d1 != d0 { (t - d0) / (d1 - d0) } else { 0.0 }).chain([0.0, 1.0]).collect()
}
None => (0..=SHADING_SAMPLES).map(|i| i as f64 / SHADING_SAMPLES as f64).collect(),
};
let mut ts = ts;
ts.sort_by(f64::total_cmp);
ts.dedup_by(|a, b| (*a - *b).abs() < 1e-9);
let radial = kind == 3.0;
let c = |i: usize| coords.get(i).copied().unwrap_or(0.0);
let (r0, r1) = (c(2).max(0.0), c(5).max(1e-6));
let mut stops = Vec::with_capacity(ts.len());
let stop = |paint: Paint, offset: f64| match paint {
Paint::Solid { color, swatch, tint } => GradientStop { swatch, tint, ..GradientStop::new(offset as f32, color) },
_ => GradientStop::new(offset as f32, Color::BLACK),
};
for t in ts {
let comps = self.eval(&f, &[d0 + (d1 - d0) * t], 0)?;
let paint = self.paint_of(&space, &comps)?;
let offset = if radial { (r0 + (r1 - r0) * t) / r1 } else { t };
// At a bound of a stitched function the pieces need not meet (a hard colour change):
// the colour just before the bound is a stop at the same offset, ahead of the one
// after it.
if stitched && t > 0.0 && t < 1.0 {
let comps = self.eval(&f, &[d0 + (d1 - d0) * (t - 1e-9)], 0)?;
let before = stop(self.paint_of(&space, &comps)?, offset);
if before.color != stop(paint.clone(), offset).color {
stops.push(before);
}
}
stops.push(stop(paint, offset));
}
let (gk, geom) = if radial {
let centre = Point::new(c(3), c(4));
let mut geom = GradientGeom { start: centre, end: centre + Vec2::new(r1, 0.0), aspect: 1.0, focal: None };
geom.set_focal(Some(Point::new(c(0), c(1))));
geom.transform(m, GradientKind::Radial);
(GradientKind::Radial, geom)
} else {
(GradientKind::Linear, GradientGeom { start: m * Point::new(c(0), c(1)), end: m * Point::new(c(2), c(3)), aspect: 1.0, focal: None })
};
let mut g = GradientPaint::new(Gradient::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<Vec<f64>> {
self.eval_inner(f, x, depth, &mut 0)
}
/// [`Self::eval`], with `visits` (shared by the whole recursion, incremented on every call)
/// counting the functions visited against [`MAX_FUNCTION_VISITS`], as [`linear_points`] counts
/// its nodes. The depth limit alone stops a function that refers to itself, but not one that
/// fans out through shared arrays: when four levels of arrays each have 100 entries that all
/// refer to the same array of the level below (at the last level, the same function), one
/// evaluation would visit 101,010,101 functions. Each function visited also counts
/// [`FUNCTION_OPS`] against the operation budget.
fn eval_inner(&mut self, f: &Obj, x: &[f64], depth: u32, visits: &mut usize) -> Res<Vec<f64>> {
*visits += 1;
if depth > 8 || *visits > MAX_FUNCTION_VISITS {
return ps_err("limitcheck", "Function");
}
self.spend(FUNCTION_OPS)?;
let d = match f {
Obj::Dict(d) => d.clone(),
Obj::Array { items, .. } => {
// Each entry is at least one more function: an array with more entries than the
// visits left raises `limitcheck` before it is copied.
if items.len() > MAX_FUNCTION_VISITS - *visits {
return ps_err("limitcheck", "Function");
}
let fs = items.to_vec();
let mut out = vec![];
for g in &fs {
out.extend(self.eval_inner(g, x, depth + 1, visits)?.first().copied());
}
return Ok(out);
}
_ => return ps_err("typecheck", "Function"),
};
let get = |k: &str| d.borrow().get(&Key::name(k)).cloned();
let nums = |o: Option<Obj>| -> Vec<f64> {
o.and_then(|o| o.items().map(|i| i.borrow().iter().filter_map(Obj::as_num).collect())).unwrap_or_default()
};
let domain = nums(get("Domain"));
let t = x.first().copied().unwrap_or(0.0);
let t = match domain.as_slice() {
[a, b, ..] => 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::<f64>() / 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<Vec<f64>> {
*visits += 1;
if depth > 8 || *visits > 2 * (MAX_STITCH_STOPS + 1) {
return None;
}
let Obj::Dict(d) = f else { return None };
let d = d.borrow();
let num = |k: &str| d.get(&Key::name(k)).and_then(Obj::as_num);
match num("FunctionType")? {
2.0 if num("N").unwrap_or(1.0) == 1.0 => Some(vec![]),
3.0 => {
let items = d.get(&Key::name("Functions"))?.items()?;
// More pieces than the stop cap allows: sample the function instead (tested before
// `to_vec`, so a huge `Functions` array is not copied only to be rejected). Each stop
// runs [`Interp::eval`], which reads one piece, so a short `Bounds` with a huge
// `Functions` would still be O(stops * Functions) to find each piece without this.
if items.len() > MAX_STITCH_STOPS + 1 {
return None;
}
let fs = items.to_vec();
if !fs.iter().all(|g| linear_points(g, depth + 1, visits).is_some_and(|p| p.is_empty())) {
return None;
}
d.get(&Key::name("Bounds"))?.items().map(|i| i.borrow().iter().filter_map(Obj::as_num).collect())
}
_ => None,
}
}
/// The clip path a clipping group of one shading rectangle (`shfill` through a clip) can be
/// filled with instead.
pub(crate) fn collapse(nodes: &mut [Arc<Node>], shadings: &HashSet<NodeId>) {
for n in nodes.iter_mut() {
let NodeKind::Group { children, clip: true } = &n.kind else {
if let Some(c) = Arc::make_mut(n).children_mut() {
collapse(c, shadings);
}
continue;
};
if let [clip, fill] = children.as_slice()
&& shadings.contains(&fill.id)
&& let (NodeKind::Path { path, rule, clipping: true, .. }, NodeKind::Path { .. }) = (&clip.kind, &fill.kind)
{
let mut p = Node::path(fill.id, path.clone(), (*fill.appearance).clone());
if let NodeKind::Path { rule: r, .. } = &mut p.kind {
*r = *rule;
}
*n = Arc::new(p);
continue;
}
if let Some(c) = Arc::make_mut(n).children_mut() {
collapse(c, shadings);
}
}
}
+428
View File
@@ -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<Point> {
let p = self.g.cur.ok_or(PsError::Ps("nocurrentpoint", String::new()))?;
let inv = self.xf();
if inv.determinant().abs() < 1e-12 {
return ps_err("undefinedresult", "");
}
Ok(inv.inverse() * p)
}
pub fn gsave(&mut self) -> Res {
if self.saved.len() >= MAX_GSAVE {
return Err(PsError::Limit("too many saved graphics states"));
}
self.alloc(std::mem::size_of_val(self.g.path.elements()))?;
self.saved.push(self.g.clone());
Ok(())
}
pub fn grestore(&mut self) {
if let Some(g) = self.saved.pop() {
self.g = g;
}
}
// ---------- paths ----------
/// Room for one more element in the current path.
pub fn grow(&self) -> Res {
if self.g.path.elements().len() >= MAX_PATH {
return Err(PsError::Limit("a path has too many points"));
}
Ok(())
}
/// Count `n` path elements an operator makes, reads or copies against the operation budget,
/// an operation each: making or reading an element takes up to about as long as an operation,
/// and copying one takes less (measured).
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<Item = PathEl>) -> Res {
let xf = self.xf();
for el in els {
self.spend_elements(1)?;
match el {
PathEl::MoveTo(p) => {
let p = xf * p;
if self.g.cur.is_some() { self.line_to(p)? } else { self.move_to(p)? }
}
PathEl::LineTo(p) => self.line_to(xf * p)?,
PathEl::QuadTo(a, p) => {
self.grow()?;
self.g.path.quad_to(xf * a, xf * p);
self.g.cur = Some(xf * p);
}
PathEl::CurveTo(a, b, p) => {
self.grow()?;
self.g.path.curve_to(xf * a, xf * b, xf * p);
self.g.cur = Some(xf * p);
}
PathEl::ClosePath => {
self.grow()?;
self.g.path.close_path();
}
}
}
Ok(())
}
/// `arc` / `arcn`: from angle `a1` to `a2` (degrees, counter-clockwise or clockwise).
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<BezPath> {
let nums: Vec<f64> = match self.stack.last() {
Some(Obj::Array { .. }) => {
let items = self.pop_array()?;
// Five elements a rectangle, counted before the array is read.
self.spend_elements(items.len() / 4 * 5)?;
let v: Option<Vec<f64>> = items.borrow().iter().map(Obj::as_num).collect();
v.ok_or(PsError::Ps("typecheck", String::new()))?
}
_ => self.nums::<4>()?.to_vec(),
};
if nums.len() / 4 * 5 >= MAX_PATH {
return Err(PsError::Limit("a path has too many points"));
}
let mut bp = BezPath::new();
for [x, y, w, h] in nums.as_chunks::<4>().0 {
bp.extend(self.rect_path(*x, *y, *w, *h));
}
Ok(bp)
}
// ---------- painting ----------
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<StrokeLayer> {
self.spend(self.g.dash.len() as u64)?;
let k = self.scale();
let w = self.g.width * k;
let mut st = StrokeLayer::new(self.g.paint.clone(), if w > 0.0 && w.is_finite() { w } else { HAIRLINE });
st.cap = self.g.cap;
st.join = self.g.join;
st.miter_limit = self.g.miter.clamp(1.0, 500.0);
st.overprint = self.g.overprint;
let pattern: Vec<f64> = self.g.dash.iter().map(|d| d * k).collect();
let dash = Dash { pattern, offset: self.g.dash_offset * k, align_corners: false };
st.dash = dash.is_dashed().then_some(dash);
Ok(st)
}
/// The current path in user space, each element as its operator and points (quadratic
/// segments as cubic ones).
fn user_elements(&self) -> Res<Vec<(Op, Vec<Point>)>> {
let xf = self.xf();
if xf.determinant().abs() < 1e-12 {
return ps_err("undefinedresult", "");
}
let inv = xf.inverse();
let mut last = Point::ZERO;
let mut out = Vec::with_capacity(self.g.path.elements().len());
for el in self.g.path.elements() {
let (op, pts) = match *el {
PathEl::MoveTo(p) => (Op::MoveTo, vec![p]),
PathEl::LineTo(p) => (Op::LineTo, vec![p]),
PathEl::QuadTo(a, p) => (Op::CurveTo, vec![last + (a - last) * (2.0 / 3.0), p + (a - p) * (2.0 / 3.0), p]),
PathEl::CurveTo(a, b, p) => (Op::CurveTo, vec![a, b, p]),
PathEl::ClosePath => (Op::ClosePath, vec![]),
};
last = pts.last().copied().unwrap_or(last);
out.push((op, pts.into_iter().map(|p| inv * p).collect()));
}
Ok(out)
}
/// `pathforall`: the current path in user space, each element through its procedure.
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<f64> = match nums {
Obj::Array { items, .. } => {
// Each entry of the data array and each byte of the operator string counts as an
// element read.
self.spend_elements(items.len().saturating_add(ops.len()))?;
items.borrow().iter().filter_map(Obj::as_num).collect()
}
_ => return ps_err("typecheck", "an encoded user path"),
};
let mut next = nums.into_iter();
let mut repeat = 1usize;
for code in ops {
if code >= 32 {
repeat = usize::from(code - 32);
continue;
}
let &(op, n) = OPS.get(usize::from(code)).ok_or(PsError::Ps("rangecheck", "a user path".into()))?;
for _ in 0..std::mem::replace(&mut repeat, 1) {
for _ in 0..n {
let v = next.next().ok_or(PsError::Ps("rangecheck", "a user path".into()))?;
self.push_num(v)?;
}
// Each operator run counts as an operation.
self.spend(1)?;
self.op(op)?;
}
}
Ok(())
}
/// `ufill`, `ueofill`, `ustroke` (with an optional matrix): paint a user path, keeping the
/// current path.
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<Obj> = [b.x0, b.y0, b.x1, b.y1].into_iter().map(Obj::Real).collect();
items.push(Obj::Op(Op::SetBBox));
for (op, pts) in elements {
items.extend(pts.iter().flat_map(|p| [Obj::Real(p.x), Obj::Real(p.y)]));
items.push(Obj::Op(op));
}
self.alloc(items.len() * std::mem::size_of::<Obj>())?;
self.push(Obj::proc(items))
}
/// `execform`: the form's `PaintProc` run with its matrix, clipped to its box.
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<f64> = get("BBox").and_then(|o| o.items().map(|i| i.borrow().iter().filter_map(Obj::as_num).collect())).unwrap_or_default();
let paint = get("PaintProc").ok_or(PsError::Ps("undefined", "PaintProc".into()))?;
self.gsave()?;
let depth = self.saved.len();
self.g.ctm *= m;
if let [x0, y0, x1, y1] = bbox[..] {
let bp = self.rect_path(x0, y0, x1 - x0, y1 - y0);
self.take_path();
self.clip_with(bp, FillRule::NonZero);
}
self.push(Obj::Dict(d))?;
let r = self.call(paint);
self.saved.truncate(depth);
self.grestore();
r
}
pub(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<Rect> {
let (bounds, len) =
self.g.clips.iter().rev().find_map(|c| c.region.as_ref()).map_or((self.out.frame, 0), |(p, _)| (p.bounding_box(), p.elements().len()));
self.spend_elements(len)?;
Ok(bounds)
}
}
+328
View File
@@ -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<Rc<str>>,
alt: Rc<Space>,
tint: Obj,
},
Indexed {
base: Rc<Space>,
table: Rc<Vec<f64>>,
},
/// Patterns; an uncoloured one paints in a colour of the underlying space.
Pattern(Option<Rc<Space>>),
}
impl Space {
/// Components a colour has in it.
pub fn n(&self) -> usize {
match self {
Self::Gray | Self::Indexed { .. } | Self::Pattern(_) => 1,
Self::Rgb => 3,
Self::Cmyk => 4,
Self::Inks { names, .. } => names.len().max(1),
}
}
/// The colour `setcolorspace` starts with.
pub(super) fn initial(&self) -> Vec<f64> {
match self {
Self::Cmyk => vec![0.0, 0.0, 0.0, 1.0],
Self::Inks { names, .. } => vec![1.0; names.len().max(1)],
s => vec![0.0; s.n()],
}
}
}
/// A process colour from components in `space` (grey, RGB or CMYK), each clamped to 0–1.
pub(crate) fn process(space: &Space, c: &[f64]) -> Option<Color> {
let v = |i: usize| c.get(i).map_or(0.0, |x| if x.is_finite() { x.clamp(0.0, 1.0) as f32 } else { 0.0 });
Some(match space {
Space::Gray => Color::gray(1.0 - v(0)),
Space::Rgb => Color::rgb(v(0), v(1), v(2)),
Space::Cmyk => Color::cmyk(v(0), v(1), v(2), v(3)),
_ => return None,
})
}
#[derive(Clone, Debug)]
pub(crate) struct GState {
/// User space → PostScript's default space.
pub ctm: Affine,
pub space: Rc<Space>,
/// The `setcolorspace` operand, for `currentcolorspace`.
pub space_obj: Obj,
pub comps: Vec<f64>,
pub paint: Paint,
pub width: f64,
pub cap: LineCap,
pub join: LineJoin,
pub miter: f64,
/// The dash pattern, shared by the states saved or copied from this one.
pub dash: Rc<[f64]>,
pub dash_offset: f64,
/// The current path, in document space.
pub path: BezPath,
pub cur: Option<Point>,
pub start: Option<Point>,
pub clips: Vec<Arc<Clip>>,
/// The clips `clipsave` saved, for `cliprestore`.
pub saved_clips: Vec<Rc<[Arc<Clip>]>>,
pub overprint: bool,
pub font: Option<DictRef>,
/// `nulldevice`: painting draws nothing.
pub null: bool,
/// The colour is a shading pattern whose shading is a mesh: a fill paints it (the shading
/// and pattern space → the document).
pub mesh: Option<(DictRef, Affine)>,
}
impl Default for GState {
fn default() -> Self {
Self {
ctm: Affine::IDENTITY,
space: Rc::new(Space::Gray),
space_obj: Obj::name("DeviceGray"),
comps: vec![0.0],
paint: Paint::solid(Color::gray(1.0)),
width: 1.0,
cap: LineCap::Butt,
join: LineJoin::Miter,
miter: 10.0,
dash: Rc::from([]),
dash_offset: 0.0,
path: BezPath::new(),
cur: None,
start: None,
clips: vec![],
saved_clips: vec![],
overprint: false,
font: None,
null: false,
mesh: None,
}
}
}
/// What the program draws.
pub(crate) struct Out {
/// The document being made: ids, images and swatches.
pub doc: Document,
pub drawn: Drawn,
pub warnings: Vec<String>,
/// PostScript's default space → document space.
pub page: Affine,
/// The page (the bounding box) in document space.
pub frame: Rect,
/// Clip and group ids are given in the order they open, so a chain of both sorts by id.
next_clip: u32,
/// The groups open (outermost first), and how many more opened past [`MAX_NEST`] (their art
/// goes in the innermost group kept).
pub(super) groups: Vec<Arc<Clip>>,
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<BezPath>,
/// Process colours painted, by model (the document opens in CMYK when most are).
pub cmyk: usize,
pub rgb: usize,
/// Spot inks used: name and colour at full tint.
pub spots: Vec<(String, Color)>,
/// The color at full tint of each spot ink in [`Self::spots`], by name.
pub(super) spot_colors: HashMap<Rc<str>, Color>,
/// Rectangles painted with a shading (`shfill`): a clipping group of one is the clip path
/// filled with it.
pub shadings: HashSet<NodeId>,
/// Tiling patterns made into pattern swatches: the pattern's `PaintProc` (a pattern made
/// again with `makepattern` shares it) and cell, the colour an uncoloured one painted in, and
/// the swatch's name. Each list holds the patterns of one [`tiling_hash`]; a pattern whose
/// cell has a NaN corner is left out.
pub(super) tilings: HashMap<u64, Vec<Tiling>>,
/// How deep art drawn apart (a pattern's cell, a Type 3 glyph) is nested.
pub apart: u32,
}
fn sane(r: Rect) -> bool {
[r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite() && v.abs() <= LIMIT)
}
/// A tiling pattern made into a pattern swatch, as [`Out::tilings`] lists it.
pub(super) type Tiling = (Shared<Obj>, Rect, Option<Paint>, String);
/// A hash of what tells tiling patterns apart in [`Out::tilings`]. Patterns that compare equal
/// hash alike: each number goes in by its bits, and −0 goes in as 0.
pub(super) fn tiling_hash(proc: &Shared<Obj>, tile: Rect, under: Option<&Paint>) -> u64 {
let mut h = DefaultHasher::new();
proc.hash(&mut h);
[tile.x0, tile.y0, tile.x1, tile.y1].map(|v| (v + 0.0).to_bits()).hash(&mut h);
if let Some(Paint::Solid { color, swatch, tint }) = under {
let c = match *color {
Color::Rgb { r, g, b } => [r, g, b, 0.0],
Color::Cmyk { c, m, y, k } => [c, m, y, k],
Color::Gray { k } => [k, 0.0, 0.0, 0.0],
Color::Lab { l, a, b } => [l, a, b, 0.0],
};
c.map(|v| (v + 0.0).to_bits()).hash(&mut h);
swatch.hash(&mut h);
(tint + 0.0).to_bits().hash(&mut h);
}
h.finish()
}
impl Out {
pub fn new(doc: Document, page: Affine, frame: Rect) -> Self {
Self {
doc,
drawn: vec![],
warnings: vec![],
page,
frame,
next_clip: 0,
groups: vec![],
too_deep: 0,
elements: 0,
merge: None,
cmyk: 0,
rgb: 0,
spots: vec![],
spot_colors: HashMap::new(),
shadings: HashSet::new(),
tilings: HashMap::new(),
apart: 0,
}
}
pub fn warn(&mut self, w: &str) {
if !self.warnings.iter().any(|x| x == w) {
self.warnings.push(w.to_string());
}
}
/// Open a group: what is drawn until it ends ([`Self::end_group`]) goes in it.
pub fn begin_group(&mut self) {
if self.groups.len() >= MAX_NEST {
self.too_deep += 1;
self.warn(&format!("groups nested more than {MAX_NEST} deep were read as part of the group around them"));
return;
}
self.next_clip += 1;
self.groups.push(Arc::new(Clip { id: self.next_clip, region: None }));
}
/// End the innermost group open (an end without a beginning ends nothing).
pub fn end_group(&mut self) {
if self.too_deep > 0 {
self.too_deep -= 1;
} else {
self.groups.pop();
}
}
/// The chain an object drawn under `clips` is drawn in: the clips and the groups open, in the
/// order they opened.
pub fn chain(&self, clips: &[Arc<Clip>]) -> Vec<Arc<Clip>> {
if self.groups.is_empty() {
return clips.to_vec();
}
let mut chain = Vec::with_capacity(clips.len() + self.groups.len());
let (mut a, mut b) = (clips.iter().peekable(), self.groups.iter().peekable());
while let (Some(x), Some(y)) = (a.peek(), b.peek()) {
let next = if x.id < y.id { a.next() } else { b.next() };
chain.extend(next.cloned());
}
chain.extend(a.chain(b).cloned());
chain
}
/// Add `node` under `clips` (in the groups open); `None` when it was left out.
pub fn push(&mut self, mut node: Node, clips: &[Arc<Clip>]) -> Option<NodeId> {
self.merge = None;
let chain = self.chain(clips);
let size = node.path_data().map_or(1, |p| p.subpaths.iter().map(|s| s.anchors.len()).sum()) + chain.len();
self.elements = self.elements.saturating_add(size);
if self.drawn.len() >= MAX_NODES || self.elements > MAX_DRAWN {
self.warn(TOO_MUCH);
return None;
}
if !node.visual_bounds().is_none_or(sane) {
self.warn(FAR_AWAY);
return None;
}
node.id = self.doc.alloc_id();
let id = node.id;
self.drawn.push((chain, node));
Some(id)
}
/// Count a paint's colour model.
fn count(&mut self, p: &Paint) {
match p {
Paint::Solid { color: Color::Cmyk { .. }, swatch: None, .. } => self.cmyk += 1,
Paint::Solid { color: Color::Rgb { .. }, swatch: None, .. } => self.rgb += 1,
_ => {}
}
}
/// Fill `bp` with `paint`.
pub(super) fn fill(&mut self, bp: BezPath, rule: FillRule, paint: Paint, overprint: bool, clips: &[Arc<Clip>]) {
self.count(&paint);
let item = AppearanceItem::Fill(FillLayer { overprint, ..FillLayer::new(paint) });
let mut n = Node::path(NodeId(0), PathData::from_bezpath(&bp), Appearance { items: vec![item], ..Appearance::default() });
if let NodeKind::Path { rule: r, .. } = &mut n.kind {
*r = rule;
}
if self.push(n, clips).is_some() {
self.merge = Some(bp);
}
}
/// Stroke `bp` with `st`: joins the object just filled with the same path, under the same
/// clips (`gsave fill grestore stroke`).
pub(super) fn stroke(&mut self, bp: BezPath, st: StrokeLayer, clips: &[Arc<Clip>]) {
self.count(&st.paint);
if self.merge.as_ref() == Some(&bp) {
let here = self.chain(clips);
if let Some((chain, last)) = self.drawn.last_mut()
&& chain.iter().map(|c| c.id).eq(here.iter().map(|c| c.id))
{
last.appearance.items.push(AppearanceItem::Stroke(st));
self.merge = None;
return;
}
}
let n = Node::path(NodeId(0), PathData::from_bezpath(&bp), Appearance { items: vec![AppearanceItem::Stroke(st)], ..Appearance::default() });
self.push(n, clips);
}
/// A new clip of `path` inside `clips`; `None` for a rectangle around the whole page, which
/// clips nothing.
pub(super) fn clip(&mut self, path: BezPath, rule: FillRule) -> Option<Arc<Clip>> {
// Only a polygon that really covers the page: a diamond touching its edges has the page as
// its bounding box but cuts its corners away (#1542).
if path.elements().len() <= 6 && vectorcraft_geom::hit::polygon_covers_rect(&path, self.frame, 0.5) {
return None;
}
self.next_clip += 1;
Some(Arc::new(Clip { id: self.next_clip, region: Some((path, rule)) }))
}
}
/// The smallest angle from `a` to `b` (radians, −π to π).
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)
}