1use crate::color::{AlphaColor, Srgb};
12use crate::glyph::FontEmbolden;
13use crate::kurbo::Join;
14use core::hash::{Hash, Hasher};
15#[cfg(not(feature = "std"))]
16use core_maths::CoreFloat as _;
17use skrifa::instance::NormalizedCoord;
18use smallvec::SmallVec;
19
20pub(crate) const SUBPIXEL_BUCKETS: u8 = 4;
25
26pub(crate) const SUBPIXEL_COLR: u8 = SUBPIXEL_BUCKETS;
34
35pub(crate) const SUBPIXEL_BITMAP: u8 = SUBPIXEL_BUCKETS + 1;
37
38#[derive(Clone, Debug)]
50pub struct GlyphCacheKey {
51 pub font_id: u64,
53 pub font_index: u32,
55 pub glyph_id: u32,
57 pub size_bits: u32,
59 pub hinted: bool,
61 pub subpixel_x: u8,
64 pub context_color: AlphaColor<Srgb>,
66 pub context_color_packed: u32,
68 pub embolden_x_bits: u32,
70 pub embolden_y_bits: u32,
72 pub embolden_join_bits: u8,
74 pub embolden_miter_limit_bits: u32,
76 pub embolden_tolerance_bits: u32,
78 pub var_coords: SmallVec<[NormalizedCoord; 4]>,
80}
81
82impl GlyphCacheKey {
83 #[inline]
88 pub fn new(
89 font_id: u64,
90 font_index: u32,
91 glyph_id: u32,
92 size: f32,
93 hinted: bool,
94 fractional_x: f32,
95 context_color: AlphaColor<Srgb>,
96 context_color_packed: u32,
97 embolden: FontEmbolden,
98 var_coords: &[NormalizedCoord],
99 ) -> Self {
100 Self {
101 font_id,
102 font_index,
103 glyph_id,
104 size_bits: size.to_bits(),
105 hinted,
106 subpixel_x: quantize_subpixel(fractional_x),
107 context_color,
108 context_color_packed,
109 embolden_x_bits: f32_bits(embolden.amount.xx),
110 embolden_y_bits: f32_bits(embolden.amount.yy),
111 embolden_join_bits: join_bits(embolden.join),
112 embolden_miter_limit_bits: f32_bits(embolden.miter_limit),
113 embolden_tolerance_bits: f32_bits(embolden.tolerance),
114 var_coords: SmallVec::from_slice(var_coords),
115 }
116 }
117}
118
119impl Hash for GlyphCacheKey {
124 #[inline]
125 fn hash<H: Hasher>(&self, state: &mut H) {
126 self.font_id.hash(state);
127 self.font_index.hash(state);
128 self.glyph_id.hash(state);
129 self.size_bits.hash(state);
130 self.hinted.hash(state);
131 self.subpixel_x.hash(state);
132 self.context_color_packed.hash(state);
133 self.embolden_x_bits.hash(state);
134 self.embolden_y_bits.hash(state);
135 self.embolden_join_bits.hash(state);
136 self.embolden_miter_limit_bits.hash(state);
137 self.embolden_tolerance_bits.hash(state);
138 }
139}
140
141impl PartialEq for GlyphCacheKey {
142 #[inline]
143 fn eq(&self, other: &Self) -> bool {
144 self.glyph_id == other.glyph_id
145 && self.subpixel_x == other.subpixel_x
146 && self.font_id == other.font_id
147 && self.font_index == other.font_index
148 && self.size_bits == other.size_bits
149 && self.hinted == other.hinted
150 && self.context_color_packed == other.context_color_packed
151 && self.embolden_x_bits == other.embolden_x_bits
152 && self.embolden_y_bits == other.embolden_y_bits
153 && self.embolden_join_bits == other.embolden_join_bits
154 && self.embolden_miter_limit_bits == other.embolden_miter_limit_bits
155 && self.embolden_tolerance_bits == other.embolden_tolerance_bits
156 }
157}
158
159impl Eq for GlyphCacheKey {}
160
161#[inline(always)]
162fn join_bits(join: Join) -> u8 {
163 match join {
164 Join::Bevel => 0,
165 Join::Miter => 1,
166 Join::Round => 2,
167 }
168}
169
170#[expect(
171 clippy::cast_possible_truncation,
172 reason = "Cache keys intentionally store embolden parameters at f32 precision."
173)]
174#[inline(always)]
175fn f32_bits(value: f64) -> u32 {
176 (value as f32).to_bits()
177}
178
179#[inline]
181pub(crate) fn pack_color(color: AlphaColor<Srgb>) -> u32 {
182 color.premultiply().to_rgba8().to_u32()
183}
184
185#[expect(
192 clippy::cast_possible_truncation,
193 reason = "result is clamped to SUBPIXEL_BUCKETS-1 which fits in u8"
194)]
195#[inline]
196fn quantize_subpixel(frac: f32) -> u8 {
197 let normalized = frac.fract();
198 let normalized = if normalized < 0.0 {
199 normalized + 1.0
200 } else {
201 normalized
202 };
203 ((normalized * SUBPIXEL_BUCKETS as f32).round() as u8).min(SUBPIXEL_BUCKETS - 1)
204}
205
206#[inline]
208pub fn subpixel_offset(quantized: u8) -> f32 {
209 quantized as f32 / SUBPIXEL_BUCKETS as f32
210}
211
212#[cfg(test)]
213mod tests {
214 use crate::color::palette::css::BLACK;
215
216 use super::*;
217
218 #[test]
219 fn test_quantize_subpixel() {
220 assert_eq!(quantize_subpixel(0.0), 0);
222 assert_eq!(quantize_subpixel(0.1), 0);
223 assert_eq!(quantize_subpixel(0.2), 1);
224 assert_eq!(quantize_subpixel(0.25), 1);
225 assert_eq!(quantize_subpixel(0.4), 2);
226 assert_eq!(quantize_subpixel(0.5), 2);
227 assert_eq!(quantize_subpixel(0.6), 2);
228 assert_eq!(quantize_subpixel(0.7), 3);
229 assert_eq!(quantize_subpixel(0.75), 3);
230 assert_eq!(quantize_subpixel(0.9), 3);
231 assert_eq!(quantize_subpixel(1.0), 0);
232 }
233
234 #[test]
235 fn test_subpixel_offset() {
236 assert_eq!(subpixel_offset(0), 0.0);
237 assert_eq!(subpixel_offset(1), 0.25);
238 assert_eq!(subpixel_offset(2), 0.5);
239 assert_eq!(subpixel_offset(3), 0.75);
240 }
241
242 #[test]
243 fn test_key_equality() {
244 let packed = pack_color(BLACK);
245 let key1 = GlyphCacheKey::new(
246 1,
247 0,
248 42,
249 16.0,
250 true,
251 0.3,
252 BLACK,
253 packed,
254 FontEmbolden::default(),
255 &[],
256 );
257 let key2 = GlyphCacheKey::new(
258 1,
259 0,
260 42,
261 16.0,
262 true,
263 0.3,
264 BLACK,
265 packed,
266 FontEmbolden::default(),
267 &[],
268 );
269 assert_eq!(key1, key2);
270 }
271
272 #[test]
273 fn test_outline_colr_bitmap_keys_never_collide() {
274 let packed = pack_color(BLACK);
275 let outline_key = GlyphCacheKey::new(
276 1,
277 0,
278 42,
279 16.0,
280 false,
281 0.0,
282 BLACK,
283 packed,
284 FontEmbolden::default(),
285 &[],
286 );
287 let colr_key = GlyphCacheKey {
288 font_id: 1,
289 font_index: 0,
290 glyph_id: 42,
291 size_bits: 16.0_f32.to_bits(),
292 hinted: false,
293 subpixel_x: SUBPIXEL_COLR,
294 context_color: BLACK,
295 context_color_packed: packed,
296 embolden_x_bits: 0,
297 embolden_y_bits: 0,
298 embolden_join_bits: join_bits(Join::Miter),
299 embolden_miter_limit_bits: 4.0_f32.to_bits(),
300 embolden_tolerance_bits: 0.1_f32.to_bits(),
301 var_coords: SmallVec::new(),
302 };
303 let bitmap_key = GlyphCacheKey {
304 font_id: 1,
305 font_index: 0,
306 glyph_id: 42,
307 size_bits: 16.0_f32.to_bits(),
308 hinted: false,
309 subpixel_x: SUBPIXEL_BITMAP,
310 context_color: BLACK,
311 context_color_packed: packed,
312 embolden_x_bits: 0,
313 embolden_y_bits: 0,
314 embolden_join_bits: join_bits(Join::Miter),
315 embolden_miter_limit_bits: 4.0_f32.to_bits(),
316 embolden_tolerance_bits: 0.1_f32.to_bits(),
317 var_coords: SmallVec::new(),
318 };
319 assert_ne!(outline_key, colr_key);
320 assert_ne!(outline_key, bitmap_key);
321 assert_ne!(colr_key, bitmap_key);
322 }
323
324 #[test]
325 fn test_sentinels_unreachable_by_quantize() {
326 for i in 0..=255_u8 {
327 let frac = i as f32 / 255.0;
328 let bucket = quantize_subpixel(frac);
329 assert!(bucket < SUBPIXEL_BUCKETS, "bucket {bucket} for frac {frac}");
330 }
331 }
332
333 #[test]
334 fn test_var_coords_excluded_from_equality() {
335 let packed = pack_color(BLACK);
336 let key1 = GlyphCacheKey::new(
339 1,
340 0,
341 42,
342 16.0,
343 true,
344 0.3,
345 BLACK,
346 packed,
347 FontEmbolden::default(),
348 &[],
349 );
350 let key2 = GlyphCacheKey::new(
351 1,
352 0,
353 42,
354 16.0,
355 true,
356 0.3,
357 BLACK,
358 packed,
359 FontEmbolden::default(),
360 &[NormalizedCoord::from_bits(100)],
361 );
362 assert_eq!(key1, key2);
363 }
364}