mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-11 23:32:17 +00:00
PDF functions and mesh shadings: charge retained values, refuse short tables, bound lattice rows and raster work (#2972)
* PDF functions and mesh shadings: charge retained values, refuse short tables, bound lattice rows and raster work Depth-limit fallbacks were not charged to the function node budget; cached copies of a function deep-copied its coefficient and program vectors while only its nodes were charged; sampled functions made up missing samples as zeros; calculator programs had no size limit; a lattice row was filled before any limit applied; and overlapping mesh triangles each scanned their whole bounding box. The node is charged before the depth check; coefficients, bounds, encode arrays and calculator operators are charged (eight to a node) so cached copies carry their cost; a sampled function shorter than its table is unsupported; calculator programs hold at most 4,096 operators; lattice rows past the triangle limit are refused and vertices stop at it; and triangle bounding boxes share a budget of 64 passes over the raster. * address review: bound calculator token count while parsing * address review: count skipped calculator tokens toward MAX_CALC_OPS * address review: skip arrays and dictionaries in a calculator program without building them * address review: scalar tokens in a skipped compound are passed over without building them
This commit is contained in:
+122
-26
@@ -215,19 +215,44 @@ pub enum PsOp {
|
||||
Proc(Vec<PsOp>),
|
||||
}
|
||||
|
||||
fn ps_parse(lx: &mut crate::object::Lexer, depth: usize) -> Vec<PsOp> {
|
||||
/// Parse a procedure. `ops` counts the tokens read (those skipped as unsupported included); past
|
||||
/// [`MAX_CALC_OPS`] the rest is not read, so an oversized program is never fully scanned or built
|
||||
/// (the caller rejects it when `ops` ends past the cap).
|
||||
fn ps_parse(lx: &mut crate::object::Lexer, depth: usize, ops: &mut usize) -> Vec<PsOp> {
|
||||
let mut out = vec![];
|
||||
while let Some(o) = lx.next() {
|
||||
match o {
|
||||
Obj::Num(n) => out.push(PsOp::Num(n)),
|
||||
Obj::Bool(b) => out.push(PsOp::Num(if b { 1.0 } else { 0.0 })),
|
||||
Obj::Op(op) if op == "{" && depth < 32 => out.push(PsOp::Proc(ps_parse(lx, depth + 1))),
|
||||
// Nested too deep: skip the procedure's opening brace (never push it as an operator).
|
||||
Obj::Op(op) if op == "{" => {}
|
||||
Obj::Op(op) if op == "}" => return out,
|
||||
Obj::Op(op) => out.push(PsOp::Op(op)),
|
||||
_ => {}
|
||||
loop {
|
||||
// A calculator holds no arrays or dictionaries: one is skipped as a single token without
|
||||
// being built (the lexer would allocate every element before this counted it).
|
||||
if lx.at_compound() {
|
||||
if *ops > MAX_CALC_OPS {
|
||||
break;
|
||||
}
|
||||
lx.skip_compound();
|
||||
*ops += 1;
|
||||
continue;
|
||||
}
|
||||
let Some(o) = lx.next() else { break };
|
||||
if *ops > MAX_CALC_OPS {
|
||||
break;
|
||||
}
|
||||
let op = match o {
|
||||
Obj::Num(n) => PsOp::Num(n),
|
||||
Obj::Bool(b) => PsOp::Num(if b { 1.0 } else { 0.0 }),
|
||||
Obj::Op(op) if op == "{" && depth < 32 => PsOp::Proc(ps_parse(lx, depth + 1, ops)),
|
||||
// Nested too deep: skip the procedure's opening brace (never push it as an operator).
|
||||
Obj::Op(op) if op == "{" => {
|
||||
*ops += 1;
|
||||
continue;
|
||||
}
|
||||
Obj::Op(op) if op == "}" => return out,
|
||||
Obj::Op(op) => PsOp::Op(op),
|
||||
_ => {
|
||||
*ops += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
*ops += 1;
|
||||
out.push(op);
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -243,6 +268,15 @@ fn ps_push(st: &mut Vec<f64>, v: f64) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The most operators and operands a calculator program may hold (nested procedures included).
|
||||
/// It has no loops, so this bounds what one evaluation runs; real programs hold a few dozen.
|
||||
const MAX_CALC_OPS: usize = 4096;
|
||||
|
||||
/// The operators and operands in `code`, nested procedures included.
|
||||
fn ps_len(code: &[PsOp]) -> usize {
|
||||
code.iter().map(|op| if let PsOp::Proc(p) = op { 1 + ps_len(p) } else { 1 }).sum()
|
||||
}
|
||||
|
||||
fn ps_run(code: &[PsOp], st: &mut Vec<f64>, depth: usize) {
|
||||
if depth > 32 {
|
||||
return;
|
||||
@@ -431,6 +465,12 @@ impl Budget {
|
||||
Budget { nodes: MAX_NODES, samples: MAX_SAMPLES, overrun: false }
|
||||
}
|
||||
|
||||
/// Charge `n` retained values (coefficients, bounds, program operators) as nodes, eight to a
|
||||
/// node, so a cached copy of a function is charged for what it holds, not just its node.
|
||||
fn take_values(&mut self, n: usize) -> bool {
|
||||
self.take_nodes(n.div_ceil(8))
|
||||
}
|
||||
|
||||
/// Charge `n` nodes; `false` (and the read is abandoned) once the cap is passed.
|
||||
fn take_nodes(&mut self, n: usize) -> bool {
|
||||
match self.nodes.checked_sub(n) {
|
||||
@@ -484,7 +524,8 @@ impl Func {
|
||||
/// [`Func::read`] nested `depth` levels deep, charging `budget`: unsupported past [`MAX_DEPTH`]
|
||||
/// or once the budget is spent.
|
||||
fn read_at(file: &File, o: &Obj, depth: usize, budget: &mut Budget) -> Func {
|
||||
if depth >= MAX_DEPTH || !budget.take_nodes(1) {
|
||||
// Charged first: the fallback past the depth limit is a node a copy keeps too.
|
||||
if !budget.take_nodes(1) || depth >= MAX_DEPTH {
|
||||
return Func::Unsupported;
|
||||
}
|
||||
let o = file.resolve(o);
|
||||
@@ -499,12 +540,14 @@ impl Func {
|
||||
let dom = file.get(d, "Domain").map(|x| file.nums(x)).unwrap_or_default();
|
||||
let domain = [dom.first().copied().unwrap_or(0.0), dom.get(1).copied().unwrap_or(1.0)];
|
||||
match file.get_num(d, "FunctionType").unwrap_or(-1.0) as i32 {
|
||||
2 => Func::Exp {
|
||||
domain,
|
||||
c0: file.get(d, "C0").map(|x| file.nums(x)).unwrap_or_else(|| vec![0.0]),
|
||||
c1: file.get(d, "C1").map(|x| file.nums(x)).unwrap_or_else(|| vec![1.0]),
|
||||
n: file.get_num(d, "N").unwrap_or(1.0),
|
||||
},
|
||||
2 => {
|
||||
let c0 = file.get(d, "C0").map(|x| file.nums(x)).unwrap_or_else(|| vec![0.0]);
|
||||
let c1 = file.get(d, "C1").map(|x| file.nums(x)).unwrap_or_else(|| vec![1.0]);
|
||||
if !budget.take_values(c0.len() + c1.len()) {
|
||||
return Func::Unsupported;
|
||||
}
|
||||
Func::Exp { domain, c0, c1, n: file.get_num(d, "N").unwrap_or(1.0) }
|
||||
}
|
||||
3 => {
|
||||
// A stitching function's pieces: no element cap (a repeating gradient lists one
|
||||
// piece once per repeat); the budget bounds the total work.
|
||||
@@ -513,12 +556,12 @@ impl Func {
|
||||
None => Some(vec![]),
|
||||
};
|
||||
let Some(funcs) = funcs else { return Func::Unsupported };
|
||||
Func::Stitch {
|
||||
domain,
|
||||
funcs,
|
||||
bounds: file.get(d, "Bounds").map(|x| file.nums(x)).unwrap_or_default(),
|
||||
encode: file.get(d, "Encode").map(|x| file.nums(x)).unwrap_or_default(),
|
||||
let bounds = file.get(d, "Bounds").map(|x| file.nums(x)).unwrap_or_default();
|
||||
let encode = file.get(d, "Encode").map(|x| file.nums(x)).unwrap_or_default();
|
||||
if !budget.take_values(bounds.len() + encode.len()) {
|
||||
return Func::Unsupported;
|
||||
}
|
||||
Func::Stitch { domain, funcs, bounds, encode }
|
||||
}
|
||||
0 => {
|
||||
let Obj::Stream(sd, raw) = o else { return Func::Unsupported };
|
||||
@@ -537,6 +580,10 @@ impl Func {
|
||||
if !budget.take_samples(count) {
|
||||
return Func::Unsupported;
|
||||
}
|
||||
// The stream must hold the table: missing bytes are not made up as zeros.
|
||||
if count.checked_mul(bps).is_none_or(|bits| bits.div_ceil(8) > data.len()) {
|
||||
return Func::Unsupported;
|
||||
}
|
||||
let enc = file.get(d, "Encode").map(|x| file.nums(x)).unwrap_or_default();
|
||||
let encode = [enc.first().copied().unwrap_or(0.0), enc.get(1).copied().unwrap_or(size as f64 - 1.0)];
|
||||
let decode = file.get(d, "Decode").map(|x| file.nums(x)).filter(|v| v.len() >= outputs * 2).unwrap_or_else(|| range.clone());
|
||||
@@ -567,10 +614,15 @@ impl Func {
|
||||
let Some(data) = decode_stream(file, sd, raw) else { return Func::Unsupported };
|
||||
let mut lx = crate::object::Lexer::new(&data, 0);
|
||||
// The program is one procedure: `{ … }`.
|
||||
let code = match ps_parse(&mut lx, 0).into_iter().next() {
|
||||
let mut scanned = 0;
|
||||
let code = match ps_parse(&mut lx, 0, &mut scanned).into_iter().next() {
|
||||
Some(PsOp::Proc(p)) => p,
|
||||
_ => return Func::Unsupported,
|
||||
};
|
||||
let ops = ps_len(&code);
|
||||
if scanned > MAX_CALC_OPS || ops > MAX_CALC_OPS || !budget.take_values(ops) {
|
||||
return Func::Unsupported;
|
||||
}
|
||||
Func::Calc { domain: dom, range: file.get(d, "Range").map(|x| file.nums(x)).unwrap_or_default(), code }
|
||||
}
|
||||
_ => Func::Unsupported,
|
||||
@@ -756,12 +808,56 @@ mod tests {
|
||||
assert_eq!(sample_count(1 << 22, usize::MAX / (1 << 22) + 1), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_calculator_program_is_cut_short() {
|
||||
let src = format!("{{ {}}}", "1 ".repeat(MAX_CALC_OPS + 10));
|
||||
let mut lx = crate::object::Lexer::new(src.as_bytes(), 0);
|
||||
let Some(PsOp::Proc(code)) = ps_parse(&mut lx, 0, &mut 0).into_iter().next() else { panic!() };
|
||||
assert_eq!(ps_len(&code), MAX_CALC_OPS + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrays_and_dictionaries_in_a_calculator_are_skipped_whole() {
|
||||
// Strings with brackets, nested arrays and a dictionary are one skipped token each; the
|
||||
// operators around them still parse.
|
||||
let src = b"{ 1 [ (a]b) [1 2] << /K [3 4] /S (>>) >> 5 ] 2 add << /A 1 >> mul }";
|
||||
let mut lx = crate::object::Lexer::new(src, 0);
|
||||
let Some(PsOp::Proc(code)) = ps_parse(&mut lx, 0, &mut 0).into_iter().next() else { panic!() };
|
||||
assert_eq!(code, vec![PsOp::Num(1.0), PsOp::Num(2.0), PsOp::Op("add".into()), PsOp::Op("mul".into())]);
|
||||
// A huge array is skipped once the cap is passed, not read to its end.
|
||||
let src = format!("{{ [ {}] 1 }}", "0 ".repeat(MAX_CALC_OPS * 100));
|
||||
let mut lx = crate::object::Lexer::new(src.as_bytes(), 0);
|
||||
let mut scanned = 0;
|
||||
let Some(PsOp::Proc(code)) = ps_parse(&mut lx, 0, &mut scanned).into_iter().next() else { panic!() };
|
||||
assert_eq!(code, vec![PsOp::Num(1.0)]);
|
||||
// A large string inside a skipped array is passed over, not decoded: escapes, nested
|
||||
// parentheses, a hexadecimal string and names all end where the lexer would end them.
|
||||
let big = format!("{{ [ ({}) <{}> /{} (a\\)b(c)) 7 ] 1 }}", "x".repeat(1 << 20), "ab".repeat(1 << 19), "N".repeat(1 << 10));
|
||||
let mut lx = crate::object::Lexer::new(big.as_bytes(), 0);
|
||||
let Some(PsOp::Proc(code)) = ps_parse(&mut lx, 0, &mut 0).into_iter().next() else { panic!() };
|
||||
assert_eq!(code, vec![PsOp::Num(1.0)]);
|
||||
// An unclosed one ends at the end of the data.
|
||||
let mut lx = crate::object::Lexer::new(b"[ 1 2 ", 0);
|
||||
lx.skip_compound();
|
||||
assert_eq!(lx.pos, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skipped_calculator_tokens_count_toward_the_cap() {
|
||||
// `null` is not a calculator token: skipped, but still scanned.
|
||||
let src = format!("{{ {}}}", "null ".repeat(MAX_CALC_OPS + 10));
|
||||
let mut lx = crate::object::Lexer::new(src.as_bytes(), 0);
|
||||
let mut scanned = 0;
|
||||
ps_parse(&mut lx, 0, &mut scanned);
|
||||
assert!(scanned > MAX_CALC_OPS && scanned < MAX_CALC_OPS + 10, "parsing stops once the cap is passed ({scanned})");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculator_functions() {
|
||||
// Two inputs → three outputs: { 2 copy mul 3 1 roll add 2 div dup 0.5 gt { pop 1 } if 0 }
|
||||
let code = b"{ 2 copy mul 3 1 roll add 2 div dup 0.5 gt { pop 1 } { 0.25 mul } ifelse 0 }";
|
||||
let mut lx = crate::object::Lexer::new(code, 0);
|
||||
let Some(PsOp::Proc(code)) = ps_parse(&mut lx, 0).into_iter().next() else { panic!() };
|
||||
let Some(PsOp::Proc(code)) = ps_parse(&mut lx, 0, &mut 0).into_iter().next() else { panic!() };
|
||||
let f = Func::Calc { domain: vec![0.0, 1.0, 0.0, 1.0], range: vec![0.0, 1.0, 0.0, 1.0, 0.0, 1.0], code };
|
||||
assert_eq!(f.eval(&[0.5, 0.4]), vec![0.2, 0.1125, 0.0]);
|
||||
assert_eq!(f.eval(&[1.0, 0.8]), vec![0.8, 1.0, 0.0]);
|
||||
@@ -771,7 +867,7 @@ mod tests {
|
||||
fn run(src: &str, mut st: Vec<f64>) -> Vec<f64> {
|
||||
let src = format!("{{ {src} }}");
|
||||
let mut lx = crate::object::Lexer::new(src.as_bytes(), 0);
|
||||
let Some(PsOp::Proc(code)) = ps_parse(&mut lx, 0).into_iter().next() else { panic!() };
|
||||
let Some(PsOp::Proc(code)) = ps_parse(&mut lx, 0, &mut 0).into_iter().next() else { panic!() };
|
||||
ps_run(&code, &mut st, 0);
|
||||
st
|
||||
}
|
||||
|
||||
@@ -82,6 +82,91 @@ impl<'a> Lexer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the next token opens an array or a dictionary (skipping white space first).
|
||||
pub fn at_compound(&mut self) -> bool {
|
||||
self.skip_ws();
|
||||
match self.data.get(self.pos) {
|
||||
Some(b'[') => true,
|
||||
Some(b'<') => self.data.get(self.pos + 1) == Some(&b'<'),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Skip the array or dictionary at the current position (see [`Lexer::at_compound`]) as
|
||||
/// [`Lexer::next`] would read it, without building it: for readers that have no use for one
|
||||
/// and must not allocate a vector per element of a hostile stream.
|
||||
pub fn skip_compound(&mut self) {
|
||||
let mut open = 0usize;
|
||||
loop {
|
||||
self.skip_ws();
|
||||
match self.data.get(self.pos) {
|
||||
None => return,
|
||||
Some(b'[') => {
|
||||
open += 1;
|
||||
self.pos += 1;
|
||||
}
|
||||
Some(b'<') if self.data.get(self.pos + 1) == Some(&b'<') => {
|
||||
open += 1;
|
||||
self.pos += 2;
|
||||
}
|
||||
Some(b']') | Some(b'>') if self.data[self.pos] == b']' || self.data.get(self.pos + 1) == Some(&b'>') => {
|
||||
open = open.saturating_sub(1);
|
||||
self.pos += if self.data[self.pos] == b']' { 1 } else { 2 };
|
||||
if open == 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Anything else is one token (a string, name, number or word), passed over
|
||||
// without building it.
|
||||
Some(_) => self.skip_scalar(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pass over one token that is not an array or dictionary bracket, as [`Lexer::next`] reads
|
||||
/// it but without building its value.
|
||||
fn skip_scalar(&mut self) {
|
||||
let Some(&c) = self.data.get(self.pos) else { return };
|
||||
match c {
|
||||
b'(' => {
|
||||
// A literal string: balanced parentheses, a backslash escapes the next byte.
|
||||
let mut depth = 0usize;
|
||||
while let Some(&d) = self.data.get(self.pos) {
|
||||
self.pos += 1;
|
||||
match d {
|
||||
b'\\' => self.pos += 1,
|
||||
b'(' => depth += 1,
|
||||
b')' => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.pos = self.pos.min(self.data.len());
|
||||
}
|
||||
b'<' => {
|
||||
// A hexadecimal string.
|
||||
while self.data.get(self.pos).is_some_and(|d| *d != b'>') {
|
||||
self.pos += 1;
|
||||
}
|
||||
self.pos = (self.pos + 1).min(self.data.len());
|
||||
}
|
||||
b'/' => {
|
||||
self.pos += 1;
|
||||
self.word();
|
||||
}
|
||||
b']' | b'{' | b'}' | b')' | b'>' => self.pos += 1,
|
||||
_ => {
|
||||
if self.word().is_empty() {
|
||||
self.pos += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn word(&mut self) -> &'a [u8] {
|
||||
let s = self.pos;
|
||||
while self.pos < self.data.len() && !is_white(self.data[self.pos]) && !is_delim(self.data[self.pos]) {
|
||||
|
||||
@@ -18,6 +18,8 @@ const MAX_SIDE: f64 = 1024.0;
|
||||
const MAX_SCALE: f64 = 4.0;
|
||||
/// The most triangles one shading draws.
|
||||
const MAX_TRIANGLES: usize = 2_000_000;
|
||||
/// How many times over the mesh raster may be covered by triangle bounding boxes in all.
|
||||
const MAX_RASTER_PASSES: usize = 64;
|
||||
|
||||
/// A mesh vertex: page-space position and its value (RGB, or `t` in `[0]` with a function).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
@@ -234,13 +236,17 @@ fn free_form(r: &mut Bits, l: &Layout, colors: &Colors, m: Affine) -> Vec<[V; 3]
|
||||
|
||||
/// Type 5: a lattice of `per_row` vertices per row, two triangles per cell.
|
||||
fn lattice(r: &mut Bits, l: &Layout, colors: &Colors, m: Affine, per_row: usize) -> Vec<[V; 3]> {
|
||||
if per_row < 2 {
|
||||
if !(2..=MAX_TRIANGLES).contains(&per_row) {
|
||||
return vec![];
|
||||
}
|
||||
let mut rows: Vec<Vec<V>> = vec![];
|
||||
let mut row = vec![];
|
||||
while let (Some(p), Some(c)) = (l.point(r), l.comps(r)) {
|
||||
r.align();
|
||||
// Vertices kept, the unfinished row's included, stay within the triangle limit.
|
||||
if (rows.len() * per_row).saturating_add(row.len()) >= MAX_TRIANGLES {
|
||||
break;
|
||||
}
|
||||
row.push(V { p: m * p, c: colors.value(&c) });
|
||||
if row.len() == per_row {
|
||||
rows.push(std::mem::take(&mut row));
|
||||
@@ -447,6 +453,9 @@ fn rasterize(tris: &[[V; 3]], colors: &Colors, page_box: [f64; 4]) -> Option<Svg
|
||||
let ss = s * 2.0;
|
||||
let mut val = vec![[0.0f32; 3]; sw * sh];
|
||||
let mut cov = vec![false; sw * sh];
|
||||
// Samples tested across all triangles: overlapping triangles that each cover the raster
|
||||
// would otherwise test it once per triangle. Past the budget the later triangles are left out.
|
||||
let mut work = (sw * sh).saturating_mul(MAX_RASTER_PASSES).saturating_add(tris.len().saturating_mul(16));
|
||||
for t in tris {
|
||||
let q = t.map(|v| Point::new((v.p.x - o.x) * ss, (o.y - v.p.y) * ss));
|
||||
let area = (q[1].x - q[0].x) * (q[2].y - q[0].y) - (q[2].x - q[0].x) * (q[1].y - q[0].y);
|
||||
@@ -457,6 +466,8 @@ fn rasterize(tris: &[[V; 3]], colors: &Colors, page_box: [f64; 4]) -> Option<Svg
|
||||
let by0 = q.iter().map(|p| p.y).fold(f64::MAX, f64::min).floor().max(0.0) as usize;
|
||||
let bx1 = (q.iter().map(|p| p.x).fold(f64::MIN, f64::max).ceil().max(0.0) as usize).min(sw);
|
||||
let by1 = (q.iter().map(|p| p.y).fold(f64::MIN, f64::max).ceil().max(0.0) as usize).min(sh);
|
||||
let Some(left) = work.checked_sub(bx1.saturating_sub(bx0).saturating_mul(by1.saturating_sub(by0))) else { break };
|
||||
work = left;
|
||||
let eps = 1e-9 * area.abs();
|
||||
for y in by0..by1 {
|
||||
for x in bx0..bx1 {
|
||||
|
||||
@@ -1125,6 +1125,35 @@ fn a_fanning_out_reference_graph_is_refused_within_the_budget() {
|
||||
assert_eq!(read_function(objs), color::Func::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sampled_function_shorter_than_its_table_is_refused() {
|
||||
// 4,096 8-bit samples declared, ten bytes given: the missing ones are not made up.
|
||||
let f = "<< /FunctionType 0 /Domain [0 1] /Size [4096] /Range [0 1] /BitsPerSample 8 >>";
|
||||
assert_eq!(read_function(vec![(1, f.into(), Some(vec![7; 10]))]), color::Func::Unsupported);
|
||||
assert_ne!(read_function(vec![(1, f.into(), Some(vec![7; 4096]))]), color::Func::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_calculator_program_past_the_operator_limit_is_refused() {
|
||||
let f = "<< /FunctionType 4 /Domain [0 1] /Range [0 1] >>";
|
||||
let long = format!("{{ {} }}", "1 pop ".repeat(3000));
|
||||
assert_eq!(read_function(vec![(1, f.into(), Some(long.into_bytes()))]), color::Func::Unsupported);
|
||||
assert_ne!(read_function(vec![(1, f.into(), Some(b"{ 1 exch sub }".to_vec()))]), color::Func::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copies_of_a_function_are_charged_for_its_coefficients() {
|
||||
// A stitching function listing one object 1,000 times, whose coefficient arrays hold 2,000
|
||||
// values: each copy is charged its values, so the budget refuses it instead of copying
|
||||
// two million values.
|
||||
let c = format!("[{}]", "0.5 ".repeat(1000));
|
||||
let objs: Objs = vec![
|
||||
(1, format!("<< /FunctionType 3 /Domain [0 1] /Functions [{}] >>", "2 0 R ".repeat(1000)), None),
|
||||
(2, format!("<< /FunctionType 2 /Domain [0 1] /C0 {c} /C1 {c} /N 1 >>"), None),
|
||||
];
|
||||
assert_eq!(read_function(objs), color::Func::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colour_space_chain_of_depth_3() {
|
||||
// Indexed → Separation → ICCBased → DeviceRGB: entry 0 is tint 1 (blue), entry 1 tint 0 (white).
|
||||
|
||||
@@ -262,3 +262,32 @@ fn corrupt_mesh_data_draws_what_parses() {
|
||||
let doc = parse(&bytes).unwrap();
|
||||
assert!(doc.skipped.iter().any(|s| s.contains("layout")), "{:?}", doc.skipped);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlapping_mesh_triangles_stop_at_the_raster_budget() {
|
||||
// 20,000 triangles each covering the whole page would test every sample 20,000 times; the
|
||||
// raster budget stops after about 64 passes, and the shading is still drawn.
|
||||
let mut m = Mesh::default();
|
||||
for _ in 0..20_000 {
|
||||
m.flag(0).pt(0, 0).c(&[255, 0, 0]);
|
||||
m.flag(0).pt(400, 0).c(&[255, 0, 0]);
|
||||
m.flag(0).pt(0, 400).c(&[255, 0, 0]);
|
||||
}
|
||||
let sh = format!("<< /ShadingType 4 /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 {RGB_DECODE} >>");
|
||||
let bytes = page_pdf("/Sh sh", "/Shading << /Sh 10 0 R >>", vec![(10, sh, Some(m.0))]);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || tx.send(parse(&bytes)).ok());
|
||||
let doc = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("the mesh finishes").unwrap();
|
||||
assert!(render(&doc).get(50, 50)[0] > 0.99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lattice_row_longer_than_the_triangle_limit_draws_nothing() {
|
||||
let mut m = Mesh::default();
|
||||
for i in 0..8 {
|
||||
m.pt(i * 10, 0).c(&[255, 0, 0]);
|
||||
}
|
||||
let sh = format!("<< /ShadingType 5 /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 /VerticesPerRow 3000000 {RGB_DECODE} >>");
|
||||
let doc = parse(&page_pdf("/Sh sh", "/Shading << /Sh 10 0 R >>", vec![(10, sh, Some(m.0))])).unwrap();
|
||||
assert!(doc.flatten().is_empty());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user