#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![warn(clippy::indexing_slicing)]
mod image;
mod target;
use std::sync::Arc;
use kurbo::{Affine, BezPath, Rect, Shape, Stroke};
use pdfrum_page::BlendMode;
use pdfrum_render::{
AlphaMask, AntiAlias, Brush, FillRule, ImageQuality, MAX_TARGET_DIMENSION, Pixmap,
RasterBackend, RasterImage, RenderDevice, pixmap,
};
use pdfrum_render::scanline::{self, Rasterizer};
use target::{Source, Target};
const FLATTEN_TOLERANCE: f64 = 0.1;
fn intersect_rows(mask: &mut AlphaMask, other: &AlphaMask, rows: core::ops::Range<u32>) {
if other.width() != mask.width() || other.height() != mask.height() {
return;
}
let width = mask.width() as usize;
let Some(start) = (rows.start as usize).checked_mul(width) else {
return;
};
let Some(end) = (rows.end as usize).checked_mul(width) else {
return;
};
let Some(src) = other.data().get(start..end) else {
return;
};
let Some(dest) = mask.data_mut().get_mut(start..end) else {
return;
};
for (a, &b) in dest.iter_mut().zip(src) {
*a = pixmap::mul255(*a, b);
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct AggBackend;
impl AggBackend {
#[must_use]
pub fn new() -> Self {
Self
}
}
#[derive(Debug)]
struct Layer {
target: Target,
blend: BlendMode,
alpha: f32,
mask: Option<AlphaMask>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Frame {
Clip,
Layer,
}
#[derive(Debug)]
pub struct AggDevice {
base: Target,
layers: Vec<Layer>,
clips: Vec<Option<Arc<AlphaMask>>>,
bands: Vec<core::ops::Range<u32>>,
planes: Vec<AlphaMask>,
frames: Vec<Frame>,
raster: Rasterizer,
}
impl AggDevice {
fn new(target: Target) -> Self {
Self {
base: target,
layers: Vec::new(),
clips: vec![None],
bands: core::iter::once(0..0).collect(),
planes: Vec::new(),
frames: Vec::new(),
raster: Rasterizer::new(),
}
}
fn target(&mut self) -> &mut Target {
match self.layers.last_mut() {
Some(layer) => &mut layer.target,
None => &mut self.base,
}
}
fn size(&self) -> (u32, u32) {
(self.base.width(), self.base.height())
}
fn scan(
&mut self,
path: &BezPath,
rule: FillRule,
aa: AntiAlias,
mut paint: impl FnMut(&mut Target, i32, i32, i32, u8),
) {
self.raster.reset();
self.raster.keep_rows(0..target_rows(self.base.height()));
self.raster.add_path(path, FLATTEN_TOLERANCE);
let rule = to_scanline_rule(rule);
let coverage = to_coverage(aa);
let (raster, layers, base) = (&mut self.raster, &mut self.layers, &mut self.base);
let target = match layers.last_mut() {
Some(layer) => &mut layer.target,
None => base,
};
raster.sweep(rule, coverage, |x, len, y, alpha| {
paint(target, x, len, y, alpha);
});
}
fn draw_image_sampled(
&mut self,
img: &RasterImage,
t: Affine,
quality: ImageQuality,
alpha: u8,
) {
let Some(sampler) = image::Sampler::new(img, t, quality) else {
return;
};
let footprint = t * rect_path(Rect::new(
0.0,
0.0,
f64::from(img.width()),
f64::from(img.height()),
));
self.scan(
&footprint,
FillRule::Winding,
AntiAlias::On,
move |target, x, len, y, cov| {
target.blend_span_with(x, len, y, cov, BlendMode::Normal, |col, row| {
sampler
.sample(col, row)
.map(|px| image::scale_alpha(px, alpha))
});
},
);
}
fn blit(&mut self, img: &RasterImage, dx: i32, dy: i32, alpha: u8) {
let target = match self.layers.last_mut() {
Some(layer) => &mut layer.target,
None => &mut self.base,
};
target.blit_image(img, dx, dy, alpha);
}
fn blank_plane(&mut self, w: u32, h: u32) -> AlphaMask {
match self.planes.pop() {
Some(plane) if plane.width() == w && plane.height() == h => plane,
_ => AlphaMask::new(w, h),
}
}
fn recycle(&mut self, plane: Option<Arc<AlphaMask>>, band: core::ops::Range<u32>) {
let Some(plane) = plane else {
return;
};
let Ok(mut plane) = Arc::try_unwrap(plane) else {
return;
};
let width = plane.width() as usize;
let (Some(start), Some(end)) = (
(band.start as usize).checked_mul(width),
(band.end as usize).checked_mul(width),
) else {
return;
};
let Some(rows) = plane.data_mut().get_mut(start..end) else {
return;
};
rows.fill(0);
self.planes.push(plane);
}
fn coverage_of(
&mut self,
path: &BezPath,
rule: FillRule,
aa: AntiAlias,
) -> (AlphaMask, core::ops::Range<u32>) {
let (w, h) = self.size();
let mut mask = self.blank_plane(w, h);
self.raster.reset();
self.raster.keep_rows(0..target_rows(h));
self.raster.add_path(path, FLATTEN_TOLERANCE);
let width = w as usize;
let mut first = h;
let mut last = 0_u32;
self.raster.sweep(
to_scanline_rule(rule),
to_coverage(aa),
|x, len, y, alpha| {
let (Ok(row), Ok(w_i32)) = (u32::try_from(y), i32::try_from(w)) else {
return;
};
if row >= h {
return;
}
let x0 = x.max(0);
let x1 = x.saturating_add(len).min(w_i32);
if x1 > x0 {
first = first.min(row);
last = last.max(row.saturating_add(1));
}
let (Ok(x0u), Ok(x1u)) = (usize::try_from(x0), usize::try_from(x1)) else {
return;
};
let Some(start) = (row as usize).checked_mul(width) else {
return;
};
let (Some(lo), Some(hi)) = (start.checked_add(x0u), start.checked_add(x1u)) else {
return;
};
if let Some(span) = mask.data_mut().get_mut(lo..hi) {
span.fill(alpha);
}
},
);
(mask, first..last.max(first))
}
fn push_clip_mask(&mut self, mut mask: AlphaMask, band: core::ops::Range<u32>) {
if let Some(current) = self.clips.last().and_then(Option::as_ref) {
intersect_rows(&mut mask, current, band.clone());
}
self.clips.push(Some(Arc::new(mask)));
self.bands.push(band);
self.frames.push(Frame::Clip);
self.sync_clip();
}
fn sync_clip(&mut self) {
let clip = self.clips.last().and_then(Option::as_ref).map(Arc::clone);
self.target().set_clip(clip);
}
fn solid(brush: &Brush<'_>) -> Option<Source> {
match brush {
Brush::Solid(color) => {
let [r, g, b, a] = color.to_rgba8().to_u8_array();
Some(Source::Straight([r, g, b], a))
}
Brush::Image(_) => None,
}
}
}
fn whole_pixel_offset(transform: Affine) -> Option<(i32, i32)> {
let [xx, yx, xy, yy, tx, ty] = transform.as_coeffs();
#[expect(
clippy::float_cmp,
reason = "the fast path must be taken only where the two paths agree \
exactly; a tolerance here would silently skip a resample"
)]
let unrotated_unscaled = xx == 1.0 && yx == 0.0 && xy == 0.0 && yy == 1.0;
if !unrotated_unscaled {
return None;
}
Some((whole(tx)?, whole(ty)?))
}
fn whole(value: f64) -> Option<i32> {
(value.fract() == 0.0 && value.abs() < f64::from(i32::MAX)).then(|| {
#[expect(
clippy::cast_possible_truncation,
reason = "guarded above: integral and within i32"
)]
let n = value as i32;
n
})
}
fn to_scanline_rule(rule: FillRule) -> scanline::FillRule {
match rule {
FillRule::Winding => scanline::FillRule::NonZero,
FillRule::EvenOdd => scanline::FillRule::EvenOdd,
}
}
fn target_rows(height: u32) -> i32 {
i32::try_from(height).unwrap_or(i32::MAX)
}
fn to_coverage(aa: AntiAlias) -> scanline::Coverage {
match aa {
AntiAlias::On => scanline::Coverage::Exact,
AntiAlias::Off => scanline::Coverage::Thresholded,
AntiAlias::FullCover => scanline::Coverage::Full,
}
}
fn rect_path(rect: Rect) -> BezPath {
rect.to_path(FLATTEN_TOLERANCE)
}
impl RenderDevice for AggDevice {
fn fill_path(
&mut self,
path: &BezPath,
t: Affine,
brush: &Brush<'_>,
rule: FillRule,
aa: AntiAlias,
) {
let Some(src) = Self::solid(brush) else {
return;
};
let device_path = t * path.clone();
self.scan(&device_path, rule, aa, move |target, x, len, y, alpha| {
target.blend_span(x, len, y, alpha, src, BlendMode::Normal);
});
}
fn stroke_path(
&mut self,
path: &BezPath,
t: Affine,
brush: &Brush<'_>,
stroke: &Stroke,
aa: AntiAlias,
) {
let Some(src) = Self::solid(brush) else {
return;
};
let outline = kurbo::stroke(
path.path_elements(FLATTEN_TOLERANCE),
stroke,
&kurbo::StrokeOpts::default(),
FLATTEN_TOLERANCE,
);
let device_path = t * outline;
self.scan(
&device_path,
FillRule::Winding,
aa,
move |target, x, len, y, alpha| {
target.blend_span(x, len, y, alpha, src, BlendMode::Normal);
},
);
}
fn draw_image(&mut self, img: &RasterImage, t: Affine, quality: ImageQuality, alpha: f32) {
let constant = pixmap::alpha_byte_truncating(alpha);
if constant == 0 {
return;
}
if let Some((dx, dy)) = whole_pixel_offset(t) {
self.blit(img, dx, dy, constant);
return;
}
self.draw_image_sampled(img, t, quality, constant);
}
fn draw_glyph_lcd(
&mut self,
glyph: &pdfrum_render::glyph::SubpixelBitmap,
origin: (f64, f64),
colour: peniko::Color,
) {
let [red, green, blue, alpha] = colour.to_rgba8().to_u8_array();
if alpha == 0 || glyph.is_empty() {
return;
}
let (Some(dx), Some(dy)) = (whole(origin.0), whole(origin.1)) else {
return;
};
let target = match self.layers.last_mut() {
Some(layer) => &mut layer.target,
None => &mut self.base,
};
for row in 0..glyph.height {
let Some(y) = row.checked_add(dy) else {
continue;
};
for col in 0..glyph.width {
let Some(x) = col.checked_add(dx) else {
continue;
};
let coverage = glyph.at(col, row);
if coverage == [0; 3] {
continue;
}
target.merge_lcd_pixel(x, y, [red, green, blue], alpha, coverage);
}
}
}
fn push_clip(&mut self, path: &BezPath, rule: FillRule) {
let (mask, band) = self.coverage_of(path, rule, AntiAlias::On);
self.push_clip_mask(mask, band);
}
fn push_clip_rect(&mut self, rect: Rect) {
let (mask, band) = self.coverage_of(&rect_path(rect), FillRule::Winding, AntiAlias::Off);
self.push_clip_mask(mask, band);
}
fn push_layer(&mut self, blend: BlendMode, alpha: f32, mask: Option<&AlphaMask>) {
let (w, h) = self.size();
let mask = mask.and_then(|m| {
debug_assert_eq!(
(m.width(), m.height()),
(w, h),
"an AlphaMask must be device-sized and device-aligned"
);
(m.width() == w && m.height() == h).then(|| m.clone())
});
let mut target = Target::new(w, h, peniko::Color::TRANSPARENT);
target.set_clip(self.clips.last().and_then(Option::as_ref).map(Arc::clone));
self.layers.push(Layer {
target,
blend,
alpha,
mask,
});
self.frames.push(Frame::Layer);
}
fn pop(&mut self) {
match self.frames.pop() {
Some(Frame::Clip) => {
let popped = if self.clips.len() > 1 {
Some((self.clips.pop(), self.bands.pop()))
} else {
None
};
self.sync_clip();
if let Some((Some(plane), Some(band))) = popped {
self.recycle(plane, band);
}
}
Some(Frame::Layer) => {
let Some(layer) = self.layers.pop() else {
return;
};
let mut pixels = layer.target.into_pixmap();
pixels.multiply_alpha(layer.alpha);
if let Some(mask) = &layer.mask {
pixels.multiply_alpha_mask(mask);
}
self.target().composite_layer(&pixels, layer.blend);
}
None => {}
}
}
}
impl RasterBackend for AggBackend {
type Device = AggDevice;
fn new_target(&self, w: u32, h: u32, clear: peniko::Color) -> Self::Device {
let (w, h) = (w.min(MAX_TARGET_DIMENSION), h.min(MAX_TARGET_DIMENSION));
AggDevice::new(Target::new(w, h, clear))
}
fn new_target_with_backdrop(&self, base: &Pixmap) -> Self::Device {
AggDevice::new(Target::from_pixmap(base.clone()))
}
fn snapshot(&self, d: &Self::Device) -> Pixmap {
debug_assert!(d.layers.is_empty(), "snapshot requires every layer popped");
d.base.pixels().clone()
}
fn finish(&self, mut d: Self::Device) -> Pixmap {
while !d.frames.is_empty() {
d.pop();
}
d.base.into_pixmap()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn square(x0: f64, y0: f64, x1: f64, y1: f64) -> BezPath {
let mut p = BezPath::new();
p.move_to((x0, y0));
p.line_to((x1, y0));
p.line_to((x1, y1));
p.line_to((x0, y1));
p.close_path();
p
}
const RED: peniko::Color = peniko::Color::from_rgba8(255, 0, 0, 255);
#[test]
fn a_cleared_target_keeps_its_colour() {
let backend = AggBackend::new();
let device = backend.new_target(4, 4, peniko::Color::WHITE);
let out = backend.finish(device);
assert_eq!(out.pixel(0, 0), Some([255, 255, 255, 255]));
}
#[test]
fn a_transparent_target_starts_empty() {
let backend = AggBackend::new();
let device = backend.new_target(4, 4, peniko::Color::TRANSPARENT);
let out = backend.finish(device);
assert_eq!(out.pixel(0, 0), Some([0, 0, 0, 0]));
}
#[test]
fn a_half_covered_edge_is_exactly_half() {
let backend = AggBackend::new();
let mut device = backend.new_target(4, 1, peniko::Color::TRANSPARENT);
device.fill_path(
&square(0.0, 0.0, 0.5, 1.0),
Affine::IDENTITY,
&Brush::Solid(peniko::Color::BLACK),
FillRule::Winding,
AntiAlias::On,
);
let out = backend.finish(device);
assert_eq!(out.pixel(0, 0).map(|px| px[3]), Some(128));
}
#[test]
fn a_stroke_covers_both_sides_of_its_centre_line_equally() {
let backend = AggBackend::new();
let mut device = backend.new_target(8, 4, peniko::Color::TRANSPARENT);
let mut line = BezPath::new();
line.move_to((4.0, 0.0));
line.line_to((4.0, 4.0));
device.stroke_path(
&line,
Affine::IDENTITY,
&Brush::Solid(peniko::Color::BLACK),
&Stroke::new(1.0),
AntiAlias::On,
);
let out = backend.finish(device);
let left = out.pixel(3, 2).map_or(0, |px| px[3]);
let right = out.pixel(4, 2).map_or(0, |px| px[3]);
assert_eq!(left, right, "the two half-covered columns must agree");
assert_eq!(left, 128, "AGG's coverage for a half-covered pixel");
}
#[test]
fn a_mitred_corner_paints_its_outer_tip() {
let backend = AggBackend::new();
let mut device = backend.new_target(16, 16, peniko::Color::TRANSPARENT);
let mut corner = BezPath::new();
corner.move_to((4.0, 12.0));
corner.line_to((4.0, 4.0));
corner.line_to((12.0, 4.0));
device.stroke_path(
&corner,
Affine::IDENTITY,
&Brush::Solid(peniko::Color::BLACK),
&Stroke::new(1.0).with_join(kurbo::Join::Miter),
AntiAlias::On,
);
let out = backend.finish(device);
let tip = out.pixel(3, 3).map_or(0, |px| px[3]);
assert_eq!(tip, 64, "the miter's outer tip is a quarter-covered pixel");
}
#[test]
fn a_clip_planes_spans_are_filled_over_exactly_their_own_rows() {
let backend = AggBackend::new();
for (x0, y0, x1, y1) in [
(0.0, 0.0, 7.0, 5.0),
(-4.0, -3.0, 3.0, 2.0),
(3.0, 2.0, 40.0, 30.0),
(-9.0, -9.0, 40.0, 30.0),
(2.0, 1.0, 2.0, 1.0),
] {
let mut device = backend.new_target(7, 5, peniko::Color::TRANSPARENT);
device.push_clip_rect(Rect::new(x0, y0, x1, y1));
device.fill_path(
&square(-20.0, -20.0, 40.0, 40.0),
Affine::IDENTITY,
&Brush::Solid(RED),
FillRule::Winding,
AntiAlias::Off,
);
device.pop();
let out = backend.finish(device);
for row in 0..5 {
for col in 0..7 {
let inside = f64::from(col) >= x0.max(0.0)
&& f64::from(col) + 1.0 <= x1.min(7.0)
&& f64::from(row) >= y0.max(0.0)
&& f64::from(row) + 1.0 <= y1.min(5.0);
let a = out.pixel(col, row).map_or(0, |px| px[3]);
assert_eq!(
a,
u8::from(inside) * 255,
"({x0},{y0},{x1},{y1}) at ({col},{row})"
);
}
}
}
}
#[test]
fn a_hard_edged_rect_clip_has_no_soft_pixels() {
let backend = AggBackend::new();
let mut device = backend.new_target(8, 1, peniko::Color::TRANSPARENT);
device.push_clip_rect(Rect::new(0.5, 0.0, 4.5, 1.0));
device.fill_path(
&square(0.0, 0.0, 8.0, 1.0),
Affine::IDENTITY,
&Brush::Solid(RED),
FillRule::Winding,
AntiAlias::Off,
);
device.pop();
let out = backend.finish(device);
for col in 0..8 {
let a = out.pixel(col, 0).map_or(0, |px| px[3]);
assert!(a == 0 || a == 255, "column {col} has soft alpha {a}");
}
}
#[test]
fn clips_nest_and_unwind() {
let backend = AggBackend::new();
let mut device = backend.new_target(8, 8, peniko::Color::TRANSPARENT);
device.push_clip_rect(Rect::new(0.0, 0.0, 4.0, 8.0));
device.push_clip_rect(Rect::new(2.0, 0.0, 8.0, 8.0));
device.fill_path(
&square(0.0, 0.0, 8.0, 8.0),
Affine::IDENTITY,
&Brush::Solid(RED),
FillRule::Winding,
AntiAlias::Off,
);
device.pop();
device.pop();
let out = backend.finish(device);
assert_eq!(out.pixel(0, 0).map(|px| px[3]), Some(0));
assert_eq!(out.pixel(3, 0).map(|px| px[3]), Some(255));
assert_eq!(out.pixel(5, 0).map(|px| px[3]), Some(0));
}
#[test]
fn a_clip_outside_the_previous_ones_rows_paints_nothing() {
let backend = AggBackend::new();
let mut device = backend.new_target(8, 8, peniko::Color::TRANSPARENT);
device.push_clip_rect(Rect::new(0.0, 0.0, 8.0, 2.0));
device.push_clip_rect(Rect::new(0.0, 4.0, 8.0, 8.0));
device.fill_path(
&square(0.0, 0.0, 8.0, 8.0),
Affine::IDENTITY,
&Brush::Solid(RED),
FillRule::Winding,
AntiAlias::Off,
);
device.pop();
device.pop();
let out = backend.finish(device);
for y in 0..8 {
for x in 0..8 {
assert_eq!(
out.pixel(x, y).map(|px| px[3]),
Some(0),
"({x}, {y}) painted through two disjoint clips"
);
}
}
}
#[test]
fn a_banded_intersection_still_carries_the_outer_clips_columns() {
let backend = AggBackend::new();
let mut device = backend.new_target(8, 8, peniko::Color::TRANSPARENT);
device.push_clip_rect(Rect::new(0.0, 0.0, 4.0, 4.0));
device.push_clip_rect(Rect::new(2.0, 2.0, 8.0, 8.0));
device.fill_path(
&square(0.0, 0.0, 8.0, 8.0),
Affine::IDENTITY,
&Brush::Solid(RED),
FillRule::Winding,
AntiAlias::Off,
);
device.pop();
device.pop();
let out = backend.finish(device);
for y in 0..8 {
for x in 0..8 {
let inside = (2..4).contains(&x) && (2..4).contains(&y);
assert_eq!(
out.pixel(x, y).map(|px| px[3]),
Some(if inside { 255 } else { 0 }),
"({x}, {y}) disagrees with the two clips' intersection"
);
}
}
}
#[test]
fn each_pop_hands_the_target_the_plane_one_level_out() {
let backend = AggBackend::new();
let widths = [8.0_f64, 6.0, 4.0, 2.0];
for (depth, &edge) in widths.iter().enumerate() {
let mut device = backend.new_target(8, 1, peniko::Color::TRANSPARENT);
for w in &widths {
device.push_clip_rect(Rect::new(0.0, 0.0, *w, 1.0));
}
for _ in 0..(widths.len() - 1 - depth) {
device.pop();
}
device.fill_path(
&square(0.0, 0.0, 8.0, 1.0),
Affine::IDENTITY,
&Brush::Solid(RED),
FillRule::Winding,
AntiAlias::Off,
);
let out = backend.finish(device);
for x in 0..8 {
let inside = f64::from(x) < edge;
assert_eq!(
out.pixel(x, 0).map(|px| px[3]),
Some(if inside { 255 } else { 0 }),
"column {x} at depth {depth} (clip edge {edge})"
);
}
}
}
#[test]
fn a_recycled_plane_carries_none_of_the_clip_it_held() {
let backend = AggBackend::new();
let mut device = backend.new_target(8, 8, peniko::Color::TRANSPARENT);
device.push_clip_rect(Rect::new(0.0, 0.0, 8.0, 4.0));
device.pop();
device.push_clip_rect(Rect::new(0.0, 4.0, 8.0, 8.0));
device.fill_path(
&square(0.0, 0.0, 8.0, 8.0),
Affine::IDENTITY,
&Brush::Solid(RED),
FillRule::Winding,
AntiAlias::Off,
);
device.pop();
let out = backend.finish(device);
for y in 0..8 {
for x in 0..8 {
assert_eq!(
out.pixel(x, y).map(|px| px[3]),
Some(if y >= 4 { 255 } else { 0 }),
"({x}, {y}) disagrees with the second clip alone"
);
}
}
}
#[test]
fn a_recycled_nested_plane_is_cleared_over_its_whole_band() {
let backend = AggBackend::new();
let mut device = backend.new_target(8, 8, peniko::Color::TRANSPARENT);
device.push_clip_rect(Rect::new(0.0, 0.0, 8.0, 4.0));
device.push_clip_rect(Rect::new(0.0, 2.0, 8.0, 6.0));
device.pop();
device.push_clip_rect(Rect::new(0.0, 0.0, 8.0, 2.0));
device.fill_path(
&square(0.0, 0.0, 8.0, 8.0),
Affine::IDENTITY,
&Brush::Solid(RED),
FillRule::Winding,
AntiAlias::Off,
);
device.pop();
device.pop();
let out = backend.finish(device);
for y in 0..8 {
for x in 0..8 {
assert_eq!(
out.pixel(x, y).map(|px| px[3]),
Some(if y < 2 { 255 } else { 0 }),
"({x}, {y}) disagrees with rows 0..4 and rows 0..2"
);
}
}
}
#[test]
fn a_clip_under_a_layer_unwinds_with_the_layer_between_them() {
let backend = AggBackend::new();
let mut device = backend.new_target(8, 8, peniko::Color::TRANSPARENT);
device.push_clip_rect(Rect::new(0.0, 0.0, 8.0, 4.0));
device.push_layer(BlendMode::Normal, 1.0, None);
device.push_clip_rect(Rect::new(0.0, 2.0, 8.0, 8.0));
device.fill_path(
&square(0.0, 0.0, 8.0, 8.0),
Affine::IDENTITY,
&Brush::Solid(RED),
FillRule::Winding,
AntiAlias::Off,
);
device.pop();
device.pop();
device.fill_path(
&square(0.0, 0.0, 8.0, 1.0),
Affine::IDENTITY,
&Brush::Solid(RED),
FillRule::Winding,
AntiAlias::Off,
);
device.pop();
let out = backend.finish(device);
for y in 0..8 {
for x in 0..8 {
let inside = (2..4).contains(&y) || y < 1;
assert_eq!(
out.pixel(x, y).map(|px| px[3]),
Some(if inside { 255 } else { 0 }),
"({x}, {y}) disagrees with the clips the two fills ran under"
);
}
}
}
#[test]
fn popping_a_clip_restores_the_previous_one() {
let backend = AggBackend::new();
let mut device = backend.new_target(8, 1, peniko::Color::TRANSPARENT);
device.push_clip_rect(Rect::new(0.0, 0.0, 4.0, 1.0));
device.push_clip_rect(Rect::new(0.0, 0.0, 2.0, 1.0));
device.pop();
device.fill_path(
&square(0.0, 0.0, 8.0, 1.0),
Affine::IDENTITY,
&Brush::Solid(RED),
FillRule::Winding,
AntiAlias::Off,
);
device.pop();
let out = backend.finish(device);
assert_eq!(out.pixel(3, 0).map(|px| px[3]), Some(255));
assert_eq!(out.pixel(5, 0).map(|px| px[3]), Some(0));
}
#[test]
fn an_antialiased_path_clip_keeps_partial_coverage() {
let backend = AggBackend::new();
let mut device = backend.new_target(4, 1, peniko::Color::TRANSPARENT);
device.push_clip(&square(0.0, 0.0, 1.5, 1.0), FillRule::Winding);
device.fill_path(
&square(0.0, 0.0, 4.0, 1.0),
Affine::IDENTITY,
&Brush::Solid(peniko::Color::BLACK),
FillRule::Winding,
AntiAlias::On,
);
device.pop();
let out = backend.finish(device);
assert_eq!(out.pixel(0, 0).map(|px| px[3]), Some(255));
assert_eq!(out.pixel(1, 0).map(|px| px[3]), Some(128), "half clipped");
assert_eq!(out.pixel(2, 0).map(|px| px[3]), Some(0));
}
#[test]
fn a_layer_composites_with_its_blend_and_alpha() {
let backend = AggBackend::new();
let mut device = backend.new_target(2, 2, peniko::Color::from_rgba8(0, 255, 0, 255));
device.push_layer(BlendMode::Normal, 0.5, None);
device.fill_path(
&square(0.0, 0.0, 2.0, 2.0),
Affine::IDENTITY,
&Brush::Solid(RED),
FillRule::Winding,
AntiAlias::Off,
);
device.pop();
let out = backend.finish(device);
let px = out.pixel(0, 0).expect("in bounds");
assert!(px[0] > 100 && px[0] < 160, "red {}", px[0]);
assert!(px[1] > 100 && px[1] < 160, "green {}", px[1]);
}
#[test]
fn a_layer_mask_must_be_device_sized() {
let backend = AggBackend::new();
let mut device = backend.new_target(4, 4, peniko::Color::TRANSPARENT);
let mask = AlphaMask::filled(4, 4, 128);
device.push_layer(BlendMode::Normal, 1.0, Some(&mask));
device.fill_path(
&square(0.0, 0.0, 4.0, 4.0),
Affine::IDENTITY,
&Brush::Solid(peniko::Color::WHITE),
FillRule::Winding,
AntiAlias::Off,
);
device.pop();
let out = backend.finish(device);
let px = out.pixel(0, 0).expect("in bounds");
assert!(
px[3] > 100 && px[3] < 160,
"the mask halved the alpha: {}",
px[3]
);
}
#[test]
fn a_layer_inherits_the_clip_and_does_not_apply_it_twice() {
let backend = AggBackend::new();
let mut device = backend.new_target(2, 1, peniko::Color::TRANSPARENT);
let mut half = AlphaMask::new(2, 1);
half.data_mut().fill(128);
device.push_clip(&square(0.0, 0.0, 2.0, 1.0), FillRule::Winding);
device.push_layer(BlendMode::Normal, 1.0, None);
device.fill_path(
&square(0.0, 0.0, 2.0, 1.0),
Affine::IDENTITY,
&Brush::Solid(peniko::Color::BLACK),
FillRule::Winding,
AntiAlias::On,
);
device.pop();
device.pop();
let out = backend.finish(device);
assert_eq!(
out.pixel(0, 0).map(|px| px[3]),
Some(255),
"a full clip must leave the layer fully opaque"
);
}
#[test]
fn snapshot_then_backdrop_round_trips() {
let backend = AggBackend::new();
let device = backend.new_target(3, 3, peniko::Color::from_rgba8(1, 2, 3, 255));
let snap = backend.snapshot(&device);
let seeded = backend.new_target_with_backdrop(&snap);
let out = backend.finish(seeded);
assert_eq!(out.pixel(1, 1), Some([1, 2, 3, 255]));
}
#[test]
fn an_image_draws_at_its_pixel_grid() {
let backend = AggBackend::new();
let mut device = backend.new_target(4, 4, peniko::Color::TRANSPARENT);
let img = Pixmap::filled(2, 2, RED);
device.draw_image(&img, Affine::IDENTITY, ImageQuality::Nearest, 1.0);
let out = backend.finish(device);
assert_eq!(out.pixel(0, 0), Some([255, 0, 0, 255]), "inside the image");
assert_eq!(out.pixel(1, 1), Some([255, 0, 0, 255]), "inside the image");
assert_eq!(
out.pixel(2, 2).map(|px| px[3]),
Some(0),
"past its 2x2 grid"
);
}
#[test]
fn an_image_translates_by_whole_pixels() {
let backend = AggBackend::new();
let mut device = backend.new_target(4, 4, peniko::Color::TRANSPARENT);
let img = Pixmap::filled(2, 2, RED);
device.draw_image(
&img,
Affine::translate((2.0, 2.0)),
ImageQuality::Nearest,
1.0,
);
let out = backend.finish(device);
assert_eq!(out.pixel(0, 0).map(|px| px[3]), Some(0));
assert_eq!(out.pixel(2, 2), Some([255, 0, 0, 255]));
assert_eq!(out.pixel(3, 3), Some([255, 0, 0, 255]));
}
#[test]
fn an_image_draw_with_alpha_does_not_panic() {
let backend = AggBackend::new();
let mut device = backend.new_target(4, 4, peniko::Color::TRANSPARENT);
let img = Pixmap::filled(4, 4, RED);
device.draw_image(&img, Affine::IDENTITY, ImageQuality::Nearest, 0.5);
let out = backend.finish(device);
let px = out.pixel(0, 0).expect("in bounds");
assert!(px[3] > 100 && px[3] < 160, "half-opacity image: {}", px[3]);
}
#[test]
fn an_image_is_clipped_like_any_other_primitive() {
let backend = AggBackend::new();
let mut device = backend.new_target(4, 1, peniko::Color::TRANSPARENT);
let img = Pixmap::filled(4, 1, RED);
device.push_clip_rect(Rect::new(0.0, 0.0, 2.0, 1.0));
device.draw_image(&img, Affine::IDENTITY, ImageQuality::Nearest, 1.0);
device.pop();
let out = backend.finish(device);
assert_eq!(out.pixel(1, 0).map(|px| px[3]), Some(255));
assert_eq!(out.pixel(3, 0).map(|px| px[3]), Some(0));
}
#[test]
fn finish_flattens_an_unpopped_layer() {
let backend = AggBackend::new();
let mut device = backend.new_target(2, 2, peniko::Color::TRANSPARENT);
device.push_layer(BlendMode::Normal, 1.0, None);
device.fill_path(
&square(0.0, 0.0, 2.0, 2.0),
Affine::IDENTITY,
&Brush::Solid(RED),
FillRule::Winding,
AntiAlias::Off,
);
let out = backend.finish(device);
assert_eq!(out.pixel(0, 0).map(|px| px[0]), Some(255));
}
#[test]
fn an_image_brush_on_a_fill_paints_nothing_rather_than_panicking() {
let backend = AggBackend::new();
let img = Pixmap::filled(2, 2, RED);
let mut device = backend.new_target(2, 2, peniko::Color::TRANSPARENT);
device.fill_path(
&square(0.0, 0.0, 2.0, 2.0),
Affine::IDENTITY,
&Brush::Image(&img),
FillRule::Winding,
AntiAlias::Off,
);
let out = backend.finish(device);
assert!(out.data().iter().all(|&b| b == 0));
}
#[test]
fn a_zero_sized_target_survives_every_operation() {
let backend = AggBackend::new();
let mut device = backend.new_target(0, 0, peniko::Color::WHITE);
device.push_clip_rect(Rect::new(0.0, 0.0, 1.0, 1.0));
device.fill_path(
&square(0.0, 0.0, 1.0, 1.0),
Affine::IDENTITY,
&Brush::Solid(RED),
FillRule::Winding,
AntiAlias::On,
);
device.pop();
let out = backend.finish(device);
assert_eq!((out.width(), out.height()), (0, 0));
}
#[test]
fn a_transform_applies_to_a_filled_path() {
let backend = AggBackend::new();
let mut device = backend.new_target(8, 8, peniko::Color::TRANSPARENT);
device.fill_path(
&square(0.0, 0.0, 2.0, 2.0),
Affine::translate((4.0, 4.0)),
&Brush::Solid(RED),
FillRule::Winding,
AntiAlias::Off,
);
let out = backend.finish(device);
assert_eq!(
out.pixel(0, 0).map(|px| px[3]),
Some(0),
"untranslated spot"
);
assert_eq!(
out.pixel(5, 5).map(|px| px[3]),
Some(255),
"translated spot"
);
}
#[test]
fn a_degenerate_thin_fill_is_not_dropped() {
let backend = AggBackend::new();
let mut device = backend.new_target(4, 1, peniko::Color::TRANSPARENT);
device.fill_path(
&square(0.0, 0.0, 4.0, 0.02),
Affine::IDENTITY,
&Brush::Solid(peniko::Color::BLACK),
FillRule::Winding,
AntiAlias::On,
);
let out = backend.finish(device);
let alpha = out.pixel(0, 0).map_or(0, |px| px[3]);
assert_eq!(alpha, 5, "0.02 coverage is floor(0.02 * 256) == 5");
}
fn checkerboard(w: u32, h: u32) -> Pixmap {
let mut p = Pixmap::new(w, h);
for y in 0..h {
for x in 0..w {
let on = (x + y) % 2 == 0;
let a = if on { 200 } else { 90 };
p.set_pixel(x, y, [a / 2, a / 3, a / 4, a]);
}
}
p
}
#[test]
fn a_whole_pixel_transform_is_recognised_and_nothing_else_is() {
assert_eq!(whole_pixel_offset(Affine::IDENTITY), Some((0, 0)));
assert_eq!(
whole_pixel_offset(Affine::translate((3.0, -7.0))),
Some((3, -7))
);
assert_eq!(whole_pixel_offset(Affine::translate((3.5, 0.0))), None);
assert_eq!(whole_pixel_offset(Affine::scale(1.000_001)), None);
assert_eq!(whole_pixel_offset(Affine::rotate(0.001)), None);
assert_eq!(whole_pixel_offset(Affine::translate((f64::NAN, 0.0))), None);
}
#[test]
fn an_integer_blit_agrees_with_the_general_path() {
let image = checkerboard(5, 4);
let backend = AggBackend::new();
let mut fast = backend.new_target(12, 10, peniko::Color::WHITE);
fast.draw_image(
&image,
Affine::translate((3.0, 2.0)),
ImageQuality::Nearest,
1.0,
);
let fast = backend.finish(fast);
let mut slow_device = backend.new_target(12, 10, peniko::Color::WHITE);
slow_device.draw_image_sampled(
&image,
Affine::translate((3.0, 2.0)),
ImageQuality::Nearest,
255,
);
let slow = backend.finish(slow_device);
assert_eq!(fast.data(), slow.data(), "the blit is the same answer");
}
#[test]
fn a_blit_respects_the_clip_and_the_edges_of_the_target() {
let image = checkerboard(6, 6);
let backend = AggBackend::new();
let mut device = backend.new_target(8, 8, peniko::Color::WHITE);
device.push_clip_rect(Rect::new(2.0, 2.0, 5.0, 5.0));
device.draw_image(
&image,
Affine::translate((-1.0, -1.0)),
ImageQuality::Nearest,
1.0,
);
device.pop();
let out = backend.finish(device);
assert_eq!(
out.pixel(1, 1),
Some([255, 255, 255, 255]),
"outside the clip"
);
assert_eq!(
out.pixel(6, 6),
Some([255, 255, 255, 255]),
"outside the clip"
);
assert_ne!(out.pixel(3, 3), Some([255, 255, 255, 255]), "inside it");
}
#[test]
fn a_partial_alpha_blit_scales_the_source_as_the_general_path_does() {
let image = checkerboard(4, 4);
let backend = AggBackend::new();
let mut fast = backend.new_target(6, 6, peniko::Color::WHITE);
fast.draw_image(
&image,
Affine::translate((1.0, 1.0)),
ImageQuality::Nearest,
0.5,
);
let fast = backend.finish(fast);
let mut slow = backend.new_target(6, 6, peniko::Color::WHITE);
slow.draw_image_sampled(
&image,
Affine::translate((1.0, 1.0)),
ImageQuality::Nearest,
pdfrum_render::pixmap::alpha_byte_truncating(0.5),
);
let slow = backend.finish(slow);
assert_eq!(fast.data(), slow.data());
}
fn lcd_strip(width: usize, stripes: &[[u8; 3]]) -> pdfrum_render::glyph::SubpixelBitmap {
let mut channels = Vec::with_capacity(width * 3);
for x in 0..width {
let px = stripes.get(x % stripes.len()).copied().unwrap_or([0; 3]);
channels.extend_from_slice(&px);
}
let width = i32::try_from(width).unwrap_or(0);
pdfrum_render::glyph::SubpixelBitmap {
left: 0,
top: 0,
width,
height: 1,
channels,
}
}
#[test]
fn a_clear_type_glyph_reaches_the_pixels_with_its_fringes_intact() {
let backend = AggBackend::new();
let mut device = backend.new_target(3, 1, peniko::Color::WHITE);
device.draw_glyph_lcd(
&lcd_strip(3, &[[255, 128, 0], [255, 255, 255], [0, 128, 255]]),
(0.0, 0.0),
peniko::Color::BLACK,
);
let out = backend.finish(device);
assert_eq!(out.pixel(0, 0), Some([0, 127, 255, 255]));
assert_eq!(out.pixel(1, 0), Some([0, 0, 0, 255]), "no stripe survives");
assert_eq!(out.pixel(2, 0), Some([255, 127, 0, 255]));
}
#[test]
fn a_clear_type_glyph_lands_where_its_origin_says() {
let backend = AggBackend::new();
let mut device = backend.new_target(4, 2, peniko::Color::WHITE);
device.draw_glyph_lcd(
&lcd_strip(2, &[[255, 255, 255]]),
(2.0, 1.0),
peniko::Color::BLACK,
);
let out = backend.finish(device);
assert_eq!(out.pixel(0, 0), Some([255, 255, 255, 255]));
assert_eq!(out.pixel(2, 1), Some([0, 0, 0, 255]));
assert_eq!(out.pixel(3, 1), Some([0, 0, 0, 255]));
}
#[test]
fn a_clear_type_glyph_is_clipped_like_any_other_primitive() {
let backend = AggBackend::new();
let mut device = backend.new_target(4, 1, peniko::Color::WHITE);
device.push_clip_rect(Rect::new(0.0, 0.0, 2.0, 1.0));
device.draw_glyph_lcd(
&lcd_strip(4, &[[255, 255, 255]]),
(0.0, 0.0),
peniko::Color::BLACK,
);
device.pop();
let out = backend.finish(device);
assert_eq!(out.pixel(1, 0), Some([0, 0, 0, 255]), "inside the clip");
assert_eq!(
out.pixel(2, 0),
Some([255, 255, 255, 255]),
"outside the clip, untouched"
);
}
#[test]
fn an_invisible_clear_type_glyph_paints_nothing() {
let backend = AggBackend::new();
let mut device = backend.new_target(2, 1, peniko::Color::WHITE);
device.draw_glyph_lcd(
&lcd_strip(2, &[[255, 255, 255]]),
(0.0, 0.0),
peniko::Color::TRANSPARENT,
);
let out = backend.finish(device);
assert_eq!(out.pixel(0, 0), Some([255, 255, 255, 255]));
}
}