Skip to main content

oxidize_pdf/fonts/
font_cache.rs

1//! Font caching for efficient font management
2
3use super::Font;
4use crate::{PdfError, Result};
5use std::collections::HashMap;
6use std::sync::{Arc, RwLock};
7
8/// Thread-safe font cache
9#[derive(Debug, Clone)]
10pub struct FontCache {
11    fonts: Arc<RwLock<HashMap<String, Arc<Font>>>>,
12}
13
14impl FontCache {
15    /// Create a new font cache
16    pub fn new() -> Self {
17        FontCache {
18            fonts: Arc::new(RwLock::new(HashMap::new())),
19        }
20    }
21
22    /// Add a font to the cache
23    pub fn add_font(&self, name: impl Into<String>, font: Font) -> Result<()> {
24        let name = name.into();
25        let mut fonts = self
26            .fonts
27            .write()
28            .map_err(|_| PdfError::InvalidOperation("Font cache lock is poisoned".to_string()))?;
29        fonts.insert(name, Arc::new(font));
30        Ok(())
31    }
32
33    /// Get a font from the cache
34    pub fn get_font(&self, name: &str) -> Option<Arc<Font>> {
35        let fonts = self.fonts.read().ok()?;
36        fonts.get(name).cloned()
37    }
38
39    /// Check if a font exists in the cache
40    pub fn has_font(&self, name: &str) -> bool {
41        let Ok(fonts) = self.fonts.read() else {
42            return false;
43        };
44        fonts.contains_key(name)
45    }
46
47    /// Get all font names in the cache
48    pub fn font_names(&self) -> Vec<String> {
49        let Ok(fonts) = self.fonts.read() else {
50            return Vec::new();
51        };
52        fonts.keys().cloned().collect()
53    }
54
55    /// Clear the cache
56    pub fn clear(&self) {
57        if let Ok(mut fonts) = self.fonts.write() {
58            fonts.clear();
59        }
60        // Silently ignore if lock is poisoned
61    }
62
63    /// Get the number of cached fonts
64    pub fn len(&self) -> usize {
65        let Ok(fonts) = self.fonts.read() else {
66            return 0;
67        };
68        fonts.len()
69    }
70
71    /// Check if the cache is empty
72    pub fn is_empty(&self) -> bool {
73        let Ok(fonts) = self.fonts.read() else {
74            return true;
75        };
76        fonts.is_empty()
77    }
78}
79
80impl Default for FontCache {
81    fn default() -> Self {
82        Self::new()
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use crate::fonts::{FontDescriptor, FontFormat, FontMetrics, GlyphMapping};
90
91    fn create_test_font(name: &str) -> Font {
92        Font {
93            name: name.to_string(),
94            data: vec![0; 100],
95            format: FontFormat::TrueType,
96            metrics: FontMetrics {
97                units_per_em: 1000,
98                ascent: 800,
99                descent: -200,
100                line_gap: 200,
101                cap_height: 700,
102                x_height: 500,
103            },
104            descriptor: FontDescriptor::new(name),
105            glyph_mapping: GlyphMapping::default(),
106        }
107    }
108
109    #[test]
110    fn test_font_cache_basic_operations() {
111        let cache = FontCache::new();
112
113        // Add fonts
114        let font1 = create_test_font("Font1");
115        let font2 = create_test_font("Font2");
116
117        cache.add_font("Font1", font1).unwrap();
118        cache.add_font("Font2", font2).unwrap();
119
120        // Check cache state
121        assert_eq!(cache.len(), 2);
122        assert!(!cache.is_empty());
123        assert!(cache.has_font("Font1"));
124        assert!(cache.has_font("Font2"));
125        assert!(!cache.has_font("Font3"));
126
127        // Get fonts
128        let retrieved = cache.get_font("Font1").unwrap();
129        assert_eq!(retrieved.name, "Font1");
130
131        // Get font names
132        let mut names = cache.font_names();
133        names.sort();
134        assert_eq!(names, vec!["Font1", "Font2"]);
135
136        // Clear cache
137        cache.clear();
138        assert_eq!(cache.len(), 0);
139        assert!(cache.is_empty());
140    }
141
142    #[test]
143    fn test_font_cache_thread_safety() {
144        use std::thread;
145
146        let cache = FontCache::new();
147        let cache_clone = cache.clone();
148
149        // Add font from another thread
150        let handle = thread::spawn(move || {
151            let font = create_test_font("ThreadFont");
152            cache_clone.add_font("ThreadFont", font).unwrap();
153        });
154
155        handle.join().unwrap();
156
157        // Check font was added
158        assert!(cache.has_font("ThreadFont"));
159    }
160
161    #[test]
162    fn test_font_cache_default() {
163        let cache = FontCache::default();
164        assert!(cache.is_empty());
165        assert_eq!(cache.len(), 0);
166    }
167
168    #[test]
169    fn test_get_nonexistent_font() {
170        let cache = FontCache::new();
171        assert!(cache.get_font("NonExistent").is_none());
172    }
173
174    #[test]
175    fn test_replace_font() {
176        let cache = FontCache::new();
177
178        // Add original font
179        let font1 = create_test_font("Original");
180        cache.add_font("TestFont", font1).unwrap();
181
182        // Replace with new font
183        let mut font2 = create_test_font("Replacement");
184        font2.metrics.units_per_em = 2048; // Different value
185        cache.add_font("TestFont", font2).unwrap();
186
187        // Verify replacement
188        let retrieved = cache.get_font("TestFont").unwrap();
189        assert_eq!(retrieved.name, "Replacement");
190        assert_eq!(retrieved.metrics.units_per_em, 2048);
191        assert_eq!(cache.len(), 1); // Still only one font
192    }
193
194    #[test]
195    fn test_multiple_threads_reading() {
196        use std::thread;
197
198        let cache = FontCache::new();
199        let font = create_test_font("SharedFont");
200        cache.add_font("SharedFont", font).unwrap();
201
202        let mut handles = vec![];
203
204        // Spawn multiple reader threads
205        for i in 0..5 {
206            let cache_clone = cache.clone();
207            let handle = thread::spawn(move || {
208                for _ in 0..10 {
209                    let font = cache_clone.get_font("SharedFont");
210                    assert!(font.is_some());
211                    assert_eq!(font.unwrap().name, "SharedFont");
212                }
213                i
214            });
215            handles.push(handle);
216        }
217
218        // Wait for all threads to complete
219        for handle in handles {
220            handle.join().unwrap();
221        }
222    }
223
224    #[test]
225    fn test_multiple_threads_writing() {
226        use std::thread;
227
228        let cache = FontCache::new();
229        let mut handles = vec![];
230
231        // Spawn multiple writer threads
232        for i in 0..5 {
233            let cache_clone = cache.clone();
234            let handle = thread::spawn(move || {
235                let font_name = format!("Font{}", i);
236                let font = create_test_font(&font_name);
237                cache_clone.add_font(&font_name, font).unwrap();
238            });
239            handles.push(handle);
240        }
241
242        // Wait for all threads to complete
243        for handle in handles {
244            handle.join().unwrap();
245        }
246
247        // Verify all fonts were added
248        assert_eq!(cache.len(), 5);
249        for i in 0..5 {
250            assert!(cache.has_font(&format!("Font{}", i)));
251        }
252    }
253
254    #[test]
255    fn test_font_names_empty_cache() {
256        let cache = FontCache::new();
257        assert_eq!(cache.font_names(), Vec::<String>::new());
258    }
259
260    #[test]
261    fn test_font_names_ordering() {
262        let cache = FontCache::new();
263
264        // Add fonts in non-alphabetical order
265        cache.add_font("Zebra", create_test_font("Zebra")).unwrap();
266        cache.add_font("Alpha", create_test_font("Alpha")).unwrap();
267        cache
268            .add_font("Middle", create_test_font("Middle"))
269            .unwrap();
270
271        let mut names = cache.font_names();
272        names.sort(); // Sort for consistent testing
273        assert_eq!(names, vec!["Alpha", "Middle", "Zebra"]);
274    }
275
276    #[test]
277    fn test_clear_and_reuse() {
278        let cache = FontCache::new();
279
280        // Add fonts
281        cache.add_font("Font1", create_test_font("Font1")).unwrap();
282        cache.add_font("Font2", create_test_font("Font2")).unwrap();
283        assert_eq!(cache.len(), 2);
284
285        // Clear
286        cache.clear();
287        assert_eq!(cache.len(), 0);
288        assert!(cache.is_empty());
289
290        // Reuse cache
291        cache.add_font("Font3", create_test_font("Font3")).unwrap();
292        assert_eq!(cache.len(), 1);
293        assert!(cache.has_font("Font3"));
294        assert!(!cache.has_font("Font1"));
295    }
296
297    #[test]
298    fn test_arc_sharing() {
299        let cache = FontCache::new();
300        let font = create_test_font("SharedFont");
301        cache.add_font("SharedFont", font).unwrap();
302
303        // Get multiple Arc references
304        let arc1 = cache.get_font("SharedFont").unwrap();
305        let arc2 = cache.get_font("SharedFont").unwrap();
306
307        // Both should point to the same font
308        assert!(Arc::ptr_eq(&arc1, &arc2));
309    }
310
311    #[test]
312    fn test_cache_with_special_names() {
313        let cache = FontCache::new();
314
315        // Test with various special characters in names
316        let special_names = vec![
317            "Font-Name",
318            "Font.Name",
319            "Font Name",
320            "Font_Name",
321            "Font/Name",
322            "Font@Name",
323            "日本語",
324            "😀Font",
325        ];
326
327        for name in &special_names {
328            cache.add_font(*name, create_test_font(name)).unwrap();
329        }
330
331        assert_eq!(cache.len(), special_names.len());
332
333        for name in &special_names {
334            assert!(cache.has_font(name));
335            let font = cache.get_font(name).unwrap();
336            assert_eq!(font.name, *name);
337        }
338    }
339
340    #[test]
341    fn test_cache_memory_efficiency() {
342        let cache = FontCache::new();
343
344        // Add same font data with different names
345        for i in 0..100 {
346            let font = create_test_font("TestFont");
347            cache.add_font(format!("Font{}", i), font).unwrap();
348        }
349
350        assert_eq!(cache.len(), 100);
351
352        // Clear should free all references
353        cache.clear();
354        assert_eq!(cache.len(), 0);
355    }
356}