EPS and PDF: huge arcs, long paths and loop passes count against the import's limits

The EPS interpreter counted one operation against its budget for each
name it ran, whatever the operator did, and paths had no length limit.
Parsing one arc at a radius of 10^300 passed 3 GiB of resident memory
within 43 seconds. A loop of arc at a radius of 2.2 x 10^18 was still
running after 60 seconds, and a loop of pathbbox on a path of 100,001
elements and { {} loop } loop did not finish within 20 seconds. PDF
content had no path limit either: parsing a page that repeats
0 0 1 1 re 4,000,000 times peaked at 2.3 GiB of resident memory.

A path now holds at most 1,048,576 elements (MAX_PATH, the limit of
VectorCraft's EPS interpreter). In EPS, moveto, lineto, curveto,
closepath, arc, charpath and the rect operators check it, and an
operator that would take a path past it stops the program with
"PostScript (path limit)". In PDF, m, l, c, v, y, h and re (each adds
at most five elements) stop the content stream with
"content (path limit)" when the path already holds more than 1,048,571.
Each path element an operator makes, reads or copies (arc, rectangle
arrays, charpath, pathbbox, clip, eoclip, gsave, save) now counts
against the EPS budget. Each pass of repeat, for, loop and forall
counts too, and so does each procedure item that is not a name. A
moveto right after a moveto replaces it, as the PostScript Language
Reference describes, so text placed with moveto and show leaves one
element.

Five tests in tests_import.rs cover the loops, the path limit in both
interpreters and runs of moveto and show.
This commit is contained in:
dotCooCoo
2026-10-14 10:19:05 -07:00
parent 05955c08b4
commit 2f437ea551
4 changed files with 228 additions and 53 deletions
+4
View File
@@ -3,6 +3,10 @@
use effectcraft_svg::{Affine, BezPath, FillRule, Geom, Group, Node, Paint, Shape, Stroke};
/// The most elements a path being built holds: an operator that would add more stops the
/// PostScript program, or the PDF content stream.
pub(crate) const MAX_PATH: usize = 1 << 20;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Kind {
Plain,
+7 -2
View File
@@ -12,7 +12,7 @@ use effectcraft_svg::{
};
use kurbo::{Point, Shape as _};
use crate::build::Builder;
use crate::build::{Builder, MAX_PATH};
use crate::color::{Cs, Func, color_space, text_string};
use crate::font::Font;
use crate::object::{Dict, File, Lexer, Obj, decode_stream};
@@ -281,7 +281,12 @@ impl<'a> Interp<'a> {
self.ext_gstate(&g, res, depth);
}
}
// Path construction.
// Path construction. Each operator adds at most five elements (`re`), so when fewer
// than five more fit within `MAX_PATH`, the operator stops the stream.
"m" | "l" | "c" | "v" | "y" | "h" | "re" if self.path.elements().len() > MAX_PATH - 5 => {
self.b.skip("content (path limit)");
return;
}
"m" => {
let p = Point::new(n(0), n(1));
self.path.move_to(p);
+123 -51
View File
@@ -14,7 +14,7 @@ use std::rc::Rc;
use effectcraft_svg::{Affine, BezPath, Cap, FillRule, Join, Paint, Stroke};
use kurbo::{PathEl, Point};
use crate::build::Builder;
use crate::build::{Builder, MAX_PATH};
use crate::font::Font;
use crate::object::{ascii85, is_white, parse_num};
@@ -441,7 +441,14 @@ impl Interp {
break;
}
}
other => self.push(other.clone()),
// Other items count against the budget as names do.
other => {
r = self.spend(1);
if r.is_err() {
break;
}
self.push(other.clone());
}
}
}
self.calls -= 1;
@@ -456,11 +463,7 @@ impl Interp {
}
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;
self.spend(1)?;
if let Some(v) = self.lookup(n) {
return match v {
V::Proc(_) => self.exec_obj(v, src),
@@ -473,6 +476,21 @@ impl Interp {
self.builtin(n, src)
}
/// Count `n` operations against the budget, and stop the program when fewer are left. An
/// operator that makes, reads or copies path elements counts each one as an operation.
fn spend(&mut self, n: usize) -> Result<(), Flow> {
match self.budget.checked_sub(n) {
Some(left) => {
self.budget = left;
Ok(())
}
None => {
self.b.skip("PostScript (operation limit)");
Err(Flow::Quit)
}
}
}
fn device_scale(&self) -> f64 {
self.gs.ctm.determinant().abs().sqrt()
}
@@ -483,13 +501,34 @@ impl Interp {
Point::new(x, y)
}
fn move_to(&mut self, p: Point) {
/// Stop the program when a path of `len` elements would be longer than [`MAX_PATH`].
fn path_fits(&mut self, len: usize) -> Result<(), Flow> {
if len <= MAX_PATH {
return Ok(());
}
self.b.skip("PostScript (path limit)");
Err(Flow::Quit)
}
/// [`Self::path_fits`] for the current path with `n` more elements.
fn room(&mut self, n: usize) -> Result<(), Flow> {
self.path_fits(self.gs.path.elements().len().saturating_add(n))
}
fn move_to(&mut self, p: Point) -> Result<(), Flow> {
// A moveto right after a moveto replaces it (PostScript Language Reference §8.2, `moveto`).
if matches!(self.gs.path.elements().last(), Some(PathEl::MoveTo(_))) {
self.gs.path.pop();
}
self.room(1)?;
let q = self.gs.ctm * p;
self.gs.path.move_to(q);
self.gs.cur = Some(q);
self.gs.start = Some(q);
Ok(())
}
fn line_to(&mut self, p: Point) {
fn line_to(&mut self, p: Point) -> Result<(), Flow> {
self.room(1 + usize::from(self.gs.cur.is_none()))?;
let q = self.gs.ctm * p;
if self.gs.cur.is_none() {
self.gs.path.move_to(q);
@@ -497,20 +536,23 @@ impl Interp {
}
self.gs.path.line_to(q);
self.gs.cur = Some(q);
Ok(())
}
fn curve_to(&mut self, a: Point, b: Point, c: Point) {
fn curve_to(&mut self, a: Point, b: Point, c: Point) -> Result<(), Flow> {
self.room(1 + usize::from(self.gs.cur.is_none()))?;
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);
Ok(())
}
fn current_user(&self) -> Point {
self.gs.ctm.inverse() * self.gs.cur.unwrap_or_default()
}
fn arc(&mut self, neg: bool) {
fn arc(&mut self, neg: bool) -> Result<(), Flow> {
let a2 = self.popn();
let a1 = self.popn();
let r = self.popn();
@@ -540,16 +582,23 @@ impl Interp {
let (a1, sweep) = ((a1 % 360.0).to_radians(), sweep.to_radians());
let start = Point::new(c.x + r * a1.cos(), c.y + r * a1.sin());
if self.gs.cur.is_some() {
self.line_to(start);
self.line_to(start)?;
} else {
self.move_to(start);
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| {
// kurbo divides an arc into more curves the larger the radius (about 3.7 × 10^49 for a
// quarter turn at a radius of 10^300, more than a usize can count), so each one counts
// against the budget and the path limit.
let mut curves = arc.append_iter(0.1);
while let Some(PathEl::CurveTo(p1, p2, p3)) = curves.next() {
self.room(1)?;
self.spend(1)?;
self.gs.path.curve_to(m * p1, m * p2, m * p3);
self.gs.cur = Some(m * p3);
});
}
Ok(())
}
fn paint(&mut self, fill: Option<FillRule>, stroke: bool) {
@@ -569,11 +618,16 @@ impl Interp {
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<BezPath> {
fn rect_path(&mut self) -> Result<Vec<BezPath>, Flow> {
// x y w h, or an array / string of numbers.
let top = self.pop();
let nums: Vec<f64> = match &top {
V::Array(_) | V::Proc(_) => top.items().unwrap_or_default().iter().filter_map(V::num).collect(),
V::Array(_) | V::Proc(_) => {
let items = top.items().unwrap_or_default();
// An array's rectangles count against the budget, five path elements each.
self.spend(items.len() / 4 * 5)?;
items.iter().filter_map(V::num).collect()
}
V::Num(h) => {
let w = self.popn();
let y = self.popn();
@@ -582,8 +636,10 @@ impl Interp {
}
_ => vec![],
};
self.path_fits(nums.len() / 4 * 5)?;
let m = self.gs.ctm;
nums.as_chunks::<4>()
Ok(nums
.as_chunks::<4>()
.0
.iter()
.map(|r| {
@@ -595,7 +651,7 @@ impl Interp {
p.close_path();
p
})
.collect()
.collect())
}
fn grestore(&mut self) {
@@ -809,10 +865,12 @@ impl Interp {
let c = matches!(self.pop(), V::Bool(true));
self.exec_obj(if c { t } else { f }, src)?;
}
// Each pass of a loop counts against the budget.
"repeat" => {
let p = self.pop();
let k = self.popn().clamp(0.0, 1e6) as usize;
for _ in 0..k {
self.spend(1)?;
match self.exec_obj(p.clone(), src) {
Err(Flow::Exit) => break,
r => r?,
@@ -826,6 +884,7 @@ impl Interp {
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.spend(1)?;
self.push(V::Num(i));
match self.exec_obj(p.clone(), src) {
Err(Flow::Exit) => break,
@@ -838,6 +897,7 @@ impl Interp {
"loop" => {
let p = self.pop();
for _ in 0..1_000_000 {
self.spend(1)?;
match self.exec_obj(p.clone(), src) {
Err(Flow::Exit) => break,
r => r?,
@@ -850,6 +910,7 @@ impl Interp {
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.spend(1)?;
self.push(V::Lit(k.into()));
self.push(v);
match self.exec_obj(p.clone(), src) {
@@ -861,6 +922,7 @@ impl Interp {
V::Str(s) => {
let bytes = s.borrow().clone();
for c in bytes {
self.spend(1)?;
self.push(V::Num(c as f64));
match self.exec_obj(p.clone(), src) {
Err(Flow::Exit) => break,
@@ -870,6 +932,7 @@ impl Interp {
}
a => {
for v in a.items().unwrap_or_default() {
self.spend(1)?;
self.push(v);
match self.exec_obj(p.clone(), src) {
Err(Flow::Exit) => break,
@@ -1127,6 +1190,7 @@ impl Interp {
"languagelevel" => self.push(V::Num(3.0)),
"version" | "product" => self.push(V::Str(Rc::new(RefCell::new(b"3010".to_vec())))),
"save" => {
self.spend(self.gs.path.elements().len())?;
self.saved.push(self.gs.clone());
self.gs.depth = self.b.depth();
self.push(V::Null);
@@ -1156,6 +1220,7 @@ impl Interp {
}
// Graphics state.
"gsave" => {
self.spend(self.gs.path.elements().len())?;
let mut g = self.gs.clone();
g.depth = self.b.depth();
self.saved.push(g);
@@ -1316,21 +1381,21 @@ impl Interp {
}
"moveto" => {
let p = self.user_point();
self.move_to(p);
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));
self.move_to(Point::new(c.x + d.x, c.y + d.y))?;
}
"lineto" => {
let p = self.user_point();
self.line_to(p);
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));
self.line_to(Point::new(c.x + d.x, c.y + d.y))?;
}
"curveto" | "rcurveto" => {
let p3 = self.user_point();
@@ -1339,25 +1404,27 @@ impl Interp {
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));
self.curve_to(o(p1), o(p2), o(p3))?;
} else {
self.curve_to(p1, p2, p3);
self.curve_to(p1, p2, p3)?;
}
}
"closepath" => {
if self.gs.cur.is_some() {
self.room(1)?;
self.gs.path.close_path();
self.gs.cur = self.gs.start;
}
}
"arc" => self.arc(false),
"arcn" => self.arc(true),
"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" => {
self.spend(self.gs.path.elements().len())?;
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));
@@ -1376,13 +1443,14 @@ impl Interp {
"stroke" => self.paint(None, true),
"clip" | "eoclip" => {
let rule = if n == "clip" { FillRule::NonZero } else { FillRule::EvenOdd };
self.spend(self.gs.path.elements().len())?;
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();
let paths = self.rect_path()?;
if n == "rectstroke" && matches!(self.st.last(), Some(V::Array(_))) {
self.pop();
}
@@ -1617,9 +1685,9 @@ impl Interp {
/// A string's glyph outlines (device space) from the current point, with an extra
/// displacement `extra(index, code)` (user space) after each glyph; moves the current
/// point.
fn text_path(&mut self, bytes: &[u8], mut extra: impl FnMut(usize, u32) -> (f64, f64)) -> Option<(BezPath, String)> {
let d = self.gs.font.clone()?;
let (font, m) = self.font_of(&d)?;
fn text_path(&mut self, bytes: &[u8], mut extra: impl FnMut(usize, u32) -> (f64, f64)) -> Result<Option<(BezPath, String)>, Flow> {
let Some(d) = self.gs.font.clone() else { return Ok(None) };
let Some((font, m)) = self.font_of(&d) else { return Ok(None) };
let mut p = self.current_user();
let mut path = BezPath::new();
let mut text = String::new();
@@ -1634,8 +1702,8 @@ impl Interp {
let (ex, ey) = extra(i, code);
p += adv + kurbo::Vec2::new(ex, ey);
}
self.move_to(p);
Some((path, text))
self.move_to(p)?;
Ok(Some((path, text)))
}
fn fill_text(&mut self, path: BezPath, text: &str) {
@@ -1651,11 +1719,12 @@ impl Interp {
}
}
fn show_with(&mut self, s: &[u8], extra: impl FnMut(usize, u32) -> (f64, f64)) {
match self.text_path(s, extra) {
fn show_with(&mut self, s: &[u8], extra: impl FnMut(usize, u32) -> (f64, f64)) -> Result<(), Flow> {
match self.text_path(s, extra)? {
Some((path, text)) => self.fill_text(path, &text),
None => self.b.skip("text (no font)"),
}
Ok(())
}
/// Text and font operators (PostScript Language Reference §5.1, §8.2).
@@ -1713,20 +1782,20 @@ impl Interp {
"StandardEncoding" | "ISOLatin1Encoding" => self.push(encoding_array(n)),
"show" => {
let s = self.pop_string();
self.show_with(&s, |_, _| (0.0, 0.0));
self.show_with(&s, |_, _| (0.0, 0.0))?;
}
"ashow" => {
let s = self.pop_string();
let ay = self.popn();
let ax = self.popn();
self.show_with(&s, |_, _| (ax, ay));
self.show_with(&s, |_, _| (ax, ay))?;
}
"widthshow" => {
let s = self.pop_string();
let ch = self.popn() as u32;
let cy = self.popn();
let cx = self.popn();
self.show_with(&s, |_, c| if c == ch { (cx, cy) } else { (0.0, 0.0) });
self.show_with(&s, |_, c| if c == ch { (cx, cy) } else { (0.0, 0.0) })?;
}
"awidthshow" => {
let s = self.pop_string();
@@ -1735,18 +1804,18 @@ impl Interp {
let ch = self.popn() as u32;
let cy = self.popn();
let cx = self.popn();
self.show_with(&s, |_, c| if c == ch { (cx + ax, cy + ay) } else { (ax, ay) });
self.show_with(&s, |_, c| if c == ch { (cx + ax, cy + ay) } else { (ax, ay) })?;
}
"kshow" | "cshow" => {
// The procedure between characters is not run.
let s = self.pop_string();
self.pop();
self.show_with(&s, |_, _| (0.0, 0.0));
self.show_with(&s, |_, _| (0.0, 0.0))?;
}
"xshow" | "yshow" | "xyshow" => {
let disp: Vec<f64> = self.pop().items().unwrap_or_default().iter().filter_map(V::num).collect();
let s = self.pop_string();
self.explicit_show(&s, &disp, n);
self.explicit_show(&s, &disp, n)?;
}
"glyphshow" => {
let name = self.pop().key();
@@ -1757,7 +1826,7 @@ impl Interp {
let tmp = transformed(&d, Affine::IDENTITY);
tmp.borrow_mut().insert("Encoding".into(), array(e));
self.gs.font = Some(tmp);
self.show_with(b"A", |_, _| (0.0, 0.0));
self.show_with(b"A", |_, _| (0.0, 0.0))?;
self.gs.font = Some(d);
}
"charpath" => {
@@ -1765,14 +1834,17 @@ impl Interp {
let s = self.pop_string();
let saved = std::mem::take(&mut self.gs.path);
let start = self.gs.start;
match self.text_path(&s, |_, _| (0.0, 0.0)) {
match self.text_path(&s, |_, _| (0.0, 0.0))? {
Some((glyphs, _)) => {
let mut p = saved;
p.extend(glyphs);
// The outlines join the path: each element counts against the budget and
// the path limit.
self.gs.path = saved;
self.room(glyphs.elements().len() + 1)?;
self.spend(glyphs.elements().len())?;
self.gs.path.extend(glyphs);
if let Some(c) = self.gs.cur {
p.move_to(c);
self.gs.path.move_to(c);
}
self.gs.path = p;
self.gs.start = start;
}
None => self.gs.path = saved,
@@ -1822,7 +1894,7 @@ impl Interp {
/// `xshow` / `yshow` / `xyshow`: glyph displacements from a number array instead of the
/// advances.
fn explicit_show(&mut self, s: &[u8], disp: &[f64], n: &str) {
fn explicit_show(&mut self, s: &[u8], disp: &[f64], n: &str) -> Result<(), Flow> {
let step = if n == "xyshow" { 2 } else { 1 };
let d: Vec<(f64, f64)> = (0..s.len())
.map(|k| {
@@ -1837,13 +1909,13 @@ impl Interp {
.collect();
let Some(fd) = self.gs.font.clone() else {
self.b.skip("text (no font)");
return;
return Ok(());
};
let Some((font, m)) = self.font_of(&fd) else { return };
let Some((font, m)) = self.font_of(&fd) else { return Ok(()) };
self.show_with(s, move |k, c| {
let adv = m * Point::new(font.width(c), 0.0) - m * Point::ZERO;
let (dx, dy) = d.get(k).copied().unwrap_or((0.0, 0.0));
(dx - adv.x, dy - adv.y)
});
})
}
}
+94
View File
@@ -359,6 +359,100 @@ fn arcs_with_huge_angles_end_at_their_second_angle() {
assert!(img.get(50, 50)[3] > 0.99, "{:?}", img.get(50, 50));
}
/// Parses `body` after a square through [`eps_soon`]: the square stays, and the program stops at
/// the operation limit.
fn runs_out_of_budget(body: &str) {
let doc = eps_soon(&format!("0 0 10 10 rectfill {body}"));
assert_eq!(doc.flatten().len(), 1, "{body}: {:?}", doc.skipped);
assert_eq!(doc.skipped, ["PostScript (operation limit)"], "{body}");
}
#[test]
fn paths_made_in_a_loop_run_out_of_the_budget() {
// Each curve, rectangle element and glyph outline element an operator makes counts against the
// operation budget: `arc` at a radius of 2.2 × 10^18 makes about 6,800 curves a call,
// `rectclip` with an array of 2,000 rectangles makes 10,000 elements, and `charpath` makes the
// outlines of 1,000 H's.
for body in [
"{ newpath 0 0 2200000000000000000 0 1440 arc } loop",
"/a 8000 array def 0 1 7999 { a exch 0 put } for { gsave a rectclip grestore } loop",
"/Helvetica findfont 10 scalefont setfont /s 1000 string def 0 1 999 { s exch 72 put } for \
{ newpath 0 0 moveto s false charpath } loop",
] {
runs_out_of_budget(body);
}
}
#[test]
fn paths_read_or_copied_in_a_loop_run_out_of_the_budget() {
// `pathbbox` reads each element of the current path, and `gsave`, `save` and `clip` copy it:
// each element counts against the operation budget. The path has 100,001 elements.
let path = "newpath 0 0 moveto 0 1 99999 { pop 1 0 rlineto } for";
for body in ["{ pathbbox pop pop pop pop } loop", "{ gsave grestore } loop", "{ save restore } loop", "{ gsave clip grestore } loop"] {
runs_out_of_budget(&format!("{path} {body}"));
}
}
#[test]
fn nested_loops_run_out_of_the_budget() {
// Each pass of `loop`, `repeat`, `for` and `forall` counts against the operation budget, and
// so does each item of a procedure that is not a name. `clear` empties the operand stack that
// the passes fill.
let literals = format!("{{ {} clear }} loop", "0 ".repeat(100_000));
for body in [
"{ {} loop } loop",
"{ 1000000 {} repeat } loop",
"{ 0 1 999999 {} for clear } loop",
"/s 1000000 string def { s {} forall clear } loop",
"/a 1000000 array def { a {} forall clear } loop",
"/d 1 dict def 0 1 99999 { d exch 0 put } for { d {} forall clear } loop",
literals.as_str(),
] {
runs_out_of_budget(body);
}
}
#[test]
fn paths_stop_at_the_path_limit() {
// A path holds at most 1,048,576 elements. An operator that would add more stops the program:
// the square drawn before it stays, and the square after it is left out.
for body in [
"newpath 0 0 moveto 1100 { 1000 { 1 0 rlineto } repeat } repeat",
"newpath 0 0 moveto 0 0 0 0 0 0 1100 { 1000 { 6 copy rcurveto } repeat } repeat",
"newpath 0 0 moveto 1100 { 1000 { closepath } repeat } repeat",
// 1,024 turns at a radius of 2 × 10^17: about 1,171,000 curves.
"newpath 0 0 200000000000000000 0 368640 arc",
// At a radius of 10^300, kurbo divides the arc into more curves than a usize can count.
"newpath 0 0 10 300 exp 0 90 arc",
// The outlines of 100,000 H's, 13 elements each.
"/Helvetica findfont 10 scalefont setfont /s 100000 string def 0 1 99999 { s exch 72 put } for \
0 0 moveto s false charpath",
// 210,000 rectangles of five elements each.
"/a 840000 array def 0 1 839999 { a exch 0 put } for a rectfill",
] {
let doc = eps_soon(&format!("0 0 10 10 rectfill {body} 20 20 10 10 rectfill"));
assert_eq!(doc.flatten().len(), 1, "{body}: {:?}", doc.skipped);
assert_eq!(doc.skipped, ["PostScript (path limit)"], "{body}");
}
// A `moveto` replaces a `moveto` just before it (PostScript Language Reference §8.2, `moveto`),
// and so does the current point that `show` sets, so 550,000 pieces of text placed with
// `moveto` and `show` leave a path of one element.
let doc = eps_soon("/Helvetica findfont 10 scalefont setfont 550 { 1000 { 0 0 moveto ( ) show } repeat } repeat 0 0 10 10 rectfill");
assert!(doc.skipped.is_empty(), "{:?}", doc.skipped);
assert_eq!(doc.flatten().len(), 1);
}
#[test]
fn pdf_paths_stop_at_the_path_limit() {
// A content stream's current path holds at most 1,048,576 elements: `re` (five elements each)
// and `h` stop the stream at the limit, and the square painted before it stays.
for (op, n) in [("0 0 1 1 re ", 210_000), ("h ", 1_100_000)] {
let doc = parse(&page_pdf(&format!("0 0 10 10 re f 0 0 m {} f 20 20 10 10 re f", op.repeat(n)), "", vec![])).unwrap();
assert_eq!(doc.flatten().len(), 1, "{op}: {:?}", doc.skipped);
assert_eq!(doc.skipped, ["content (path limit)"], "{op}");
}
}
#[test]
fn closing_an_empty_path_is_a_no_op() {
// "s", "b" and "b*" closed the path without a current point (kurbo asserts on that).