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