1use super::header::PdfHeader;
7use super::object_stream::ObjectStream;
8use super::objects::{PdfDictionary, PdfObject};
9use super::stack_safe::StackSafeContext;
10use super::trailer::PdfTrailer;
11use super::xref::XRefTable;
12use super::{ParseError, ParseOptions, ParseResult};
13use crate::memory::{LruCache, MemoryOptions, MemoryStats};
14use crate::objects::ObjectId;
15use std::collections::HashMap;
16use std::fs::File;
17use std::io::{BufReader, Read, Seek, SeekFrom};
18use std::path::Path;
19use std::sync::Arc;
20
21pub struct OptimizedPdfReader<R: Read + Seek> {
23 reader: BufReader<R>,
24 header: PdfHeader,
25 xref: XRefTable,
26 trailer: PdfTrailer,
27 object_cache: LruCache<ObjectId, Arc<PdfObject>>,
29 object_stream_cache: HashMap<u32, ObjectStream>,
31 #[allow(dead_code)]
33 page_tree: Option<super::page_tree::PageTree>,
34 #[allow(dead_code)]
36 parse_context: StackSafeContext,
37 options: super::ParseOptions,
39 #[allow(dead_code)]
41 memory_options: MemoryOptions,
42 memory_stats: MemoryStats,
44}
45
46impl<R: Read + Seek> OptimizedPdfReader<R> {
47 pub fn options(&self) -> &super::ParseOptions {
49 &self.options
50 }
51
52 pub fn memory_stats(&self) -> &MemoryStats {
54 &self.memory_stats
55 }
56
57 pub fn clear_cache(&mut self) {
59 self.object_cache.clear();
60 self.object_stream_cache.clear();
61 }
62}
63
64impl OptimizedPdfReader<File> {
65 pub fn open<P: AsRef<Path>>(path: P) -> ParseResult<Self> {
67 let file = File::open(path)?;
68 let options = super::ParseOptions::lenient();
69 let memory_options = MemoryOptions::default();
70 Self::new_with_options(file, options, memory_options)
71 }
72
73 pub fn open_with_memory<P: AsRef<Path>>(
75 path: P,
76 memory_options: MemoryOptions,
77 ) -> ParseResult<Self> {
78 let file = File::open(path)?;
79 let options = super::ParseOptions::lenient();
80 Self::new_with_options(file, options, memory_options)
81 }
82
83 pub fn open_strict<P: AsRef<Path>>(path: P) -> ParseResult<Self> {
85 let file = File::open(path)?;
86 let options = super::ParseOptions::strict();
87 let memory_options = MemoryOptions::default();
88 Self::new_with_options(file, options, memory_options)
89 }
90}
91
92impl<R: Read + Seek> OptimizedPdfReader<R> {
93 pub fn new(reader: R) -> ParseResult<Self> {
95 Self::new_with_options(
96 reader,
97 super::ParseOptions::default(),
98 MemoryOptions::default(),
99 )
100 }
101
102 pub fn new_with_options(
104 reader: R,
105 options: super::ParseOptions,
106 memory_options: MemoryOptions,
107 ) -> ParseResult<Self> {
108 let mut buf_reader = BufReader::new(reader);
109
110 let start_pos = buf_reader.stream_position()?;
112 buf_reader.seek(SeekFrom::End(0))?;
113 let file_size = buf_reader.stream_position()?;
114 buf_reader.seek(SeekFrom::Start(start_pos))?;
115
116 if file_size == 0 {
117 return Err(ParseError::EmptyFile);
118 }
119
120 let header = PdfHeader::parse(&mut buf_reader)?;
122
123 let xref = XRefTable::parse_with_options(&mut buf_reader, &options)?;
125
126 let trailer_dict = xref.trailer().ok_or(ParseError::InvalidTrailer)?.clone();
128
129 let xref_offset = xref.xref_offset();
130 let trailer = PdfTrailer::from_dict(trailer_dict, xref_offset)?;
131
132 trailer.validate()?;
134
135 let cache_size = memory_options.cache_size.max(1);
137 let object_cache = LruCache::new(cache_size);
138
139 Ok(Self {
140 reader: buf_reader,
141 header,
142 xref,
143 trailer,
144 object_cache,
145 object_stream_cache: HashMap::new(),
146 page_tree: None,
147 parse_context: StackSafeContext::new(),
148 options,
149 memory_options,
150 memory_stats: MemoryStats::default(),
151 })
152 }
153
154 pub fn version(&self) -> &super::header::PdfVersion {
156 &self.header.version
157 }
158
159 pub fn catalog(&mut self) -> ParseResult<&PdfDictionary> {
161 let (obj_num, gen_num) = match self.trailer.root() {
163 Ok(root) => root,
164 Err(_) => {
165 #[cfg(debug_assertions)]
167 eprintln!("Warning: Trailer missing Root entry, attempting recovery");
168
169 if let Some(root) = self.trailer.find_root_fallback() {
171 root
172 } else {
173 if let Ok(catalog_ref) = self.find_catalog_object() {
175 catalog_ref
176 } else {
177 return Err(ParseError::MissingKey("Root".to_string()));
178 }
179 }
180 }
181 };
182
183 let catalog = self.get_object(obj_num, gen_num)?;
184
185 catalog.as_dict().ok_or_else(|| ParseError::SyntaxError {
186 position: 0,
187 message: "Catalog is not a dictionary".to_string(),
188 })
189 }
190
191 pub fn info(&mut self) -> ParseResult<Option<&PdfDictionary>> {
193 match self.trailer.info() {
194 Some((obj_num, gen_num)) => {
195 let info = self.get_object(obj_num, gen_num)?;
196 Ok(info.as_dict())
197 }
198 None => Ok(None),
199 }
200 }
201
202 pub fn get_object(&mut self, obj_num: u32, gen_num: u16) -> ParseResult<&PdfObject> {
204 let object_id = ObjectId::new(obj_num, gen_num);
205
206 if let Some(cached_obj) = self.object_cache.get(&object_id) {
208 self.memory_stats.cache_hits += 1;
209 let ptr = Arc::as_ptr(cached_obj);
212 return Ok(unsafe { &*ptr });
213 }
214
215 self.memory_stats.cache_misses += 1;
216
217 let obj = self.load_object_from_disk(obj_num, gen_num)?;
219
220 let arc_obj = Arc::new(obj);
222 self.object_cache.put(object_id, arc_obj.clone());
223 self.memory_stats.cached_objects = self.object_cache.len();
224
225 self.object_cache
229 .get(&object_id)
230 .map(|arc| unsafe { &*Arc::as_ptr(arc) })
231 .ok_or(ParseError::SyntaxError {
232 position: 0,
233 message: "Object not in cache after insertion".to_string(),
234 })
235 }
236
237 fn load_object_from_disk(&mut self, obj_num: u32, gen_num: u16) -> ParseResult<PdfObject> {
239 if let Some(ext_entry) = self.xref.get_extended_entry(obj_num) {
241 if let Some((stream_obj_num, index_in_stream)) = ext_entry.compressed_info {
242 return self.get_compressed_object_direct(
244 obj_num,
245 gen_num,
246 stream_obj_num,
247 index_in_stream,
248 );
249 }
250 }
251
252 let entry = self
254 .xref
255 .get_entry(obj_num)
256 .ok_or(ParseError::InvalidReference(obj_num, gen_num))?;
257
258 if !entry.in_use {
259 return Ok(PdfObject::Null);
261 }
262
263 if entry.generation != gen_num {
264 return Err(ParseError::InvalidReference(obj_num, gen_num));
265 }
266
267 self.reader.seek(std::io::SeekFrom::Start(entry.offset))?;
269
270 let mut lexer =
272 super::lexer::Lexer::new_with_options(&mut self.reader, self.options.clone());
273
274 let token = lexer.next_token()?;
276 let read_obj_num = match token {
277 super::lexer::Token::Integer(n) => n as u32,
278 _ => {
279 if self.options.lenient_syntax {
281 if self.options.collect_warnings {
282 eprintln!(
283 "Warning: Using expected object number {obj_num} instead of parsed token"
284 );
285 }
286 obj_num
287 } else {
288 return Err(ParseError::SyntaxError {
289 position: entry.offset as usize,
290 message: "Expected object number".to_string(),
291 });
292 }
293 }
294 };
295
296 if read_obj_num != obj_num && !self.options.lenient_syntax {
297 return Err(ParseError::SyntaxError {
298 position: entry.offset as usize,
299 message: format!(
300 "Object number mismatch: expected {obj_num}, found {read_obj_num}"
301 ),
302 });
303 }
304
305 let token = lexer.next_token()?;
307 let read_gen_num = match token {
308 super::lexer::Token::Integer(n) => n as u16,
309 _ => {
310 if self.options.lenient_syntax {
311 if self.options.collect_warnings {
312 eprintln!(
313 "Warning: Using generation 0 instead of parsed token for object {obj_num}"
314 );
315 }
316 0
317 } else {
318 return Err(ParseError::SyntaxError {
319 position: entry.offset as usize,
320 message: "Expected generation number".to_string(),
321 });
322 }
323 }
324 };
325
326 if read_gen_num != gen_num && !self.options.lenient_syntax {
327 return Err(ParseError::SyntaxError {
328 position: entry.offset as usize,
329 message: format!(
330 "Generation number mismatch: expected {gen_num}, found {read_gen_num}"
331 ),
332 });
333 }
334
335 let token = lexer.next_token()?;
337 match token {
338 super::lexer::Token::Obj => {}
339 _ => {
340 if self.options.lenient_syntax {
341 if self.options.collect_warnings {
342 eprintln!("Warning: Missing 'obj' keyword for object {obj_num}");
343 }
344 } else {
345 return Err(ParseError::SyntaxError {
346 position: entry.offset as usize,
347 message: "Expected 'obj' keyword".to_string(),
348 });
349 }
350 }
351 }
352
353 let object = PdfObject::parse(&mut lexer)?;
355
356 if let Ok(token) = lexer.peek_token() {
358 if let super::lexer::Token::EndObj = token {
359 let _ = lexer.next_token();
360 } else if !self.options.lenient_syntax && self.options.collect_warnings {
361 eprintln!("Warning: Missing 'endobj' for object {obj_num}");
362 }
363 }
364
365 Ok(object)
366 }
367
368 fn get_compressed_object_direct(
370 &mut self,
371 obj_num: u32,
372 _gen_num: u16,
373 stream_obj_num: u32,
374 _index_in_stream: u32,
375 ) -> ParseResult<PdfObject> {
376 if !self.object_stream_cache.contains_key(&stream_obj_num) {
378 let stream_obj = self.load_object_from_disk(stream_obj_num, 0)?;
380
381 if let PdfObject::Stream(stream) = stream_obj {
382 let obj_stream = ObjectStream::parse(stream, &ParseOptions::default())?;
383 self.object_stream_cache.insert(stream_obj_num, obj_stream);
384 } else {
385 return Err(ParseError::SyntaxError {
386 position: 0,
387 message: "Object stream is not a stream object".to_string(),
388 });
389 }
390 }
391
392 let obj_stream = self
394 .object_stream_cache
395 .get(&stream_obj_num)
396 .ok_or_else(|| ParseError::SyntaxError {
397 position: 0,
398 message: "Object stream not found in cache".to_string(),
399 })?;
400
401 obj_stream
402 .get_object(obj_num)
403 .cloned()
404 .ok_or(ParseError::InvalidReference(obj_num, 0))
405 }
406
407 fn find_catalog_object(&mut self) -> ParseResult<(u32, u16)> {
409 for obj_num in 1..100 {
412 if let Ok(PdfObject::Dictionary(dict)) = self.get_object(obj_num, 0) {
413 if let Some(PdfObject::Name(type_name)) = dict.get("Type") {
414 if type_name.0.as_bytes() == b"Catalog" {
415 return Ok((obj_num, 0));
416 }
417 }
418 }
419 }
420 Err(ParseError::MissingKey("Catalog".to_string()))
421 }
422
423 pub fn reader(&mut self) -> &mut BufReader<R> {
425 &mut self.reader
426 }
427}
428
429pub fn estimate_object_size(obj: &PdfObject) -> usize {
431 match obj {
432 PdfObject::Null => 8,
433 PdfObject::Boolean(_) => 16,
434 PdfObject::Integer(_) => 16,
435 PdfObject::Real(_) => 16,
436 PdfObject::String(s) => 24 + s.as_bytes().len(),
437 PdfObject::Name(n) => 24 + n.0.len(),
438 PdfObject::Array(arr) => {
439 24 + arr.len() * 8 + arr.0.iter().map(estimate_object_size).sum::<usize>()
440 }
441 PdfObject::Dictionary(dict) => {
442 24 + dict.0.len() * 16
443 + dict
444 .0
445 .iter()
446 .map(|(k, v)| k.0.len() + estimate_object_size(v))
447 .sum::<usize>()
448 }
449 PdfObject::Stream(s) => {
450 48 + s.data.len() + estimate_object_size(&PdfObject::Dictionary(s.dict.clone()))
451 }
452 PdfObject::Reference(_, _) => 16,
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459 use crate::parser::objects::{PdfArray, PdfDictionary, PdfName, PdfStream, PdfString};
460 use std::io::Cursor;
461
462 fn create_minimal_pdf() -> Vec<u8> {
463 b"%PDF-1.4\n\
4641 0 obj\n\
465<< /Type /Catalog /Pages 2 0 R >>\n\
466endobj\n\
4672 0 obj\n\
468<< /Type /Pages /Kids [3 0 R] /Count 1 >>\n\
469endobj\n\
4703 0 obj\n\
471<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\n\
472endobj\n\
473xref\n\
4740 4\n\
4750000000000 65535 f \n\
4760000000009 00000 n \n\
4770000000058 00000 n \n\
4780000000117 00000 n \n\
479trailer\n\
480<< /Size 4 /Root 1 0 R >>\n\
481startxref\n\
482193\n\
483%%EOF"
484 .to_vec()
485 }
486
487 fn create_empty_pdf() -> Vec<u8> {
488 Vec::new()
489 }
490
491 fn create_invalid_pdf() -> Vec<u8> {
492 b"Not a PDF file".to_vec()
493 }
494
495 #[test]
496 fn test_memory_options_integration() {
497 let options = MemoryOptions::default().with_cache_size(100);
498 assert_eq!(options.cache_size, 100);
499
500 let options = MemoryOptions::default().with_cache_size(0);
501 assert_eq!(options.cache_size, 0);
502 }
503
504 #[test]
505 fn test_object_size_estimation_basic_types() {
506 let obj = PdfObject::Null;
508 assert_eq!(estimate_object_size(&obj), 8);
509
510 let obj = PdfObject::Boolean(true);
512 assert_eq!(estimate_object_size(&obj), 16);
513
514 let obj = PdfObject::Boolean(false);
515 assert_eq!(estimate_object_size(&obj), 16);
516
517 let obj = PdfObject::Integer(42);
519 assert_eq!(estimate_object_size(&obj), 16);
520
521 let obj = PdfObject::Integer(-1000);
522 assert_eq!(estimate_object_size(&obj), 16);
523
524 let obj = PdfObject::Real(3.14159);
526 assert_eq!(estimate_object_size(&obj), 16);
527
528 let obj = PdfObject::Reference(5, 0);
530 assert_eq!(estimate_object_size(&obj), 16);
531 }
532
533 #[test]
534 fn test_object_size_estimation_string_types() {
535 let obj = PdfObject::String(PdfString::new(b"".to_vec()));
537 assert_eq!(estimate_object_size(&obj), 24);
538
539 let obj = PdfObject::String(PdfString::new(b"Hello".to_vec()));
541 assert_eq!(estimate_object_size(&obj), 24 + 5);
542
543 let long_text = "A".repeat(1000);
545 let obj = PdfObject::String(PdfString::new(long_text.as_bytes().to_vec()));
546 assert_eq!(estimate_object_size(&obj), 24 + 1000);
547
548 let obj = PdfObject::Name(PdfName::new("Type".to_string()));
550 assert_eq!(estimate_object_size(&obj), 24 + 4);
551
552 let obj = PdfObject::Name(PdfName::new("".to_string()));
553 assert_eq!(estimate_object_size(&obj), 24);
554 }
555
556 #[test]
557 fn test_object_size_estimation_array() {
558 let obj = PdfObject::Array(PdfArray(vec![]));
560 assert_eq!(estimate_object_size(&obj), 24);
561
562 let obj = PdfObject::Array(PdfArray(vec![
564 PdfObject::Integer(1),
565 PdfObject::Integer(2),
566 PdfObject::Integer(3),
567 ]));
568 assert_eq!(estimate_object_size(&obj), 24 + 3 * 8 + 3 * 16);
569
570 let inner_array = PdfObject::Array(PdfArray(vec![
572 PdfObject::Integer(10),
573 PdfObject::Integer(20),
574 ]));
575 let obj = PdfObject::Array(PdfArray(vec![PdfObject::Integer(1), inner_array]));
576 let expected = 24 + 2 * 8 + 16 + (24 + 2 * 8 + 2 * 16);
577 assert_eq!(estimate_object_size(&obj), expected);
578 }
579
580 #[test]
581 fn test_object_size_estimation_dictionary() {
582 let obj = PdfObject::Dictionary(PdfDictionary::new());
584 assert_eq!(estimate_object_size(&obj), 24);
585
586 let mut dict = PdfDictionary::new();
588 dict.insert(
589 "Type".to_string(),
590 PdfObject::Name(PdfName::new("Catalog".to_string())),
591 );
592 dict.insert("Count".to_string(), PdfObject::Integer(5));
593
594 let obj = PdfObject::Dictionary(dict);
595 let expected = 24 + 2 * 16 + (4 + 24 + 7) + (5 + 16);
596 assert_eq!(estimate_object_size(&obj), expected);
597 }
598
599 #[test]
600 fn test_object_size_estimation_stream() {
601 let mut dict = PdfDictionary::new();
602 dict.insert("Length".to_string(), PdfObject::Integer(10));
603
604 let stream = PdfObject::Stream(PdfStream {
605 dict: dict.clone(),
606 data: b"Hello Test".to_vec(),
607 });
608
609 let dict_size = estimate_object_size(&PdfObject::Dictionary(dict));
610 let expected = 48 + 10 + dict_size;
611 assert_eq!(estimate_object_size(&stream), expected);
612 }
613
614 #[test]
615 fn test_object_size_estimation_complex_structure() {
616 let mut inner_dict = PdfDictionary::new();
618 inner_dict.insert(
619 "Font".to_string(),
620 PdfObject::Name(PdfName::new("Helvetica".to_string())),
621 );
622 inner_dict.insert("Size".to_string(), PdfObject::Integer(12));
623
624 let array = PdfObject::Array(PdfArray(vec![
625 PdfObject::String(PdfString::new(b"Text content".to_vec())),
626 PdfObject::Dictionary(inner_dict),
627 PdfObject::Reference(10, 0),
628 ]));
629
630 let mut main_dict = PdfDictionary::new();
631 main_dict.insert(
632 "Type".to_string(),
633 PdfObject::Name(PdfName::new("Page".to_string())),
634 );
635 main_dict.insert("Contents".to_string(), array);
636
637 let obj = PdfObject::Dictionary(main_dict);
638
639 let size = estimate_object_size(&obj);
641 assert!(size > 100);
642 assert!(size < 1000);
643 }
644
645 #[test]
646 fn test_optimized_reader_empty_file() {
647 let data = create_empty_pdf();
648 let cursor = Cursor::new(data);
649
650 let result = OptimizedPdfReader::new(cursor);
651 assert!(result.is_err());
652 if let Err(ParseError::EmptyFile) = result {
653 } else {
655 panic!("Expected EmptyFile error");
656 }
657 }
658
659 #[test]
660 fn test_optimized_reader_invalid_file() {
661 let data = create_invalid_pdf();
662 let cursor = Cursor::new(data);
663
664 let result = OptimizedPdfReader::new(cursor);
665 assert!(result.is_err());
666 }
668
669 #[test]
670 fn test_optimized_reader_creation_with_options() {
671 let data = create_minimal_pdf();
672 let cursor = Cursor::new(data);
673
674 let parse_options = ParseOptions {
675 lenient_syntax: true,
676 collect_warnings: false,
677 ..Default::default()
678 };
679
680 let memory_options = MemoryOptions::default().with_cache_size(50);
681
682 let result = OptimizedPdfReader::new_with_options(cursor, parse_options, memory_options);
683 if result.is_err() {
684 return;
686 }
687
688 let reader = result.unwrap();
689 assert!(reader.options().lenient_syntax);
690 assert!(!reader.options().collect_warnings);
691 }
692
693 #[test]
694 fn test_optimized_reader_version_access() {
695 let data = create_minimal_pdf();
696 let cursor = Cursor::new(data);
697
698 let result = OptimizedPdfReader::new(cursor);
699 if result.is_err() {
700 return;
702 }
703
704 let reader = result.unwrap();
705 let version = reader.version();
706
707 assert_eq!(version.major, 1);
709 assert_eq!(version.minor, 4);
710 }
711
712 #[test]
713 fn test_memory_options_validation() {
714 let data = create_minimal_pdf();
715 let cursor = Cursor::new(data);
716
717 let memory_options = MemoryOptions::default().with_cache_size(0);
719 let parse_options = ParseOptions::default();
720
721 let result = OptimizedPdfReader::new_with_options(cursor, parse_options, memory_options);
722 if result.is_err() {
723 let memory_opts = MemoryOptions::default().with_cache_size(0);
725 let cache_size = memory_opts.cache_size.max(1);
726 assert_eq!(cache_size, 1);
727 }
728 }
729
730 #[test]
731 fn test_estimate_object_size_edge_cases() {
732 let large_array = PdfObject::Array(PdfArray((0..1000).map(PdfObject::Integer).collect()));
734 let size = estimate_object_size(&large_array);
735 assert!(size > 16000); let mut large_dict = PdfDictionary::new();
739 for i in 0..100 {
740 large_dict.insert(
741 format!("Key{i}"),
742 PdfObject::String(PdfString::new(format!("Value{i}").as_bytes().to_vec())),
743 );
744 }
745 let obj = PdfObject::Dictionary(large_dict);
746 let size = estimate_object_size(&obj);
747 assert!(size > 1000);
748 }
749
750 #[test]
751 fn test_memory_options_default_values() {
752 let options = MemoryOptions::default();
753
754 assert!(options.cache_size > 0);
756 assert!(options.cache_size < 10000); }
758
759 #[test]
760 fn test_memory_options_builder_pattern() {
761 let options = MemoryOptions::default().with_cache_size(500);
762
763 assert_eq!(options.cache_size, 500);
764 }
765
766 #[test]
767 fn test_object_size_estimation_consistency() {
768 let obj1 = PdfObject::String(PdfString::new(b"Test".to_vec()));
770 let obj2 = PdfObject::String(PdfString::new(b"Test".to_vec()));
771
772 assert_eq!(estimate_object_size(&obj1), estimate_object_size(&obj2));
773
774 let obj3 = PdfObject::String(PdfString::new(b"Different".to_vec()));
776 assert_ne!(estimate_object_size(&obj1), estimate_object_size(&obj3));
777 }
778
779 #[test]
780 fn test_object_size_estimation_zero_values() {
781 let obj = PdfObject::Integer(0);
783 assert_eq!(estimate_object_size(&obj), 16);
784
785 let obj = PdfObject::Real(0.0);
787 assert_eq!(estimate_object_size(&obj), 16);
788
789 let obj = PdfObject::Reference(0, 0);
791 assert_eq!(estimate_object_size(&obj), 16);
792 }
793
794 #[test]
795 fn test_object_size_estimation_negative_values() {
796 let obj = PdfObject::Integer(-42);
797 assert_eq!(estimate_object_size(&obj), 16);
798
799 let obj = PdfObject::Real(-3.14159);
800 assert_eq!(estimate_object_size(&obj), 16);
801 }
802
803 #[test]
804 fn test_object_size_estimation_unicode_strings() {
805 let unicode_text = "Hello δΈη π";
807 let obj = PdfObject::String(PdfString::new(unicode_text.as_bytes().to_vec()));
808 let expected_size = 24 + unicode_text.len();
809 assert_eq!(estimate_object_size(&obj), expected_size);
810 }
811
812 #[test]
813 fn test_object_size_estimation_mixed_array() {
814 let obj = PdfObject::Array(PdfArray(vec![
815 PdfObject::Null,
816 PdfObject::Boolean(true),
817 PdfObject::Integer(42),
818 PdfObject::Real(3.14),
819 PdfObject::String(PdfString::new(b"test".to_vec())),
820 PdfObject::Name(PdfName::new("Name".to_string())),
821 PdfObject::Reference(1, 0),
822 ]));
823
824 let expected = 24 + 7 * 8 + 8 + 16 + 16 + 16 + (24 + 4) + (24 + 4) + 16;
825 assert_eq!(estimate_object_size(&obj), expected);
826 }
827
828 #[test]
829 fn test_find_catalog_object_range() {
830 let data = create_minimal_pdf();
833 let cursor = Cursor::new(data);
834
835 if let Ok(mut reader) = OptimizedPdfReader::new(cursor) {
838 let _result = reader.find_catalog_object();
841 }
843 }
844
845 #[test]
846 fn test_memory_stats_tracking() {
847 let data = create_minimal_pdf();
849 let cursor = Cursor::new(data);
850
851 if let Ok(reader) = OptimizedPdfReader::new(cursor) {
852 assert_eq!(reader.memory_stats.cache_hits, 0);
854 assert_eq!(reader.memory_stats.cache_misses, 0);
855 assert_eq!(reader.memory_stats.cached_objects, 0);
856 }
857 }
858}