use crate::color::{AlphaColor, Srgb};
use crate::glyph::FontEmbolden;
use crate::kurbo::Join;
use core::hash::{Hash, Hasher};
#[cfg(not(feature = "std"))]
use core_maths::CoreFloat as _;
use skrifa::instance::NormalizedCoord;
use smallvec::SmallVec;
pub(crate) const SUBPIXEL_BUCKETS: u8 = 4;
pub(crate) const SUBPIXEL_COLR: u8 = SUBPIXEL_BUCKETS;
pub(crate) const SUBPIXEL_BITMAP: u8 = SUBPIXEL_BUCKETS + 1;
#[derive(Clone, Debug)]
pub struct GlyphCacheKey {
pub font_id: u64,
pub font_index: u32,
pub glyph_id: u32,
pub size_bits: u32,
pub hinted: bool,
pub subpixel_x: u8,
pub context_color: AlphaColor<Srgb>,
pub context_color_packed: u32,
pub embolden_x_bits: u32,
pub embolden_y_bits: u32,
pub embolden_join_bits: u8,
pub embolden_miter_limit_bits: u32,
pub embolden_tolerance_bits: u32,
pub var_coords: SmallVec<[NormalizedCoord; 4]>,
}
impl GlyphCacheKey {
#[inline]
pub fn new(
font_id: u64,
font_index: u32,
glyph_id: u32,
size: f32,
hinted: bool,
fractional_x: f32,
context_color: AlphaColor<Srgb>,
context_color_packed: u32,
embolden: FontEmbolden,
var_coords: &[NormalizedCoord],
) -> Self {
Self {
font_id,
font_index,
glyph_id,
size_bits: size.to_bits(),
hinted,
subpixel_x: quantize_subpixel(fractional_x),
context_color,
context_color_packed,
embolden_x_bits: f32_bits(embolden.amount.xx),
embolden_y_bits: f32_bits(embolden.amount.yy),
embolden_join_bits: join_bits(embolden.join),
embolden_miter_limit_bits: f32_bits(embolden.miter_limit),
embolden_tolerance_bits: f32_bits(embolden.tolerance),
var_coords: SmallVec::from_slice(var_coords),
}
}
}
impl Hash for GlyphCacheKey {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
self.font_id.hash(state);
self.font_index.hash(state);
self.glyph_id.hash(state);
self.size_bits.hash(state);
self.hinted.hash(state);
self.subpixel_x.hash(state);
self.context_color_packed.hash(state);
self.embolden_x_bits.hash(state);
self.embolden_y_bits.hash(state);
self.embolden_join_bits.hash(state);
self.embolden_miter_limit_bits.hash(state);
self.embolden_tolerance_bits.hash(state);
}
}
impl PartialEq for GlyphCacheKey {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.glyph_id == other.glyph_id
&& self.subpixel_x == other.subpixel_x
&& self.font_id == other.font_id
&& self.font_index == other.font_index
&& self.size_bits == other.size_bits
&& self.hinted == other.hinted
&& self.context_color_packed == other.context_color_packed
&& self.embolden_x_bits == other.embolden_x_bits
&& self.embolden_y_bits == other.embolden_y_bits
&& self.embolden_join_bits == other.embolden_join_bits
&& self.embolden_miter_limit_bits == other.embolden_miter_limit_bits
&& self.embolden_tolerance_bits == other.embolden_tolerance_bits
}
}
impl Eq for GlyphCacheKey {}
#[inline(always)]
fn join_bits(join: Join) -> u8 {
match join {
Join::Bevel => 0,
Join::Miter => 1,
Join::Round => 2,
}
}
#[expect(
clippy::cast_possible_truncation,
reason = "Cache keys intentionally store embolden parameters at f32 precision."
)]
#[inline(always)]
fn f32_bits(value: f64) -> u32 {
(value as f32).to_bits()
}
#[inline]
pub(crate) fn pack_color(color: AlphaColor<Srgb>) -> u32 {
color.premultiply().to_rgba8().to_u32()
}
#[expect(
clippy::cast_possible_truncation,
reason = "result is clamped to SUBPIXEL_BUCKETS-1 which fits in u8"
)]
#[inline]
fn quantize_subpixel(frac: f32) -> u8 {
let normalized = frac.fract();
let normalized = if normalized < 0.0 {
normalized + 1.0
} else {
normalized
};
((normalized * SUBPIXEL_BUCKETS as f32).round() as u8).min(SUBPIXEL_BUCKETS - 1)
}
#[inline]
pub fn subpixel_offset(quantized: u8) -> f32 {
quantized as f32 / SUBPIXEL_BUCKETS as f32
}
#[cfg(test)]
mod tests {
use crate::color::palette::css::BLACK;
use super::*;
#[test]
fn test_quantize_subpixel() {
assert_eq!(quantize_subpixel(0.0), 0);
assert_eq!(quantize_subpixel(0.1), 0);
assert_eq!(quantize_subpixel(0.2), 1);
assert_eq!(quantize_subpixel(0.25), 1);
assert_eq!(quantize_subpixel(0.4), 2);
assert_eq!(quantize_subpixel(0.5), 2);
assert_eq!(quantize_subpixel(0.6), 2);
assert_eq!(quantize_subpixel(0.7), 3);
assert_eq!(quantize_subpixel(0.75), 3);
assert_eq!(quantize_subpixel(0.9), 3);
assert_eq!(quantize_subpixel(1.0), 0);
}
#[test]
fn test_subpixel_offset() {
assert_eq!(subpixel_offset(0), 0.0);
assert_eq!(subpixel_offset(1), 0.25);
assert_eq!(subpixel_offset(2), 0.5);
assert_eq!(subpixel_offset(3), 0.75);
}
#[test]
fn test_key_equality() {
let packed = pack_color(BLACK);
let key1 = GlyphCacheKey::new(
1,
0,
42,
16.0,
true,
0.3,
BLACK,
packed,
FontEmbolden::default(),
&[],
);
let key2 = GlyphCacheKey::new(
1,
0,
42,
16.0,
true,
0.3,
BLACK,
packed,
FontEmbolden::default(),
&[],
);
assert_eq!(key1, key2);
}
#[test]
fn test_outline_colr_bitmap_keys_never_collide() {
let packed = pack_color(BLACK);
let outline_key = GlyphCacheKey::new(
1,
0,
42,
16.0,
false,
0.0,
BLACK,
packed,
FontEmbolden::default(),
&[],
);
let colr_key = GlyphCacheKey {
font_id: 1,
font_index: 0,
glyph_id: 42,
size_bits: 16.0_f32.to_bits(),
hinted: false,
subpixel_x: SUBPIXEL_COLR,
context_color: BLACK,
context_color_packed: packed,
embolden_x_bits: 0,
embolden_y_bits: 0,
embolden_join_bits: join_bits(Join::Miter),
embolden_miter_limit_bits: 4.0_f32.to_bits(),
embolden_tolerance_bits: 0.1_f32.to_bits(),
var_coords: SmallVec::new(),
};
let bitmap_key = GlyphCacheKey {
font_id: 1,
font_index: 0,
glyph_id: 42,
size_bits: 16.0_f32.to_bits(),
hinted: false,
subpixel_x: SUBPIXEL_BITMAP,
context_color: BLACK,
context_color_packed: packed,
embolden_x_bits: 0,
embolden_y_bits: 0,
embolden_join_bits: join_bits(Join::Miter),
embolden_miter_limit_bits: 4.0_f32.to_bits(),
embolden_tolerance_bits: 0.1_f32.to_bits(),
var_coords: SmallVec::new(),
};
assert_ne!(outline_key, colr_key);
assert_ne!(outline_key, bitmap_key);
assert_ne!(colr_key, bitmap_key);
}
#[test]
fn test_sentinels_unreachable_by_quantize() {
for i in 0..=255_u8 {
let frac = i as f32 / 255.0;
let bucket = quantize_subpixel(frac);
assert!(bucket < SUBPIXEL_BUCKETS, "bucket {bucket} for frac {frac}");
}
}
#[test]
fn test_var_coords_excluded_from_equality() {
let packed = pack_color(BLACK);
let key1 = GlyphCacheKey::new(
1,
0,
42,
16.0,
true,
0.3,
BLACK,
packed,
FontEmbolden::default(),
&[],
);
let key2 = GlyphCacheKey::new(
1,
0,
42,
16.0,
true,
0.3,
BLACK,
packed,
FontEmbolden::default(),
&[NormalizedCoord::from_bits(100)],
);
assert_eq!(key1, key2);
}
}