Merge pull request #1329 from dotCooCoo/feat/eps-uncounted-per-call-path-work
EPS: huge arcs, fine dashes and rectangle arrays in a loop no longer hang the import
This commit is contained in:
commit
9d14654260
|
|
@ -402,6 +402,13 @@ impl Interp<'_> {
|
|||
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.
|
||||
|
|
@ -424,10 +431,12 @@ impl Interp<'_> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Path elements in user space appended (the first move a line when there is a current point).
|
||||
/// 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;
|
||||
|
|
@ -516,6 +525,8 @@ impl Interp<'_> {
|
|||
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()))?
|
||||
}
|
||||
|
|
@ -606,6 +617,7 @@ impl Interp<'_> {
|
|||
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)?;
|
||||
|
|
@ -649,7 +661,12 @@ impl Interp<'_> {
|
|||
(Op::UCache, 0),
|
||||
];
|
||||
let nums: Vec<f64> = match nums {
|
||||
Obj::Array { items, .. } => items.borrow().iter().filter_map(Obj::as_num).collect(),
|
||||
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();
|
||||
|
|
@ -665,6 +682,8 @@ impl Interp<'_> {
|
|||
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)?;
|
||||
}
|
||||
}
|
||||
|
|
@ -752,9 +771,13 @@ impl Interp<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
/// The area the clip leaves, in document space (the page without a clip).
|
||||
fn clip_bounds(&self) -> Rect {
|
||||
self.g.clips.iter().rev().find_map(|c| c.region.as_ref()).map_or(self.out.frame, |(p, _)| p.bounding_box())
|
||||
/// The area the clip leaves, in document space (the page without a clip). The clip path read
|
||||
/// counts against the operation budget.
|
||||
fn clip_bounds(&mut self) -> Res<Rect> {
|
||||
let (bounds, len) =
|
||||
self.g.clips.iter().rev().find_map(|c| c.region.as_ref()).map_or((self.out.frame, 0), |(p, _)| (p.bounding_box(), p.elements().len()));
|
||||
self.spend_elements(len)?;
|
||||
Ok(bounds)
|
||||
}
|
||||
|
||||
// ---------- colour ----------
|
||||
|
|
@ -1181,11 +1204,16 @@ impl Interp<'_> {
|
|||
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());
|
||||
if st.dash.is_some() && !(period > 0.0 && bp.perimeter(1e-3) / period < MAX_DASHES) {
|
||||
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"));
|
||||
}
|
||||
|
|
@ -1245,13 +1273,20 @@ impl Interp<'_> {
|
|||
self.push(Obj::Int(0))?;
|
||||
self.push(Obj::Int(0))?;
|
||||
}
|
||||
GStateNew => self.push(Obj::GState(Rc::new(self.g.clone())))?,
|
||||
GStateNew => {
|
||||
self.spend_elements(self.g.path.elements().len())?;
|
||||
self.push(Obj::GState(Rc::new(self.g.clone())))?;
|
||||
}
|
||||
CurrentGState => {
|
||||
self.pop()?;
|
||||
self.spend_elements(self.g.path.elements().len())?;
|
||||
self.push(Obj::GState(Rc::new(self.g.clone())))?;
|
||||
}
|
||||
SetGState => match self.pop()? {
|
||||
Obj::GState(g) => self.g = (*g).clone(),
|
||||
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(),
|
||||
|
|
@ -1573,13 +1608,14 @@ impl Interp<'_> {
|
|||
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();
|
||||
let b = self.clip_bounds()?;
|
||||
self.take_path();
|
||||
self.g.path = b.to_path(0.1);
|
||||
self.g.cur = Some(b.origin());
|
||||
|
|
@ -1600,6 +1636,7 @@ impl Interp<'_> {
|
|||
}
|
||||
}
|
||||
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 });
|
||||
}
|
||||
|
|
@ -1643,7 +1680,7 @@ impl Interp<'_> {
|
|||
}
|
||||
let m = self.xf();
|
||||
if let Some(g) = self.gradient(&d, m)? {
|
||||
let area = self.clip_bounds();
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -669,6 +669,75 @@ fn read_soon(body: &str) -> crate::Imported {
|
|||
rx.recv_timeout(std::time::Duration::from_secs(20)).unwrap_or_else(|_| panic!("reading {body} did not finish")).unwrap()
|
||||
}
|
||||
|
||||
/// Reads `body` after a square through [`read_soon`]: the square stays, and the program runs out
|
||||
/// of the operation budget.
|
||||
fn runs_out_of_budget(body: &str) {
|
||||
let r = read_soon(&format!("0 0 10 10 rectfill {body}"));
|
||||
assert_eq!(objects(&r.document).len(), 1, "{body}: {:?}", r.warnings);
|
||||
assert!(r.warnings.iter().any(|w| w.contains("the program runs too long")), "{body}: {:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// A loop of operators whose work grows with the path they make runs out of the operation budget
|
||||
/// (PLRM 3rd ed., chapter 8): `arc` and `arct` with a huge radius, which make thousands of curve
|
||||
/// segments, `strokepath` with a dash far finer than the line, dashes of length zero included, and
|
||||
/// `rectfill` with an array of 2,000 rectangles.
|
||||
#[test]
|
||||
fn paths_made_in_a_loop_run_out_of_the_budget() {
|
||||
for body in [
|
||||
"{ newpath 0 0 2.2e18 0 1440 arc } loop",
|
||||
"{ newpath 0 0 moveto 1 0 1 1 1e30 arct } loop",
|
||||
"[1] 0 setdash { newpath 0 0 moveto 9900 0 lineto strokepath } loop",
|
||||
"[0 1] 0 setdash { newpath 0 0 moveto 9900 0 lineto strokepath } loop",
|
||||
"/a [ 0 1 1999 { pop 0 0 1 1 } for ] def nulldevice { a rectfill } loop",
|
||||
] {
|
||||
runs_out_of_budget(body);
|
||||
}
|
||||
}
|
||||
|
||||
/// A loop of operators that read or copy the whole current path or clip path runs out of the
|
||||
/// operation budget (PLRM 3rd ed., chapter 8): `pathbbox`, `clip`, `clippath`, `shfill` (which
|
||||
/// reads the clip path for the area it paints), `pathforall` (which copies the path before it runs
|
||||
/// a procedure), `gstate`, `currentgstate` and `setgstate`.
|
||||
#[test]
|
||||
fn paths_read_or_copied_in_a_loop_run_out_of_the_budget() {
|
||||
// 10,001 elements reaching 10,000,000 points to the right: what `shfill` paints in a clip of
|
||||
// them lies far outside the page and is left out.
|
||||
let path = "newpath 0 0 moveto 0 1 9999 { pop 1000 0 rlineto } for";
|
||||
let sh = "/sh << /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 10 0] \
|
||||
/Function << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> >> def";
|
||||
for body in [
|
||||
"{ pathbbox pop pop pop pop } loop",
|
||||
"{ clip } loop",
|
||||
"clip newpath { clippath } loop",
|
||||
"clip newpath { sh shfill } loop",
|
||||
"{ { pop pop exit } { } { } { } pathforall } loop",
|
||||
"{ gstate pop } loop",
|
||||
"/g gstate def { g currentgstate pop } loop",
|
||||
"/g gstate def { g setgstate } loop",
|
||||
] {
|
||||
runs_out_of_budget(&format!("{sh} {path} {body}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// A loop of `uappend` on an encoded user path runs out of the operation budget (PLRM 3rd ed.,
|
||||
/// section 4.6.2 and Table 4.3: each byte of the operator string is an operator, or a count of up
|
||||
/// to 223 repeats of the next one): one that runs `ucache` millions of times, one of repeat counts
|
||||
/// alone, and one with a long data array.
|
||||
#[test]
|
||||
fn encoded_user_paths_in_a_loop_run_out_of_the_budget() {
|
||||
for body in [
|
||||
// 100,000 pairs of a count of 223 repeats (byte 255) and `ucache` (byte 11): each
|
||||
// `uappend` runs `ucache` 22,300,000 times.
|
||||
"/s 200000 string def 0 2 199998 { dup s exch 255 put 1 add s exch 11 put } for /up [ [ ] s ] def",
|
||||
// 200,000 repeat counts and no operator.
|
||||
"/s 200000 string def 0 1 199999 { s exch 255 put } for /up [ [ ] s ] def",
|
||||
// A data array of 400,000 nulls and an empty operator string.
|
||||
"/up [ 400000 array () ] def",
|
||||
] {
|
||||
runs_out_of_budget(&format!("{body} {{ up uappend }} loop"));
|
||||
}
|
||||
}
|
||||
|
||||
/// `arc` raises its second angle by multiples of 360 until it is at least the first, and `arcn`
|
||||
/// lowers it until it is at most the first (PLRM 3rd ed., `arc` and `arcn` in chapter 8), however
|
||||
/// large the angles: the arc runs between the angles' remainders after whole turns and ends at
|
||||
|
|
|
|||
Loading…
Reference in New Issue