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:
parent
f14b575e13
commit
b0b8d4c01a
|
|
@ -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],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in New Issue