1use dashmap::DashMap;
13use rpdfium_core::ObjectId;
14
15pub use crate::type3_glyph_map::Type3GlyphEntry;
16
17const DEFAULT_TYPE3_CACHE_CAPACITY: usize = 1024;
19
20type Type3CacheKey = (ObjectId, u8, [u32; 6]);
25
26fn 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
38pub struct Type3GlyphCache {
45 cache: DashMap<Type3CacheKey, Type3GlyphEntry>,
46 max_capacity: usize,
47}
48
49impl Type3GlyphCache {
50 pub fn new() -> Self {
52 Self {
53 cache: DashMap::new(),
54 max_capacity: DEFAULT_TYPE3_CACHE_CAPACITY,
55 }
56 }
57
58 pub fn with_capacity(max_capacity: usize) -> Self {
60 Self {
61 cache: DashMap::new(),
62 max_capacity,
63 }
64 }
65
66 pub fn max_capacity(&self) -> usize {
68 self.max_capacity
69 }
70
71 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 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 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 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 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 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 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 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 let cache = Type3GlyphCache::with_capacity(5);
286
287 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 assert!(cache.cache.len() <= 7);
302 }
303}