1use crate::util::hash128;
2use kurbo::Affine;
3use pdf_syntax::object::{Array, Dict, MaybeRef, Name, Null, ObjRef, Object, Stream};
4use std::any::Any;
5use std::collections::hash_map::Entry;
6use std::collections::{HashMap, VecDeque};
7use std::sync::{Arc, Mutex};
8
9type CacheMap = HashMap<u128, Option<Box<dyn Any + Send + Sync>>>;
10
11const IMAGE_CACHE_CAPACITY: usize = 32;
21
22const IMAGE_CACHE_MAX_BYTES: usize = 256 * 1024 * 1024;
29
30struct ImageCache {
31 map: HashMap<u128, (Box<dyn Any + Send + Sync>, usize)>,
33 order: VecDeque<u128>,
35 total_bytes: usize,
37}
38
39#[derive(Clone)]
41pub struct Cache {
42 generic: Arc<Mutex<CacheMap>>,
43 images: Arc<Mutex<ImageCache>>,
44}
45
46impl Default for Cache {
47 fn default() -> Self {
48 Self::new()
49 }
50}
51
52impl Cache {
53 pub fn new() -> Self {
55 Self {
56 generic: Arc::new(Mutex::new(HashMap::new())),
57 images: Arc::new(Mutex::new(ImageCache {
58 map: HashMap::new(),
59 order: VecDeque::new(),
60 total_bytes: 0,
61 })),
62 }
63 }
64
65 pub(crate) fn get_or_insert_with<T: Clone + Send + Sync + 'static>(
66 &self,
67 id: u128,
68 f: impl FnOnce() -> Option<T>,
69 ) -> Option<T> {
70 let mut locked = self.generic.lock().unwrap();
71
72 match locked.entry(id) {
75 Entry::Occupied(o) => o
76 .get()
77 .as_ref()
78 .and_then(|val| val.downcast_ref::<T>().cloned()),
79 Entry::Vacant(_) => {
80 drop(locked);
81 let val = f();
82 self.generic.lock().unwrap().insert(
83 id,
84 val.clone()
85 .map(|val| Box::new(val) as Box<dyn Any + Send + Sync>),
86 );
87
88 val
89 }
90 }
91 }
92
93 pub(crate) fn get_or_insert_some<T: Clone + Send + Sync + 'static>(
101 &self,
102 id: u128,
103 f: impl FnOnce() -> Option<T>,
104 ) -> Option<T> {
105 if let Some(Some(val)) = self.generic.lock().unwrap().get(&id)
106 && let Some(val) = val.downcast_ref::<T>()
107 {
108 return Some(val.clone());
109 }
110 let val = f()?;
111 self.generic.lock().unwrap().insert(
112 id,
113 Some(Box::new(val.clone()) as Box<dyn Any + Send + Sync>),
114 );
115 Some(val)
116 }
117
118 pub(crate) fn font_stream_key(tag: &[u8], stream: &Stream<'_>) -> u128 {
129 hash128(&(tag, stream.cache_key(), stream.raw_data().as_ref()))
130 }
131
132 pub(crate) fn font_data_key(tag: &[u8], index: u32, data: &[u8]) -> u128 {
136 hash128(&(tag, index, data))
137 }
138
139 pub(crate) fn get_or_insert_image<T: Clone + Send + Sync + 'static>(
147 &self,
148 id: u128,
149 f: impl FnOnce() -> Option<T>,
150 weight: impl FnOnce(&T) -> usize,
151 ) -> Option<T> {
152 let mut locked = self.images.lock().unwrap();
153 if locked.map.contains_key(&id) {
154 if let Some(pos) = locked.order.iter().position(|&x| x == id) {
155 locked.order.remove(pos);
156 }
157 locked.order.push_back(id);
158 let (val, _) = locked.map.get(&id).unwrap();
159 return val.downcast_ref::<T>().cloned();
160 }
161
162 drop(locked);
163 let val = f();
164 if let Some(ref v) = val {
165 let w = weight(v);
166 let mut locked = self.images.lock().unwrap();
167 if let Some((_, old_w)) = locked
169 .map
170 .insert(id, (Box::new(v.clone()) as Box<dyn Any + Send + Sync>, w))
171 {
172 locked.total_bytes = locked.total_bytes.saturating_sub(old_w);
173 if let Some(pos) = locked.order.iter().position(|&x| x == id) {
174 locked.order.remove(pos);
175 }
176 }
177 locked.total_bytes += w;
178 locked.order.push_back(id);
179
180 while !locked.order.is_empty()
183 && (locked.order.len() > IMAGE_CACHE_CAPACITY
184 || locked.total_bytes > IMAGE_CACHE_MAX_BYTES)
185 {
186 if let Some(oldest_id) = locked.order.pop_front()
187 && let Some((_, evicted_w)) = locked.map.remove(&oldest_id)
188 {
189 locked.total_bytes = locked.total_bytes.saturating_sub(evicted_w);
190 }
191 }
192 }
193 val
194 }
195
196 #[cfg(test)]
202 pub(crate) fn image_cache_stats(&self) -> (usize, usize) {
203 let locked = self.images.lock().unwrap();
204 (locked.map.len(), locked.total_bytes)
205 }
206}
207
208pub trait CacheKey {
210 fn cache_key(&self) -> u128;
212}
213
214impl<T: CacheKey, U: CacheKey> CacheKey for (T, U) {
215 fn cache_key(&self) -> u128 {
216 hash128(&(self.0.cache_key(), self.1.cache_key()))
217 }
218}
219
220impl CacheKey for Dict<'_> {
221 fn cache_key(&self) -> u128 {
222 hash128(self.data())
223 }
224}
225
226impl CacheKey for Stream<'_> {
227 fn cache_key(&self) -> u128 {
228 self.dict().cache_key()
229 }
230}
231
232impl CacheKey for Null {
233 fn cache_key(&self) -> u128 {
234 hash128(self)
235 }
236}
237
238impl CacheKey for bool {
239 fn cache_key(&self) -> u128 {
240 hash128(self)
241 }
242}
243
244impl CacheKey for pdf_syntax::object::Number {
245 fn cache_key(&self) -> u128 {
246 hash128(&self.as_f64().to_bits())
247 }
248}
249
250impl CacheKey for pdf_syntax::object::String {
251 fn cache_key(&self) -> u128 {
252 hash128(self.as_ref())
253 }
254}
255
256impl CacheKey for Name {
257 fn cache_key(&self) -> u128 {
258 hash128(self)
259 }
260}
261
262impl CacheKey for Array<'_> {
263 fn cache_key(&self) -> u128 {
264 hash128(self.data())
265 }
266}
267
268impl CacheKey for Object<'_> {
269 fn cache_key(&self) -> u128 {
270 match self {
271 Object::Null(n) => n.cache_key(),
272 Object::Boolean(b) => b.cache_key(),
273 Object::Number(n) => n.cache_key(),
274 Object::String(s) => s.cache_key(),
275 Object::Name(n) => n.cache_key(),
276 Object::Dict(d) => d.cache_key(),
277 Object::Array(a) => a.cache_key(),
278 Object::Stream(s) => s.cache_key(),
279 }
280 }
281}
282
283impl CacheKey for ObjRef {
284 fn cache_key(&self) -> u128 {
285 hash128(self)
286 }
287}
288
289impl<T: CacheKey> CacheKey for MaybeRef<T> {
290 fn cache_key(&self) -> u128 {
291 match self {
292 Self::Ref(r) => r.cache_key(),
293 Self::NotRef(o) => o.cache_key(),
294 }
295 }
296}
297
298impl CacheKey for Affine {
299 fn cache_key(&self) -> u128 {
300 let c = self.as_coeffs();
301 hash128(&[
302 c[0].to_bits(),
303 c[1].to_bits(),
304 c[2].to_bits(),
305 c[3].to_bits(),
306 c[4].to_bits(),
307 c[5].to_bits(),
308 ])
309 }
310}
311
312impl CacheKey for u128 {
313 fn cache_key(&self) -> u128 {
314 hash128(self)
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 #[test]
324 fn a_font_stream_is_keyed_by_its_bytes_and_not_by_its_object_number() {
325 use pdf_syntax::object::{ObjectIdentifier, Stream};
326
327 fn pdf_with_object_5(program: &[u8]) -> Vec<u8> {
328 let objects: Vec<Vec<u8>> = vec![
329 b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
330 b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
331 b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] /Contents 4 0 R >>".to_vec(),
332 b"<< /Length 0 >>\nstream\n\nendstream".to_vec(),
333 [
334 format!("<< /Length {} >>\nstream\n", program.len()).into_bytes(),
335 program.to_vec(),
336 b"\nendstream".to_vec(),
337 ]
338 .concat(),
339 ];
340 let mut out = b"%PDF-1.7\n".to_vec();
341 let mut offsets = Vec::new();
342 for (i, body) in objects.iter().enumerate() {
343 offsets.push(out.len());
344 out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
345 out.extend_from_slice(body);
346 out.extend_from_slice(b"\nendobj\n");
347 }
348 let xref_pos = out.len();
349 out.extend_from_slice(format!("xref\n0 {}\n", objects.len() + 1).as_bytes());
350 out.extend_from_slice(b"0000000000 65535 f \n");
351 for offset in offsets {
352 out.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
353 }
354 out.extend_from_slice(
355 format!(
356 "trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref_pos}\n%%EOF\n",
357 objects.len() + 1
358 )
359 .as_bytes(),
360 );
361 out
362 }
363
364 let first = pdf_syntax::Pdf::new(pdf_with_object_5(b"FONT-PROGRAM-A")).expect("first");
366 let second = pdf_syntax::Pdf::new(pdf_with_object_5(b"FONT-PROGRAM-B")).expect("second");
367 let stream_of = |pdf: &pdf_syntax::Pdf| -> u128 {
368 let stream = pdf
369 .xref()
370 .get::<Stream<'_>>(ObjectIdentifier::new(5, 0))
371 .expect("object 5 is a stream");
372 super::Cache::font_stream_key(b"type1", &stream)
373 };
374 let (a, b) = (stream_of(&first), stream_of(&second));
375 assert_ne!(
376 a, b,
377 "two different programs under one object number share a key"
378 );
379 assert_eq!(a, stream_of(&first), "the key of one stream is stable");
380
381 let cache = super::Cache::new();
384 let got_a = cache.get_or_insert_some(a, || Some(1u8));
385 let got_b = cache.get_or_insert_some(b, || Some(2u8));
386 assert_eq!((got_a, got_b), (Some(1), Some(2)));
387 }
388
389 use super::*;
390
391 #[test]
392 fn test_generic_cache() {
393 let cache = Cache::new();
394 let val = cache.get_or_insert_with(12345, || Some("hello".to_string()));
395 assert_eq!(val, Some("hello".to_string()));
396
397 let cached = cache.get_or_insert_with(12345, || Some("world".to_string()));
398 assert_eq!(cached, Some("hello".to_string()));
399 }
400
401 fn tiny(_: &i32) -> usize {
403 4
404 }
405
406 #[test]
407 fn test_image_cache_lru_eviction() {
408 let cache = Cache::new();
409
410 for i in 0..32 {
412 let val = cache.get_or_insert_image(i as u128, || Some(i), tiny);
413 assert_eq!(val, Some(i));
414 }
415
416 for i in 0..32 {
418 let cached = cache.get_or_insert_image::<i32>(i as u128, || None, tiny);
419 assert_eq!(cached, Some(i));
420 }
421
422 let first_accessed = cache.get_or_insert_image::<i32>(0, || None, tiny);
424 assert_eq!(first_accessed, Some(0));
425
426 let val32 = cache.get_or_insert_image(32, || Some(32), tiny);
429 assert_eq!(val32, Some(32));
430
431 let evicted1 = cache.get_or_insert_image::<i32>(1, || None, tiny);
433 assert_eq!(evicted1, None);
434
435 let kept0 = cache.get_or_insert_image::<i32>(0, || None, tiny);
437 assert_eq!(kept0, Some(0));
438 }
439
440 #[test]
441 fn test_image_cache_evicts_by_byte_budget() {
442 let cache = Cache::new();
443 let big = 100 * 1024 * 1024; cache.get_or_insert_image(1u128, || Some(1i32), |_| big);
446 cache.get_or_insert_image(2u128, || Some(2i32), |_| big);
447 cache.get_or_insert_image(3u128, || Some(3i32), |_| big);
448
449 let (count, bytes) = cache.image_cache_stats();
452 assert_eq!(count, 2, "byte budget should drop the cache to two entries");
453 assert!(
454 bytes <= IMAGE_CACHE_MAX_BYTES,
455 "total bytes must stay within budget"
456 );
457 assert_eq!(
458 cache.get_or_insert_image::<i32>(1, || None, |_| 0),
459 None,
460 "oldest entry evicted by byte budget"
461 );
462 assert_eq!(cache.get_or_insert_image::<i32>(2, || None, |_| 0), Some(2));
463 assert_eq!(cache.get_or_insert_image::<i32>(3, || None, |_| 0), Some(3));
464 }
465
466 #[test]
467 fn test_image_cache_byte_budget_respects_lru_touch() {
468 let cache = Cache::new();
469 let big = 100 * 1024 * 1024;
470
471 cache.get_or_insert_image(1u128, || Some(1i32), |_| big);
472 cache.get_or_insert_image(2u128, || Some(2i32), |_| big);
473 assert_eq!(cache.get_or_insert_image::<i32>(1, || None, |_| 0), Some(1));
475 cache.get_or_insert_image(3u128, || Some(3i32), |_| big);
477
478 assert_eq!(
479 cache.get_or_insert_image::<i32>(2, || None, |_| 0),
480 None,
481 "least-recently-used key 2 evicted, not the touched key 1"
482 );
483 assert_eq!(cache.get_or_insert_image::<i32>(1, || None, |_| 0), Some(1));
484 assert_eq!(cache.get_or_insert_image::<i32>(3, || None, |_| 0), Some(3));
485 }
486
487 #[test]
488 fn test_image_cache_oversized_item_not_retained() {
489 let cache = Cache::new();
490 let huge = IMAGE_CACHE_MAX_BYTES + 1; assert_eq!(
494 cache.get_or_insert_image(1u128, || Some(1i32), |_| huge),
495 Some(1)
496 );
497 let (count, bytes) = cache.image_cache_stats();
499 assert_eq!(count, 0, "an item larger than the budget is not retained");
500 assert_eq!(bytes, 0);
501 assert_eq!(cache.get_or_insert_image::<i32>(1, || None, |_| 0), None);
502 }
503
504 #[test]
505 fn test_image_cache_stats_stay_within_both_bounds() {
506 let cache = Cache::new();
507 for i in 0..100u128 {
510 cache.get_or_insert_image(i, || Some(i as i32), |_| 1024);
511 }
512 let (count, bytes) = cache.image_cache_stats();
513 assert_eq!(count, IMAGE_CACHE_CAPACITY, "count cap holds at 32 entries");
514 assert_eq!(
515 bytes,
516 IMAGE_CACHE_CAPACITY * 1024,
517 "32 retained * 1 KiB each"
518 );
519 assert!(bytes <= IMAGE_CACHE_MAX_BYTES);
520 }
521}