use std::sync::Arc;
use pdfrum_page::BlendMode;
use pdfrum_render::{AlphaMask, Pixmap, blend, pixmap};
use crate::image::scale_alpha;
#[derive(Debug, Clone)]
pub struct Target {
pixels: Pixmap,
clip: Option<Arc<AlphaMask>>,
}
impl Target {
#[must_use]
pub fn new(width: u32, height: u32, clear: peniko::Color) -> Self {
Self {
pixels: if clear == peniko::Color::TRANSPARENT {
Pixmap::new(width, height)
} else {
Pixmap::filled(width, height, clear)
},
clip: None,
}
}
#[must_use]
pub fn from_pixmap(base: Pixmap) -> Self {
Self {
pixels: base,
clip: None,
}
}
#[must_use]
pub fn width(&self) -> u32 {
self.pixels.width()
}
#[must_use]
pub fn height(&self) -> u32 {
self.pixels.height()
}
#[must_use]
pub fn pixels(&self) -> &Pixmap {
&self.pixels
}
#[must_use]
pub fn into_pixmap(self) -> Pixmap {
self.pixels
}
#[cfg(test)]
#[must_use]
fn clip(&self) -> Option<&Arc<AlphaMask>> {
self.clip.as_ref()
}
pub fn set_clip(&mut self, clip: Option<Arc<AlphaMask>>) {
self.clip = clip;
}
#[cfg(test)]
#[must_use]
fn clip_at(&self, x: u32, y: u32) -> u8 {
let Some(mask) = self.clip.as_ref() else {
return 255;
};
let Some(i) = (y as usize)
.checked_mul(mask.width() as usize)
.and_then(|row| row.checked_add(x as usize))
else {
return 0;
};
mask.data().get(i).copied().unwrap_or(0)
}
pub fn blend_span(
&mut self,
x: i32,
len: i32,
y: i32,
coverage: u8,
src: Source,
mode: BlendMode,
) {
let Some((x0, x1, row)) = self.span_range(x, len, y) else {
return;
};
let Self { pixels, clip } = self;
let clip = clip_span(clip.as_deref(), x0, x1, row);
let width = pixels.width() as usize;
let Some(start) = (row as usize).checked_mul(width) else {
return;
};
let Some(pixels) = pixels
.data_mut()
.get_mut((start + x0 as usize) * 4..(start + x1 as usize) * 4)
else {
return;
};
if let Some(clip) = clip {
for (dest, &mask) in pixels.as_chunks_mut::<4>().0.iter_mut().zip(clip.iter()) {
let cov = pixmap::mul255(coverage, mask);
if cov == 0 {
continue;
}
blend_into(dest, src, cov, mode);
}
} else {
if coverage == 0 {
return;
}
for dest in pixels.as_chunks_mut::<4>().0 {
blend_into(dest, src, coverage, mode);
}
}
}
pub fn blend_span_with(
&mut self,
x: i32,
len: i32,
y: i32,
coverage: u8,
mode: BlendMode,
mut sample: impl FnMut(u32, u32) -> Option<[u8; 4]>,
) {
let Some((x0, x1, row)) = self.span_range(x, len, y) else {
return;
};
let Self { pixels, clip } = self;
let clip = clip_span(clip.as_deref(), x0, x1, row);
let width = pixels.width() as usize;
let Some(start) = (row as usize).checked_mul(width) else {
return;
};
let Some(pixels) = pixels
.data_mut()
.get_mut((start + x0 as usize) * 4..(start + x1 as usize) * 4)
else {
return;
};
for (i, dest) in pixels.as_chunks_mut::<4>().0.iter_mut().enumerate() {
let cov = match &clip {
Some(clip) => match clip.get(i) {
Some(&mask) => pixmap::mul255(coverage, mask),
None => continue,
},
None => coverage,
};
if cov == 0 {
continue;
}
let Ok(col) = u32::try_from(x0 as usize + i) else {
continue;
};
let Some(src) = sample(col, row) else {
continue;
};
blend_into(dest, Source::Premultiplied(src), cov, mode);
}
}
pub fn blit_image(&mut self, img: &Pixmap, dx: i32, dy: i32, alpha: u8) {
let end = i64::from(dx) + i64::from(img.width());
let (Ok(x0), Ok(x1)) = (
u32::try_from(dx.max(0)),
u32::try_from(end.clamp(0, i64::from(self.width()))),
) else {
return;
};
if x0 >= x1 {
return;
}
let span = (x1 - x0) as usize;
let Ok(src_x0) = usize::try_from(i64::from(x0) - i64::from(dx)) else {
return;
};
let height = i64::from(img.height());
let (Ok(first), Ok(last)) = (
u32::try_from(i64::from(-dy).clamp(0, height)),
u32::try_from((i64::from(self.height()) - i64::from(dy)).clamp(0, height)),
) else {
return;
};
if first >= last {
return;
}
let opaque = alpha == 255;
let Self { pixels, clip } = self;
let dest_stride = pixels.width() as usize * 4;
let src_stride = img.width() as usize * 4;
let clip = clip.as_deref();
let clip_width = clip.map_or(0, |mask| mask.width() as usize);
if clip.is_some() && (x1 as usize) > clip_width {
return;
}
for row in first..last {
let Ok(y) = usize::try_from(i64::from(row) + i64::from(dy)) else {
continue;
};
let Some(dest) = y
.checked_mul(dest_stride)
.and_then(|start| {
let lo = start.checked_add(x0 as usize * 4)?;
Some(lo..lo.checked_add(span * 4)?)
})
.and_then(|range| pixels.data_mut().get_mut(range))
else {
continue;
};
let Some(src) = (row as usize)
.checked_mul(src_stride)
.and_then(|start| {
let lo = start.checked_add(src_x0 * 4)?;
Some(lo..lo.checked_add(span * 4)?)
})
.and_then(|range| img.data().get(range))
else {
continue;
};
match clip {
None => {
for (dest, &px) in dest
.as_chunks_mut::<4>()
.0
.iter_mut()
.zip(src.as_chunks::<4>().0)
{
let px = if opaque { px } else { scale_alpha(px, alpha) };
blend_into(dest, Source::Premultiplied(px), 255, BlendMode::Normal);
}
}
Some(mask) => {
let Some(band) = y
.checked_mul(clip_width)
.and_then(|start| {
let lo = start.checked_add(x0 as usize)?;
Some(lo..lo.checked_add(span)?)
})
.and_then(|range| mask.data().get(range))
else {
continue;
};
for ((dest, &px), &cov) in dest
.as_chunks_mut::<4>()
.0
.iter_mut()
.zip(src.as_chunks::<4>().0)
.zip(band.iter())
{
if cov == 0 {
continue;
}
let px = if opaque { px } else { scale_alpha(px, alpha) };
blend_into(dest, Source::Premultiplied(px), cov, BlendMode::Normal);
}
}
}
}
}
pub fn composite_layer(&mut self, layer: &Pixmap, mode: BlendMode) {
let rows = self.height().min(layer.height()) as usize;
let cols = self.width().min(layer.width()) as usize;
if rows == 0 || cols == 0 {
return;
}
let dest_stride = self.width() as usize * 4;
let src_stride = layer.width() as usize * 4;
let span = cols * 4;
let pixels = self.pixels.data_mut();
let src_all = layer.data();
for row in 0..rows {
let (Some(dest), Some(src)) = (
row.checked_mul(dest_stride)
.and_then(|lo| pixels.get_mut(lo..lo.checked_add(span)?)),
row.checked_mul(src_stride)
.and_then(|lo| src_all.get(lo..lo.checked_add(span)?)),
) else {
continue;
};
for (dest, &px) in dest
.as_chunks_mut::<4>()
.0
.iter_mut()
.zip(src.as_chunks::<4>().0)
{
if px[3] == 0 {
continue;
}
blend_into(dest, Source::Premultiplied(px), 255, mode);
}
}
}
pub fn merge_lcd_pixel(
&mut self,
x: i32,
y: i32,
colour: [u8; 3],
colour_alpha: u8,
coverage: [u8; 3],
) {
let (Ok(col), Ok(row)) = (u32::try_from(x), u32::try_from(y)) else {
return;
};
if col >= self.width() || row >= self.height() {
return;
}
let mask = match self.clip.as_ref() {
None => 255,
Some(mask) => (row as usize)
.checked_mul(mask.width() as usize)
.and_then(|r| r.checked_add(col as usize))
.and_then(|i| mask.data().get(i).copied())
.unwrap_or(0),
};
if mask == 0 {
return;
}
let width = self.pixels.width() as usize;
let Some(start) = (row as usize)
.checked_mul(width)
.map(|r| (r + col as usize) * 4)
else {
return;
};
let Some(dest) = self.pixels.data_mut().get_mut(start..start + 4) else {
return;
};
for i in 0..3 {
let (Some(&cov), Some(&ch)) = (coverage.get(i), colour.get(i)) else {
continue;
};
let alpha = pixmap::mul255(pixmap::mul255(cov, colour_alpha), mask);
if alpha == 0 {
continue;
}
if let Some(slot) = dest.get_mut(i) {
*slot = pixmap::alpha_merge(*slot, ch, alpha);
}
}
if let Some(slot) = dest.get_mut(3) {
*slot = 255;
}
}
fn span_range(&self, x: i32, len: i32, y: i32) -> Option<(u32, u32, u32)> {
if len <= 0 || y < 0 {
return None;
}
let row = u32::try_from(y).ok()?;
if row >= self.height() {
return None;
}
let end = i64::from(x).checked_add(i64::from(len))?;
let x0 = u32::try_from(x.max(0)).ok()?;
let x1 = u32::try_from(end.clamp(0, i64::from(self.width()))).ok()?;
(x0 < x1).then_some((x0, x1, row))
}
}
fn clip_span(
mask: Option<&AlphaMask>,
x0: u32,
x1: u32,
row: u32,
) -> Option<std::borrow::Cow<'_, [u8]>> {
let mask = mask?;
let width = mask.width() as usize;
let len = (x1.saturating_sub(x0)) as usize;
let start = (row as usize)
.checked_mul(width)
.and_then(|row| row.checked_add(x0 as usize));
let slice = start.and_then(|start| {
((x1 as usize) <= width)
.then_some(())
.and_then(|()| mask.data().get(start..start.checked_add(len)?))
});
Some(slice.map_or_else(
|| std::borrow::Cow::Owned(vec![0_u8; len]),
std::borrow::Cow::Borrowed,
))
}
#[derive(Debug, Clone, Copy)]
pub enum Source {
Straight([u8; 3], u8),
Premultiplied([u8; 4]),
}
fn blend_into(dest: &mut [u8], src: Source, coverage: u8, mode: BlendMode) {
let (Some(&r), Some(&g), Some(&b), Some(&a)) =
(dest.first(), dest.get(1), dest.get(2), dest.get(3))
else {
return;
};
let out = match src {
Source::Straight(rgb, alpha) => {
blend::composite_solid([r, g, b, a], rgb, alpha, coverage, mode)
}
Source::Premultiplied(px) => {
blend::composite_premultiplied([r, g, b, a], px, coverage, mode)
}
};
if let Some(slot) = dest.get_mut(..4) {
slot.copy_from_slice(&out);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn blit_by_spans(target: &mut Target, img: &Pixmap, dx: i32, dy: i32, alpha: u8) {
for row in 0..img.height() {
let Ok(row_i32) = i32::try_from(row) else {
continue;
};
let Some(y) = row_i32.checked_add(dy) else {
continue;
};
let Ok(width) = i32::try_from(img.width()) else {
continue;
};
target.blend_span_with(dx, width, y, 255, BlendMode::Normal, |col, _| {
let src_col = u32::try_from(i64::from(col) - i64::from(dx)).ok()?;
img.pixel(src_col, row).map(|px| scale_alpha(px, alpha))
});
}
}
fn noisy(w: u32, h: u32, seed: u64) -> Pixmap {
let mut p = Pixmap::new(w, h);
let mut state = seed | 1;
for y in 0..h {
for x in 0..w {
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
u8::try_from(state & 0xff).unwrap_or(0)
};
let a = next();
let px = [
pixmap::mul255(next(), a),
pixmap::mul255(next(), a),
pixmap::mul255(next(), a),
a,
];
p.set_pixel(x, y, px);
}
}
p
}
#[test]
fn a_blit_matches_the_span_loop_it_replaced() {
let img = noisy(5, 4, 0x9E37_79B9);
for clip in [None, Some(0), Some(1)] {
for dy in -6_i32..=8 {
for dx in -6_i32..=10 {
for alpha in [255_u8, 128, 0] {
let seed = |t: &mut Target| {
if let Some(kind) = clip {
let mut mask = AlphaMask::filled(8, 6, 200);
if kind == 1 {
for (i, b) in mask.data_mut().iter_mut().enumerate() {
*b = u8::try_from((i * 37) % 256).unwrap_or(0);
}
}
t.set_clip(Some(Arc::new(mask)));
}
};
let mut fast = Target::new(8, 6, peniko::Color::WHITE);
seed(&mut fast);
fast.blit_image(&img, dx, dy, alpha);
let mut slow = Target::new(8, 6, peniko::Color::WHITE);
seed(&mut slow);
blit_by_spans(&mut slow, &img, dx, dy, alpha);
assert_eq!(
fast.pixels().data(),
slow.pixels().data(),
"dx={dx} dy={dy} alpha={alpha} clip={clip:?}"
);
}
}
}
}
}
const ALL_BLEND_MODES: [BlendMode; 17] = [
BlendMode::Normal,
BlendMode::Compatible,
BlendMode::Multiply,
BlendMode::Screen,
BlendMode::Overlay,
BlendMode::Darken,
BlendMode::Lighten,
BlendMode::ColorDodge,
BlendMode::ColorBurn,
BlendMode::HardLight,
BlendMode::SoftLight,
BlendMode::Difference,
BlendMode::Exclusion,
BlendMode::Hue,
BlendMode::Saturation,
BlendMode::Color,
BlendMode::Luminosity,
];
fn composite_layer_by_pixels(target: &mut Target, layer: &Pixmap, mode: BlendMode) {
let (w, h) = (layer.width(), layer.height());
let saved = target.clip().map(Arc::clone);
target.set_clip(None);
for y in 0..h {
for x in 0..w {
let Some(src) = layer.pixel(x, y) else {
continue;
};
if src[3] == 0 {
continue;
}
let (Ok(col), Ok(row)) = (i32::try_from(x), i32::try_from(y)) else {
continue;
};
target.blend_span(col, 1, row, 255, Source::Premultiplied(src), mode);
}
}
target.set_clip(saved);
}
#[test]
fn a_layer_composite_matches_the_span_loop_it_replaced() {
let layer = noisy(9, 7, 0x0123_4567);
for mode in ALL_BLEND_MODES {
for clip in [false, true] {
let seed = |t: &mut Target| {
if clip {
let mut mask = AlphaMask::filled(9, 7, 255);
for (i, b) in mask.data_mut().iter_mut().enumerate() {
*b = u8::try_from((i * 37) % 256).unwrap_or(0);
}
t.set_clip(Some(Arc::new(mask)));
}
};
let mut fast = Target::new(9, 7, peniko::Color::WHITE);
seed(&mut fast);
fast.composite_layer(&layer, mode);
let mut slow = Target::new(9, 7, peniko::Color::WHITE);
seed(&mut slow);
composite_layer_by_pixels(&mut slow, &layer, mode);
assert_eq!(
fast.pixels().data(),
slow.pixels().data(),
"mode={mode:?} clip={clip}"
);
}
}
}
#[test]
fn a_transparent_source_pixel_is_the_identity_under_every_mode() {
for mode in ALL_BLEND_MODES {
for a in [0_u8, 1, 63, 128, 254, 255] {
for c in [0_u8, 1, 127, 254] {
let dest = [c.min(a), a.saturating_sub(c).min(a), c.min(a), a];
let out = blend::composite_premultiplied(dest, [0, 0, 0, 0], 255, mode);
assert_eq!(out, dest, "mode={mode:?} dest={dest:?}");
}
}
}
}
#[test]
fn a_clip_narrower_than_the_blit_paints_nothing() {
let img = noisy(6, 2, 7);
let mut t = Target::new(8, 4, peniko::Color::WHITE);
t.set_clip(Some(Arc::new(AlphaMask::filled(4, 4, 255))));
t.blit_image(&img, 0, 0, 255);
assert!(
t.pixels().data().iter().all(|&b| b == 255),
"a clip that does not reach the span's last column paints nothing"
);
}
#[test]
fn each_row_of_a_blit_lands_on_its_own_row() {
let mut img = Pixmap::new(3, 5);
for y in 0..5_u32 {
let v = u8::try_from(y + 1).unwrap_or(1) * 40;
for x in 0..3_u32 {
img.set_pixel(x, y, [v, v, v, 255]);
}
}
let mut t = Target::new(6, 9, peniko::Color::TRANSPARENT);
t.blit_image(&img, 2, 3, 255);
for y in 0..5_u32 {
let v = u8::try_from(y + 1).unwrap_or(1) * 40;
for x in 0..3_u32 {
assert_eq!(
t.pixels().pixel(x + 2, y + 3),
Some([v, v, v, 255]),
"row {y} of the image belongs at device row {}",
y + 3
);
}
}
assert_eq!(t.pixels().pixel(1, 3), Some([0, 0, 0, 0]));
assert_eq!(t.pixels().pixel(5, 3), Some([0, 0, 0, 0]));
assert_eq!(t.pixels().pixel(2, 2), Some([0, 0, 0, 0]));
assert_eq!(t.pixels().pixel(2, 8), Some([0, 0, 0, 0]));
}
fn opaque(r: u8, g: u8, b: u8) -> Source {
Source::Straight([r, g, b], 255)
}
fn awkward_mask(width: u32, height: u32) -> AlphaMask {
let mut mask = AlphaMask::new(width, height);
for (i, slot) in mask.data_mut().iter_mut().enumerate() {
#[expect(clippy::cast_possible_truncation, reason = "the modulus is 251")]
let byte = (i % 251) as u8;
*slot = byte;
}
mask
}
#[test]
fn clip_span_reproduces_clip_at_exactly() {
let mut target = Target::new(7, 5, peniko::Color::TRANSPARENT);
target.set_clip(Some(Arc::new(awkward_mask(7, 5))));
for row in 0..5 {
for x0 in 0..7 {
for x1 in (x0 + 1)..=7 {
let span = clip_span(target.clip().map(Arc::as_ref), x0, x1, row)
.expect("a clip is set, so this is Some");
for (i, &byte) in span.iter().enumerate() {
#[expect(clippy::cast_possible_truncation, reason = "i < 7")]
let col = x0 + i as u32;
assert_eq!(
byte,
target.clip_at(col, row),
"row={row} x0={x0} x1={x1} col={col}"
);
}
}
}
}
}
#[test]
fn a_clip_narrower_than_the_target_reads_zero_past_its_edge() {
let mut target = Target::new(8, 3, peniko::Color::TRANSPARENT);
target.set_clip(Some(Arc::new(awkward_mask(4, 3))));
let span = clip_span(target.clip().map(Arc::as_ref), 0, 8, 0).expect("a clip is set");
assert_eq!(span.len(), 8);
assert!(span.iter().all(|&b| b == 0), "{span:?}");
for col in 0..4 {
#[expect(clippy::cast_possible_truncation, reason = "col < 4")]
let expected = col as u8;
assert_eq!(target.clip_at(col, 0), expected);
}
assert_eq!(target.clip_at(4, 0), 4, "this is row 1's first byte");
target.blend_span(0, 8, 0, 255, opaque(255, 0, 0), BlendMode::Normal);
for col in 0..8 {
assert_eq!(
target.pixels().pixel(col, 0),
Some([0, 0, 0, 0]),
"col={col}"
);
}
}
#[test]
fn an_absent_clip_is_full_coverage_not_an_empty_span() {
let target = Target::new(4, 1, peniko::Color::TRANSPARENT);
assert!(clip_span(target.clip().map(Arc::as_ref), 0, 4, 0).is_none());
}
#[test]
fn a_span_paints_its_columns_and_no_others() {
let mut t = Target::new(8, 1, peniko::Color::TRANSPARENT);
t.blend_span(2, 3, 0, 255, opaque(255, 0, 0), BlendMode::Normal);
assert_eq!(t.pixels().pixel(1, 0).map(|p| p[3]), Some(0));
assert_eq!(t.pixels().pixel(2, 0), Some([255, 0, 0, 255]));
assert_eq!(t.pixels().pixel(4, 0), Some([255, 0, 0, 255]));
assert_eq!(t.pixels().pixel(5, 0).map(|p| p[3]), Some(0));
}
#[test]
fn a_span_running_off_both_edges_is_clipped_not_wrapped() {
let mut t = Target::new(4, 2, peniko::Color::TRANSPARENT);
t.blend_span(-10, 100, 1, 255, opaque(0, 255, 0), BlendMode::Normal);
for col in 0..4 {
assert_eq!(t.pixels().pixel(col, 1).map(|p| p[3]), Some(255));
assert_eq!(
t.pixels().pixel(col, 0).map(|p| p[3]),
Some(0),
"row 0 untouched"
);
}
}
#[test]
fn a_span_off_the_target_paints_nothing() {
let mut t = Target::new(4, 4, peniko::Color::TRANSPARENT);
t.blend_span(0, 4, 9, 255, opaque(255, 0, 0), BlendMode::Normal);
t.blend_span(0, 4, -1, 255, opaque(255, 0, 0), BlendMode::Normal);
t.blend_span(10, 4, 0, 255, opaque(255, 0, 0), BlendMode::Normal);
assert!(t.pixels().data().iter().all(|&b| b == 0));
}
#[test]
fn the_clip_multiplies_coverage_truncating() {
let mut t = Target::new(1, 1, peniko::Color::TRANSPARENT);
t.set_clip(Some(Arc::new(AlphaMask::filled(1, 1, 128))));
t.blend_span(0, 1, 0, 128, opaque(255, 255, 255), BlendMode::Normal);
assert_eq!(t.pixels().pixel(0, 0).map(|p| p[3]), Some(64));
}
#[test]
fn a_zero_clip_paints_nothing() {
let mut t = Target::new(2, 2, peniko::Color::WHITE);
let before = t.pixels().clone();
t.set_clip(Some(Arc::new(AlphaMask::new(2, 2))));
t.blend_span(0, 2, 0, 255, opaque(255, 0, 0), BlendMode::Normal);
assert_eq!(t.pixels(), &before);
}
#[test]
fn an_lcd_pixel_merges_each_stripe_into_its_own_channel() {
let mut t = Target::new(1, 1, peniko::Color::WHITE);
t.merge_lcd_pixel(0, 0, [0, 0, 0], 255, [255, 128, 0]);
assert_eq!(t.pixels().pixel(0, 0), Some([0, 127, 255, 255]));
}
#[test]
fn an_lcd_pixel_is_left_opaque_however_little_it_covered() {
let mut t = Target::new(1, 1, peniko::Color::WHITE);
t.merge_lcd_pixel(0, 0, [0, 0, 0], 255, [1, 0, 0]);
assert_eq!(t.pixels().pixel(0, 0).map(|p| p[3]), Some(255));
}
#[test]
fn an_lcd_pixel_with_no_coverage_anywhere_leaves_the_pixel_alone() {
let mut t = Target::new(1, 1, peniko::Color::WHITE);
t.merge_lcd_pixel(0, 0, [0, 0, 0], 255, [0, 0, 0]);
assert_eq!(t.pixels().pixel(0, 0), Some([255, 255, 255, 255]));
}
#[test]
fn an_lcd_pixel_folds_the_clip_into_every_channels_alpha() {
let mut t = Target::new(1, 1, peniko::Color::WHITE);
t.set_clip(Some(Arc::new(AlphaMask::filled(1, 1, 128))));
t.merge_lcd_pixel(0, 0, [0, 0, 0], 255, [255, 255, 255]);
let px = t.pixels().pixel(0, 0).expect("a pixel");
assert_eq!([px[0], px[1], px[2]], [127, 127, 127]);
let mut t = Target::new(1, 1, peniko::Color::WHITE);
t.set_clip(Some(Arc::new(AlphaMask::filled(1, 1, 0))));
t.merge_lcd_pixel(0, 0, [0, 0, 0], 255, [255, 255, 255]);
assert_eq!(t.pixels().pixel(0, 0), Some([255, 255, 255, 255]));
}
#[test]
fn an_lcd_pixel_outside_the_target_is_a_no_op() {
let mut t = Target::new(2, 2, peniko::Color::WHITE);
for (x, y) in [(-1, 0), (0, -1), (2, 0), (0, 2), (99, 99)] {
t.merge_lcd_pixel(x, y, [0, 0, 0], 255, [255, 255, 255]);
}
for y in 0..2 {
for x in 0..2 {
assert_eq!(t.pixels().pixel(x, y), Some([255, 255, 255, 255]));
}
}
}
#[test]
fn a_translucent_text_colour_scales_every_stripe() {
let mut t = Target::new(1, 1, peniko::Color::WHITE);
t.merge_lcd_pixel(0, 0, [0, 0, 0], 128, [255, 255, 255]);
let px = t.pixels().pixel(0, 0).expect("a pixel");
assert_eq!([px[0], px[1], px[2]], [127, 127, 127]);
}
#[test]
fn a_varying_span_skips_where_the_source_has_nothing() {
let mut t = Target::new(4, 1, peniko::Color::TRANSPARENT);
t.blend_span_with(0, 4, 0, 255, BlendMode::Normal, |x, _| {
(x % 2 == 0).then_some([0, 0, 255, 255])
});
assert_eq!(t.pixels().pixel(0, 0).map(|p| p[3]), Some(255));
assert_eq!(t.pixels().pixel(1, 0).map(|p| p[3]), Some(0));
assert_eq!(t.pixels().pixel(2, 0).map(|p| p[3]), Some(255));
}
}