#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub struct SubpixelOffset(u8);
impl SubpixelOffset {
pub const N: u8 = 4;
pub fn from_float(frac: f32) -> Self {
let bucket = (frac.fract() * Self::N as f32).floor() as i32;
let bucket = bucket.rem_euclid(Self::N as i32) as u8;
SubpixelOffset(bucket.min(Self::N - 1))
}
pub fn as_float(self) -> f32 {
self.0 as f32 / Self::N as f32
}
#[inline]
pub fn bucket(self) -> u8 {
self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SubpixelCacheKey {
pub glyph_id: u16,
pub px_size_times_64: u32,
pub subpixel: SubpixelOffset,
}
impl SubpixelCacheKey {
pub fn new(glyph_id: u16, px_size: f32, x_offset: f32) -> Self {
Self {
glyph_id,
px_size_times_64: (px_size * 64.0).round() as u32,
subpixel: SubpixelOffset::from_float(x_offset),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SubpixelBuckets {
#[default]
Four,
Eight,
Sixteen,
}
impl SubpixelBuckets {
pub const fn count(self) -> u32 {
match self {
SubpixelBuckets::Four => 4,
SubpixelBuckets::Eight => 8,
SubpixelBuckets::Sixteen => 16,
}
}
}
impl SubpixelOffset {
pub fn bucket_with_count(frac: f32, buckets: SubpixelBuckets) -> Self {
let n = buckets.count();
let idx = (frac.rem_euclid(1.0) * n as f32).round() as u32 % n;
SubpixelOffset(idx as u8)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct SubpixelOffsetXY {
pub x: SubpixelOffset,
pub y: SubpixelOffset,
}
impl SubpixelOffsetXY {
pub fn new(x: f32, y: f32) -> Self {
Self {
x: SubpixelOffset::from_float(x),
y: SubpixelOffset::from_float(y),
}
}
}
pub fn rasterize_with_offset(
face_data: &[u8],
glyph_id: u16,
px_size: f32,
x_offset: f32,
) -> Option<(SubpixelCacheKey, Vec<u8>)> {
use ab_glyph::{Font, FontRef, GlyphId as AbGlyphId, PxScale};
let font = FontRef::try_from_slice(face_data).ok()?;
let scale = PxScale::from(px_size);
let ab_gid = AbGlyphId(glyph_id);
let frac_x = x_offset.fract().abs();
let glyph = ab_gid.with_scale_and_position(scale, ab_glyph::point(frac_x, 0.0));
let outlined = font.outline_glyph(glyph)?;
let bounds = outlined.px_bounds();
let w = bounds.width().ceil() as usize;
let h = bounds.height().ceil() as usize;
if w == 0 || h == 0 {
let key = SubpixelCacheKey::new(glyph_id, px_size, x_offset);
return Some((key, Vec::new()));
}
let mut coverage = vec![0u8; w * h];
outlined.draw(|x, y, c| {
let idx = y as usize * w + x as usize;
if idx < coverage.len() {
coverage[idx] = (c * 255.0).round() as u8;
}
});
let key = SubpixelCacheKey::new(glyph_id, px_size, x_offset);
Some((key, coverage))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn subpixel_offset_buckets() {
assert_eq!(SubpixelOffset::from_float(0.0).bucket(), 0);
assert_eq!(SubpixelOffset::from_float(0.25).bucket(), 1);
assert_eq!(SubpixelOffset::from_float(0.5).bucket(), 2);
assert_eq!(SubpixelOffset::from_float(0.75).bucket(), 3);
}
#[test]
fn subpixel_offset_as_float_round_trips() {
for i in 0..SubpixelOffset::N {
let frac = i as f32 / SubpixelOffset::N as f32;
let off = SubpixelOffset::from_float(frac);
assert!(
(off.as_float() - frac).abs() < 1e-5,
"round-trip failed for bucket {i}: got {}",
off.as_float()
);
}
}
#[test]
fn cache_key_uniqueness() {
let k0 = SubpixelCacheKey::new(36, 16.0, 0.0);
let k1 = SubpixelCacheKey::new(36, 16.0, 0.5);
assert_ne!(k0, k1, "keys with different subpixel offsets must differ");
assert_ne!(k0.subpixel, k1.subpixel);
}
#[test]
fn subpixel_xy_default() {
let xy = SubpixelOffsetXY::default();
assert_eq!(xy.x, SubpixelOffset::default());
assert_eq!(xy.y, SubpixelOffset::default());
}
#[test]
fn subpixel_xy_new_round_trip() {
let xy = SubpixelOffsetXY::new(0.25, 0.5);
assert_eq!(xy.x.bucket(), 1);
assert_eq!(xy.y.bucket(), 2);
}
}