mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-11 23:32:17 +00:00
EPS: bound copies, storage, text outlines, group nesting and font discovery (#2953)
* EPS: bound copies, storage, text outlines, group nesting and font discovery The PostScript interpreter's numeric copy grew the operand stack past its limit and charged nothing for the values copied; array and string constructors retained storage without charging it; show and charpath copied every glyph outline into the text path before any path or budget check; and the font pre-scan searched the whole prefix again for every eexec, as the bounding-box scan did for every rejected marker on a line. Clipping groups also nested without limit. Copies past the operand limit copy nothing; copies, arrays and strings count against the operation budget; each outline is charged and checked against the path limit before it is copied; headers are found in one pass and each is tried once; a rejected bounding-box comment is skipped to its line end; and the output builder drops groups past 256 open ones. * address review: EPS font look-back stops at the previous font; charge input characters * address review: count enclosing groups against the nesting limit in pattern and mask builders * address review: close a replaced soft mask before nesting the new one; skip patterns whose wrappers pass the depth limit
This commit is contained in:
+32
-4
@@ -7,6 +7,9 @@ use effectcraft_svg::{Affine, BezPath, FillRule, Geom, Group, Node, Paint, Shape
|
||||
/// PostScript program, or the PDF content stream.
|
||||
pub(crate) const MAX_PATH: usize = 1 << 20;
|
||||
|
||||
/// The deepest the open groups nest (the render tree and its consumers recurse through them).
|
||||
const MAX_DEPTH: usize = 256;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Kind {
|
||||
Plain,
|
||||
@@ -20,6 +23,9 @@ pub(crate) struct Builder {
|
||||
pub top_layers: Vec<bool>,
|
||||
pub skipped: Vec<String>,
|
||||
pub shapes: usize,
|
||||
/// Groups the finished nodes will sit under once placed (an enclosing builder's open groups
|
||||
/// and the pattern or mask groups around them), counted against [`MAX_DEPTH`].
|
||||
base: usize,
|
||||
}
|
||||
|
||||
pub(crate) fn group(name: &str) -> Group {
|
||||
@@ -28,13 +34,25 @@ pub(crate) fn group(name: &str) -> Group {
|
||||
|
||||
impl Builder {
|
||||
pub fn new() -> Builder {
|
||||
Builder { stack: vec![(group("page"), Kind::Plain)], top_layers: vec![], skipped: vec![], shapes: 0 }
|
||||
Builder { stack: vec![(group("page"), Kind::Plain)], top_layers: vec![], skipped: vec![], shapes: 0, base: 0 }
|
||||
}
|
||||
|
||||
/// A builder for content that will be placed inside this one's open groups and `wrap` more
|
||||
/// groups (a pattern's `Pattern` and `Tile`, a soft mask's two), so its nesting limit counts
|
||||
/// them.
|
||||
pub fn nested(&self, wrap: usize) -> Builder {
|
||||
Builder { base: self.base.saturating_add(self.stack.len()).saturating_add(wrap), ..Builder::new() }
|
||||
}
|
||||
|
||||
pub fn depth(&self) -> usize {
|
||||
self.stack.len()
|
||||
}
|
||||
|
||||
/// `wrap` more groups fit under the open ones within [`MAX_DEPTH`].
|
||||
pub fn has_room(&self, wrap: usize) -> bool {
|
||||
self.base.saturating_add(self.stack.len()).saturating_add(wrap) <= MAX_DEPTH
|
||||
}
|
||||
|
||||
pub fn skip(&mut self, what: &str) {
|
||||
if !self.skipped.iter().any(|s| s == what) {
|
||||
self.skipped.push(what.to_string());
|
||||
@@ -62,23 +80,33 @@ impl Builder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Open `g` as the group drawn into from now on; past [`MAX_DEPTH`] the group is dropped
|
||||
/// (what would have been drawn inside it goes to the open group) and noted as skipped.
|
||||
fn push_group(&mut self, g: Group, kind: Kind) {
|
||||
if self.base.saturating_add(self.stack.len()) >= MAX_DEPTH {
|
||||
self.skip("group nesting limit");
|
||||
return;
|
||||
}
|
||||
self.stack.push((g, kind));
|
||||
}
|
||||
|
||||
/// Children drawn from now on are clipped by `path` (page space) until the group closes.
|
||||
pub fn clip(&mut self, path: BezPath, rule: FillRule) {
|
||||
let mut g = group("Clip Group");
|
||||
g.clip.push((path, rule));
|
||||
// Clipping is not a transparency group: blend modes inside reach the backdrop.
|
||||
g.isolated = false;
|
||||
self.stack.push((g, Kind::Clip));
|
||||
self.push_group(g, Kind::Clip);
|
||||
}
|
||||
|
||||
/// Open an optional-content layer (or a named group).
|
||||
pub fn open(&mut self, name: &str, layer: bool) {
|
||||
self.stack.push((group(name), if layer { Kind::Layer } else { Kind::Plain }));
|
||||
self.push_group(group(name), if layer { Kind::Layer } else { Kind::Plain });
|
||||
}
|
||||
|
||||
/// Open a group (blend mode, soft mask) closed with the graphics state.
|
||||
pub fn open_group(&mut self, g: Group) {
|
||||
self.stack.push((g, Kind::Plain));
|
||||
self.push_group(g, Kind::Plain);
|
||||
}
|
||||
|
||||
/// Add a finished node (an image, pattern tiles).
|
||||
|
||||
+14
-5
@@ -669,7 +669,12 @@ impl<'a> Interp<'a> {
|
||||
let bc = f.get(&sm, "BC").map(|b| f.nums(b)).unwrap_or_default();
|
||||
let gcs = f.get_dict(&gd, "Group").and_then(|gr| f.get(gr, "CS")).map(|c| color_space(f, c, Some(res))).unwrap_or(Cs::Rgb);
|
||||
let backdrop = if bc.is_empty() { [0.0; 3] } else { gcs.to_rgb(&bc) };
|
||||
let saved_b = std::mem::replace(&mut self.b, Builder::new());
|
||||
// The mask it replaces closes first: its groups don't enclose the new one.
|
||||
if let Some(d) = self.gs.mask_depth {
|
||||
self.close_to(d);
|
||||
}
|
||||
let inner = self.b.nested(2);
|
||||
let saved_b = std::mem::replace(&mut self.b, inner);
|
||||
let ctm = self.gs.ctm;
|
||||
let saved_gs = std::mem::replace(&mut self.gs, GState { ctm, ..GState::default() });
|
||||
let saved_stack = std::mem::take(&mut self.saved);
|
||||
@@ -681,9 +686,6 @@ impl<'a> Interp<'a> {
|
||||
for s in sk {
|
||||
self.b.skip(&s);
|
||||
}
|
||||
if let Some(d) = self.gs.mask_depth {
|
||||
self.close_to(d);
|
||||
}
|
||||
let mut content = Group::new("Soft Mask");
|
||||
content.children = nodes;
|
||||
let mut grp = Group::new("Soft Mask Group");
|
||||
@@ -784,8 +786,15 @@ impl<'a> Interp<'a> {
|
||||
if depth > 8 {
|
||||
return;
|
||||
}
|
||||
// The `Pattern` and `Tile` wrappers must fit under the open groups too.
|
||||
if !self.b.has_room(2) {
|
||||
self.b.skip("group nesting limit");
|
||||
return;
|
||||
}
|
||||
// The cell, drawn once in pattern space.
|
||||
let saved_b = std::mem::replace(&mut self.b, Builder::new());
|
||||
// The cell ends up under the open groups and this pattern's `Pattern` and `Tile`.
|
||||
let inner = self.b.nested(2);
|
||||
let saved_b = std::mem::replace(&mut self.b, inner);
|
||||
let saved_gs = std::mem::take(&mut self.gs);
|
||||
let saved_stack = std::mem::take(&mut self.saved);
|
||||
let saved_path = std::mem::take(&mut self.path);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::eps::find_from;
|
||||
use super::exec::MAX_STACK;
|
||||
use super::scanner::Src;
|
||||
use super::text::encoding_array;
|
||||
use super::value::{Flow, V, array, matrix_of, matrix_val, new_dict};
|
||||
@@ -181,8 +182,13 @@ impl Interp {
|
||||
match self.pop() {
|
||||
V::Num(k) => {
|
||||
let k = (k.max(0.0) as usize).min(self.st.len());
|
||||
let tail: Vec<V> = self.st[self.st.len() - k..].to_vec();
|
||||
self.st.extend(tail);
|
||||
// A copy past the operand limit copies nothing, and the values copied
|
||||
// count as operations (one for eight): repeated doubling spends the budget.
|
||||
if self.st.len().saturating_add(k) <= MAX_STACK {
|
||||
self.charge(k / 8);
|
||||
let tail: Vec<V> = self.st[self.st.len() - k..].to_vec();
|
||||
self.st.extend(tail);
|
||||
}
|
||||
}
|
||||
V::Array(dst) => {
|
||||
let s = self.pop().items().unwrap_or_default();
|
||||
@@ -507,12 +513,16 @@ impl Interp {
|
||||
fn try_collection_op(&mut self, n: &str) -> bool {
|
||||
match n {
|
||||
// Arrays and strings.
|
||||
// The storage made counts as operations (a value, or eight bytes, each), so a loop
|
||||
// of large constructors spends the budget rather than the memory.
|
||||
"array" => {
|
||||
let k = self.popn().clamp(0.0, 1e6) as usize;
|
||||
self.charge(k);
|
||||
self.push(array(vec![V::Null; k]));
|
||||
}
|
||||
"string" => {
|
||||
let k = self.popn().clamp(0.0, 1e7) as usize;
|
||||
self.charge(k / 8);
|
||||
self.push(V::Str(Rc::new(RefCell::new(vec![0; k]))));
|
||||
}
|
||||
"length" => {
|
||||
|
||||
@@ -19,7 +19,8 @@ pub fn eps_parts(bytes: &[u8]) -> Option<(&[u8], [f64; 4])> {
|
||||
if v.len() == 4 && v[2] > v[0] && v[3] > v[1] {
|
||||
return Some([v[0], v[1], v[2], v[3]]);
|
||||
}
|
||||
from = s;
|
||||
// A rejected comment is done with: the scan goes on past its line, not over it again.
|
||||
from = e;
|
||||
}
|
||||
None
|
||||
};
|
||||
@@ -31,8 +32,15 @@ pub(super) fn find_from(hay: &[u8], needle: &[u8], from: usize) -> Option<usize>
|
||||
hay.get(from..)?.windows(needle.len()).position(|w| w == needle).map(|p| p + from)
|
||||
}
|
||||
|
||||
fn rfind_before(hay: &[u8], needle: &[u8], to: usize) -> Option<usize> {
|
||||
hay.get(..to)?.windows(needle.len()).rposition(|w| w == needle)
|
||||
/// Every position of `needle` in `hay`, in order (one pass over the file).
|
||||
fn positions(hay: &[u8], needle: &[u8]) -> Vec<usize> {
|
||||
let mut out = vec![];
|
||||
let mut from = 0;
|
||||
while let Some(p) = find_from(hay, needle, from) {
|
||||
out.push(p);
|
||||
from = p + needle.len();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The name after `key` (`/FontName /Name`).
|
||||
@@ -48,20 +56,26 @@ fn name_after(b: &[u8], key: &[u8]) -> Option<String> {
|
||||
/// … cleartomark`, hexadecimal or binary) and CFF font sets (`… <length> StartData <bytes>`).
|
||||
pub(super) fn embedded_fonts(ps: &[u8]) -> HashMap<String, (Rc<Vec<u8>>, bool)> {
|
||||
let mut out = HashMap::new();
|
||||
// The headers are found once, in one pass each; every `eexec` then takes the last header
|
||||
// before it that no earlier `eexec` has used, so no prefix of the file is searched again.
|
||||
let mut heads = positions(ps, b"%!PS-AdobeFont");
|
||||
heads.extend(positions(ps, b"%!FontType1"));
|
||||
heads.sort_unstable();
|
||||
let names = positions(ps, b"/FontName");
|
||||
let mut from = 0;
|
||||
let mut used = 0;
|
||||
while let Some(ee) = find_from(ps, b"eexec", from) {
|
||||
from = ee + 5;
|
||||
let start = [b"%!PS-AdobeFont".as_slice(), b"%!FontType1"]
|
||||
.iter()
|
||||
.filter_map(|h| rfind_before(ps, h, ee))
|
||||
.max()
|
||||
.or_else(|| rfind_before(ps, b"/FontName", ee).map(|p| p.saturating_sub(512)));
|
||||
let last_before = |v: &[usize]| v[..v.partition_point(|&p| p < ee)].last().copied().filter(|&p| p >= used);
|
||||
// The fallback look-back stops at the previous font's end, so it cannot pick up its header.
|
||||
let start = last_before(&heads).or_else(|| last_before(&names).map(|p| p.saturating_sub(512).max(used)));
|
||||
let Some(start) = start else { continue };
|
||||
let end = find_from(ps, b"cleartomark", ee).map_or(ps.len(), |p| p + 11);
|
||||
if let Some(name) = name_after(&ps[start..ee], b"/FontName") {
|
||||
out.insert(name, (Rc::new(ps[start..end].to_vec()), false));
|
||||
}
|
||||
from = end;
|
||||
used = end;
|
||||
}
|
||||
let mut from = 0;
|
||||
while let Some(sd) = find_from(ps, b"StartData", from) {
|
||||
@@ -77,3 +91,30 @@ pub(super) fn embedded_fonts(ps: &[u8]) -> HashMap<String, (Rc<Vec<u8>>, bool)>
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::embedded_fonts;
|
||||
|
||||
#[test]
|
||||
fn font_discovery_looks_back_once_per_header() {
|
||||
// 20,000 `eexec` words before any font header: each used to search the whole prefix
|
||||
// for a header again. The font after them is still found.
|
||||
let mut ps = b"%!PS-Adobe-3.0 EPSF-3.0\n".to_vec();
|
||||
ps.extend_from_slice("eexec ".repeat(20_000).as_bytes());
|
||||
ps.extend_from_slice(b"\n%!PS-AdobeFont-1.0: Real\n/FontName /Real def\ncurrentfile eexec\n0000\ncleartomark\n");
|
||||
let fonts = embedded_fonts(&ps);
|
||||
assert_eq!(fonts.len(), 1);
|
||||
assert!(fonts.contains_key("Real"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_less_font_after_a_short_one_does_not_take_its_header() {
|
||||
let a = "%!PS-AdobeFont-1.0: A\n/FontName /A def\ncurrentfile eexec\n0000\ncleartomark\n";
|
||||
let b = "/FontName /B def\ncurrentfile eexec\n0000\ncleartomark\n";
|
||||
let ps = format!("%!PS-Adobe-3.0 EPSF-3.0\n{a}{b}");
|
||||
let fonts = embedded_fonts(ps.as_bytes());
|
||||
let (data, _) = fonts.get("B").expect("second font found");
|
||||
assert!(!String::from_utf8_lossy(data).contains("AdobeFont"), "{}", String::from_utf8_lossy(data));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ use super::{Affine, BezPath, Builder, Cap, HashMap, Interp, Join};
|
||||
/// The deepest procedure nesting run (deeper recursion stops the program).
|
||||
const MAX_CALL_DEPTH: usize = 256;
|
||||
|
||||
/// The most values the operand stack holds: a push past it is dropped, and `copy` copies
|
||||
/// nothing rather than grow the stack past it.
|
||||
pub(super) const MAX_STACK: usize = 100_000;
|
||||
|
||||
impl Interp {
|
||||
pub fn new(page: [f64; 4]) -> Interp {
|
||||
Interp {
|
||||
@@ -56,11 +60,17 @@ impl Interp {
|
||||
}
|
||||
|
||||
pub(super) fn push(&mut self, v: V) {
|
||||
if self.st.len() < 100_000 {
|
||||
if self.st.len() < MAX_STACK {
|
||||
self.st.push(v);
|
||||
}
|
||||
}
|
||||
|
||||
/// Count `n` units of work (values copied, storage allocated) against the budget without
|
||||
/// stopping the program here: once it is spent, the next operator stops it.
|
||||
pub(super) fn charge(&mut self, n: usize) {
|
||||
self.budget = self.budget.saturating_sub(n);
|
||||
}
|
||||
|
||||
pub(super) fn lookup(&self, k: &str) -> Option<V> {
|
||||
self.dicts.iter().rev().find_map(|d| d.borrow().get(k).cloned())
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ impl Interp {
|
||||
}
|
||||
|
||||
/// Stop the program when a path of `len` elements would be longer than [`MAX_PATH`].
|
||||
fn path_fits(&mut self, len: usize) -> Result<(), Flow> {
|
||||
pub(super) fn path_fits(&mut self, len: usize) -> Result<(), Flow> {
|
||||
if len <= MAX_PATH {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -95,7 +95,14 @@ impl Interp {
|
||||
let ctm = self.gs.ctm;
|
||||
for (i, &code) in bytes.iter().enumerate() {
|
||||
let code = code as u32;
|
||||
// Every input character costs at least one unit, so empty outlines are not free.
|
||||
self.spend(1)?;
|
||||
if let Some(g) = font.glyph(code) {
|
||||
// Each outline element counts against the path limit and the budget before it
|
||||
// is copied, so a long string cannot expand into unbounded geometry.
|
||||
let n = g.elements().len();
|
||||
self.path_fits(path.elements().len().saturating_add(n))?;
|
||||
self.spend(n)?;
|
||||
path.extend(ctm * Affine::translate(p.to_vec2()) * m * (*g).clone());
|
||||
}
|
||||
text.push_str(&font.unicode(code));
|
||||
|
||||
@@ -748,6 +748,63 @@ fn tiling_patterns_coloured_and_uncoloured() {
|
||||
assert_eq!(at(115, 15)[3], 0.0);
|
||||
}
|
||||
|
||||
/// A pattern cell nests under the page's open clipping groups and the pattern's own groups, and
|
||||
/// the render tree stays within the group depth limit that counts all of them.
|
||||
#[test]
|
||||
fn a_pattern_cell_counts_the_groups_it_is_nested_in() {
|
||||
fn depth(g: &effectcraft_svg::Group) -> usize {
|
||||
1 + g.children.iter().filter_map(|c| if let effectcraft_svg::Node::Group(g) = c { Some(depth(g)) } else { None }).max().unwrap_or(0)
|
||||
}
|
||||
let clips = "0 0 200 100 re W n ".repeat(250);
|
||||
let cell = format!("{} 1 0 0 rg 0 0 10 10 re f", "0 0 20 20 re W n ".repeat(250)).into_bytes();
|
||||
let bytes = page_pdf(
|
||||
&format!("{clips} /Pattern cs /P1 scn 0 0 100 100 re f"),
|
||||
"/Pattern << /P1 10 0 R >>",
|
||||
vec![(10, "<< /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 20 20] /XStep 20 /YStep 20 /Resources << >> >>".into(), Some(cell))],
|
||||
);
|
||||
let doc = parse(&bytes).unwrap();
|
||||
assert!(depth(&doc.root) <= 260, "{}", depth(&doc.root));
|
||||
assert!(doc.skipped.iter().any(|s| s == "group nesting limit"), "{:?}", doc.skipped);
|
||||
}
|
||||
|
||||
/// A pattern whose `Pattern` and `Tile` wrappers would pass the depth limit is skipped, not built
|
||||
/// past it.
|
||||
#[test]
|
||||
fn a_pattern_whose_wrappers_pass_the_depth_limit_is_skipped() {
|
||||
let clips = "0 0 200 100 re W n ".repeat(255);
|
||||
let bytes = page_pdf(
|
||||
&format!("{clips} /Pattern cs /P1 scn 0 0 100 100 re f"),
|
||||
"/Pattern << /P1 10 0 R >>",
|
||||
vec![(
|
||||
10,
|
||||
"<< /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 20 20] /XStep 20 /YStep 20 /Resources << >> >>".into(),
|
||||
Some(b"0 0 10 10 re f".to_vec()),
|
||||
)],
|
||||
);
|
||||
let doc = parse(&bytes).unwrap();
|
||||
assert!(doc.skipped.iter().any(|s| s == "group nesting limit"), "{:?}", doc.skipped);
|
||||
}
|
||||
|
||||
/// A soft mask replacing another is nested under what is open once the old one has closed, so
|
||||
/// groups inside the old mask don't count against the new mask's content.
|
||||
#[test]
|
||||
fn a_replaced_soft_mask_does_not_count_against_the_new_one() {
|
||||
let form = |n: &str| {
|
||||
(
|
||||
n.parse().unwrap(),
|
||||
"<< /Type /XObject /Subtype /Form /BBox [0 0 200 100] /Group << /S /Transparency /CS /DeviceGray >> >>".to_string(),
|
||||
Some(format!("{} 1 g 0 0 100 100 re f", "0 0 200 100 re W n ".repeat(250)).into_bytes()),
|
||||
)
|
||||
};
|
||||
let bytes = page_pdf(
|
||||
&format!("/GS1 gs {} /GS2 gs 1 0 0 rg 0 0 200 100 re f", "0 0 200 100 re W n ".repeat(250)),
|
||||
"/ExtGState << /GS1 << /SMask << /S /Luminosity /G 10 0 R >> >> /GS2 << /SMask << /S /Luminosity /G 11 0 R >> >> >>",
|
||||
vec![form("10"), form("11")],
|
||||
);
|
||||
let doc = parse(&bytes).unwrap();
|
||||
assert!(doc.skipped.iter().all(|s| s != "group nesting limit"), "{:?}", doc.skipped);
|
||||
}
|
||||
|
||||
/// Build isolated interpreter objects without presenting a function or colour space as a
|
||||
/// document catalog. Complete-document parsing is covered by the page and reader fixtures.
|
||||
fn object_file(objs: Objs) -> object::File {
|
||||
|
||||
@@ -513,6 +513,9 @@ fn paths_stop_at_the_path_limit() {
|
||||
// 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",
|
||||
// The same outlines shown: the text path is bounded like the current path.
|
||||
"/Helvetica findfont 10 scalefont setfont /s 100000 string def 0 1 99999 { s exch 72 put } for \
|
||||
0 0 moveto s show",
|
||||
// 210,000 rectangles of five elements each.
|
||||
"/a 840000 array def 0 1 839999 { a exch 0 put } for a rectfill",
|
||||
] {
|
||||
@@ -528,6 +531,45 @@ fn paths_stop_at_the_path_limit() {
|
||||
assert_eq!(doc.flatten().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copies_past_the_operand_limit_copy_nothing() {
|
||||
// `count copy` doubles the operand stack: 40 doublings would be 2^40 values. A copy that would
|
||||
// pass the operand limit (100,000) copies nothing, so the program runs on to its end.
|
||||
let doc = eps_soon("0 0 10 10 rectfill 1 40 { count copy } repeat clear 20 20 10 10 rectfill");
|
||||
assert!(doc.skipped.is_empty(), "{:?}", doc.skipped);
|
||||
assert_eq!(doc.flatten().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storage_made_in_a_loop_runs_out_of_the_budget() {
|
||||
// Each value of an array and each eight bytes of a string count as an operation when they
|
||||
// are made, so a loop keeping large arrays or strings spends the budget, not the memory.
|
||||
for body in ["/a 1000 array def 0 1 999 { a exch 1000000 array put } for", "/a 1000 array def 0 1 999 { a exch 10000000 string put } for"] {
|
||||
runs_out_of_budget(body);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn groups_stop_nesting_at_the_depth_limit() {
|
||||
// Every `rectclip` opens a clipping group inside the last: past 256 open groups the group is
|
||||
// dropped (and noted), and what is drawn inside goes to the open one.
|
||||
let doc = eps_soon("300 { 0 0 100 100 rectclip } repeat 0 0 10 10 rectfill");
|
||||
assert_eq!(doc.skipped, ["group nesting limit"]);
|
||||
assert_eq!(doc.flatten().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounding_box_comments_are_scanned_once() {
|
||||
// A rejected `%%BoundingBox:` comment is not scanned again from each later marker on its
|
||||
// line: 20,000 markers on one line would otherwise take quadratic work.
|
||||
let mut src = b"%!PS-Adobe-3.0 EPSF-3.0\n".to_vec();
|
||||
src.extend_from_slice("%%BoundingBox: x ".repeat(20_000).as_bytes());
|
||||
src.extend_from_slice(b"\n%%BoundingBox: 0 0 50 50\n0 0 10 10 rectfill\n");
|
||||
let doc = parse(&src).unwrap();
|
||||
assert_eq!((doc.width, doc.height), (50.0, 50.0));
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user