oxidize_pdf/fonts/
font_cache.rs1use super::Font;
4use crate::{PdfError, Result};
5use std::collections::HashMap;
6use std::sync::{Arc, RwLock};
7
8#[derive(Debug, Clone)]
10pub struct FontCache {
11 fonts: Arc<RwLock<HashMap<String, Arc<Font>>>>,
12}
13
14impl FontCache {
15 pub fn new() -> Self {
17 FontCache {
18 fonts: Arc::new(RwLock::new(HashMap::new())),
19 }
20 }
21
22 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 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 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 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 pub fn clear(&self) {
65 if let Ok(mut fonts) = self.fonts.write() {
66 fonts.clear();
67 }
68 }
70
71 pub fn len(&self) -> usize {
73 let Ok(fonts) = self.fonts.read() else {
74 return 0;
75 };
76 fonts.len()
77 }
78
79 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 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 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 let retrieved = cache.get_font("Font1").unwrap();
137 assert_eq!(retrieved.name, "Font1");
138
139 let mut names = cache.font_names();
141 names.sort();
142 assert_eq!(names, vec!["Font1", "Font2"]);
143
144 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 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 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 let font1 = create_test_font("Original");
188 cache.add_font("TestFont", font1).unwrap();
189
190 let mut font2 = create_test_font("Replacement");
192 font2.metrics.units_per_em = 2048; cache.add_font("TestFont", font2).unwrap();
194
195 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); }
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 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 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 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 for handle in handles {
252 handle.join().unwrap();
253 }
254
255 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 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(); assert_eq!(names, vec!["Alpha", "Middle", "Zebra"]);
282 }
283
284 #[test]
285 fn test_clear_and_reuse() {
286 let cache = FontCache::new();
287
288 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 cache.clear();
295 assert_eq!(cache.len(), 0);
296 assert!(cache.is_empty());
297
298 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 let arc1 = cache.get_font("SharedFont").unwrap();
313 let arc2 = cache.get_font("SharedFont").unwrap();
314
315 assert!(Arc::ptr_eq(&arc1, &arc2));
317 }
318
319 #[test]
320 fn test_cache_with_special_names() {
321 let cache = FontCache::new();
322
323 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 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 cache.clear();
362 assert_eq!(cache.len(), 0);
363 }
364}