M13.12: pdf mesh shadings (types 4-7) and function-based shadings

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-03 08:42:49 -04:00
parent f14b575e13
commit b0b8d4c01a
6 changed files with 968 additions and 13 deletions

View File

@ -167,6 +167,16 @@ pub enum Func {
decode: Vec<f64>,
samples: Vec<f64>,
},
/// Type 0 with several inputs (function-based shadings): multilinear interpolation, the
/// first input varying fastest in the sample table.
SampledN {
domain: Vec<f64>,
size: Vec<usize>,
outputs: usize,
encode: Vec<f64>,
decode: Vec<f64>,
samples: Vec<f64>,
},
/// An array of one-output functions.
Array(Vec<Func>),
/// Type 4: a PostScript calculator program (§7.10.5).
@ -385,20 +395,23 @@ impl Func {
0 => {
let Obj::Stream(sd, raw) = o else { return Func::Unsupported };
let Some(data) = decode_stream(file, sd, raw) else { return Func::Unsupported };
let size = file.get(d, "Size").map(|x| file.nums(x)).and_then(|v| v.first().copied()).unwrap_or(0.0) as usize;
let sizes: Vec<usize> = file.get(d, "Size").map(|x| file.nums(x)).unwrap_or_default().iter().map(|v| v.max(0.0) as usize).collect();
let size = sizes.first().copied().unwrap_or(0);
let bps = file.get_num(d, "BitsPerSample").unwrap_or(8.0) as usize;
let range = file.get(d, "Range").map(|x| file.nums(x)).unwrap_or_default();
let outputs = range.len() / 2;
if size == 0 || outputs == 0 || !matches!(bps, 1 | 2 | 4 | 8 | 12 | 16 | 24 | 32) {
return Func::Unsupported;
}
let total = sizes.iter().try_fold(1usize, |a, b| a.checked_mul(*b)).filter(|t| *t > 0 && *t <= 1 << 22);
let Some(total) = total else { 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());
let max = ((1u64 << bps) - 1) as f64;
let mut samples = Vec::with_capacity(size * outputs);
let mut bit = 0usize;
for _ in 0..size * outputs {
for _ in 0..total * outputs {
let mut v = 0u64;
for _ in 0..bps {
let byte = data.get(bit / 8).copied().unwrap_or(0);
@ -407,6 +420,13 @@ impl Func {
}
samples.push(v as f64 / max);
}
if sizes.len() > 1 {
let m = sizes.len();
let encode =
(0..m).flat_map(|i| [enc.get(2 * i).copied().unwrap_or(0.0), enc.get(2 * i + 1).copied().unwrap_or(sizes[i] as f64 - 1.0)]).collect();
let domain = (0..m).flat_map(|i| [dom.get(2 * i).copied().unwrap_or(0.0), dom.get(2 * i + 1).copied().unwrap_or(1.0)]).collect();
return Func::SampledN { domain, size: sizes, outputs, encode, decode, samples };
}
Func::Sampled { domain, size, outputs, encode, decode, samples }
}
4 => {
@ -441,8 +461,50 @@ impl Func {
let k = st.len().saturating_sub(n);
return st[k..].iter().enumerate().map(|(i, v)| v.clamp(range[2 * i], range[2 * i + 1])).collect();
}
let t = input.first().copied().unwrap_or(0.0);
self.eval1(t)
match self {
Func::SampledN { domain, size, outputs, encode, decode, samples } => {
let m = size.len();
// Each input → a sample-table coordinate, then the 2^m surrounding samples.
let mut base = vec![0usize; m];
let mut frac = vec![0.0f64; m];
for i in 0..m {
let x = input.get(i).copied().unwrap_or(0.0);
let (d0, d1) = (domain[2 * i], domain[2 * i + 1]);
let x = x.clamp(d0.min(d1), d0.max(d1));
let e = if d1 != d0 { encode[2 * i] + (x - d0) / (d1 - d0) * (encode[2 * i + 1] - encode[2 * i]) } else { encode[2 * i] };
let e = if e.is_finite() { e.clamp(0.0, size[i] as f64 - 1.0) } else { 0.0 };
base[i] = e.floor() as usize;
frac[i] = e - e.floor();
}
let corners = if m <= 6 { 1usize << m } else { 1 };
let mut out = vec![0.0; *outputs];
for c in 0..corners {
let mut w = 1.0;
let mut idx = 0usize;
let mut stride = 1usize;
for i in 0..m {
let up = m <= 6 && (c >> i) & 1 == 1;
w *= if up { frac[i] } else { 1.0 - frac[i] };
let k = if up { (base[i] + 1).min(size[i] - 1) } else { base[i] };
idx += k * stride;
stride *= size[i];
}
if w == 0.0 {
continue;
}
for (o, v) in out.iter_mut().enumerate() {
*v += w * samples.get(idx * outputs + o).copied().unwrap_or(0.0);
}
}
for (o, v) in out.iter_mut().enumerate() {
let (d0, d1) = (decode.get(2 * o).copied().unwrap_or(0.0), decode.get(2 * o + 1).copied().unwrap_or(1.0));
*v = d0 + *v * (d1 - d0);
}
out
}
Func::Array(fs) if input.len() > 1 => fs.iter().map(|f| f.eval(input).first().copied().unwrap_or(0.0)).collect(),
_ => self.eval1(input.first().copied().unwrap_or(0.0)),
}
}
pub fn eval1(&self, t: f64) -> Vec<f64> {
@ -481,7 +543,7 @@ impl Func {
.collect()
}
Func::Array(fs) => fs.iter().map(|f| f.eval1(t).first().copied().unwrap_or(0.0)).collect(),
Func::Calc { .. } => self.eval(&[t]),
Func::Calc { .. } | Func::SampledN { .. } => self.eval(&[t]),
Func::Unsupported => vec![0.5],
}
}

View File

@ -16,6 +16,7 @@ mod image;
mod object;
mod page;
mod ps;
mod shading;
mod type1;
pub mod write;
@ -192,3 +193,5 @@ pub fn layer_doc(doc: &Doc, index: usize) -> Doc {
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_shading;

View File

@ -90,11 +90,20 @@ pub(crate) struct Tiling {
uncolored: Option<[f64; 3]>,
}
/// A shading pattern without a gradient equivalent (function-based or mesh shadings),
/// rasterised when it fills a path.
pub(crate) struct ShadingFill {
sh: Obj,
/// Shading space → page.
space: Affine,
}
#[derive(Clone)]
enum Fill {
Color([f64; 3]),
Paint(Paint),
Tiling(Rc<Tiling>),
Shading(Rc<ShadingFill>),
None,
}
@ -650,6 +659,10 @@ impl<'a> Interp<'a> {
self.b.skip("tiling pattern stroke");
Some(Paint::Color(t.uncolored.unwrap_or([0.5; 3])))
}
Fill::Shading(_) => {
self.b.skip("shading pattern stroke");
Some(Paint::Color([0.5; 3]))
}
Fill::None => None,
}
}
@ -681,6 +694,21 @@ impl<'a> Interp<'a> {
self.tile(&t, area, depth);
self.close_to(d);
}
Fill::Shading(sf) => {
let page = ctm * path.clone();
let d = self.b.depth();
let mut sk = vec![];
let img = crate::shading::shading_image(self.file, &sf.sh, sf.space, self.page_box, &mut sk);
for s in sk {
self.b.skip(&s);
}
if let Some(mut img) = img {
img.opacity = self.gs.fill_alpha;
self.b.clip(page, rule);
self.b.push(Node::Image(img));
self.close_to(d);
}
}
f => fill_paint = self.paint_of(&f).map(|p| (p, self.gs.fill_alpha, rule)),
}
}
@ -811,7 +839,11 @@ impl<'a> Interp<'a> {
let m = f.get(p, "Matrix").map(|m| affine(&f.nums(m))).unwrap_or(Affine::IDENTITY);
match f.get_num(p, "PatternType").unwrap_or(0.0) as i32 {
2 => {
let Some(sh) = f.get(p, "Shading").and_then(Obj::dict) else { return Fill::None };
let Some(sh_obj) = f.get(p, "Shading") else { return Fill::None };
let Some(sh) = sh_obj.dict() else { return Fill::None };
if !matches!(f.get_num(sh, "ShadingType").unwrap_or(0.0) as i32, 2 | 3) {
return Fill::Shading(Rc::new(ShadingFill { sh: sh_obj.clone(), space: base * m }));
}
let sh = sh.clone();
match self.shading_gradient(&sh) {
// Gradient space → shape user space (shapes carry the CTM).
@ -848,7 +880,21 @@ impl<'a> Interp<'a> {
fn shade(&mut self, res: &Dict, name: &str) {
let f = self.file;
let Some(sh) = f.get_dict(res, "Shading").and_then(|s| f.get_dict(s, name)).cloned() else { return };
let Some(sh_obj) = f.get_dict(res, "Shading").and_then(|s| f.get(s, name)) else { return };
let Some(sh) = sh_obj.dict().cloned() else { return };
if !matches!(f.get_num(&sh, "ShadingType").unwrap_or(0.0) as i32, 2 | 3) {
// Function-based and mesh shadings paint only their own extent.
let mut sk = vec![];
let img = crate::shading::shading_image(f, sh_obj, self.gs.ctm, self.page_box, &mut sk);
for s in sk {
self.b.skip(&s);
}
if let Some(mut img) = img {
img.opacity = self.gs.fill_alpha;
self.b.push(Node::Image(img));
}
return;
}
let Some(g) = self.shading_gradient(&sh) else { return };
// Fill everything (the current clip limits it): the page box in user space.
let inv = self.gs.ctm.inverse();

580
crates/pdf/src/shading.rs Normal file
View File

@ -0,0 +1,580 @@
//! Shadings the render tree has no gradient for (ISO 32000-1 §8.7.4.5): function-based
//! shadings (type 1) and the mesh shadings — free-form and lattice-form Gouraud-shaded triangle
//! meshes (types 4 and 5), Coons patch meshes (type 6) and tensor-product patch meshes (type 7).
//! Patches are tessellated into triangles; triangles are rasterised with their colours (or the
//! parametric `t` of a shading `Function`) interpolated across them, into an image node in page
//! space (2×2 supersampled, so the mesh's outer edges are anti-aliased without seams between
//! triangles).
use effectcraft_svg::{Affine, Image as SvgImage};
use kurbo::Point;
use crate::color::{Cs, Func, color_space};
use crate::object::{Dict, File, Obj, decode_stream};
/// The longest side of a rasterised shading, in image pixels.
const MAX_SIDE: f64 = 1024.0;
/// The most image pixels per PDF point.
const MAX_SCALE: f64 = 4.0;
/// The most triangles one shading draws.
const MAX_TRIANGLES: usize = 2_000_000;
/// A mesh vertex: page-space position and its value (RGB, or `t` in `[0]` with a function).
#[derive(Clone, Copy, Debug, PartialEq)]
struct V {
p: Point,
c: [f64; 3],
}
/// How vertex values become colours.
struct Colors {
cs: Cs,
/// With a function: `t` (domain `[t0, t1]`) → RGB through a 1024-entry table.
lut: Option<(f64, f64, Vec<[u8; 3]>)>,
}
impl Colors {
fn new(file: &File, sh: &Dict) -> (Colors, usize) {
let cs = file.get(sh, "ColorSpace").map(|c| color_space(file, c, None)).unwrap_or(Cs::Rgb);
let n = cs.components().max(1);
match file.get(sh, "Function") {
Some(fo) => {
let func = Func::read(file, fo);
let dom = file.get(sh, "Domain").map(|d| file.nums(d)).filter(|d| d.len() == 2).unwrap_or_else(|| vec![0.0, 1.0]);
let (t0, t1) = (dom[0], dom[1]);
let lut = (0..1024)
.map(|i| {
let t = t0 + (t1 - t0) * i as f64 / 1023.0;
to8(cs.to_rgb(&func.eval(&[t])))
})
.collect();
(Colors { cs, lut: Some((t0, t1, lut)) }, 1)
}
None => (Colors { cs, lut: None }, n),
}
}
/// A vertex value from its components (RGB, or `t`).
fn value(&self, comps: &[f64]) -> [f64; 3] {
match self.lut {
Some(_) => [comps.first().copied().unwrap_or(0.0), 0.0, 0.0],
None => self.cs.to_rgb(comps),
}
}
/// An interpolated value → RGB8.
fn rgb(&self, v: [f64; 3]) -> [u8; 3] {
match &self.lut {
Some((t0, t1, lut)) => {
let k = if t1 != t0 { (v[0] - t0) / (t1 - t0) } else { 0.0 };
let k = if k.is_finite() { k.clamp(0.0, 1.0) } else { 0.0 };
lut[(k * 1023.0).round() as usize]
}
None => to8(v),
}
}
}
fn to8(c: [f64; 3]) -> [u8; 3] {
c.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
}
/// Big-endian bit reader over mesh data.
struct Bits<'a> {
data: &'a [u8],
bit: usize,
}
impl Bits<'_> {
fn left(&self) -> usize {
(self.data.len() * 8).saturating_sub(self.bit)
}
fn read(&mut self, n: usize) -> Option<u64> {
if n == 0 || n > 32 || self.left() < n {
return None;
}
let mut v = 0u64;
for _ in 0..n {
let byte = self.data[self.bit / 8];
v = (v << 1) | ((byte >> (7 - self.bit % 8)) & 1) as u64;
self.bit += 1;
}
Some(v)
}
fn align(&mut self) {
self.bit = self.bit.div_ceil(8) * 8;
}
}
/// The layout of a mesh's vertex data (§8.7.4.5.5 Table 83 and following).
struct Layout {
bpc: usize,
bpcomp: usize,
bpf: usize,
/// Value components per vertex (1 with a function).
n: usize,
decode: Vec<f64>,
}
impl Layout {
fn read(file: &File, sh: &Dict, n: usize) -> Option<Layout> {
let bpc = file.get_num(sh, "BitsPerCoordinate")? as usize;
let bpcomp = file.get_num(sh, "BitsPerComponent")? as usize;
let bpf = file.get_num(sh, "BitsPerFlag").unwrap_or(8.0) as usize;
let decode = file.get(sh, "Decode").map(|d| file.nums(d)).unwrap_or_default();
let ok = |b: usize| matches!(b, 1 | 2 | 4 | 8 | 12 | 16 | 24 | 32);
if !ok(bpc) || !ok(bpcomp) || !matches!(bpf, 2 | 4 | 8) || decode.len() < 4 + 2 * n {
return None;
}
Some(Layout { bpc, bpcomp, bpf, n, decode })
}
fn scale(raw: u64, bits: usize, lo: f64, hi: f64) -> f64 {
let max = ((1u64 << bits) - 1) as f64;
lo + raw as f64 * (hi - lo) / max
}
fn point(&self, r: &mut Bits) -> Option<Point> {
let x = r.read(self.bpc)?;
let y = r.read(self.bpc)?;
let d = &self.decode;
Some(Point::new(Self::scale(x, self.bpc, d[0], d[1]), Self::scale(y, self.bpc, d[2], d[3])))
}
fn comps(&self, r: &mut Bits) -> Option<Vec<f64>> {
(0..self.n)
.map(|i| {
let v = r.read(self.bpcomp)?;
Some(Self::scale(v, self.bpcomp, self.decode[4 + 2 * i], self.decode[5 + 2 * i]))
})
.collect()
}
}
/// A shading rasterised into an image node in page space, for `sh` or a shading-pattern fill.
/// `sh` is the shading dictionary or stream, `to_page` maps shading space to page space and
/// `page_box` limits the image (page space). `None` with a note in `skip` when nothing draws.
pub(crate) fn shading_image(file: &File, sh: &Obj, to_page: Affine, page_box: [f64; 4], skip: &mut Vec<String>) -> Option<SvgImage> {
let sh_obj = file.resolve(sh);
let d = sh_obj.dict()?;
let ty = file.get_num(d, "ShadingType").unwrap_or(0.0) as i32;
if !to_page.as_coeffs().iter().all(|v| v.is_finite()) || to_page.determinant().abs() < 1e-12 {
return None;
}
let tris = match ty {
1 => return function_shading(file, d, to_page, page_box, skip),
4..=7 => {
let Obj::Stream(_, raw) = sh_obj else {
skip.push(format!("shading type {ty} (no data)"));
return None;
};
let Some(data) = decode_stream(file, d, raw) else {
skip.push(format!("shading type {ty} (filters)"));
return None;
};
let (colors, n) = Colors::new(file, d);
let Some(layout) = Layout::read(file, d, n) else {
skip.push(format!("shading type {ty} (layout)"));
return None;
};
let mut r = Bits { data: &data, bit: 0 };
let tris = match ty {
4 => free_form(&mut r, &layout, &colors, to_page),
5 => {
let per_row = file.get_num(d, "VerticesPerRow").unwrap_or(0.0) as usize;
lattice(&mut r, &layout, &colors, to_page, per_row)
}
_ => patches(&mut r, &layout, &colors, to_page, ty == 7, page_box),
};
(tris, colors)
}
_ => {
skip.push(format!("shading type {ty}"));
return None;
}
};
let (tris, colors) = tris;
if tris.is_empty() {
return None;
}
rasterize(&tris, &colors, page_box)
}
/// Type 4: free-form triangles with edge flags (0 starts a triangle; 1 and 2 share an edge with
/// the previous one).
fn free_form(r: &mut Bits, l: &Layout, colors: &Colors, m: Affine) -> Vec<[V; 3]> {
let mut out = vec![];
let mut pending: Vec<V> = vec![];
let mut last: Option<[V; 3]> = None;
while out.len() < MAX_TRIANGLES {
let Some(flag) = r.read(l.bpf) else { break };
let (Some(p), Some(c)) = (l.point(r), l.comps(r)) else { break };
r.align();
let v = V { p: m * p, c: colors.value(&c) };
// A triangle being started takes the next two vertices whatever their flags.
if !pending.is_empty() || flag == 0 {
pending.push(v);
if pending.len() == 3 {
let t = [pending[0], pending[1], pending[2]];
out.push(t);
last = Some(t);
pending.clear();
}
continue;
}
let Some([a, b, c]) = last else { continue };
let t = if flag == 1 { [b, c, v] } else { [a, c, v] };
out.push(t);
last = Some(t);
}
out
}
/// 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 {
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();
row.push(V { p: m * p, c: colors.value(&c) });
if row.len() == per_row {
rows.push(std::mem::take(&mut row));
if rows.len() * per_row > MAX_TRIANGLES {
break;
}
}
}
let mut out = vec![];
for w in rows.windows(2) {
for i in 0..per_row - 1 {
let (a, b, c, d) = (w[0][i], w[0][i + 1], w[1][i], w[1][i + 1]);
out.push([a, b, c]);
out.push([b, d, c]);
}
}
out
}
fn bez(p: [Point; 4], t: f64) -> Point {
let s = 1.0 - t;
let (a, b, c, d) = (s * s * s, 3.0 * s * s * t, 3.0 * s * t * t, t * t * t);
Point::new(a * p[0].x + b * p[1].x + c * p[2].x + d * p[3].x, a * p[0].y + b * p[1].y + c * p[2].y + d * p[3].y)
}
/// Types 6 and 7: Coons and tensor-product patches. The 12 boundary points run around the
/// patch from its first corner (`p00 p01 p02 p03 p13 p23 p33 p32 p31 p30 p20 p10`); tensor
/// patches add `p11 p12 p22 p21`. Corner colours belong to `p00`, `p03`, `p33` and `p30`.
/// Flags 1–3 reuse the previous patch's second, third or fourth side as the first.
fn patches(r: &mut Bits, l: &Layout, colors: &Colors, m: Affine, tensor: bool, page_box: [f64; 4]) -> Vec<[V; 3]> {
let mut out = vec![];
let mut prev: Option<([Point; 12], [[f64; 3]; 4])> = None;
while out.len() < MAX_TRIANGLES {
let Some(flag) = r.read(l.bpf) else { break };
let mut lp = [Point::ZERO; 12];
let mut cs = [[0.0; 3]; 4];
let first = match (flag, &prev) {
(0, _) => 0,
(1..=3, Some((pp, pc))) => {
let (k, ci) = match flag {
1 => (3, 1),
2 => (6, 2),
_ => (9, 3),
};
for i in 0..4 {
lp[i] = pp[(k + i) % 12];
}
cs[0] = pc[ci];
cs[1] = pc[(ci + 1) % 4];
4
}
_ => break,
};
let mut ok = true;
for p in lp.iter_mut().skip(first) {
match l.point(r) {
Some(q) => *p = q,
None => ok = false,
}
}
let mut inner = [Point::ZERO; 4];
if tensor {
for p in &mut inner {
match l.point(r) {
Some(q) => *p = q,
None => ok = false,
}
}
}
for c in cs.iter_mut().skip(if first == 0 { 0 } else { 2 }) {
match l.comps(r) {
Some(v) => *c = colors.value(&v),
None => ok = false,
}
}
r.align();
if !ok {
break;
}
// Shading space → page space before tessellating (the rasteriser works in page space).
let lp_page = lp.map(|p| m * p);
let inner_page = inner.map(|p| m * p);
// The 4×4 grid g[i][j]: row i = 0 is p00 … p03.
let mut g = [[Point::ZERO; 4]; 4];
g[0] = [lp_page[0], lp_page[1], lp_page[2], lp_page[3]];
g[1][3] = lp_page[4];
g[2][3] = lp_page[5];
g[3] = [lp_page[9], lp_page[8], lp_page[7], lp_page[6]];
g[2][0] = lp_page[10];
g[1][0] = lp_page[11];
g[1][1] = inner_page[0];
g[1][2] = inner_page[1];
g[2][2] = inner_page[2];
g[2][1] = inner_page[3];
tessellate(&g, tensor, cs, page_box, &mut out);
prev = Some((lp, cs));
}
out
}
/// A patch → `n`×`n` cells of two triangles; `u` runs along `p00 → p03`, `v` along `p00 → p30`.
fn tessellate(g: &[[Point; 4]; 4], tensor: bool, cs: [[f64; 3]; 4], page_box: [f64; 4], out: &mut Vec<[V; 3]>) {
let (mut lo, mut hi) = (Point::new(f64::MAX, f64::MAX), Point::new(f64::MIN, f64::MIN));
for p in g.iter().flatten() {
lo = Point::new(lo.x.min(p.x), lo.y.min(p.y));
hi = Point::new(hi.x.max(p.x), hi.y.max(p.y));
}
if !(lo.x.is_finite() && hi.x.is_finite() && lo.y.is_finite() && hi.y.is_finite()) {
return;
}
// Patches entirely off the page draw nothing.
if hi.x < page_box[0] || lo.x > page_box[2] || hi.y < page_box[1] || lo.y > page_box[3] {
return;
}
let size = (hi.x - lo.x).max(hi.y - lo.y).min(page_box[2] - page_box[0] + page_box[3] - page_box[1]);
let n = ((size * MAX_SCALE / 6.0).ceil() as usize).clamp(2, 48);
let eval = |u: f64, v: f64| -> Point {
if tensor {
let rows = g.map(|row| bez(row, u));
bez(rows, v)
} else {
let cb = bez(g[0], u);
let ct = bez(g[3], u);
let cl = bez([g[0][0], g[1][0], g[2][0], g[3][0]], v);
let cr = bez([g[0][3], g[1][3], g[2][3], g[3][3]], v);
let corner = |a: Point, w: f64| a.to_vec2() * w;
let s = cb.to_vec2() * (1.0 - v) + ct.to_vec2() * v + cl.to_vec2() * (1.0 - u) + cr.to_vec2() * u
- corner(g[0][0], (1.0 - u) * (1.0 - v))
- corner(g[0][3], u * (1.0 - v))
- corner(g[3][0], (1.0 - u) * v)
- corner(g[3][3], u * v);
s.to_point()
}
};
let color = |u: f64, v: f64| -> [f64; 3] {
// c0 (0,0), c1 (1,0), c2 (1,1), c3 (0,1).
let mut c = [0.0; 3];
for (k, ck) in c.iter_mut().enumerate() {
*ck = cs[0][k] * (1.0 - u) * (1.0 - v) + cs[1][k] * u * (1.0 - v) + cs[2][k] * u * v + cs[3][k] * (1.0 - u) * v;
}
c
};
let mut grid = Vec::with_capacity((n + 1) * (n + 1));
for j in 0..=n {
for i in 0..=n {
let (u, v) = (i as f64 / n as f64, j as f64 / n as f64);
grid.push(V { p: eval(u, v), c: color(u, v) });
}
}
for j in 0..n {
for i in 0..n {
let a = grid[j * (n + 1) + i];
let b = grid[j * (n + 1) + i + 1];
let c = grid[(j + 1) * (n + 1) + i];
let d = grid[(j + 1) * (n + 1) + i + 1];
out.push([a, b, c]);
out.push([b, d, c]);
}
}
}
/// The page-space raster frame for content covering `lo..hi`: origin (top-left, page space),
/// scale (pixels per point) and size, or `None` when it misses the page.
fn frame(lo: Point, hi: Point, page_box: [f64; 4]) -> Option<(Point, f64, usize, usize)> {
let x0 = lo.x.max(page_box[0]);
let y0 = lo.y.max(page_box[1]);
let x1 = hi.x.min(page_box[2]);
let y1 = hi.y.min(page_box[3]);
let finite = x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite();
if !finite || x1 <= x0 || y1 <= y0 {
return None;
}
let s = (MAX_SIDE / (x1 - x0).max(y1 - y0)).min(MAX_SCALE);
let w = ((x1 - x0) * s).ceil().max(1.0) as usize;
let h = ((y1 - y0) * s).ceil().max(1.0) as usize;
Some((Point::new(x0, y1), s, w, h))
}
/// The image node for an RGBA8 raster with origin `o` (page space, top-left) and `s` px/pt.
fn image_node(rgba: Vec<u8>, w: usize, h: usize, o: Point, s: f64) -> SvgImage {
SvgImage {
name: "Shading".into(),
transform: Affine::new([1.0 / s, 0.0, 0.0, -1.0 / s, o.x, o.y]),
width: w as u32,
height: h as u32,
rgba: std::sync::Arc::new(rgba),
opacity: 1.0,
}
}
/// Gouraud-shade the triangles (page space) into an image: 2×2 samples per pixel, later
/// triangles over earlier ones.
fn rasterize(tris: &[[V; 3]], colors: &Colors, page_box: [f64; 4]) -> Option<SvgImage> {
let (mut lo, mut hi) = (Point::new(f64::MAX, f64::MAX), Point::new(f64::MIN, f64::MIN));
for v in tris.iter().flatten() {
if v.p.x.is_finite() && v.p.y.is_finite() {
lo = Point::new(lo.x.min(v.p.x), lo.y.min(v.p.y));
hi = Point::new(hi.x.max(v.p.x), hi.y.max(v.p.y));
}
}
let (o, s, w, h) = frame(lo, hi, page_box)?;
// Supersampled buffer: value + covered.
let (sw, sh) = (w * 2, h * 2);
let ss = s * 2.0;
let mut val = vec![[0.0f32; 3]; sw * sh];
let mut cov = vec![false; sw * sh];
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);
if area.abs() < 1e-12 || !area.is_finite() {
continue;
}
let bx0 = q.iter().map(|p| p.x).fold(f64::MAX, f64::min).floor().max(0.0) as usize;
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 eps = 1e-9 * area.abs();
for y in by0..by1 {
for x in bx0..bx1 {
let p = Point::new(x as f64 + 0.5, y as f64 + 0.5);
let w0 = ((q[1].x - p.x) * (q[2].y - p.y) - (q[2].x - p.x) * (q[1].y - p.y)) / area;
let w1 = ((q[2].x - p.x) * (q[0].y - p.y) - (q[0].x - p.x) * (q[2].y - p.y)) / area;
let w2 = 1.0 - w0 - w1;
if w0 < -eps || w1 < -eps || w2 < -eps {
continue;
}
let i = y * sw + x;
for k in 0..3 {
val[i][k] = (t[0].c[k] * w0 + t[1].c[k] * w1 + t[2].c[k] * w2) as f32;
}
cov[i] = true;
}
}
}
// Resolve: average the covered samples' colours, alpha = covered fraction.
let mut rgba = vec![0u8; w * h * 4];
for y in 0..h {
for x in 0..w {
let mut acc = [0.0f64; 3];
let mut n = 0;
for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
let i = (2 * y + dy) * sw + 2 * x + dx;
if cov[i] {
let c = colors.rgb(val[i].map(f64::from));
for k in 0..3 {
acc[k] += c[k] as f64;
}
n += 1;
}
}
if n > 0 {
let o = (y * w + x) * 4;
for k in 0..3 {
rgba[o + k] = (acc[k] / n as f64).round() as u8;
}
rgba[o + 3] = (n * 255 / 4) as u8;
}
}
}
Some(image_node(rgba, w, h, o, s))
}
/// Type 1: `Function(x, y)` over `Domain` `[x0 x1 y0 y1]`, mapped by `Matrix`.
fn function_shading(file: &File, d: &Dict, to_page: Affine, page_box: [f64; 4], skip: &mut Vec<String>) -> Option<SvgImage> {
let Some(fo) = file.get(d, "Function") else {
skip.push("shading type 1 (no function)".into());
return None;
};
let func = Func::read(file, fo);
if func == Func::Unsupported {
skip.push("shading function".into());
return None;
}
let cs = file.get(d, "ColorSpace").map(|c| color_space(file, c, None)).unwrap_or(Cs::Rgb);
let dom = file.get(d, "Domain").map(|x| file.nums(x)).filter(|v| v.len() == 4).unwrap_or_else(|| vec![0.0, 1.0, 0.0, 1.0]);
let mtx = file.get(d, "Matrix").map(|x| file.nums(x)).filter(|v| v.len() == 6).map(|v| Affine::new([v[0], v[1], v[2], v[3], v[4], v[5]]));
let full = to_page * mtx.unwrap_or(Affine::IDENTITY);
if full.determinant().abs() < 1e-12 || !full.as_coeffs().iter().all(|v| v.is_finite()) {
return None;
}
let corners = [(dom[0], dom[2]), (dom[1], dom[2]), (dom[1], dom[3]), (dom[0], dom[3])].map(|(x, y)| full * Point::new(x, y));
let lo = Point::new(corners.iter().map(|p| p.x).fold(f64::MAX, f64::min), corners.iter().map(|p| p.y).fold(f64::MAX, f64::min));
let hi = Point::new(corners.iter().map(|p| p.x).fold(f64::MIN, f64::max), corners.iter().map(|p| p.y).fold(f64::MIN, f64::max));
let (o, s, w, h) = frame(lo, hi, page_box)?;
let inv = full.inverse();
let (xa, xb) = (dom[0].min(dom[1]), dom[0].max(dom[1]));
let (ya, yb) = (dom[2].min(dom[3]), dom[2].max(dom[3]));
let mut rgba = vec![0u8; w * h * 4];
for y in 0..h {
for x in 0..w {
// 2×2 samples: anti-aliased domain edges.
let mut acc = [0.0f64; 3];
let mut n = 0;
for (dx, dy) in [(0.25, 0.25), (0.75, 0.25), (0.25, 0.75), (0.75, 0.75)] {
let page = Point::new(o.x + (x as f64 + dx) / s, o.y - (y as f64 + dy) / s);
let q = inv * page;
if q.x < xa - 1e-9 || q.x > xb + 1e-9 || q.y < ya - 1e-9 || q.y > yb + 1e-9 {
continue;
}
let c = cs.to_rgb(&func.eval(&[q.x, q.y]));
for k in 0..3 {
acc[k] += c[k];
}
n += 1;
}
if n > 0 {
let i = (y * w + x) * 4;
let c = to8(acc.map(|v| v / n as f64));
rgba[i..i + 3].copy_from_slice(&c);
rgba[i + 3] = (n * 255 / 4) as u8;
}
}
}
Some(image_node(rgba, w, h, o, s))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bits_reader_and_alignment() {
let mut r = Bits { data: &[0b1010_0000, 0xFF], bit: 0 };
assert_eq!(r.read(3), Some(0b101));
r.align();
assert_eq!(r.read(8), Some(0xFF));
assert_eq!(r.read(1), None);
assert_eq!(r.read(33), None);
}
#[test]
fn bezier_ends() {
let p = [Point::new(0.0, 0.0), Point::new(1.0, 2.0), Point::new(2.0, 2.0), Point::new(3.0, 0.0)];
assert_eq!(bez(p, 0.0), p[0]);
assert_eq!(bez(p, 1.0), p[3]);
}
}

View File

@ -152,11 +152,11 @@ fn filters_decode() {
// ------------------------------------------------------------------ M13.6: text, images,
// patterns, soft masks, blend modes, pages
type Objs = Vec<(u32, String, Option<Vec<u8>>)>;
pub(crate) type Objs = Vec<(u32, String, Option<Vec<u8>>)>;
/// A one-page 200×100 PDF: `resources` is the page's resource dictionary body, `extra` more
/// objects (numbered from 10).
fn page_pdf(content: &str, resources: &str, extra: Objs) -> Vec<u8> {
pub(crate) fn page_pdf(content: &str, resources: &str, extra: Objs) -> Vec<u8> {
let mut objs: Objs = vec![
(1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None),
(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), None),
@ -167,12 +167,12 @@ fn page_pdf(content: &str, resources: &str, extra: Objs) -> Vec<u8> {
pdf(&objs, 1)
}
fn render(doc: &Doc) -> effectcraft_raster::Image {
pub(crate) fn render(doc: &Doc) -> effectcraft_raster::Image {
let (w, h) = doc.pixel_size();
effectcraft_svg::rasterize(doc, w, h, 1.0)
}
fn coverage(img: &effectcraft_raster::Image, x0: i64, y0: i64, x1: i64, y1: i64) -> f32 {
pub(crate) fn coverage(img: &effectcraft_raster::Image, x0: i64, y0: i64, x1: i64, y1: i64) -> f32 {
let mut s = 0.0;
for y in y0..y1 {
for x in x0..x1 {
@ -182,7 +182,7 @@ fn coverage(img: &effectcraft_raster::Image, x0: i64, y0: i64, x1: i64, y1: i64)
s
}
fn shape_names(doc: &Doc) -> Vec<String> {
pub(crate) fn shape_names(doc: &Doc) -> Vec<String> {
fn walk(g: &effectcraft_svg::Group, out: &mut Vec<String>) {
for c in &g.children {
match c {
@ -223,7 +223,7 @@ fn square_cff() -> Vec<u8> {
const SQUARE_FONT: &str = "<< /Type /Font /Subtype /Type1 /BaseFont /Square /FirstChar 65 /LastChar 65 /Widths [600] /Encoding << /Differences [65 /square] >> /FontDescriptor 11 0 R >>";
fn square_font_objs() -> Objs {
pub(crate) fn square_font_objs() -> Objs {
vec![
(10, SQUARE_FONT.into(), None),
(11, "<< /Type /FontDescriptor /FontName /Square /Flags 32 /FontFile3 12 0 R >>".into(), None),

View File

@ -0,0 +1,264 @@
//! M13.12: function-based and mesh shadings (generated fixtures).
use super::*;
use crate::tests::{Objs, page_pdf, render};
/// Mesh vertex data: 8-bit flags, 16-bit coordinates (Decode `[0 65535 0 65535 …]`, so raw
/// values are page units) and 8-bit components.
#[derive(Default)]
struct Mesh(Vec<u8>);
impl Mesh {
fn flag(&mut self, f: u8) -> &mut Self {
self.0.push(f);
self
}
fn pt(&mut self, x: u16, y: u16) -> &mut Self {
self.0.extend_from_slice(&x.to_be_bytes());
self.0.extend_from_slice(&y.to_be_bytes());
self
}
fn c(&mut self, c: &[u8]) -> &mut Self {
self.0.extend_from_slice(c);
self
}
}
const RGB_DECODE: &str = "/Decode [0 65535 0 65535 0 1 0 1 0 1]";
fn close(a: [f32; 4], b: [f32; 3], tol: f32) -> bool {
a[3] > 0.99 && (0..3).all(|i| (a[i] - b[i]).abs() < tol)
}
fn sh_page(shading: &str, data: Vec<u8>) -> Doc {
let bytes = page_pdf("/Sh sh", "/Shading << /Sh 10 0 R >>", vec![(10, shading.into(), Some(data))]);
let doc = parse(&bytes).unwrap();
assert!(doc.skipped.is_empty(), "{:?}", doc.skipped);
doc
}
#[test]
fn free_form_triangle_mesh_with_edge_flags() {
let mut m = Mesh::default();
// Triangle (20,20) red, (80,20) green, (20,80) blue; then flag 1: (80,20) (20,80) (80,80) white.
m.flag(0).pt(20, 20).c(&[255, 0, 0]);
m.flag(0).pt(80, 20).c(&[0, 255, 0]);
m.flag(0).pt(20, 80).c(&[0, 0, 255]);
m.flag(1).pt(80, 80).c(&[255, 255, 255]);
// Flag 2 off the shared edge (a, c): (80,20) (80,80) → (140,20) black.
m.flag(2).pt(140, 20).c(&[0, 0, 0]);
let doc = sh_page(&format!("<< /ShadingType 4 /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 {RGB_DECODE} >>"), m.0);
let img = render(&doc);
let at = |x: i64, y: i64| img.get(x, 99 - y);
assert!(close(at(21, 21), [1.0, 0.0, 0.0], 0.06), "{:?}", at(21, 21));
// Centroid of the first triangle: the average of its colours.
assert!(close(at(40, 40), [1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0], 0.04), "{:?}", at(40, 40));
assert!(close(at(79, 79), [1.0, 1.0, 1.0], 0.06), "{:?}", at(79, 79));
// Third triangle (a = (80,20) green, c = (80,80) white, (140,20) black).
assert!(close(at(135, 22), [0.0, 0.0, 0.0], 0.1), "{:?}", at(135, 22));
// Outside the mesh: nothing.
assert_eq!(at(10, 50)[3], 0.0);
assert_eq!(at(150, 90)[3], 0.0);
// The shading is an image node (no Gouraud primitive in the render tree).
assert!(crate::tests::shape_names(&doc).contains(&"Shading".to_string()));
}
#[test]
fn lattice_mesh_with_a_function() {
// Two rows of three vertices; t = 0 at the left, 1 at the right; grey ramp function.
let mut m = Mesh::default();
for y in [10u16, 90] {
for (x, t) in [(10u16, 0u8), (100, 128), (190, 255)] {
m.pt(x, y).c(&[t]);
}
}
let doc = sh_page(
"<< /ShadingType 5 /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 /VerticesPerRow 3 /Decode [0 65535 0 65535 0 1] \
/Function << /FunctionType 2 /Domain [0 1] /C0 [0 0 0] /C1 [1 1 1] /N 1 >> >>",
m.0,
);
let img = render(&doc);
let at = |x: i64, y: i64| img.get(x, 99 - y);
for x in [20i64, 60, 100, 140, 180] {
let want = (x as f32 + 0.5 - 10.0) / 180.0;
let p = at(x, 50);
assert!(p[3] > 0.99 && (p[0] - want).abs() < 0.03 && (p[1] - p[0]).abs() < 0.01, "x {x}: {p:?} want {want}");
}
assert_eq!(at(5, 50)[3], 0.0);
}
/// A straight-sided rectangle as the 12 boundary points from `(x0, y0)` up the left side,
/// along the top, down the right side and back along the bottom.
fn rect_loop(x0: u16, y0: u16, x1: u16, y1: u16) -> Vec<(u16, u16)> {
let lerp = |a: u16, b: u16, k: u16| ((a as u32 * (3 - k as u32) + b as u32 * k as u32) / 3) as u16;
let mut v = vec![];
for k in 0..3 {
v.push((x0, lerp(y0, y1, k)));
}
for k in 0..3 {
v.push((lerp(x0, x1, k), y1));
}
for k in 0..3 {
v.push((x1, lerp(y1, y0, k)));
}
for k in 0..3 {
v.push((lerp(x1, x0, k), y0));
}
v
}
fn patch_doc(tensor: bool) -> Doc {
let mut m = Mesh::default();
// Patch 1 over x 100..140, y 20..80: corners (100,20) red, (100,80) green, (140,80) blue,
// (140,20) black.
m.flag(0);
let l1 = rect_loop(100, 20, 140, 80);
for (x, y) in &l1 {
m.pt(*x, *y);
}
if tensor {
// Interior points p11 p12 p22 p21 at the thirds.
for (x, y) in [(113, 40), (113, 60), (127, 60), (127, 40)] {
m.pt(x, y);
}
}
m.c(&[255, 0, 0]).c(&[0, 255, 0]).c(&[0, 0, 255]).c(&[0, 0, 0]);
// Patch 2 (flag 2): shares patch 1's third side (140,80) → (140,20); then (180,20) white,
// (180,80) red.
m.flag(2);
// The 8 new points: from (140,20) along the bottom to (180,20), up to (180,80), back
// along the top towards (140,80).
let new_pts = [(153, 20), (167, 20), (180, 20), (180, 40), (180, 60), (180, 80), (167, 80), (153, 80)];
for (x, y) in new_pts {
m.pt(x, y);
}
if tensor {
for (x, y) in [(153, 60), (153, 40), (167, 40), (167, 60)] {
m.pt(x, y);
}
}
m.c(&[255, 255, 255]).c(&[255, 0, 0]);
let ty = if tensor { 7 } else { 6 };
sh_page(&format!("<< /ShadingType {ty} /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 {RGB_DECODE} >>"), m.0)
}
#[test]
fn coons_and_tensor_patch_meshes() {
for tensor in [false, true] {
let doc = patch_doc(tensor);
let img = render(&doc);
let at = |x: i64, y: i64| img.get(x, 99 - y);
let k = if tensor { "tensor" } else { "coons" };
assert!(close(at(100, 20), [1.0, 0.0, 0.0], 0.08), "{k} {:?}", at(100, 20));
assert!(close(at(100, 79), [0.0, 1.0, 0.0], 0.08), "{k} {:?}", at(100, 79));
assert!(close(at(139, 79), [0.0, 0.0, 1.0], 0.08), "{k} {:?}", at(139, 79));
// Patch centre: the average of the corners.
assert!(close(at(120, 50), [0.25, 0.25, 0.25], 0.04), "{k} {:?}", at(120, 50));
// Patch 2 inherits blue / black on its shared side.
assert!(close(at(179, 20), [1.0, 1.0, 1.0], 0.08), "{k} {:?}", at(179, 20));
assert!(close(at(179, 79), [1.0, 0.0, 0.0], 0.08), "{k} {:?}", at(179, 79));
assert!(close(at(141, 22), [0.0, 0.0, 0.0], 0.1), "{k} {:?}", at(141, 22));
assert_eq!(at(90, 50)[3], 0.0);
assert_eq!(at(190, 50)[3], 0.0);
}
}
#[test]
fn curved_coons_patch_follows_its_boundary() {
// A patch whose top side bulges up to y = 95 between x 40 and 60.
let mut m = Mesh::default();
m.flag(0);
let pts = [(20, 20), (20, 40), (20, 60), (20, 80), (40, 107), (60, 107), (80, 80), (80, 60), (80, 40), (80, 20), (60, 20), (40, 20)];
for (x, y) in pts {
m.pt(x, y);
}
m.c(&[255, 0, 0]).c(&[255, 0, 0]).c(&[255, 0, 0]).c(&[255, 0, 0]);
let doc = sh_page(&format!("<< /ShadingType 6 /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 {RGB_DECODE} >>"), m.0);
let img = render(&doc);
let at = |x: i64, y: i64| img.get(x, 99 - y);
// The top curve peaks at 80 + 0.75·27 ≈ 100 at x = 50.
assert!(at(50, 95)[3] > 0.99, "{:?}", at(50, 95));
assert_eq!(at(22, 95)[3], 0.0);
}
#[test]
fn function_based_shadings_calculator_and_sampled() {
let bytes = page_pdf(
"/A sh /B sh",
"/Shading << /A 10 0 R /B 11 0 R >>",
vec![
(10, "<< /ShadingType 1 /ColorSpace /DeviceRGB /Matrix [100 0 0 50 0 0] /Function 12 0 R >>".into(), None),
(11, "<< /ShadingType 1 /ColorSpace /DeviceRGB /Domain [0 1 0 1] /Matrix [100 0 0 50 100 50] /Function 13 0 R >>".into(), None),
(12, "<< /FunctionType 4 /Domain [0 1 0 1] /Range [0 1 0 1 0 1] >>".into(), Some(b"{ 0 }".to_vec())),
(
13,
"<< /FunctionType 0 /Domain [0 1 0 1] /Range [0 1 0 1 0 1] /Size [2 2] /BitsPerSample 8 >>".into(),
Some(vec![255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255]),
),
],
);
let doc = parse(&bytes).unwrap();
assert!(doc.skipped.is_empty(), "{:?}", doc.skipped);
let img = render(&doc);
let at = |x: i64, y: i64| img.get(x, 99 - y);
// Calculator: (x, y) → (x, y, 0).
let p = at(90, 10);
assert!(close(p, [0.905, 0.21, 0.0], 0.03), "{p:?}");
// Only the domain is painted.
assert_eq!(at(50, 75)[3], 0.0);
// Sampled 2-in function, bilinear: red (0,0), green (1,0), blue (0,1), white (1,1).
assert!(close(at(150, 75), [0.5, 0.5, 0.5], 0.03), "{:?}", at(150, 75));
assert!(close(at(100, 50), [1.0, 0.0, 0.0], 0.04), "{:?}", at(100, 50));
assert!(close(at(199, 99), [1.0, 1.0, 1.0], 0.04), "{:?}", at(199, 99));
}
#[test]
fn mesh_shading_pattern_fills_a_path() {
let mut m = Mesh::default();
m.flag(0).pt(0, 0).c(&[255, 0, 0]);
m.flag(0).pt(200, 0).c(&[255, 0, 0]);
m.flag(0).pt(0, 200).c(&[255, 0, 0]);
let objs: Objs = vec![
(10, "<< /PatternType 2 /Shading 11 0 R >>".into(), None),
(11, format!("<< /ShadingType 4 /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 {RGB_DECODE} >>"), Some(m.0)),
];
let bytes = page_pdf("/Pattern cs /P scn 10 10 30 30 re f", "/Pattern << /P 10 0 R >>", objs);
let doc = parse(&bytes).unwrap();
assert!(doc.skipped.is_empty(), "{:?}", doc.skipped);
let img = render(&doc);
let at = |x: i64, y: i64| img.get(x, 99 - y);
assert!(close(at(20, 20), [1.0, 0.0, 0.0], 0.02), "{:?}", at(20, 20));
// Clipped to the path.
assert_eq!(at(50, 20)[3], 0.0);
assert_eq!(at(5, 5)[3], 0.0);
}
#[test]
fn corrupt_mesh_data_draws_what_parses() {
// Truncated and garbage vertex data never panic.
for ty in [4, 5, 6, 7] {
for data in [vec![], vec![0u8; 3], vec![0xFF; 7], (0..=255u8).collect::<Vec<u8>>(), vec![3u8; 200]] {
let bytes = page_pdf(
"/Sh sh /Pattern cs /P scn 0 0 50 50 re f",
"/Shading << /Sh 10 0 R >> /Pattern << /P << /PatternType 2 /Shading 10 0 R >> >>",
vec![(
10,
format!(
"<< /ShadingType {ty} /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 /VerticesPerRow 2 {RGB_DECODE} >>"
),
Some(data),
)],
);
let doc = parse(&bytes).unwrap();
let _ = render(&doc);
}
}
// Bad layouts are reported.
let bytes = page_pdf(
"/Sh sh",
"/Shading << /Sh 10 0 R >>",
vec![(10, "<< /ShadingType 4 /ColorSpace /DeviceRGB /BitsPerCoordinate 7 >>".into(), Some(vec![0; 16]))],
);
let doc = parse(&bytes).unwrap();
assert!(doc.skipped.iter().any(|s| s.contains("layout")), "{:?}", doc.skipped);
}