Skip to main content

rpdfium_render/
type3_cache.rs

1// Derived from PDFium's cpdf_type3cache.cpp
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//! Cache for Type 3 glyph outlines.
7//!
8//! Corresponds to `CPDF_Type3Cache` in PDFium's `cpdf_type3cache.h`.
9//! The per-glyph entry type is in [`type3_glyph_map`](crate::type3_glyph_map),
10//! corresponding to `CPDF_Type3GlyphMap` in PDFium's `cpdf_type3glyphmap.h`.
11
12use dashmap::DashMap;
13use rpdfium_core::ObjectId;
14
15pub use crate::type3_glyph_map::Type3GlyphEntry;
16
17/// Default maximum number of entries in the Type 3 glyph cache.
18const DEFAULT_TYPE3_CACHE_CAPACITY: usize = 1024;
19
20/// Cache key for Type 3 glyphs: (stream ObjectId, character code, font matrix as bits).
21///
22/// The font matrix is included because the same Type 3 font object may be
23/// referenced at different sizes or transforms, producing different glyphs.
24type Type3CacheKey = (ObjectId, u8, [u32; 6]);
25
26/// Convert a font matrix `[f32; 6]` to `[u32; 6]` via `f32::to_bits()`.
27fn matrix_to_bits(m: &[f32; 6]) -> [u32; 6] {
28    [
29        m[0].to_bits(),
30        m[1].to_bits(),
31        m[2].to_bits(),
32        m[3].to_bits(),
33        m[4].to_bits(),
34        m[5].to_bits(),
35    ]
36}
37
38/// Thread-safe cache for Type 3 glyph outlines with a size limit.
39///
40/// Caches the parsed path operations for each (stream ObjectId, character code,
41/// font matrix) triple so that repeated rendering of the same glyph avoids
42/// re-parsing. When the cache reaches `max_capacity`, approximately 20% of
43/// entries are evicted to make room for new ones.
44pub struct Type3GlyphCache {
45    cache: DashMap<Type3CacheKey, Type3GlyphEntry>,
46    max_capacity: usize,
47}
48
49impl Type3GlyphCache {
50    /// Create a new empty cache with the default capacity (1024).
51    pub fn new() -> Self {
52        Self {
53            cache: DashMap::new(),
54            max_capacity: DEFAULT_TYPE3_CACHE_CAPACITY,
55        }
56    }
57
58    /// Create a new cache with the specified maximum capacity.
59    pub fn with_capacity(max_capacity: usize) -> Self {
60        Self {
61            cache: DashMap::new(),
62            max_capacity,
63        }
64    }
65
66    /// Returns the maximum capacity.
67    pub fn max_capacity(&self) -> usize {
68        self.max_capacity
69    }
70
71    /// Get a cached entry or compute and insert it.
72    ///
73    /// If the (stream_id, code, font_matrix) triple is already cached, returns
74    /// the cached entry. Otherwise, calls `compute` to produce a new entry,
75    /// inserts it, and returns it.
76    pub fn get_or_insert(
77        &self,
78        stream_id: ObjectId,
79        code: u8,
80        font_matrix: &[f32; 6],
81        compute: impl FnOnce() -> Type3GlyphEntry,
82    ) -> Type3GlyphEntry {
83        let key = (stream_id, code, matrix_to_bits(font_matrix));
84        if let Some(cached) = self.cache.get(&key) {
85            return cached.clone();
86        }
87
88        // Evict entries if cache is at capacity
89        if self.cache.len() >= self.max_capacity {
90            let to_remove = self.max_capacity / 5;
91            let mut removed = 0;
92            self.cache.retain(|_, _| {
93                if removed < to_remove {
94                    removed += 1;
95                    false
96                } else {
97                    true
98                }
99            });
100        }
101
102        self.cache.entry(key).or_insert_with(compute).clone()
103    }
104}
105
106impl Default for Type3GlyphCache {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use rpdfium_graphics::PathOp;
116
117    /// Standard Type 3 font matrix used across tests.
118    const T3_MATRIX: [f32; 6] = [0.001, 0.0, 0.0, 0.001, 0.0, 0.0];
119
120    #[test]
121    fn test_cache_insert_and_retrieve() {
122        let cache = Type3GlyphCache::new();
123        let id = ObjectId::new(10, 0);
124
125        let entry = cache.get_or_insert(id, 65, &T3_MATRIX, || Type3GlyphEntry {
126            ops: vec![PathOp::MoveTo { x: 0.0, y: 0.0 }, PathOp::Close],
127            metrics: None,
128        });
129
130        assert_eq!(entry.ops.len(), 2);
131        assert!(entry.metrics.is_none());
132    }
133
134    #[test]
135    fn test_cache_returns_cached_value() {
136        let cache = Type3GlyphCache::new();
137        let id = ObjectId::new(10, 0);
138
139        // First insert
140        let entry1 = cache.get_or_insert(id, 65, &T3_MATRIX, || Type3GlyphEntry {
141            ops: vec![PathOp::MoveTo { x: 1.0, y: 2.0 }],
142            metrics: None,
143        });
144
145        // Second call should return cached value, not call compute
146        let entry2 = cache.get_or_insert(id, 65, &T3_MATRIX, || Type3GlyphEntry {
147            ops: vec![PathOp::MoveTo { x: 99.0, y: 99.0 }],
148            metrics: None,
149        });
150
151        // Should be the same as the first insert
152        assert_eq!(entry1.ops, entry2.ops);
153        match &entry2.ops[0] {
154            PathOp::MoveTo { x, y } => {
155                assert_eq!(*x, 1.0);
156                assert_eq!(*y, 2.0);
157            }
158            _ => panic!("expected MoveTo"),
159        }
160    }
161
162    #[test]
163    fn test_cache_different_codes() {
164        let cache = Type3GlyphCache::new();
165        let id = ObjectId::new(10, 0);
166
167        cache.get_or_insert(id, 65, &T3_MATRIX, || Type3GlyphEntry {
168            ops: vec![PathOp::MoveTo { x: 1.0, y: 1.0 }],
169            metrics: None,
170        });
171
172        let entry_b = cache.get_or_insert(id, 66, &T3_MATRIX, || Type3GlyphEntry {
173            ops: vec![PathOp::MoveTo { x: 2.0, y: 2.0 }],
174            metrics: None,
175        });
176
177        match &entry_b.ops[0] {
178            PathOp::MoveTo { x, y } => {
179                assert_eq!(*x, 2.0);
180                assert_eq!(*y, 2.0);
181            }
182            _ => panic!("expected MoveTo"),
183        }
184    }
185
186    #[test]
187    fn test_cache_different_stream_ids() {
188        let cache = Type3GlyphCache::new();
189
190        cache.get_or_insert(ObjectId::new(10, 0), 65, &T3_MATRIX, || Type3GlyphEntry {
191            ops: vec![PathOp::MoveTo { x: 1.0, y: 1.0 }],
192            metrics: None,
193        });
194
195        let entry = cache.get_or_insert(ObjectId::new(20, 0), 65, &T3_MATRIX, || Type3GlyphEntry {
196            ops: vec![PathOp::MoveTo { x: 3.0, y: 3.0 }],
197            metrics: None,
198        });
199
200        match &entry.ops[0] {
201            PathOp::MoveTo { x, y } => {
202                assert_eq!(*x, 3.0);
203                assert_eq!(*y, 3.0);
204            }
205            _ => panic!("expected MoveTo"),
206        }
207    }
208
209    #[test]
210    fn test_cache_different_font_matrices() {
211        let cache = Type3GlyphCache::new();
212        let id = ObjectId::new(10, 0);
213        let matrix_a = [0.001, 0.0, 0.0, 0.001, 0.0, 0.0];
214        let matrix_b = [0.002, 0.0, 0.0, 0.002, 0.0, 0.0];
215
216        // Insert with matrix_a
217        cache.get_or_insert(id, 65, &matrix_a, || Type3GlyphEntry {
218            ops: vec![PathOp::MoveTo { x: 1.0, y: 1.0 }],
219            metrics: None,
220        });
221
222        // Same stream_id and code but different matrix should produce separate entry
223        let entry = cache.get_or_insert(id, 65, &matrix_b, || Type3GlyphEntry {
224            ops: vec![PathOp::MoveTo { x: 5.0, y: 5.0 }],
225            metrics: None,
226        });
227
228        match &entry.ops[0] {
229            PathOp::MoveTo { x, y } => {
230                assert_eq!(*x, 5.0);
231                assert_eq!(*y, 5.0);
232            }
233            _ => panic!("expected MoveTo"),
234        }
235    }
236
237    #[test]
238    fn test_cache_with_metrics() {
239        use rpdfium_font::type3_font::parse_d1;
240
241        let cache = Type3GlyphCache::new();
242        let id = ObjectId::new(10, 0);
243
244        let entry = cache.get_or_insert(id, 65, &T3_MATRIX, || Type3GlyphEntry {
245            ops: vec![PathOp::MoveTo { x: 0.0, y: 0.0 }, PathOp::Close],
246            metrics: Some(parse_d1(500.0, 0.0, 0.0, -10.0, 400.0, 700.0)),
247        });
248
249        let m = entry.metrics.unwrap();
250        assert_eq!(m.wx, 500.0);
251        assert_eq!(m.bbox, Some([0.0, -10.0, 400.0, 700.0]));
252    }
253
254    #[test]
255    fn test_cache_is_send_sync() {
256        fn assert_send_sync<T: Send + Sync>() {}
257        assert_send_sync::<Type3GlyphCache>();
258    }
259
260    #[test]
261    fn test_cache_default() {
262        let cache = Type3GlyphCache::default();
263        let entry = cache.get_or_insert(ObjectId::new(1, 0), 0, &T3_MATRIX, || Type3GlyphEntry {
264            ops: vec![],
265            metrics: None,
266        });
267        assert!(entry.ops.is_empty());
268    }
269
270    #[test]
271    fn test_cache_default_capacity() {
272        let cache = Type3GlyphCache::new();
273        assert_eq!(cache.max_capacity(), DEFAULT_TYPE3_CACHE_CAPACITY);
274    }
275
276    #[test]
277    fn test_cache_custom_capacity() {
278        let cache = Type3GlyphCache::with_capacity(512);
279        assert_eq!(cache.max_capacity(), 512);
280    }
281
282    #[test]
283    fn test_cache_eviction_at_capacity() {
284        // Create a very small cache
285        let cache = Type3GlyphCache::with_capacity(5);
286
287        // Fill beyond capacity
288        for i in 0..7 {
289            cache.get_or_insert(ObjectId::new(i as u32, 0), 0, &T3_MATRIX, || {
290                Type3GlyphEntry {
291                    ops: vec![PathOp::MoveTo {
292                        x: i as f32,
293                        y: 0.0,
294                    }],
295                    metrics: None,
296                }
297            });
298        }
299
300        // Cache should have evicted some entries — size should be <= max_capacity
301        assert!(cache.cache.len() <= 7);
302    }
303}