Skip to main content

oxidize_pdf/memory/
lazy_loader.rs

1//! Lazy loading implementation for PDF documents
2//!
3//! Provides on-demand loading of PDF objects and pages to minimize memory usage
4//! when working with large documents.
5
6use crate::error::{PdfError, Result};
7use crate::memory::{MemoryManager, MemoryOptions};
8use crate::objects::ObjectId;
9use crate::parser::{ParsedPage, PdfObject, PdfReader};
10use std::collections::HashMap;
11use std::fs::File;
12use std::io::{Read, Seek};
13use std::sync::{Arc, RwLock};
14
15/// A lazily-loaded PDF object that loads its content on first access
16pub enum LazyObject {
17    /// Not yet loaded - contains file offset
18    NotLoaded { offset: u64 },
19    /// Loaded and cached
20    Loaded(Arc<PdfObject>),
21    /// Currently being loaded (prevents recursive loading)
22    Loading,
23}
24
25/// PDF document with lazy loading support
26pub struct LazyDocument<R: Read + Seek> {
27    #[allow(dead_code)]
28    reader: Arc<RwLock<PdfReader<R>>>,
29    memory_manager: Arc<MemoryManager>,
30    object_map: Arc<RwLock<HashMap<ObjectId, LazyObject>>>,
31    page_count: u32,
32}
33
34impl LazyDocument<File> {
35    /// Open a PDF file with lazy loading
36    pub fn open<P: AsRef<std::path::Path>>(path: P, options: MemoryOptions) -> Result<Self> {
37        let reader = PdfReader::open(path).map_err(|e| PdfError::ParseError(e.to_string()))?;
38        Self::new(reader, options)
39    }
40}
41
42impl<R: Read + Seek> LazyDocument<R> {
43    /// Create a new lazy document from a reader
44    pub fn new(reader: PdfReader<R>, options: MemoryOptions) -> Result<Self> {
45        let memory_manager = Arc::new(MemoryManager::new(options));
46
47        // For now, use a fixed page count
48        // In a real implementation, we would parse the catalog to get page count
49        let page_count = 0;
50
51        let reader = Arc::new(RwLock::new(reader));
52        let object_map = Arc::new(RwLock::new(HashMap::new()));
53
54        Ok(Self {
55            reader,
56            memory_manager,
57            object_map,
58            page_count,
59        })
60    }
61
62    /// Get the total number of pages
63    pub fn page_count(&self) -> u32 {
64        self.page_count
65    }
66
67    /// Get a page (loads on demand)
68    pub fn get_page(&self, index: u32) -> Result<ParsedPage> {
69        if index >= self.page_count {
70            return Err(PdfError::InvalidPageNumber(index));
71        }
72
73        self.memory_manager.record_cache_miss();
74
75        // For now, return an error as we can't easily clone the reader
76        // In a real implementation, we would need a different approach
77        Err(PdfError::ParseError(
78            "Lazy loading not fully implemented".to_string(),
79        ))
80    }
81
82    /// Get an object by ID (loads on demand)
83    pub fn get_object(&self, id: &ObjectId) -> Result<Arc<PdfObject>> {
84        // Check cache first
85        if let Some(cache) = self.memory_manager.cache() {
86            if let Some(obj) = cache.get(id) {
87                self.memory_manager.record_cache_hit();
88                return Ok(obj);
89            }
90        }
91
92        self.memory_manager.record_cache_miss();
93
94        // Check if we have this object in our map
95        if let Ok(mut map) = self.object_map.write() {
96            match map.get_mut(id) {
97                Some(LazyObject::Loaded(obj)) => {
98                    return Ok(obj.clone());
99                }
100                Some(LazyObject::Loading) => {
101                    return Err(PdfError::ParseError(
102                        "Circular reference detected".to_string(),
103                    ));
104                }
105                Some(LazyObject::NotLoaded { offset }) => {
106                    let offset = *offset;
107                    // Mark as loading to prevent recursion
108                    map.insert(*id, LazyObject::Loading);
109
110                    // Load the object
111                    let obj = self.load_object_at_offset(offset)?;
112                    let obj_arc = Arc::new(obj);
113
114                    // Cache it
115                    if let Some(cache) = self.memory_manager.cache() {
116                        cache.put(*id, obj_arc.clone());
117                    }
118
119                    // Update map
120                    map.insert(*id, LazyObject::Loaded(obj_arc.clone()));
121
122                    return Ok(obj_arc);
123                }
124                None => {
125                    // For now, return error as we can't easily access objects
126                    // In a real implementation, we would need direct object access
127                }
128            }
129        }
130
131        Err(PdfError::InvalidObjectReference(
132            id.number(),
133            id.generation(),
134        ))
135    }
136
137    /// Preload objects for a specific page
138    pub fn preload_page(&self, index: u32) -> Result<()> {
139        let page = self.get_page(index)?;
140
141        // Preload common page resources
142        if let Some(resources) = page.get_resources() {
143            // Preload fonts
144            if let Some(fonts) = resources.get("Font").and_then(|f| f.as_dict()) {
145                for font_ref in fonts.0.values() {
146                    if let PdfObject::Reference(num, gen) = font_ref {
147                        let id = ObjectId::new(*num, *gen);
148                        let _ = self.get_object(&id);
149                    }
150                }
151            }
152
153            // Preload XObjects (images)
154            if let Some(xobjects) = resources.get("XObject").and_then(|x| x.as_dict()) {
155                for xobj_ref in xobjects.0.values() {
156                    if let PdfObject::Reference(num, gen) = xobj_ref {
157                        let id = ObjectId::new(*num, *gen);
158                        let _ = self.get_object(&id);
159                    }
160                }
161            }
162        }
163
164        Ok(())
165    }
166
167    /// Get memory statistics
168    pub fn memory_stats(&self) -> crate::memory::MemoryStats {
169        self.memory_manager.stats()
170    }
171
172    /// Clear all caches
173    pub fn clear_cache(&self) {
174        if let Some(cache) = self.memory_manager.cache() {
175            cache.clear();
176        }
177
178        if let Ok(mut map) = self.object_map.write() {
179            // Keep NotLoaded entries, only clear Loaded ones
180            map.retain(|_, obj| matches!(obj, LazyObject::NotLoaded { .. }));
181        }
182    }
183
184    fn load_object_at_offset(&self, _offset: u64) -> Result<PdfObject> {
185        // In a real implementation, this would seek to the offset and parse the object
186        // For now, return a placeholder
187        Ok(PdfObject::Null)
188    }
189}
190
191/// Lazy page iterator
192pub struct LazyPageIterator<R: Read + Seek> {
193    document: Arc<LazyDocument<R>>,
194    current: u32,
195    total: u32,
196}
197
198impl<R: Read + Seek> LazyPageIterator<R> {
199    pub fn new(document: Arc<LazyDocument<R>>) -> Self {
200        let total = document.page_count();
201        Self {
202            document,
203            current: 0,
204            total,
205        }
206    }
207}
208
209impl<R: Read + Seek> Iterator for LazyPageIterator<R> {
210    type Item = Result<ParsedPage>;
211
212    fn next(&mut self) -> Option<Self::Item> {
213        if self.current >= self.total {
214            return None;
215        }
216
217        let result = self.document.get_page(self.current);
218        self.current += 1;
219        Some(result)
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use crate::parser::test_helpers;
227    use std::io::Cursor;
228
229    #[test]
230    fn test_lazy_object_states() {
231        let not_loaded = LazyObject::NotLoaded { offset: 1234 };
232        match not_loaded {
233            LazyObject::NotLoaded { offset } => assert_eq!(offset, 1234),
234            _ => panic!("Wrong state"),
235        }
236
237        let loaded = LazyObject::Loaded(Arc::new(PdfObject::Integer(42)));
238        match loaded {
239            LazyObject::Loaded(obj) => {
240                assert_eq!(*obj, PdfObject::Integer(42));
241            }
242            _ => panic!("Wrong state"),
243        }
244
245        let loading = LazyObject::Loading;
246        match loading {
247            LazyObject::Loading => assert!(true),
248            _ => panic!("Wrong state"),
249        }
250    }
251
252    #[test]
253    fn test_lazy_document_creation() {
254        let pdf_data = test_helpers::create_minimal_pdf();
255        let cursor = Cursor::new(pdf_data);
256        let reader = PdfReader::new(cursor).unwrap();
257        let options = MemoryOptions::default();
258
259        let lazy_doc = LazyDocument::new(reader, options).unwrap();
260        assert_eq!(lazy_doc.page_count(), 0); // Minimal PDF has no pages
261    }
262
263    #[test]
264    fn test_memory_stats() {
265        let pdf_data = test_helpers::create_minimal_pdf();
266        let cursor = Cursor::new(pdf_data);
267        let reader = PdfReader::new(cursor).unwrap();
268        let options = MemoryOptions::default();
269
270        let lazy_doc = LazyDocument::new(reader, options).unwrap();
271        let stats = lazy_doc.memory_stats();
272
273        assert_eq!(stats.cache_hits, 0);
274        assert_eq!(stats.cache_misses, 0);
275    }
276
277    #[test]
278    fn test_clear_cache() {
279        let pdf_data = test_helpers::create_minimal_pdf();
280        let cursor = Cursor::new(pdf_data);
281        let reader = PdfReader::new(cursor).unwrap();
282        let options = MemoryOptions::default().with_cache_size(10);
283
284        let lazy_doc = LazyDocument::new(reader, options).unwrap();
285
286        // Clear cache should not panic
287        lazy_doc.clear_cache();
288    }
289
290    #[test]
291    fn test_page_iterator() {
292        let pdf_data = test_helpers::create_minimal_pdf();
293        let cursor = Cursor::new(pdf_data);
294        let reader = PdfReader::new(cursor).unwrap();
295        let options = MemoryOptions::default();
296
297        let lazy_doc = Arc::new(LazyDocument::new(reader, options).unwrap());
298        let mut iterator = LazyPageIterator::new(lazy_doc);
299
300        // Should have no pages for minimal PDF
301        assert!(iterator.next().is_none());
302    }
303
304    #[test]
305    fn test_lazy_object_not_loaded() {
306        let obj = LazyObject::NotLoaded { offset: 42 };
307
308        match obj {
309            LazyObject::NotLoaded { offset } => {
310                assert_eq!(offset, 42);
311            }
312            _ => panic!("Expected NotLoaded variant"),
313        }
314    }
315
316    #[test]
317    fn test_lazy_object_loaded() {
318        let pdf_obj = Arc::new(PdfObject::Boolean(true));
319        let obj = LazyObject::Loaded(pdf_obj.clone());
320
321        match obj {
322            LazyObject::Loaded(arc_obj) => {
323                assert_eq!(*arc_obj, PdfObject::Boolean(true));
324                assert!(Arc::ptr_eq(&arc_obj, &pdf_obj));
325            }
326            _ => panic!("Expected Loaded variant"),
327        }
328    }
329
330    #[test]
331    fn test_lazy_object_loading() {
332        let obj = LazyObject::Loading;
333
334        match obj {
335            LazyObject::Loading => {
336                // Success - no additional assertions needed
337            }
338            _ => panic!("Expected Loading variant"),
339        }
340    }
341
342    #[test]
343    fn test_lazy_document_page_count() {
344        let pdf_data = test_helpers::create_minimal_pdf();
345        let cursor = Cursor::new(pdf_data);
346        let reader = PdfReader::new(cursor).unwrap();
347        let options = MemoryOptions::default();
348
349        let lazy_doc = LazyDocument::new(reader, options).unwrap();
350
351        assert_eq!(lazy_doc.page_count(), 0);
352    }
353
354    #[test]
355    fn test_lazy_document_get_page_invalid_index() {
356        let pdf_data = test_helpers::create_minimal_pdf();
357        let cursor = Cursor::new(pdf_data);
358        let reader = PdfReader::new(cursor).unwrap();
359        let options = MemoryOptions::default();
360
361        let lazy_doc = LazyDocument::new(reader, options).unwrap();
362
363        // Try to get page 0 when document has 0 pages
364        let result = lazy_doc.get_page(0);
365        assert!(result.is_err());
366
367        match result {
368            Err(PdfError::InvalidPageNumber(num)) => {
369                assert_eq!(num, 0);
370            }
371            _ => panic!("Expected InvalidPageNumber error"),
372        }
373
374        // Try to get page 5 when document has 0 pages
375        let result = lazy_doc.get_page(5);
376        assert!(result.is_err());
377
378        match result {
379            Err(PdfError::InvalidPageNumber(num)) => {
380                assert_eq!(num, 5);
381            }
382            _ => panic!("Expected InvalidPageNumber error"),
383        }
384    }
385
386    #[test]
387    fn test_lazy_document_get_object_not_found() {
388        let pdf_data = test_helpers::create_minimal_pdf();
389        let cursor = Cursor::new(pdf_data);
390        let reader = PdfReader::new(cursor).unwrap();
391        let options = MemoryOptions::default();
392
393        let lazy_doc = LazyDocument::new(reader, options).unwrap();
394        let obj_id = ObjectId::new(999, 0);
395
396        let result = lazy_doc.get_object(&obj_id);
397        assert!(result.is_err());
398
399        match result {
400            Err(PdfError::InvalidObjectReference(num, gen)) => {
401                assert_eq!(num, 999);
402                assert_eq!(gen, 0);
403            }
404            _ => panic!("Expected InvalidObjectReference error"),
405        }
406    }
407
408    #[test]
409    fn test_lazy_document_get_object_circular_reference() {
410        let pdf_data = test_helpers::create_minimal_pdf();
411        let cursor = Cursor::new(pdf_data);
412        let reader = PdfReader::new(cursor).unwrap();
413        let options = MemoryOptions::default();
414
415        let lazy_doc = LazyDocument::new(reader, options).unwrap();
416        let obj_id = ObjectId::new(1, 0);
417
418        // Manually insert a Loading object to simulate circular reference
419        {
420            let mut map = lazy_doc.object_map.write().unwrap();
421            map.insert(obj_id, LazyObject::Loading);
422        }
423
424        let result = lazy_doc.get_object(&obj_id);
425        assert!(result.is_err());
426
427        match result {
428            Err(PdfError::ParseError(msg)) => {
429                assert!(msg.contains("Circular reference"));
430            }
431            _ => panic!("Expected ParseError for circular reference"),
432        }
433    }
434
435    #[test]
436    fn test_lazy_document_get_object_not_loaded_then_loaded() {
437        let pdf_data = test_helpers::create_minimal_pdf();
438        let cursor = Cursor::new(pdf_data);
439        let reader = PdfReader::new(cursor).unwrap();
440        let options = MemoryOptions::default();
441
442        let lazy_doc = LazyDocument::new(reader, options).unwrap();
443        let obj_id = ObjectId::new(1, 0);
444
445        // Manually insert a NotLoaded object
446        {
447            let mut map = lazy_doc.object_map.write().unwrap();
448            map.insert(obj_id, LazyObject::NotLoaded { offset: 100 });
449        }
450
451        let result = lazy_doc.get_object(&obj_id);
452        assert!(result.is_ok());
453
454        let obj = result.unwrap();
455        assert_eq!(*obj, PdfObject::Null); // load_object_at_offset returns Null
456
457        // Object should now be cached as Loaded
458        {
459            let map = lazy_doc.object_map.read().unwrap();
460            match map.get(&obj_id) {
461                Some(LazyObject::Loaded(_)) => {}
462                _ => panic!("Expected object to be cached as Loaded"),
463            }
464        }
465    }
466
467    #[test]
468    fn test_lazy_document_get_object_already_loaded() {
469        let pdf_data = test_helpers::create_minimal_pdf();
470        let cursor = Cursor::new(pdf_data);
471        let reader = PdfReader::new(cursor).unwrap();
472        let options = MemoryOptions::default();
473
474        let lazy_doc = LazyDocument::new(reader, options).unwrap();
475        let obj_id = ObjectId::new(1, 0);
476        let test_obj = Arc::new(PdfObject::Boolean(true));
477
478        // Manually insert a Loaded object
479        {
480            let mut map = lazy_doc.object_map.write().unwrap();
481            map.insert(obj_id, LazyObject::Loaded(test_obj.clone()));
482        }
483
484        let result = lazy_doc.get_object(&obj_id);
485        assert!(result.is_ok());
486
487        let obj = result.unwrap();
488        assert_eq!(*obj, PdfObject::Boolean(true));
489        assert!(Arc::ptr_eq(&obj, &test_obj));
490    }
491
492    #[test]
493    fn test_lazy_document_preload_page_invalid_index() {
494        let pdf_data = test_helpers::create_minimal_pdf();
495        let cursor = Cursor::new(pdf_data);
496        let reader = PdfReader::new(cursor).unwrap();
497        let options = MemoryOptions::default();
498
499        let lazy_doc = LazyDocument::new(reader, options).unwrap();
500
501        let result = lazy_doc.preload_page(0);
502        assert!(result.is_err());
503
504        match result {
505            Err(PdfError::InvalidPageNumber(num)) => {
506                assert_eq!(num, 0);
507            }
508            _ => panic!("Expected InvalidPageNumber error"),
509        }
510    }
511
512    #[test]
513    fn test_lazy_document_memory_stats_initial() {
514        let pdf_data = test_helpers::create_minimal_pdf();
515        let cursor = Cursor::new(pdf_data);
516        let reader = PdfReader::new(cursor).unwrap();
517        let options = MemoryOptions::default();
518
519        let lazy_doc = LazyDocument::new(reader, options).unwrap();
520        let stats = lazy_doc.memory_stats();
521
522        // Initial stats should be zero
523        assert_eq!(stats.cache_hits, 0);
524        assert_eq!(stats.cache_misses, 0);
525        assert_eq!(stats.allocated_bytes, 0);
526    }
527
528    #[test]
529    fn test_lazy_document_memory_stats_after_operations() {
530        let pdf_data = test_helpers::create_minimal_pdf();
531        let cursor = Cursor::new(pdf_data);
532        let reader = PdfReader::new(cursor).unwrap();
533        let options = MemoryOptions::default().with_cache_size(10);
534
535        let lazy_doc = LazyDocument::new(reader, options).unwrap();
536        let obj_id = ObjectId::new(1, 0);
537
538        // Try to get an object (should result in cache miss)
539        let _ = lazy_doc.get_object(&obj_id);
540
541        let stats = lazy_doc.memory_stats();
542        assert!(stats.cache_misses > 0);
543    }
544
545    #[test]
546    fn test_lazy_document_clear_cache_empty() {
547        let pdf_data = test_helpers::create_minimal_pdf();
548        let cursor = Cursor::new(pdf_data);
549        let reader = PdfReader::new(cursor).unwrap();
550        let options = MemoryOptions::default();
551
552        let lazy_doc = LazyDocument::new(reader, options).unwrap();
553
554        // Clear cache on empty document should not panic
555        lazy_doc.clear_cache();
556
557        // Verify object map is still accessible
558        let map = lazy_doc.object_map.read().unwrap();
559        assert!(map.is_empty());
560    }
561
562    #[test]
563    fn test_lazy_document_clear_cache_with_objects() {
564        let pdf_data = test_helpers::create_minimal_pdf();
565        let cursor = Cursor::new(pdf_data);
566        let reader = PdfReader::new(cursor).unwrap();
567        let options = MemoryOptions::default().with_cache_size(10);
568
569        let lazy_doc = LazyDocument::new(reader, options).unwrap();
570        let obj_id1 = ObjectId::new(1, 0);
571        let obj_id2 = ObjectId::new(2, 0);
572        let obj_id3 = ObjectId::new(3, 0);
573
574        // Add different types of objects
575        {
576            let mut map = lazy_doc.object_map.write().unwrap();
577            map.insert(obj_id1, LazyObject::NotLoaded { offset: 100 });
578            map.insert(
579                obj_id2,
580                LazyObject::Loaded(Arc::new(PdfObject::Integer(42))),
581            );
582            map.insert(obj_id3, LazyObject::Loading);
583        }
584
585        lazy_doc.clear_cache();
586
587        // Verify that only NotLoaded objects remain
588        {
589            let map = lazy_doc.object_map.read().unwrap();
590            assert_eq!(map.len(), 1); // Only NotLoaded should remain
591            assert!(matches!(
592                map.get(&obj_id1),
593                Some(LazyObject::NotLoaded { .. })
594            ));
595            assert!(!map.contains_key(&obj_id2));
596            assert!(!map.contains_key(&obj_id3));
597        }
598    }
599
600    #[test]
601    fn test_lazy_page_iterator_creation() {
602        let pdf_data = test_helpers::create_minimal_pdf();
603        let cursor = Cursor::new(pdf_data);
604        let reader = PdfReader::new(cursor).unwrap();
605        let options = MemoryOptions::default();
606
607        let lazy_doc = Arc::new(LazyDocument::new(reader, options).unwrap());
608        let iterator = LazyPageIterator::new(lazy_doc.clone());
609
610        assert_eq!(iterator.current, 0);
611        assert_eq!(iterator.total, 0); // Minimal PDF has 0 pages
612        assert!(Arc::ptr_eq(&iterator.document, &lazy_doc));
613    }
614
615    #[test]
616    fn test_lazy_page_iterator_empty_document() {
617        let pdf_data = test_helpers::create_minimal_pdf();
618        let cursor = Cursor::new(pdf_data);
619        let reader = PdfReader::new(cursor).unwrap();
620        let options = MemoryOptions::default();
621
622        let lazy_doc = Arc::new(LazyDocument::new(reader, options).unwrap());
623        let mut iterator = LazyPageIterator::new(lazy_doc);
624
625        // Should immediately return None for empty document
626        assert!(iterator.next().is_none());
627        assert!(iterator.next().is_none()); // Multiple calls should be safe
628    }
629
630    #[test]
631    fn test_lazy_page_iterator_multiple_calls() {
632        let pdf_data = test_helpers::create_minimal_pdf();
633        let cursor = Cursor::new(pdf_data);
634        let reader = PdfReader::new(cursor).unwrap();
635        let options = MemoryOptions::default();
636
637        let lazy_doc = Arc::new(LazyDocument::new(reader, options).unwrap());
638        let mut iterator = LazyPageIterator::new(lazy_doc);
639
640        // Multiple calls on empty iterator should return None
641        for _ in 0..5 {
642            assert!(iterator.next().is_none());
643        }
644    }
645
646    #[test]
647    fn test_lazy_document_with_different_memory_options() {
648        let pdf_data = test_helpers::create_minimal_pdf();
649        let cursor = Cursor::new(pdf_data);
650        let reader = PdfReader::new(cursor).unwrap();
651
652        // Test with custom memory options
653        let options = MemoryOptions::default().with_cache_size(100);
654
655        let lazy_doc = LazyDocument::new(reader, options).unwrap();
656
657        assert_eq!(lazy_doc.page_count(), 0);
658
659        let stats = lazy_doc.memory_stats();
660        assert_eq!(stats.cache_hits, 0);
661        assert_eq!(stats.cache_misses, 0);
662    }
663
664    #[test]
665    fn test_lazy_document_load_object_at_offset() {
666        let pdf_data = test_helpers::create_minimal_pdf();
667        let cursor = Cursor::new(pdf_data);
668        let reader = PdfReader::new(cursor).unwrap();
669        let options = MemoryOptions::default();
670
671        let lazy_doc = LazyDocument::new(reader, options).unwrap();
672
673        // Test the private method through public interface
674        let obj_id = ObjectId::new(1, 0);
675
676        // Add NotLoaded object to trigger load_object_at_offset
677        {
678            let mut map = lazy_doc.object_map.write().unwrap();
679            map.insert(obj_id, LazyObject::NotLoaded { offset: 1234 });
680        }
681
682        let result = lazy_doc.get_object(&obj_id);
683        assert!(result.is_ok());
684
685        let obj = result.unwrap();
686        assert_eq!(*obj, PdfObject::Null); // Current implementation returns Null
687    }
688
689    #[test]
690    fn test_lazy_document_cache_hit_path() {
691        let pdf_data = test_helpers::create_minimal_pdf();
692        let cursor = Cursor::new(pdf_data);
693        let reader = PdfReader::new(cursor).unwrap();
694        let options = MemoryOptions::default().with_cache_size(10);
695
696        let lazy_doc = LazyDocument::new(reader, options).unwrap();
697        let obj_id = ObjectId::new(1, 0);
698        let test_obj = Arc::new(PdfObject::Real(3.14));
699
700        // Manually add object to cache
701        if let Some(cache) = lazy_doc.memory_manager.cache() {
702            cache.put(obj_id, test_obj.clone());
703        }
704
705        let result = lazy_doc.get_object(&obj_id);
706        assert!(result.is_ok());
707
708        let obj = result.unwrap();
709        assert_eq!(*obj, PdfObject::Real(3.14));
710        assert!(Arc::ptr_eq(&obj, &test_obj));
711
712        // Should have recorded a cache hit
713        let stats = lazy_doc.memory_stats();
714        assert!(stats.cache_hits > 0);
715    }
716
717    #[test]
718    fn test_lazy_document_object_map_locking() {
719        let pdf_data = test_helpers::create_minimal_pdf();
720        let cursor = Cursor::new(pdf_data);
721        let reader = PdfReader::new(cursor).unwrap();
722        let options = MemoryOptions::default();
723
724        let lazy_doc = LazyDocument::new(reader, options).unwrap();
725        let obj_id = ObjectId::new(1, 0);
726
727        // Test that we can acquire read and write locks
728        {
729            let _read_lock = lazy_doc.object_map.read().unwrap();
730        }
731
732        {
733            let mut write_lock = lazy_doc.object_map.write().unwrap();
734            write_lock.insert(obj_id, LazyObject::NotLoaded { offset: 500 });
735        }
736
737        // Verify the insertion worked
738        {
739            let read_lock = lazy_doc.object_map.read().unwrap();
740            assert!(read_lock.contains_key(&obj_id));
741        }
742    }
743
744    #[test]
745    fn test_lazy_document_open_nonexistent_file() {
746        let nonexistent_path = "/path/that/does/not/exist.pdf";
747        let options = MemoryOptions::default();
748
749        let result = LazyDocument::open(nonexistent_path, options);
750        assert!(result.is_err());
751
752        match result {
753            Err(PdfError::ParseError(_)) => {}
754            _ => panic!("Expected ParseError for nonexistent file"),
755        }
756    }
757
758    #[test]
759    fn test_lazy_object_enum_all_variants() {
760        // Test all variants of LazyObject enum
761        let variants = vec![
762            LazyObject::NotLoaded { offset: 12345 },
763            LazyObject::Loaded(Arc::new(PdfObject::String(crate::parser::PdfString::new(
764                b"test".to_vec(),
765            )))),
766            LazyObject::Loading,
767        ];
768
769        for (i, variant) in variants.into_iter().enumerate() {
770            match variant {
771                LazyObject::NotLoaded { offset } if i == 0 => {
772                    assert_eq!(offset, 12345);
773                }
774                LazyObject::Loaded(obj) if i == 1 => match &*obj {
775                    PdfObject::String(_) => {}
776                    _ => panic!("Expected String object"),
777                },
778                LazyObject::Loading if i == 2 => {
779                    // Success
780                }
781                _ => panic!("Unexpected variant at index {i}"),
782            }
783        }
784    }
785
786    #[test]
787    fn test_lazy_page_iterator_with_document_reference() {
788        let pdf_data = test_helpers::create_minimal_pdf();
789        let cursor = Cursor::new(pdf_data);
790        let reader = PdfReader::new(cursor).unwrap();
791        let options = MemoryOptions::default();
792
793        let lazy_doc = Arc::new(LazyDocument::new(reader, options).unwrap());
794        let _iterator = LazyPageIterator::new(lazy_doc.clone());
795
796        // Verify the document is still accessible after creating iterator
797        assert_eq!(lazy_doc.page_count(), 0);
798    }
799
800    #[test]
801    fn test_lazy_document_concurrent_access_simulation() {
802        use std::sync::Arc;
803        use std::thread;
804
805        let pdf_data = test_helpers::create_minimal_pdf();
806        let cursor = Cursor::new(pdf_data);
807        let reader = PdfReader::new(cursor).unwrap();
808        let options = MemoryOptions::default().with_cache_size(10);
809
810        let lazy_doc = Arc::new(LazyDocument::new(reader, options).unwrap());
811        let mut handles = vec![];
812
813        // Simulate concurrent access
814        for i in 0..3 {
815            let doc_clone = lazy_doc.clone();
816            let handle = thread::spawn(move || {
817                let obj_id = ObjectId::new(i + 1, 0);
818
819                // Try to get object (will fail but shouldn't panic)
820                let _result = doc_clone.get_object(&obj_id);
821
822                // Get memory stats
823                let _stats = doc_clone.memory_stats();
824
825                // Clear cache
826                doc_clone.clear_cache();
827            });
828            handles.push(handle);
829        }
830
831        for handle in handles {
832            handle.join().unwrap();
833        }
834
835        // Document should still be accessible
836        assert_eq!(lazy_doc.page_count(), 0);
837    }
838}