use crate::path::Subpath;
use crate::Pixmap;
const SUBSAMPLES: u32 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FillRule {
NonZero,
EvenOdd,
}
#[derive(Debug, Default)]
pub(crate) struct RasterScratch {
row: Vec<f32>,
edges: Vec<Edge>,
active: Vec<usize>,
crossings: Vec<(f32, i32)>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Mask {
pub width: u32,
pub height: u32,
pub x0: u32,
pub y0: u32,
pub bbox_w: u32,
pub bbox_h: u32,
pub data: Vec<u8>,
pub opaque: bool,
}
impl Mask {
pub(crate) fn new(width: u32, height: u32) -> Mask {
Mask {
width,
height,
x0: 0,
y0: 0,
bbox_w: width,
bbox_h: height,
data: vec![0; width as usize * height as usize],
opaque: false,
}
}
fn empty(width: u32, height: u32) -> Mask {
Mask {
width,
height,
x0: 0,
y0: 0,
bbox_w: 0,
bbox_h: 0,
data: Vec::new(),
opaque: false,
}
}
pub(crate) fn from_path(
width: u32,
height: u32,
scratch: &mut RasterScratch,
polys: &[Subpath],
rule: FillRule,
) -> Mask {
prepare_edges(&mut scratch.edges, polys);
if scratch.edges.is_empty() || width == 0 || height == 0 {
return Mask::empty(width, height);
}
let mut xmin = f32::MAX;
let mut xmax = f32::MIN;
let mut ymin = f32::MAX;
let mut ymax = f32::MIN;
for e in &scratch.edges {
xmin = xmin.min(e.x0).min(e.x1);
xmax = xmax.max(e.x0).max(e.x1);
ymin = ymin.min(e.y0);
ymax = ymax.max(e.y1);
}
let bx0 = xmin.floor().max(0.0) as u32;
let bx1 = (xmax.ceil().max(0.0) as u32).min(width);
let by0 = ymin.floor().max(0.0) as u32;
let by1 = (ymax.ceil().max(0.0) as u32).min(height);
if bx1 <= bx0 || by1 <= by0 {
return Mask::empty(width, height);
}
let bbox_w = bx1 - bx0;
let bbox_h = by1 - by0;
let mut mask = Mask {
width,
height,
x0: bx0,
y0: by0,
bbox_w,
bbox_h,
data: vec![0u8; bbox_w as usize * bbox_h as usize],
opaque: false,
};
let bw = bbox_w as usize;
sweep_rows(scratch, width, height, rule, |y, row, lo, hi| {
let base = (y - by0) as usize * bw;
let local_lo = lo - bx0 as usize;
let local_hi = hi - bx0 as usize;
let dst = &mut mask.data[base + local_lo..base + local_hi];
for (cov, out) in row[lo..hi].iter().zip(dst.iter_mut()) {
*out = (cov.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
}
});
mask.opaque = mask.data.iter().all(|&b| b == 255);
mask
}
#[inline]
pub(crate) fn coverage(&self, x: u32, y: u32) -> u8 {
if x < self.x0 || y < self.y0 {
return 0;
}
let (lx, ly) = (x - self.x0, y - self.y0);
if lx >= self.bbox_w || ly >= self.bbox_h {
return 0;
}
self.data[ly as usize * self.bbox_w as usize + lx as usize]
}
pub(crate) fn intersect(&mut self, other: &Mask) {
*self = Mask::intersected(self, other);
}
pub(crate) fn intersected(a: &Mask, b: &Mask) -> Mask {
debug_assert_eq!((a.width, a.height), (b.width, b.height));
let x0 = a.x0.max(b.x0);
let y0 = a.y0.max(b.y0);
let x1 = (a.x0 + a.bbox_w).min(b.x0 + b.bbox_w);
let y1 = (a.y0 + a.bbox_h).min(b.y0 + b.bbox_h);
if x1 <= x0 || y1 <= y0 {
return Mask::empty(a.width, a.height);
}
let bbox_w = x1 - x0;
let bbox_h = y1 - y0;
let mut data = vec![0u8; bbox_w as usize * bbox_h as usize];
for y in y0..y1 {
let a_base = (y - a.y0) as usize * a.bbox_w as usize;
let b_base = (y - b.y0) as usize * b.bbox_w as usize;
let dst_base = (y - y0) as usize * bbox_w as usize;
for x in x0..x1 {
let av = a.data[a_base + (x - a.x0) as usize];
let bv = b.data[b_base + (x - b.x0) as usize];
data[dst_base + (x - x0) as usize] = av.min(bv);
}
}
Mask {
width: a.width,
height: a.height,
x0,
y0,
bbox_w,
bbox_h,
data,
opaque: a.opaque && b.opaque,
}
}
}
#[derive(Debug)]
struct Edge {
x0: f32,
y0: f32,
x1: f32,
y1: f32,
dir: i32,
}
impl Edge {
fn x_at(&self, y: f32) -> f32 {
self.x0 + (y - self.y0) * (self.x1 - self.x0) / (self.y1 - self.y0)
}
}
fn prepare_edges(edges: &mut Vec<Edge>, polys: &[Subpath]) {
edges.clear();
for sub in polys {
let pts = &sub.points;
if pts.len() < 2 {
continue;
}
for i in 0..pts.len() {
let p = pts[i];
let q = pts[(i + 1) % pts.len()];
if !(p.x.is_finite() && p.y.is_finite() && q.x.is_finite() && q.y.is_finite()) {
continue;
}
if p.y == q.y {
continue;
}
let (top, bot, dir) = if p.y < q.y { (p, q, 1) } else { (q, p, -1) };
edges.push(Edge {
x0: top.x,
y0: top.y,
x1: bot.x,
y1: bot.y,
dir,
});
}
}
edges.sort_by(|a, b| a.y0.total_cmp(&b.y0));
}
fn add_span(
row: &mut [f32],
x0: f32,
x1: f32,
weight: f32,
dirty_lo: &mut usize,
dirty_hi: &mut usize,
) {
let w = row.len() as f32;
let x0 = x0.max(0.0);
let x1 = x1.min(w);
if x1 <= x0 {
return;
}
let first = x0.floor() as usize;
let last = (x1.ceil() as usize).min(row.len());
*dirty_lo = (*dirty_lo).min(first);
*dirty_hi = (*dirty_hi).max(last);
if last == first + 1 {
row[first] += (x1.min(first as f32 + 1.0) - x0) * weight;
return;
}
row[first] += (first as f32 + 1.0 - x0) * weight;
for slot in &mut row[first + 1..last - 1] {
*slot += weight;
}
row[last - 1] += (x1 - (last - 1) as f32) * weight;
}
fn sweep_rows<F: FnMut(u32, &[f32], usize, usize)>(
scratch: &mut RasterScratch,
width: u32,
height: u32,
rule: FillRule,
mut emit: F,
) {
if width == 0 || height == 0 {
return;
}
let RasterScratch {
row,
edges,
active,
crossings,
} = scratch;
if edges.is_empty() {
return;
}
let mut ymin = f32::MAX;
let mut ymax = f32::MIN;
for e in edges.iter() {
ymin = ymin.min(e.y0);
ymax = ymax.max(e.y1);
}
let row_start = ymin.floor().max(0.0) as u32;
let row_end = (ymax.ceil().max(0.0) as u32).min(height);
let full = width as usize;
if row.len() < full {
row.resize(full, 0.0);
}
let row = &mut row[..full];
active.clear();
let mut next = 0usize;
let weight = 1.0 / SUBSAMPLES as f32;
let mut dirty_lo = full;
let mut dirty_hi = 0usize;
for y in row_start..row_end {
if dirty_lo < dirty_hi {
row[dirty_lo..dirty_hi].iter_mut().for_each(|c| *c = 0.0);
}
dirty_lo = full;
dirty_hi = 0;
for s in 0..SUBSAMPLES {
let ys = y as f32 + (s as f32 + 0.5) / SUBSAMPLES as f32;
while next < edges.len() && edges[next].y0 <= ys {
active.push(next);
next += 1;
}
active.retain(|&i| edges[i].y1 > ys);
crossings.clear();
for &i in active.iter() {
crossings.push((edges[i].x_at(ys), edges[i].dir));
}
if crossings.len() < 2 {
continue;
}
crossings.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut wind = 0i32;
let mut span_start = 0.0f32;
for &(x, dir) in crossings.iter() {
let was_inside = inside(wind, rule);
wind += dir;
let is_inside = inside(wind, rule);
if !was_inside && is_inside {
span_start = x;
} else if was_inside && !is_inside {
add_span(row, span_start, x, weight, &mut dirty_lo, &mut dirty_hi);
}
}
}
if dirty_lo < dirty_hi {
emit(y, row, dirty_lo, dirty_hi);
}
}
if dirty_lo < dirty_hi {
row[dirty_lo..dirty_hi].iter_mut().for_each(|c| *c = 0.0);
}
}
fn inside(wind: i32, rule: FillRule) -> bool {
match rule {
FillRule::NonZero => wind != 0,
FillRule::EvenOdd => wind % 2 != 0,
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum BlendMode {
#[default]
Normal,
Multiply,
Screen,
Overlay,
Darken,
Lighten,
ColorDodge,
ColorBurn,
HardLight,
SoftLight,
Difference,
Exclusion,
}
impl BlendMode {
pub(crate) fn blend(self, cb: [u8; 3], cs: [u8; 3]) -> [u8; 3] {
if self == BlendMode::Normal {
return cs;
}
let mut out = [0u8; 3];
for i in 0..3 {
let b = cb[i] as f32 / 255.0;
let s = cs[i] as f32 / 255.0;
let v = match self {
BlendMode::Normal => s,
BlendMode::Multiply => b * s,
BlendMode::Screen => b + s - b * s,
BlendMode::Overlay => hard_light(s, b),
BlendMode::Darken => b.min(s),
BlendMode::Lighten => b.max(s),
BlendMode::ColorDodge => {
if s >= 1.0 {
1.0
} else {
(b / (1.0 - s)).min(1.0)
}
}
BlendMode::ColorBurn => {
if s <= 0.0 {
0.0
} else {
1.0 - ((1.0 - b) / s).min(1.0)
}
}
BlendMode::HardLight => hard_light(b, s),
BlendMode::SoftLight => {
let d = if b <= 0.25 {
((16.0 * b - 12.0) * b + 4.0) * b
} else {
b.sqrt()
};
if s <= 0.5 {
b - (1.0 - 2.0 * s) * b * (1.0 - b)
} else {
b + (2.0 * s - 1.0) * (d - b)
}
}
BlendMode::Difference => (b - s).abs(),
BlendMode::Exclusion => b + s - 2.0 * b * s,
};
out[i] = (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
}
out
}
}
fn hard_light(b: f32, s: f32) -> f32 {
if s <= 0.5 {
b * (2.0 * s)
} else {
let s2 = 2.0 * s - 1.0;
b + s2 - b * s2
}
}
static UNIT: [f32; 256] = {
let mut t = [0.0f32; 256];
let mut i = 0;
while i < 256 {
t[i] = i as f32 / 255.0;
i += 1;
}
t
};
pub(crate) fn composite_over(dst: &mut [u8], rgb: [u8; 3], a: f32) {
let da = dst[3] as f32 / 255.0;
let oa = a + da * (1.0 - a);
if oa <= 0.0 {
dst.copy_from_slice(&[0, 0, 0, 0]);
return;
}
for i in 0..3 {
let s = rgb[i] as f32;
let d = dst[i] as f32;
let c = (s * a + d * da * (1.0 - a)) / oa;
dst[i] = (c + 0.5) as u8;
}
dst[3] = (oa * 255.0 + 0.5) as u8;
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn fill_path(
pix: &mut Pixmap,
scratch: &mut RasterScratch,
polys: &[Subpath],
rule: FillRule,
rgba: [u8; 4],
alpha: f32,
clip: Option<&Mask>,
blend: BlendMode,
) {
let alpha = if alpha.is_finite() {
alpha.clamp(0.0, 1.0)
} else {
1.0
};
let base_a = rgba[3] as f32 / 255.0 * alpha;
if base_a <= 0.0 {
return;
}
let rgb = [rgba[0], rgba[1], rgba[2]];
let w = pix.width as usize;
prepare_edges(&mut scratch.edges, polys);
sweep_rows(
scratch,
pix.width,
pix.height,
rule,
|y, row, mut lo, mut hi| {
let mask_row = match clip {
None => None,
Some(m) => {
if y < m.y0 || y - m.y0 >= m.bbox_h {
return;
}
let mx0 = m.x0 as usize;
lo = lo.max(mx0);
hi = hi.min(mx0 + m.bbox_w as usize);
if hi <= lo {
return;
}
if m.opaque {
None
} else {
let base = (y - m.y0) as usize * m.bbox_w as usize;
Some(&m.data[base + lo - mx0..base + hi - mx0])
}
}
};
let base = (y as usize * w + lo) * 4;
let dst_row = &mut pix.data[base..base + (hi - lo) * 4];
if blend == BlendMode::Normal {
blend_row::<true>(dst_row, &row[lo..hi], mask_row, base_a, rgb, blend);
} else {
blend_row::<false>(dst_row, &row[lo..hi], mask_row, base_a, rgb, blend);
}
},
);
}
#[inline(always)]
fn blend_row<const NORMAL: bool>(
dst_row: &mut [u8],
covs: &[f32],
mask_row: Option<&[u8]>,
base_a: f32,
rgb: [u8; 3],
blend: BlendMode,
) {
let opaque = [rgb[0], rgb[1], rgb[2], 255];
let solid_src = NORMAL && base_a >= 1.0;
let n = covs.len();
let dst_row = &mut dst_row[..n * 4];
match mask_row {
None => {
let mut x = 0;
while x < n {
let cov = covs[x];
if solid_src && cov >= 1.0 {
let start = x;
x += 1;
while x < n && covs[x] >= 1.0 {
x += 1;
}
fill_run(&mut dst_row[start * 4..x * 4], opaque);
continue;
}
let a = cov.clamp(0.0, 1.0) * base_a;
paint_pixel::<NORMAL>(&mut dst_row[x * 4..(x + 1) * 4], a, rgb, opaque, blend);
x += 1;
}
}
Some(mrow) => {
let mrow = &mrow[..n];
let mut x = 0;
while x < n {
let cov = covs[x];
if solid_src && cov >= 1.0 && mrow[x] == 255 {
let start = x;
x += 1;
while x < n && covs[x] >= 1.0 && mrow[x] == 255 {
x += 1;
}
fill_run(&mut dst_row[start * 4..x * 4], opaque);
continue;
}
let a = (cov.clamp(0.0, 1.0) * base_a) * UNIT[mrow[x] as usize];
paint_pixel::<NORMAL>(&mut dst_row[x * 4..(x + 1) * 4], a, rgb, opaque, blend);
x += 1;
}
}
}
}
fn fill_run(dst: &mut [u8], px: [u8; 4]) {
for chunk in dst.as_chunks_mut::<4>().0 {
*chunk = px;
}
}
#[inline(always)]
fn paint_pixel<const NORMAL: bool>(
dst: &mut [u8],
a: f32,
rgb: [u8; 3],
opaque: [u8; 4],
blend: BlendMode,
) {
if a <= 0.0 {
return;
}
if NORMAL {
if a >= 1.0 {
dst.copy_from_slice(&opaque);
} else {
composite_over(dst, rgb, a);
}
return;
}
let rgb = blend.blend([dst[0], dst[1], dst[2]], rgb);
if a >= 1.0 {
dst.copy_from_slice(&[rgb[0], rgb[1], rgb[2], 255]);
} else {
composite_over(dst, rgb, a);
}
}
#[cfg(test)]
mod tests {
use super::*;
use pdfboss_core::geom::Point;
fn fill_path(
pix: &mut Pixmap,
polys: &[Subpath],
rule: FillRule,
rgba: [u8; 4],
alpha: f32,
clip: Option<&Mask>,
blend: BlendMode,
) {
super::fill_path(
pix,
&mut RasterScratch::default(),
polys,
rule,
rgba,
alpha,
clip,
blend,
);
}
fn mask_from_path(width: u32, height: u32, polys: &[Subpath], rule: FillRule) -> Mask {
Mask::from_path(width, height, &mut RasterScratch::default(), polys, rule)
}
fn rect_poly(x0: f32, y0: f32, x1: f32, y1: f32) -> Subpath {
Subpath {
points: vec![
Point::new(x0, y0),
Point::new(x1, y0),
Point::new(x1, y1),
Point::new(x0, y1),
],
closed: true,
}
}
fn alpha_at(pix: &Pixmap, x: u32, y: u32) -> u8 {
pix.data[((y * pix.width + x) * 4 + 3) as usize]
}
fn rgba_at(pix: &Pixmap, x: u32, y: u32) -> [u8; 4] {
let off = ((y * pix.width + x) * 4) as usize;
pix.data[off..off + 4].try_into().unwrap()
}
const RED: [u8; 4] = [255, 0, 0, 255];
#[test]
fn axis_aligned_rect_exact_interior() {
let mut pix = Pixmap::new(10, 10);
let polys = [rect_poly(2.0, 2.0, 8.0, 8.0)];
fill_path(
&mut pix,
&polys,
FillRule::NonZero,
RED,
1.0,
None,
BlendMode::Normal,
);
for y in 0..10 {
for x in 0..10 {
let inside = (2..8).contains(&x) && (2..8).contains(&y);
if inside {
assert_eq!(rgba_at(&pix, x, y), RED, "pixel ({x},{y})");
} else {
assert_eq!(alpha_at(&pix, x, y), 0, "pixel ({x},{y})");
}
}
}
}
#[test]
fn half_pixel_horizontal_edge_antialiases() {
let mut pix = Pixmap::new(10, 10);
let polys = [rect_poly(2.5, 2.0, 8.0, 8.0)];
fill_path(
&mut pix,
&polys,
FillRule::NonZero,
RED,
1.0,
None,
BlendMode::Normal,
);
let a = alpha_at(&pix, 2, 4);
assert!((127..=129).contains(&a), "edge alpha {a}");
assert_eq!(alpha_at(&pix, 3, 4), 255);
assert_eq!(alpha_at(&pix, 1, 4), 0);
}
#[test]
fn half_pixel_vertical_edge_antialiases() {
let mut pix = Pixmap::new(10, 10);
let polys = [rect_poly(2.0, 2.5, 8.0, 8.0)];
fill_path(
&mut pix,
&polys,
FillRule::NonZero,
RED,
1.0,
None,
BlendMode::Normal,
);
let a = alpha_at(&pix, 4, 2);
assert!((115..=140).contains(&a), "edge alpha {a}");
assert_eq!(alpha_at(&pix, 4, 3), 255);
assert_eq!(alpha_at(&pix, 4, 1), 0);
}
#[test]
fn triangle_half_plane_sanity() {
let mut pix = Pixmap::new(10, 10);
let tri = Subpath {
points: vec![
Point::new(1.0, 1.0),
Point::new(9.0, 1.0),
Point::new(5.0, 9.0),
],
closed: true,
};
fill_path(
&mut pix,
&[tri],
FillRule::NonZero,
RED,
1.0,
None,
BlendMode::Normal,
);
assert_eq!(alpha_at(&pix, 5, 4), 255, "interior");
assert_eq!(alpha_at(&pix, 4, 2), 255, "interior near top");
assert_eq!(alpha_at(&pix, 0, 5), 0, "left of triangle");
assert_eq!(alpha_at(&pix, 9, 8), 0, "right of apex");
assert_eq!(alpha_at(&pix, 5, 0), 0, "above");
}
#[test]
fn even_odd_donut_has_hole() {
let mut pix = Pixmap::new(12, 12);
let polys = [
rect_poly(1.0, 1.0, 11.0, 11.0),
rect_poly(4.0, 4.0, 8.0, 8.0),
];
fill_path(
&mut pix,
&polys,
FillRule::EvenOdd,
RED,
1.0,
None,
BlendMode::Normal,
);
assert_eq!(alpha_at(&pix, 6, 6), 0, "hole must be empty");
assert_eq!(alpha_at(&pix, 2, 6), 255, "ring left");
assert_eq!(alpha_at(&pix, 9, 6), 255, "ring right");
assert_eq!(alpha_at(&pix, 6, 2), 255, "ring top");
assert_eq!(alpha_at(&pix, 0, 6), 0, "outside");
}
#[test]
fn nonzero_same_winding_donut_fills_solid() {
let mut pix = Pixmap::new(12, 12);
let polys = [
rect_poly(1.0, 1.0, 11.0, 11.0),
rect_poly(4.0, 4.0, 8.0, 8.0),
];
fill_path(
&mut pix,
&polys,
FillRule::NonZero,
RED,
1.0,
None,
BlendMode::Normal,
);
assert_eq!(alpha_at(&pix, 6, 6), 255, "center filled under nonzero");
assert_eq!(alpha_at(&pix, 2, 6), 255, "ring");
assert_eq!(alpha_at(&pix, 0, 6), 0, "outside");
}
#[test]
fn nonzero_opposite_winding_donut_has_hole() {
let mut pix = Pixmap::new(12, 12);
let inner = Subpath {
points: vec![
Point::new(4.0, 4.0),
Point::new(4.0, 8.0),
Point::new(8.0, 8.0),
Point::new(8.0, 4.0),
],
closed: true,
};
let polys = [rect_poly(1.0, 1.0, 11.0, 11.0), inner];
fill_path(
&mut pix,
&polys,
FillRule::NonZero,
RED,
1.0,
None,
BlendMode::Normal,
);
assert_eq!(alpha_at(&pix, 6, 6), 0, "reversed inner rect punches hole");
assert_eq!(alpha_at(&pix, 2, 6), 255, "ring");
}
#[test]
fn clip_mask_restricts_fill() {
let mut pix = Pixmap::new(10, 10);
let clip = mask_from_path(10, 10, &[rect_poly(0.0, 0.0, 5.0, 10.0)], FillRule::NonZero);
let polys = [rect_poly(0.0, 0.0, 10.0, 10.0)];
fill_path(
&mut pix,
&polys,
FillRule::NonZero,
RED,
1.0,
Some(&clip),
BlendMode::Normal,
);
for y in 0..10 {
for x in 0..10 {
if x < 5 {
assert_eq!(alpha_at(&pix, x, y), 255, "inside clip ({x},{y})");
} else {
assert_eq!(alpha_at(&pix, x, y), 0, "outside clip untouched ({x},{y})");
}
}
}
}
#[test]
fn mask_intersect_takes_minimum() {
let mut a = mask_from_path(8, 8, &[rect_poly(0.0, 0.0, 6.0, 8.0)], FillRule::NonZero);
let b = mask_from_path(8, 8, &[rect_poly(4.0, 0.0, 8.0, 8.0)], FillRule::NonZero);
a.intersect(&b);
assert_eq!(a.coverage(2, 4), 0, "only in a");
assert_eq!(a.coverage(7, 4), 0, "only in b");
assert_eq!(a.coverage(5, 4), 255, "in both");
}
#[test]
fn mask_from_path_bbox_is_tight_not_full_page() {
let mask = mask_from_path(
1000,
1000,
&[rect_poly(10.0, 20.0, 30.0, 50.0)],
FillRule::NonZero,
);
assert_eq!(mask.x0, 10);
assert_eq!(mask.y0, 20);
assert_eq!(mask.bbox_w, 20);
assert_eq!(mask.bbox_h, 30);
assert_eq!(mask.data.len(), 20 * 30);
assert_eq!(mask.coverage(15, 25), 255, "inside clip");
assert_eq!(mask.coverage(500, 500), 0, "far outside clip bbox");
assert_eq!(mask.coverage(0, 0), 0, "outside clip bbox but inside page");
}
#[test]
fn mask_intersect_disjoint_bboxes_is_empty() {
let mut a = mask_from_path(
100,
100,
&[rect_poly(0.0, 0.0, 10.0, 10.0)],
FillRule::NonZero,
);
let b = mask_from_path(
100,
100,
&[rect_poly(50.0, 50.0, 60.0, 60.0)],
FillRule::NonZero,
);
a.intersect(&b);
assert_eq!(a.bbox_w, 0);
assert_eq!(a.bbox_h, 0);
for y in 0..100 {
for x in 0..100 {
assert_eq!(a.coverage(x, y), 0, "disjoint clips leave nothing visible");
}
}
}
#[test]
fn mask_intersect_shrinks_bbox_to_overlap() {
let mut a = mask_from_path(
100,
100,
&[rect_poly(0.0, 0.0, 20.0, 20.0)],
FillRule::NonZero,
);
let b = mask_from_path(
100,
100,
&[rect_poly(10.0, 10.0, 30.0, 30.0)],
FillRule::NonZero,
);
a.intersect(&b);
assert_eq!(a.x0, 10);
assert_eq!(a.y0, 10);
assert_eq!(a.bbox_w, 10);
assert_eq!(a.bbox_h, 10);
assert_eq!(a.coverage(15, 15), 255, "in overlap");
assert_eq!(a.coverage(5, 5), 0, "only in a");
assert_eq!(a.coverage(25, 25), 0, "only in b");
}
#[test]
fn opaque_clip_shortcut_matches_full_mask_math() {
let polys = [rect_poly(1.5, 1.5, 9.5, 9.5)];
let clip = mask_from_path(12, 12, &[rect_poly(2.0, 0.0, 8.0, 12.0)], FillRule::NonZero);
assert!(clip.opaque, "integer-coordinate rect clip is fully opaque");
let mut dull = clip.clone();
dull.opaque = false;
let mut a = Pixmap::new(12, 12);
let mut b = Pixmap::new(12, 12);
fill_path(
&mut a,
&polys,
FillRule::NonZero,
RED,
0.7,
Some(&clip),
BlendMode::Normal,
);
fill_path(
&mut b,
&polys,
FillRule::NonZero,
RED,
0.7,
Some(&dull),
BlendMode::Normal,
);
assert_eq!(a.data, b.data, "opaque shortcut must not change pixels");
}
#[test]
fn fractional_clip_is_not_marked_opaque() {
let m = mask_from_path(12, 12, &[rect_poly(2.5, 2.0, 8.0, 10.0)], FillRule::NonZero);
assert!(!m.opaque, "partial edge coverage forbids the opaque flag");
}
#[test]
fn mask_new_is_full_page_and_directly_indexable() {
let mask = Mask::new(8, 8);
assert_eq!(mask.bbox_w, 8);
assert_eq!(mask.bbox_h, 8);
assert_eq!(mask.data.len(), 64);
assert!(mask.data.iter().all(|&b| b == 0));
}
#[test]
fn constant_alpha_composites_over_white() {
let mut pix = Pixmap::new(4, 4);
pix.fill([255, 255, 255, 255]);
let polys = [rect_poly(0.0, 0.0, 4.0, 4.0)];
fill_path(
&mut pix,
&polys,
FillRule::NonZero,
RED,
0.5,
None,
BlendMode::Normal,
);
let px = rgba_at(&pix, 2, 2);
assert_eq!(px[0], 255);
assert!((127..=129).contains(&px[1]), "green {}", px[1]);
assert!((127..=129).contains(&px[2]), "blue {}", px[2]);
assert_eq!(px[3], 255);
}
#[test]
fn open_subpath_is_implicitly_closed_for_fill() {
let mut pix = Pixmap::new(10, 10);
let tri = Subpath {
points: vec![
Point::new(1.0, 1.0),
Point::new(9.0, 1.0),
Point::new(5.0, 9.0),
],
closed: false,
};
fill_path(
&mut pix,
&[tri],
FillRule::NonZero,
RED,
1.0,
None,
BlendMode::Normal,
);
assert_eq!(alpha_at(&pix, 5, 4), 255);
}
}