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> {
49 let Ok(fonts) = self.fonts.read() else {
50 return Vec::new();
51 };
52 fonts.keys().cloned().collect()
53 }
54
55 pub fn clear(&self) {
57 if let Ok(mut fonts) = self.fonts.write() {
58 fonts.clear();
59 }
60 }
62
63 pub fn len(&self) -> usize {
65 let Ok(fonts) = self.fonts.read() else {
66 return 0;
67 };
68 fonts.len()
69 }
70
71 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 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 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 let retrieved = cache.get_font("Font1").unwrap();
129 assert_eq!(retrieved.name, "Font1");
130
131 let mut names = cache.font_names();
133 names.sort();
134 assert_eq!(names, vec!["Font1", "Font2"]);
135
136 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 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 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 let font1 = create_test_font("Original");
180 cache.add_font("TestFont", font1).unwrap();
181
182 let mut font2 = create_test_font("Replacement");
184 font2.metrics.units_per_em = 2048; cache.add_font("TestFont", font2).unwrap();
186
187 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); }
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 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 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 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 for handle in handles {
244 handle.join().unwrap();
245 }
246
247 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 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(); assert_eq!(names, vec!["Alpha", "Middle", "Zebra"]);
274 }
275
276 #[test]
277 fn test_clear_and_reuse() {
278 let cache = FontCache::new();
279
280 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 cache.clear();
287 assert_eq!(cache.len(), 0);
288 assert!(cache.is_empty());
289
290 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 let arc1 = cache.get_font("SharedFont").unwrap();
305 let arc2 = cache.get_font("SharedFont").unwrap();
306
307 assert!(Arc::ptr_eq(&arc1, &arc2));
309 }
310
311 #[test]
312 fn test_cache_with_special_names() {
313 let cache = FontCache::new();
314
315 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 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 cache.clear();
354 assert_eq!(cache.len(), 0);
355 }
356}