Skip to main content

rpdfium_render/
cfx_glyphcache.rs

1// Derived from PDFium's rasterized glyph cache concepts
2// Original: Copyright 2014 The PDFium Authors
3// Licensed under BSD-3-Clause / Apache-2.0
4// See pdfium-upstream/LICENSE for the original license.
5
6//! Rasterized glyph cache — caches pre-rendered glyph bitmaps for fast
7//! text rendering at common sizes.
8//!
9//! Glyph outlines are rasterized to small alpha bitmaps and cached by
10//! (font identity, glyph ID, quantized size). This avoids re-rasterizing
11//! the same glyph at the same size for every occurrence.
12
13use std::collections::HashMap;
14
15/// Maximum number of entries in the rasterized glyph cache.
16const MAX_RASTER_CACHE: usize = 2048;
17
18/// Maximum font size (in points) for which rasterized glyphs are cached.
19/// Larger sizes benefit less from caching and use more memory per glyph.
20pub const MAX_CACHED_SIZE: f32 = 48.0;
21
22/// Cache key identifying a specific glyph at a specific size.
23#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
24pub struct RasterGlyphKey {
25    /// Identity of the font (pointer address or hash).
26    pub font_id: usize,
27    /// Glyph ID within the font.
28    pub glyph_id: u16,
29    /// Quantized size (to nearest 0.5pt, encoded as `(size * 2).round() as u32`).
30    pub size_q: u32,
31}
32
33/// A pre-rasterized glyph bitmap (alpha channel only).
34#[derive(Debug, Clone)]
35pub struct RasterGlyph {
36    /// Width of the rasterized bitmap in pixels.
37    pub width: u32,
38    /// Height of the rasterized bitmap in pixels.
39    pub height: u32,
40    /// Horizontal bearing (x offset from pen position to left edge of bitmap).
41    pub bearing_x: i32,
42    /// Vertical bearing (y offset from pen position to top edge of bitmap).
43    pub bearing_y: i32,
44    /// Alpha channel data, row-major, one byte per pixel.
45    pub alpha: Vec<u8>,
46}
47
48/// Cache for pre-rasterized glyph alpha bitmaps.
49///
50/// Stores rendered glyphs indexed by font, glyph ID, and quantized size.
51/// `None` entries mean "this glyph was looked up but has no outline" (negative cache).
52#[derive(Debug)]
53pub struct RasterizedGlyphCache {
54    cache: HashMap<RasterGlyphKey, Option<RasterGlyph>>,
55}
56
57impl Default for RasterizedGlyphCache {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63impl RasterizedGlyphCache {
64    /// Create a new empty cache.
65    pub fn new() -> Self {
66        Self {
67            cache: HashMap::new(),
68        }
69    }
70
71    /// Quantize a font size to the nearest 0.5pt for cache key generation.
72    pub fn quantize_size(size: f32) -> u32 {
73        (size * 2.0).round() as u32
74    }
75
76    /// Look up a cached glyph. Returns:
77    /// - `Some(Some(glyph))` — cached rasterized glyph
78    /// - `Some(None)` — negative cache hit (glyph has no outline)
79    /// - `None` — cache miss
80    pub fn get(&self, key: &RasterGlyphKey) -> Option<&Option<RasterGlyph>> {
81        self.cache.get(key)
82    }
83
84    /// Insert a rasterized glyph (or negative cache entry) into the cache.
85    ///
86    /// If the cache is full, evicts approximately 25% of entries.
87    pub fn insert(&mut self, key: RasterGlyphKey, glyph: Option<RasterGlyph>) {
88        if self.cache.len() >= MAX_RASTER_CACHE {
89            self.evict();
90        }
91        self.cache.insert(key, glyph);
92    }
93
94    /// Returns the number of entries in the cache.
95    pub fn len(&self) -> usize {
96        self.cache.len()
97    }
98
99    /// Returns `true` if the cache is empty.
100    pub fn is_empty(&self) -> bool {
101        self.cache.is_empty()
102    }
103
104    /// Evict approximately 25% of entries when the cache is full.
105    fn evict(&mut self) {
106        let target = self.cache.len() / 4;
107        let keys: Vec<RasterGlyphKey> = self.cache.keys().take(target).copied().collect();
108        for key in keys {
109            self.cache.remove(&key);
110        }
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn test_quantize_size_rounds() {
120        assert_eq!(RasterizedGlyphCache::quantize_size(12.0), 24);
121        assert_eq!(RasterizedGlyphCache::quantize_size(12.5), 25);
122        assert_eq!(RasterizedGlyphCache::quantize_size(12.25), 25); // rounds to 12.5
123        assert_eq!(RasterizedGlyphCache::quantize_size(12.74), 25);
124        assert_eq!(RasterizedGlyphCache::quantize_size(0.0), 0);
125    }
126
127    #[test]
128    fn test_cache_miss_returns_none() {
129        let cache = RasterizedGlyphCache::new();
130        let key = RasterGlyphKey {
131            font_id: 1,
132            glyph_id: 42,
133            size_q: 24,
134        };
135        assert!(cache.get(&key).is_none());
136    }
137
138    #[test]
139    fn test_cache_insert_and_hit() {
140        let mut cache = RasterizedGlyphCache::new();
141        let key = RasterGlyphKey {
142            font_id: 1,
143            glyph_id: 42,
144            size_q: 24,
145        };
146        let glyph = RasterGlyph {
147            width: 10,
148            height: 12,
149            bearing_x: 1,
150            bearing_y: 11,
151            alpha: vec![128; 120],
152        };
153        cache.insert(key, Some(glyph));
154        assert_eq!(cache.len(), 1);
155        let result = cache.get(&key);
156        assert!(result.is_some());
157        let glyph = result.unwrap().as_ref().unwrap();
158        assert_eq!(glyph.width, 10);
159        assert_eq!(glyph.height, 12);
160    }
161
162    #[test]
163    fn test_negative_cache_entry() {
164        let mut cache = RasterizedGlyphCache::new();
165        let key = RasterGlyphKey {
166            font_id: 1,
167            glyph_id: 999,
168            size_q: 24,
169        };
170        cache.insert(key, None);
171        assert_eq!(cache.len(), 1);
172        let result = cache.get(&key);
173        assert!(result.is_some());
174        assert!(result.unwrap().is_none());
175    }
176
177    #[test]
178    fn test_cache_eviction() {
179        let mut cache = RasterizedGlyphCache::new();
180        // Fill beyond MAX_RASTER_CACHE
181        for i in 0..MAX_RASTER_CACHE + 10 {
182            let key = RasterGlyphKey {
183                font_id: 1,
184                glyph_id: i as u16,
185                size_q: 24,
186            };
187            cache.insert(key, None);
188        }
189        // After eviction, cache should be smaller than MAX_RASTER_CACHE + 10
190        assert!(cache.len() <= MAX_RASTER_CACHE);
191    }
192
193    #[test]
194    fn test_cache_empty_and_len() {
195        let mut cache = RasterizedGlyphCache::new();
196        assert!(cache.is_empty());
197        assert_eq!(cache.len(), 0);
198        let key = RasterGlyphKey {
199            font_id: 1,
200            glyph_id: 1,
201            size_q: 24,
202        };
203        cache.insert(key, None);
204        assert!(!cache.is_empty());
205        assert_eq!(cache.len(), 1);
206    }
207
208    #[test]
209    fn test_different_sizes_different_keys() {
210        let mut cache = RasterizedGlyphCache::new();
211        let key1 = RasterGlyphKey {
212            font_id: 1,
213            glyph_id: 42,
214            size_q: 24, // 12pt
215        };
216        let key2 = RasterGlyphKey {
217            font_id: 1,
218            glyph_id: 42,
219            size_q: 48, // 24pt
220        };
221        cache.insert(
222            key1,
223            Some(RasterGlyph {
224                width: 10,
225                height: 12,
226                bearing_x: 1,
227                bearing_y: 11,
228                alpha: vec![],
229            }),
230        );
231        cache.insert(
232            key2,
233            Some(RasterGlyph {
234                width: 20,
235                height: 24,
236                bearing_x: 2,
237                bearing_y: 22,
238                alpha: vec![],
239            }),
240        );
241        assert_eq!(cache.len(), 2);
242        assert_eq!(cache.get(&key1).unwrap().as_ref().unwrap().width, 10);
243        assert_eq!(cache.get(&key2).unwrap().as_ref().unwrap().width, 20);
244    }
245
246    #[test]
247    fn test_raster_glyph_key_eq_and_hash() {
248        let key1 = RasterGlyphKey {
249            font_id: 1,
250            glyph_id: 42,
251            size_q: 24,
252        };
253        let key2 = RasterGlyphKey {
254            font_id: 1,
255            glyph_id: 42,
256            size_q: 24,
257        };
258        assert_eq!(key1, key2);
259
260        let mut set = std::collections::HashSet::new();
261        set.insert(key1);
262        assert!(set.contains(&key2));
263    }
264
265    #[test]
266    fn test_max_cached_size_constant() {
267        assert_eq!(MAX_CACHED_SIZE, 48.0);
268    }
269}