use kurbo::{Affine, BezPath, Shape};
use crate::scanline::{Coverage, FillRule, Rasterizer};
pub(crate) const LCD_FIR5: [i32; 5] = [0x08, 0x4d, 0x56, 0x4d, 0x08];
pub(crate) const LCD_PADDING_26_6: i64 = 43;
pub(crate) const TEXT_GAMMA_ADJUST: [u8; 256] = [
0, 2, 3, 4, 6, 7, 8, 10, 11, 12, 13, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 29, 30,
31, 32, 33, 34, 35, 36, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 51, 52, 53, 54, 55, 56,
57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81,
82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103,
104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122,
123, 124, 125, 126, 127, 128, 129, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140,
141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 156, 157, 158,
159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 174, 175, 176,
177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 190, 191, 192, 193, 194,
195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 204, 205, 206, 207, 208, 209, 210, 211, 212,
213, 214, 215, 216, 217, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 228, 229,
230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 239, 240, 241, 242, 243, 244, 245, 246, 247,
248, 249, 250, 250, 251, 252, 253, 254, 255,
];
pub(crate) const MAX_GLYPH_DIMENSION: i32 = 2048;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubpixelBitmap {
pub left: i32,
pub top: i32,
pub width: i32,
pub height: i32,
pub channels: Vec<u8>,
}
impl SubpixelBitmap {
#[must_use]
pub fn at(&self, x: i32, y: i32) -> [u8; 3] {
if x < 0 || y < 0 || x >= self.width || y >= self.height {
return [0; 3];
}
let Ok(i) = usize::try_from((y * self.width + x) * 3) else {
return [0; 3];
};
let get = |k: usize| self.channels.get(i + k).copied().unwrap_or(0);
[get(0), get(1), get(2)]
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.width <= 0 || self.height <= 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct GlyphBitmap {
pub left: i32,
pub top: i32,
pub width: i32,
pub height: i32,
pub coverage: Vec<u8>,
}
impl GlyphBitmap {
#[cfg(test)]
#[must_use]
pub fn at(&self, x: i32, y: i32) -> u8 {
if x < 0 || y < 0 || x >= self.width || y >= self.height {
return 0;
}
let Ok(i) = usize::try_from(y * self.width + x) else {
return 0;
};
self.coverage.get(i).copied().unwrap_or(0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub(crate) enum SubpixelPhase {
Zero,
One,
Two,
}
impl SubpixelPhase {
#[must_use]
pub fn of(x: f64) -> Self {
#[expect(
clippy::cast_possible_truncation,
reason = "the C++ is `static_cast<int>(x * 3) % 3`; a device origin \
beyond i32 has already been clamped by the ±32000 rule"
)]
let n = (x * 3.0) as i32 % 3;
match n {
1 | -2 => Self::One,
2 | -1 => Self::Two,
_ => Self::Zero,
}
}
#[must_use]
pub fn shift(self) -> usize {
match self {
Self::Zero => 0,
Self::One => 1,
Self::Two => 2,
}
}
}
#[cfg(test)]
#[must_use]
pub(crate) fn rasterize(outline: &BezPath, phase: SubpixelPhase) -> Option<GlyphBitmap> {
Some(render_lcd(outline)?.to_gray(phase))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct LcdBitmap {
pub left: i32,
pub top: i32,
pub width: i32,
pub height: i32,
pub subpixels: Vec<u8>,
}
impl LcdBitmap {
#[must_use]
pub fn byte_size(&self) -> usize {
self.subpixels.len() + std::mem::size_of::<Self>()
}
}
fn gamma_of_mean(sum: u32) -> u8 {
#[expect(
clippy::cast_possible_truncation,
reason = "at most `3 * 255 / 3`, which is 255"
)]
let mean = (sum / 3) as u8;
TEXT_GAMMA_ADJUST
.get(usize::from(mean))
.copied()
.unwrap_or(0)
}
#[must_use]
pub(crate) fn render_lcd(outline: &BezPath) -> Option<LcdBitmap> {
let bbox = outline.bounding_box();
if !bbox.x0.is_finite() || !bbox.y0.is_finite() || !bbox.x1.is_finite() || !bbox.y1.is_finite()
{
return None;
}
let quantise = |v: f64, outward: fn(f64) -> f64| -> Option<i64> {
let steps = outward(v * 64.0);
(steps.abs() < 1e15).then(|| {
#[expect(
clippy::cast_possible_truncation,
reason = "guarded above: finite, integral, and within i64"
)]
let n = steps as i64;
n
})
};
let low = |v: f64| quantise(v, f64::floor);
let high = |v: f64| quantise(v, f64::ceil);
let left = div_floor(low(bbox.x0)? - LCD_PADDING_26_6, 64);
let right = div_ceil(high(bbox.x1)? + LCD_PADDING_26_6, 64);
let top = div_floor(low(bbox.y0)?, 64);
let bottom = div_ceil(high(bbox.y1)?, 64);
let width = i32::try_from(right - left).ok()?;
let height = i32::try_from(bottom - top).ok()?;
if width <= 0 || height <= 0 || width > MAX_GLYPH_DIMENSION || height > MAX_GLYPH_DIMENSION {
return None;
}
let sub_width = width.checked_mul(3)?;
let cells = usize::try_from(sub_width.checked_mul(height)?).ok()?;
let mut subpixels = vec![0u8; cells];
#[expect(
clippy::cast_precision_loss,
reason = "a bitmap origin is bounded by MAX_GLYPH_DIMENSION"
)]
let placed = Affine::scale_non_uniform(3.0, 1.0)
* Affine::translate((-(left as f64), -(top as f64)))
* outline.clone();
let mut ras = Rasterizer::new();
ras.keep_rows(0..height);
ras.add_path(&placed, FLATTEN_TOLERANCE);
ras.sweep(FillRule::NonZero, Coverage::Exact, |x, len, y, alpha| {
if y < 0 || y >= height {
return;
}
let row = y * sub_width;
for i in 0..len {
for (k, weight) in LCD_FIR5.iter().enumerate() {
let Ok(k) = i32::try_from(k) else { continue };
let dx = x + i + k - 2;
if dx < 0 || dx >= sub_width {
continue;
}
let Ok(idx) = usize::try_from(row + dx) else {
continue;
};
let add =
u8::try_from(((i32::from(alpha) * weight + 85) >> 8).max(0)).unwrap_or(u8::MAX);
if let Some(cell) = subpixels.get_mut(idx) {
*cell = cell.saturating_add(add);
}
}
}
});
Some(LcdBitmap {
left: i32::try_from(left).ok()?,
top: i32::try_from(top).ok()?,
width,
height,
subpixels,
})
}
const FLATTEN_TOLERANCE: f64 = 0.0333;
impl LcdBitmap {
#[cfg(test)]
#[must_use]
pub fn to_gray(&self, phase: SubpixelPhase) -> GlyphBitmap {
let mut coverage = Vec::new();
self.gray_coverage_into(phase, &mut coverage);
GlyphBitmap {
left: self.left,
top: self.top,
width: self.width,
height: self.height,
coverage,
}
}
pub(crate) fn gray_coverage_into(&self, phase: SubpixelPhase, out: &mut Vec<u8>) {
let shift = phase.shift();
let (Ok(width), Ok(height)) = (usize::try_from(self.width), usize::try_from(self.height))
else {
out.clear();
return;
};
let sub_width = width * 3;
if sub_width == 0 {
out.clear();
return;
}
out.clear();
out.resize(width * height, 0);
for (dst_row, src_row) in out
.chunks_exact_mut(width)
.zip(self.subpixels.chunks_exact(sub_width))
{
let (head, body) = src_row.split_at(3 - shift);
let mut cells = dst_row.iter_mut();
if let Some(cell) = cells.next() {
*cell = gamma_of_mean(head.iter().map(|v| u32::from(*v)).sum());
}
for (cell, taps) in cells.zip(body.as_chunks::<3>().0) {
*cell = gamma_of_mean(taps.iter().map(|v| u32::from(*v)).sum());
}
}
}
#[must_use]
pub fn to_subpixel(&self, phase: SubpixelPhase) -> SubpixelBitmap {
let shift = i32::try_from(phase.shift()).unwrap_or(0);
let sub_width = self.width * 3;
let mut channels = vec![0u8; self.subpixels.len()];
for y in 0..self.height {
let row = y * sub_width;
for x in 0..self.width {
let start = row + x * 3 - shift;
for k in 0..3 {
let idx = start + k;
if idx < row {
continue;
}
let raw = usize::try_from(idx)
.ok()
.and_then(|i| self.subpixels.get(i))
.copied()
.unwrap_or(0);
let gamma = TEXT_GAMMA_ADJUST
.get(usize::from(raw))
.copied()
.unwrap_or(0);
if let Ok(i) = usize::try_from((y * self.width + x) * 3 + k)
&& let Some(cell) = channels.get_mut(i)
{
*cell = gamma;
}
}
}
}
SubpixelBitmap {
left: self.left,
top: self.top,
width: self.width,
height: self.height,
channels,
}
}
}
fn div_floor(a: i64, b: i64) -> i64 {
let q = a / b;
if a % b != 0 && (a < 0) != (b < 0) {
q - 1
} else {
q
}
}
fn div_ceil(a: i64, b: i64) -> i64 {
let q = a / b;
if a % b != 0 && (a < 0) == (b < 0) {
q + 1
} else {
q
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct BitmapKey {
pub glyph: pdfrum_font::GlyphKey,
pub matrix: [i32; 4],
}
impl BitmapKey {
#[must_use]
pub fn new(glyph: pdfrum_font::GlyphKey, matrix: Affine) -> Self {
#[expect(
clippy::cast_possible_truncation,
reason = "the C++ is `static_cast<int>(m * 10000)`; a matrix \
coefficient past i32 belongs to a glyph the ±32000 \
coordinate rule has already rejected"
)]
fn ten_thousandths(v: f64) -> i32 {
(v * 10_000.0) as i32
}
let [xx, yx, xy, yy, _, _] = matrix.as_coeffs();
Self {
glyph,
matrix: [
ten_thousandths(xx),
ten_thousandths(yx),
ten_thousandths(xy),
ten_thousandths(yy),
],
}
}
}
pub(crate) const BITMAP_CACHE_BUDGET: usize = 16 * 1024 * 1024;
#[derive(Debug, Default)]
pub(crate) struct BitmapCache {
entries: std::collections::HashMap<BitmapKey, Option<LcdBitmap>, pdfrum_common::FxBuildHasher>,
bytes: usize,
}
#[derive(Debug)]
pub(crate) enum Cached<'a> {
Hit(&'a LcdBitmap),
Uncached(LcdBitmap),
}
impl std::ops::Deref for Cached<'_> {
type Target = LcdBitmap;
fn deref(&self) -> &LcdBitmap {
match self {
Self::Hit(b) => b,
Self::Uncached(b) => b,
}
}
}
impl BitmapCache {
pub fn get_or_insert(
&mut self,
key: BitmapKey,
render: impl FnOnce() -> Option<LcdBitmap>,
) -> Option<Cached<'_>> {
if !self.entries.contains_key(&key) {
let bitmap = render();
let size = bitmap.as_ref().map_or(0, LcdBitmap::byte_size);
if self.bytes.saturating_add(size) > BITMAP_CACHE_BUDGET && !self.entries.is_empty() {
return bitmap.map(Cached::Uncached);
}
self.bytes = self.bytes.saturating_add(size);
self.entries.insert(key, bitmap);
}
self.entries
.get(&key)
.and_then(Option::as_ref)
.map(Cached::Hit)
}
}
#[must_use]
pub(crate) fn average_to_gray(bitmap: &SubpixelBitmap) -> Option<GlyphBitmap> {
if bitmap.is_empty() {
return None;
}
let mut coverage = Vec::with_capacity(bitmap.channels.len() / 3);
for triple in bitmap.channels.as_chunks::<3>().0 {
let sum: u32 = triple.iter().map(|&v| u32::from(v)).sum();
#[expect(
clippy::cast_possible_truncation,
reason = "three bytes divided by three is at most 255"
)]
let byte = (sum / 3) as u8;
coverage.push(byte);
}
Some(GlyphBitmap {
left: bitmap.left,
top: bitmap.top,
width: bitmap.width,
height: bitmap.height,
coverage,
})
}
#[must_use]
pub(crate) fn recolour(bitmap: &GlyphBitmap, colour: peniko::Color) -> Option<crate::Pixmap> {
let mut pixmap = crate::Pixmap::new(0, 0);
let by_ref = GlyphBitmapRef {
width: bitmap.width,
height: bitmap.height,
coverage: &bitmap.coverage,
};
recolour_ref_into(by_ref, colour, &mut pixmap).then_some(pixmap)
}
pub(crate) fn recolour_glyph_into(
lcd: &LcdBitmap,
phase: SubpixelPhase,
colour: peniko::Color,
scratch: &mut crate::ctx::GlyphBlitScratch,
) -> bool {
lcd.gray_coverage_into(phase, &mut scratch.coverage);
let bitmap = GlyphBitmapRef {
width: lcd.width,
height: lcd.height,
coverage: &scratch.coverage,
};
recolour_ref_into(bitmap, colour, &mut scratch.pixels)
}
#[derive(Clone, Copy)]
struct GlyphBitmapRef<'a> {
width: i32,
height: i32,
coverage: &'a [u8],
}
fn recolour_ref_into(
bitmap: GlyphBitmapRef<'_>,
colour: peniko::Color,
out: &mut crate::Pixmap,
) -> bool {
if bitmap.width <= 0 || bitmap.height <= 0 {
return false;
}
let [r, g, b, alpha] = colour.to_rgba8().to_u8_array();
if alpha == 0 {
return false;
}
let (Ok(width), Ok(height)) = (u32::try_from(bitmap.width), u32::try_from(bitmap.height))
else {
return false;
};
out.reshape_keeping_pixels(width, height);
let stride = width as usize;
let data = out.data_mut();
for (dst_row, cov_row) in data
.chunks_exact_mut(stride * 4)
.zip(bitmap.coverage.chunks_exact(stride))
{
for (dest, coverage) in dst_row.as_chunks_mut::<4>().0.iter_mut().zip(cov_row) {
let a = crate::pixmap::mul255(*coverage, alpha);
dest.copy_from_slice(&[
crate::pixmap::mul255(r, a),
crate::pixmap::mul255(g, a),
crate::pixmap::mul255(b, a),
a,
]);
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
fn split_box(w: f64, h: f64) -> BezPath {
let mut p = square(w, h);
let x = w + 3.0;
p.move_to((x, 0.0));
p.line_to((x + w, 0.0));
p.line_to((x + w, h));
p.line_to((x, h));
p.close_path();
p
}
fn square(w: f64, h: f64) -> BezPath {
let mut p = BezPath::new();
p.move_to((0.0, 0.0));
p.line_to((w, 0.0));
p.line_to((w, h));
p.line_to((0.0, h));
p.close_path();
p
}
#[test]
fn the_gamma_table_is_the_oracles() {
assert_eq!(TEXT_GAMMA_ADJUST.len(), 256);
assert_eq!(TEXT_GAMMA_ADJUST[0], 0);
assert_eq!(TEXT_GAMMA_ADJUST[255], 255);
for w in TEXT_GAMMA_ADJUST.windows(2) {
let (Some(a), Some(b)) = (w.first(), w.last()) else {
continue;
};
assert!(a <= b, "the table never descends: {a} then {b}");
}
assert!(TEXT_GAMMA_ADJUST[1] > 1);
assert!(TEXT_GAMMA_ADJUST[128] > 128);
}
#[test]
fn the_filter_weights_sum_to_a_whole_scale() {
assert_eq!(LCD_FIR5.iter().sum::<i32>(), 256);
assert_eq!(LCD_FIR5[0], LCD_FIR5[4]);
assert_eq!(LCD_FIR5[1], LCD_FIR5[3]);
}
#[test]
fn the_bitmap_is_wider_than_the_outline_by_the_filters_reach() {
let bmp = rasterize(&square(2.0, 2.0), SubpixelPhase::Zero).expect("a square rasterizes");
assert_eq!(bmp.top, 0);
assert_eq!(bmp.height, 2);
assert_eq!(bmp.left, -1, "one pixel of padding on the left");
assert_eq!(bmp.width, 4, "two pixels of glyph plus one each side");
}
#[test]
fn the_padding_columns_carry_the_filters_tails() {
let bmp = rasterize(&square(2.0, 2.0), SubpixelPhase::Zero).expect("a square rasterizes");
assert!(
bmp.at(0, 0) > 0,
"the padding column must carry ink, not zero"
);
assert!(bmp.at(0, 0) < bmp.at(1, 0));
assert!(bmp.at(1, 0) > 200, "the interior is nearly opaque");
assert_eq!(bmp.at(0, 0), bmp.at(3, 0));
}
#[test]
fn a_phase_shifts_the_window_and_changes_the_gray() {
let outline = square(1.5, 2.0);
let zero = rasterize(&outline, SubpixelPhase::Zero).expect("rasterizes");
let one = rasterize(&outline, SubpixelPhase::One).expect("rasterizes");
let two = rasterize(&outline, SubpixelPhase::Two).expect("rasterizes");
assert_eq!((zero.left, zero.width), (one.left, one.width));
assert_eq!((zero.left, zero.width), (two.left, two.width));
assert_ne!(zero.coverage, one.coverage);
assert_ne!(one.coverage, two.coverage);
}
#[test]
fn the_phase_of_an_x_is_the_c_remainder() {
assert_eq!(SubpixelPhase::of(10.0), SubpixelPhase::Zero);
assert_eq!(SubpixelPhase::of(10.32), SubpixelPhase::Zero);
assert_eq!(SubpixelPhase::of(10.34), SubpixelPhase::One);
assert_eq!(SubpixelPhase::of(10.5), SubpixelPhase::One);
assert_eq!(SubpixelPhase::of(10.66), SubpixelPhase::One);
assert_eq!(SubpixelPhase::of(10.7), SubpixelPhase::Two);
assert_eq!(SubpixelPhase::of(10.99), SubpixelPhase::Two);
assert_eq!(SubpixelPhase::of(11.0), SubpixelPhase::Zero);
assert_eq!(SubpixelPhase::of(-10.4), SubpixelPhase::Two);
assert_eq!(SubpixelPhase::of(-10.7), SubpixelPhase::One);
assert_eq!(SubpixelPhase::of(-10.0), SubpixelPhase::Zero);
}
#[test]
fn an_enormous_glyph_renders_as_nothing() {
let huge = square(f64::from(MAX_GLYPH_DIMENSION) + 10.0, 4.0);
assert!(rasterize(&huge, SubpixelPhase::Zero).is_none());
}
#[test]
fn a_degenerate_outline_yields_nothing_rather_than_an_empty_bitmap() {
let mut zero_height = BezPath::new();
zero_height.move_to((0.0, 0.0));
zero_height.line_to((4.0, 0.0));
zero_height.close_path();
assert!(rasterize(&zero_height, SubpixelPhase::Zero).is_none());
}
#[test]
fn the_first_column_averages_over_three_however_few_it_has() {
let outline = square(2.0, 1.0);
let two = rasterize(&outline, SubpixelPhase::Two).expect("rasterizes");
let zero = rasterize(&outline, SubpixelPhase::Zero).expect("rasterizes");
assert!(
two.at(0, 0) <= zero.at(0, 0),
"borrowing from off the left edge cannot brighten the column"
);
}
#[test]
fn coverage_outside_the_bitmap_reads_as_zero() {
let bmp = rasterize(&square(1.0, 1.0), SubpixelPhase::Zero).expect("rasterizes");
assert_eq!(bmp.at(-1, 0), 0);
assert_eq!(bmp.at(0, -1), 0);
assert_eq!(bmp.at(bmp.width, 0), 0);
assert_eq!(bmp.at(0, bmp.height), 0);
}
#[test]
fn the_subpixel_bitmap_reads_the_same_window_as_the_gray_one() {
let outline = square(3.0, 2.0);
let lcd = render_lcd(&outline).expect("rasterizes");
for phase in [SubpixelPhase::Zero, SubpixelPhase::One, SubpixelPhase::Two] {
let gray = lcd.to_gray(phase);
let sub = lcd.to_subpixel(phase);
assert_eq!((sub.width, sub.height), (gray.width, gray.height));
assert_eq!((sub.left, sub.top), (gray.left, gray.top));
for y in 0..gray.height {
for x in 0..gray.width {
let three = sub.at(x, y);
let (lo, hi) = (
three.iter().copied().min().unwrap_or(0),
three.iter().copied().max().unwrap_or(0),
);
let g = gray.at(x, y);
assert!(
g >= lo.saturating_sub(2) && g <= hi.saturating_add(2),
"{phase:?} at ({x},{y}): gray {g} outside subpixels {three:?}"
);
}
}
}
}
#[test]
fn a_fully_covered_pixel_has_no_fringe_and_an_edge_does() {
let lcd = render_lcd(&square(6.0, 2.0)).expect("rasterizes");
let sub = lcd.to_subpixel(SubpixelPhase::Zero);
let interior = sub.at(sub.width / 2, 0);
assert_eq!(
interior[0], interior[2],
"a fully covered pixel must have no fringe, got {interior:?}"
);
let edges: Vec<_> = (0..sub.width)
.map(|x| sub.at(x, 0))
.filter(|c| c[0] != c[2])
.collect();
assert!(
!edges.is_empty(),
"no pixel had unequal stripes; the triples are being averaged \
somewhere they should not be"
);
}
#[test]
fn the_gamma_table_is_applied_once_per_stripe_not_once_per_pixel() {
let lcd = render_lcd(&square(4.0, 1.0)).expect("rasterizes");
let sub = lcd.to_subpixel(SubpixelPhase::One);
for &byte in &sub.channels {
assert!(
TEXT_GAMMA_ADJUST.contains(&byte),
"{byte} is not in the gamma table's range"
);
}
}
#[test]
fn averaging_back_to_gray_keeps_the_bitmaps_shape() {
let lcd = render_lcd(&square(3.0, 2.0)).expect("rasterizes");
let sub = lcd.to_subpixel(SubpixelPhase::Zero);
let gray = average_to_gray(&sub).expect("has pixels");
assert_eq!((gray.width, gray.height), (sub.width, sub.height));
assert_eq!((gray.left, gray.top), (sub.left, sub.top));
assert_eq!(gray.coverage.len(), sub.channels.len() / 3);
}
#[test]
fn a_reused_scratch_carries_none_of_the_glyph_before_it() {
let mut scratch = crate::ctx::GlyphBlitScratch::default();
let colour = peniko::Color::from_rgba8(200, 100, 50, 255);
let big = render_lcd(&square(20.0, 9.0)).expect("rasterizes");
let small = render_lcd(&split_box(3.0, 5.0)).expect("rasterizes");
assert!(recolour_glyph_into(
&big,
SubpixelPhase::Zero,
colour,
&mut scratch
));
assert!(recolour_glyph_into(
&small,
SubpixelPhase::Zero,
colour,
&mut scratch
));
let fresh = recolour(&small.to_gray(SubpixelPhase::Zero), colour).expect("draws");
assert_eq!(
(scratch.pixels.width(), scratch.pixels.height()),
(fresh.width(), fresh.height()),
"the reused pixmap must be resized to this glyph"
);
assert_eq!(
scratch.pixels.data(),
fresh.data(),
"a reused buffer must not leak the previous glyph's pixels"
);
}
#[test]
fn the_fused_glyph_blit_is_the_two_functions_it_replaces() {
let mut scratch = crate::ctx::GlyphBlitScratch::default();
let colour = peniko::Color::from_rgba8(17, 200, 99, 255);
let lcd = render_lcd(&square(5.0, 3.0)).expect("rasterizes");
for phase in [SubpixelPhase::Zero, SubpixelPhase::One, SubpixelPhase::Two] {
let expected = recolour(&lcd.to_gray(phase), colour).expect("draws");
assert!(recolour_glyph_into(&lcd, phase, colour, &mut scratch));
assert_eq!(
scratch.pixels.data(),
expected.data(),
"the fused blit must be byte-identical at phase {phase:?}"
);
}
}
#[test]
fn a_transparent_colour_draws_no_glyph() {
let mut scratch = crate::ctx::GlyphBlitScratch::default();
let lcd = render_lcd(&square(4.0, 2.0)).expect("rasterizes");
assert!(!recolour_glyph_into(
&lcd,
SubpixelPhase::Zero,
peniko::Color::from_rgba8(1, 2, 3, 0),
&mut scratch
));
}
fn gray_coverage_per_subpixel(lcd: &LcdBitmap, phase: SubpixelPhase) -> Vec<u8> {
let shift = i32::try_from(phase.shift()).unwrap_or(0);
let sub_width = lcd.width * 3;
let mut coverage = vec![0u8; lcd.subpixels.len() / 3];
for y in 0..lcd.height {
let row = y * sub_width;
for x in 0..lcd.width {
let start = row + x * 3 - shift;
let sum: i32 = (0..3)
.map(|k| {
let idx = start + k;
if idx < row {
return 0;
}
usize::try_from(idx)
.ok()
.and_then(|i| lcd.subpixels.get(i))
.map_or(0, |v| i32::from(*v))
})
.sum();
let average = (sum / 3).clamp(0, 255);
let Ok(average) = usize::try_from(average) else {
continue;
};
let gamma = TEXT_GAMMA_ADJUST.get(average).copied().unwrap_or(0);
if let Ok(i) = usize::try_from(y * lcd.width + x)
&& let Some(cell) = coverage.get_mut(i)
{
*cell = gamma;
}
}
}
coverage
}
#[test]
fn the_sliced_coverage_walk_matches_the_per_subpixel_one() {
let shapes: [BezPath; 6] = [
square(1.0, 1.0),
square(1.0, 7.0),
square(9.0, 1.0),
square(5.0, 3.0),
split_box(3.0, 5.0),
split_box(1.0, 4.0),
];
let mut compared = 0;
for shape in &shapes {
let lcd = render_lcd(shape).expect("rasterizes");
for phase in [SubpixelPhase::Zero, SubpixelPhase::One, SubpixelPhase::Two] {
let expected = gray_coverage_per_subpixel(&lcd, phase);
let mut got = Vec::new();
lcd.gray_coverage_into(phase, &mut got);
assert_eq!(
got, expected,
"coverage differs at phase {phase:?} on a {}x{} bitmap",
lcd.width, lcd.height
);
compared += 1;
}
}
assert_eq!(compared, 18, "every shape must have been compared");
}
#[test]
fn the_first_column_darkens_at_a_shifted_phase() {
let lcd = render_lcd(&square(4.0, 2.0)).expect("rasterizes");
let mut zero = Vec::new();
lcd.gray_coverage_into(SubpixelPhase::Zero, &mut zero);
let mut two = Vec::new();
lcd.gray_coverage_into(SubpixelPhase::Two, &mut two);
let width = usize::try_from(lcd.width).expect("positive");
let first = |v: &[u8]| v.first().copied().expect("a non-empty bitmap");
assert!(first(&zero) > 0, "the glyph must cover its first column");
assert!(
first(&two) < first(&zero),
"phase two drops two of the first column's three taps: {} vs {}",
first(&two),
first(&zero)
);
assert_eq!(
two,
gray_coverage_per_subpixel(&lcd, SubpixelPhase::Two),
"only the first column of each row may differ from an unshifted read"
);
assert_eq!(
two.len(),
width * usize::try_from(lcd.height).expect("positive")
);
}
#[test]
fn the_row_zipped_recolour_is_the_indexed_arithmetic() {
for colour in [
peniko::Color::from_rgba8(255, 255, 255, 255),
peniko::Color::from_rgba8(0, 0, 0, 255),
peniko::Color::from_rgba8(200, 100, 50, 255),
peniko::Color::from_rgba8(3, 251, 128, 137),
peniko::Color::from_rgba8(17, 200, 99, 1),
] {
let [r, g, b, alpha] = colour.to_rgba8().to_u8_array();
let coverage: Vec<u8> = (0..=255).collect();
let bitmap = GlyphBitmapRef {
width: 256,
height: 1,
coverage: &coverage,
};
let mut out = crate::Pixmap::new(0, 0);
assert!(recolour_ref_into(bitmap, colour, &mut out));
for (i, dest) in out.data().as_chunks::<4>().0.iter().enumerate() {
#[expect(clippy::cast_possible_truncation, reason = "the index runs 0..256")]
let cov = i as u8;
let a = crate::pixmap::mul255(cov, alpha);
assert_eq!(
*dest,
[
crate::pixmap::mul255(r, a),
crate::pixmap::mul255(g, a),
crate::pixmap::mul255(b, a),
a,
],
"coverage {cov} under {colour:?}"
);
}
}
}
#[test]
fn a_zero_width_bitmap_yields_no_coverage() {
for (width, height) in [(0, 4), (0, 0), (4, 0)] {
let lcd = LcdBitmap {
left: 0,
top: 0,
width,
height,
subpixels: Vec::new(),
};
for phase in [SubpixelPhase::Zero, SubpixelPhase::One, SubpixelPhase::Two] {
let mut out = vec![7u8; 12];
lcd.gray_coverage_into(phase, &mut out);
assert!(
out.is_empty(),
"a {width}x{height} bitmap has no coverage at phase {phase:?}"
);
}
}
}
#[test]
fn an_empty_subpixel_bitmap_averages_to_nothing() {
let empty = SubpixelBitmap {
left: 0,
top: 0,
width: 0,
height: 0,
channels: Vec::new(),
};
assert!(empty.is_empty());
assert!(average_to_gray(&empty).is_none());
assert_eq!(empty.at(0, 0), [0; 3]);
}
}