oxidize_pdf/
document.rs

1use crate::error::Result;
2use crate::objects::{Object, ObjectId};
3use crate::page::Page;
4use crate::writer::PdfWriter;
5use chrono::{DateTime, Local, Utc};
6use std::collections::HashMap;
7
8/// A PDF document that can contain multiple pages and metadata.
9///
10/// # Example
11///
12/// ```rust
13/// use oxidize_pdf::{Document, Page};
14///
15/// let mut doc = Document::new();
16/// doc.set_title("My Document");
17/// doc.set_author("John Doe");
18///
19/// let page = Page::a4();
20/// doc.add_page(page);
21///
22/// doc.save("output.pdf").unwrap();
23/// ```
24pub struct Document {
25    pub(crate) pages: Vec<Page>,
26    #[allow(dead_code)]
27    pub(crate) objects: HashMap<ObjectId, Object>,
28    #[allow(dead_code)]
29    pub(crate) next_object_id: u32,
30    pub(crate) metadata: DocumentMetadata,
31}
32
33/// Metadata for a PDF document.
34#[derive(Debug, Clone)]
35pub struct DocumentMetadata {
36    /// Document title
37    pub title: Option<String>,
38    /// Document author
39    pub author: Option<String>,
40    /// Document subject
41    pub subject: Option<String>,
42    /// Document keywords
43    pub keywords: Option<String>,
44    /// Software that created the original document
45    pub creator: Option<String>,
46    /// Software that produced the PDF
47    pub producer: Option<String>,
48    /// Date and time the document was created
49    pub creation_date: Option<DateTime<Utc>>,
50    /// Date and time the document was last modified
51    pub modification_date: Option<DateTime<Utc>>,
52}
53
54impl Default for DocumentMetadata {
55    fn default() -> Self {
56        let now = Utc::now();
57        Self {
58            title: None,
59            author: None,
60            subject: None,
61            keywords: None,
62            creator: Some("oxidize_pdf".to_string()),
63            producer: Some(format!("oxidize_pdf v{}", env!("CARGO_PKG_VERSION"))),
64            creation_date: Some(now),
65            modification_date: Some(now),
66        }
67    }
68}
69
70impl Document {
71    /// Creates a new empty PDF document.
72    pub fn new() -> Self {
73        Self {
74            pages: Vec::new(),
75            objects: HashMap::new(),
76            next_object_id: 1,
77            metadata: DocumentMetadata::default(),
78        }
79    }
80
81    /// Adds a page to the document.
82    pub fn add_page(&mut self, page: Page) {
83        self.pages.push(page);
84    }
85
86    /// Sets the document title.
87    pub fn set_title(&mut self, title: impl Into<String>) {
88        self.metadata.title = Some(title.into());
89    }
90
91    /// Sets the document author.
92    pub fn set_author(&mut self, author: impl Into<String>) {
93        self.metadata.author = Some(author.into());
94    }
95
96    /// Sets the document subject.
97    pub fn set_subject(&mut self, subject: impl Into<String>) {
98        self.metadata.subject = Some(subject.into());
99    }
100
101    /// Sets the document keywords.
102    pub fn set_keywords(&mut self, keywords: impl Into<String>) {
103        self.metadata.keywords = Some(keywords.into());
104    }
105
106    /// Sets the document creator (software that created the original document).
107    pub fn set_creator(&mut self, creator: impl Into<String>) {
108        self.metadata.creator = Some(creator.into());
109    }
110
111    /// Sets the document producer (software that produced the PDF).
112    pub fn set_producer(&mut self, producer: impl Into<String>) {
113        self.metadata.producer = Some(producer.into());
114    }
115
116    /// Sets the document creation date.
117    pub fn set_creation_date(&mut self, date: DateTime<Utc>) {
118        self.metadata.creation_date = Some(date);
119    }
120
121    /// Sets the document creation date using local time.
122    pub fn set_creation_date_local(&mut self, date: DateTime<Local>) {
123        self.metadata.creation_date = Some(date.with_timezone(&Utc));
124    }
125
126    /// Sets the document modification date.
127    pub fn set_modification_date(&mut self, date: DateTime<Utc>) {
128        self.metadata.modification_date = Some(date);
129    }
130
131    /// Sets the document modification date using local time.
132    pub fn set_modification_date_local(&mut self, date: DateTime<Local>) {
133        self.metadata.modification_date = Some(date.with_timezone(&Utc));
134    }
135
136    /// Sets the modification date to the current time.
137    pub fn update_modification_date(&mut self) {
138        self.metadata.modification_date = Some(Utc::now());
139    }
140
141    /// Gets the number of pages in the document.
142    pub fn page_count(&self) -> usize {
143        self.pages.len()
144    }
145
146    /// Saves the document to a file.
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if the file cannot be created or written.
151    pub fn save(&mut self, path: impl AsRef<std::path::Path>) -> Result<()> {
152        // Update modification date before saving
153        self.update_modification_date();
154
155        let mut writer = PdfWriter::new(path)?;
156        writer.write_document(self)?;
157        Ok(())
158    }
159
160    /// Writes the document to a buffer.
161    ///
162    /// # Errors
163    ///
164    /// Returns an error if the PDF cannot be generated.
165    pub fn write(&mut self, buffer: &mut Vec<u8>) -> Result<()> {
166        // Update modification date before writing
167        self.update_modification_date();
168
169        let mut writer = PdfWriter::new_with_writer(buffer);
170        writer.write_document(self)?;
171        Ok(())
172    }
173
174    #[allow(dead_code)]
175    pub(crate) fn allocate_object_id(&mut self) -> ObjectId {
176        let id = ObjectId::new(self.next_object_id, 0);
177        self.next_object_id += 1;
178        id
179    }
180
181    #[allow(dead_code)]
182    pub(crate) fn add_object(&mut self, obj: Object) -> ObjectId {
183        let id = self.allocate_object_id();
184        self.objects.insert(id, obj);
185        id
186    }
187}
188
189impl Default for Document {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_document_new() {
201        let doc = Document::new();
202        assert!(doc.pages.is_empty());
203        assert!(doc.objects.is_empty());
204        assert_eq!(doc.next_object_id, 1);
205        assert!(doc.metadata.title.is_none());
206        assert!(doc.metadata.author.is_none());
207        assert!(doc.metadata.subject.is_none());
208        assert!(doc.metadata.keywords.is_none());
209        assert_eq!(doc.metadata.creator, Some("oxidize_pdf".to_string()));
210        assert!(doc
211            .metadata
212            .producer
213            .as_ref()
214            .unwrap()
215            .starts_with("oxidize_pdf"));
216    }
217
218    #[test]
219    fn test_document_default() {
220        let doc = Document::default();
221        assert!(doc.pages.is_empty());
222        assert_eq!(doc.next_object_id, 1);
223    }
224
225    #[test]
226    fn test_add_page() {
227        let mut doc = Document::new();
228        let page1 = Page::a4();
229        let page2 = Page::letter();
230
231        doc.add_page(page1);
232        assert_eq!(doc.pages.len(), 1);
233
234        doc.add_page(page2);
235        assert_eq!(doc.pages.len(), 2);
236    }
237
238    #[test]
239    fn test_set_title() {
240        let mut doc = Document::new();
241        assert!(doc.metadata.title.is_none());
242
243        doc.set_title("Test Document");
244        assert_eq!(doc.metadata.title, Some("Test Document".to_string()));
245
246        doc.set_title(String::from("Another Title"));
247        assert_eq!(doc.metadata.title, Some("Another Title".to_string()));
248    }
249
250    #[test]
251    fn test_set_author() {
252        let mut doc = Document::new();
253        assert!(doc.metadata.author.is_none());
254
255        doc.set_author("John Doe");
256        assert_eq!(doc.metadata.author, Some("John Doe".to_string()));
257    }
258
259    #[test]
260    fn test_set_subject() {
261        let mut doc = Document::new();
262        assert!(doc.metadata.subject.is_none());
263
264        doc.set_subject("Test Subject");
265        assert_eq!(doc.metadata.subject, Some("Test Subject".to_string()));
266    }
267
268    #[test]
269    fn test_set_keywords() {
270        let mut doc = Document::new();
271        assert!(doc.metadata.keywords.is_none());
272
273        doc.set_keywords("test, pdf, rust");
274        assert_eq!(doc.metadata.keywords, Some("test, pdf, rust".to_string()));
275    }
276
277    #[test]
278    fn test_metadata_default() {
279        let metadata = DocumentMetadata::default();
280        assert!(metadata.title.is_none());
281        assert!(metadata.author.is_none());
282        assert!(metadata.subject.is_none());
283        assert!(metadata.keywords.is_none());
284        assert_eq!(metadata.creator, Some("oxidize_pdf".to_string()));
285        assert!(metadata
286            .producer
287            .as_ref()
288            .unwrap()
289            .starts_with("oxidize_pdf"));
290    }
291
292    #[test]
293    fn test_allocate_object_id() {
294        let mut doc = Document::new();
295
296        let id1 = doc.allocate_object_id();
297        assert_eq!(id1.number(), 1);
298        assert_eq!(id1.generation(), 0);
299        assert_eq!(doc.next_object_id, 2);
300
301        let id2 = doc.allocate_object_id();
302        assert_eq!(id2.number(), 2);
303        assert_eq!(id2.generation(), 0);
304        assert_eq!(doc.next_object_id, 3);
305    }
306
307    #[test]
308    fn test_add_object() {
309        let mut doc = Document::new();
310        assert!(doc.objects.is_empty());
311
312        let obj = Object::Boolean(true);
313        let id = doc.add_object(obj.clone());
314
315        assert_eq!(id.number(), 1);
316        assert_eq!(doc.objects.len(), 1);
317        assert!(doc.objects.contains_key(&id));
318    }
319
320    #[test]
321    fn test_write_to_buffer() {
322        let mut doc = Document::new();
323        doc.set_title("Buffer Test");
324        doc.add_page(Page::a4());
325
326        let mut buffer = Vec::new();
327        let result = doc.write(&mut buffer);
328
329        assert!(result.is_ok());
330        assert!(!buffer.is_empty());
331        assert!(buffer.starts_with(b"%PDF-1.7"));
332    }
333
334    #[test]
335    fn test_document_with_multiple_pages() {
336        let mut doc = Document::new();
337        doc.set_title("Multi-page Document");
338        doc.set_author("Test Author");
339        doc.set_subject("Testing multiple pages");
340        doc.set_keywords("test, multiple, pages");
341
342        for _ in 0..5 {
343            doc.add_page(Page::a4());
344        }
345
346        assert_eq!(doc.pages.len(), 5);
347        assert_eq!(doc.metadata.title, Some("Multi-page Document".to_string()));
348        assert_eq!(doc.metadata.author, Some("Test Author".to_string()));
349    }
350
351    #[test]
352    fn test_empty_document_write() {
353        let mut doc = Document::new();
354        let mut buffer = Vec::new();
355
356        // Empty document should still produce valid PDF
357        let result = doc.write(&mut buffer);
358        assert!(result.is_ok());
359        assert!(!buffer.is_empty());
360        assert!(buffer.starts_with(b"%PDF-1.7"));
361    }
362
363    // Integration tests for Document ↔ Writer ↔ Parser interactions
364    mod integration_tests {
365        use super::*;
366        use crate::graphics::Color;
367        use crate::text::Font;
368        use std::fs;
369        use tempfile::TempDir;
370
371        #[test]
372        fn test_document_writer_roundtrip() {
373            let temp_dir = TempDir::new().unwrap();
374            let file_path = temp_dir.path().join("test.pdf");
375
376            // Create document with content
377            let mut doc = Document::new();
378            doc.set_title("Integration Test");
379            doc.set_author("Test Author");
380            doc.set_subject("Writer Integration");
381            doc.set_keywords("test, writer, integration");
382
383            let mut page = Page::a4();
384            page.text()
385                .set_font(Font::Helvetica, 12.0)
386                .at(100.0, 700.0)
387                .write("Integration Test Content")
388                .unwrap();
389
390            doc.add_page(page);
391
392            // Write to file
393            let result = doc.save(&file_path);
394            assert!(result.is_ok());
395
396            // Verify file exists and has content
397            assert!(file_path.exists());
398            let metadata = fs::metadata(&file_path).unwrap();
399            assert!(metadata.len() > 0);
400
401            // Read file back to verify PDF format
402            let content = fs::read(&file_path).unwrap();
403            assert!(content.starts_with(b"%PDF-1.7"));
404            // Check for %%EOF with or without newline
405            assert!(content.ends_with(b"%%EOF\n") || content.ends_with(b"%%EOF"));
406        }
407
408        #[test]
409        fn test_document_with_complex_content() {
410            let temp_dir = TempDir::new().unwrap();
411            let file_path = temp_dir.path().join("complex.pdf");
412
413            let mut doc = Document::new();
414            doc.set_title("Complex Content Test");
415
416            // Create page with mixed content
417            let mut page = Page::a4();
418
419            // Add text
420            page.text()
421                .set_font(Font::Helvetica, 14.0)
422                .at(50.0, 750.0)
423                .write("Complex Content Test")
424                .unwrap();
425
426            // Add graphics
427            page.graphics()
428                .set_fill_color(Color::rgb(0.8, 0.2, 0.2))
429                .rectangle(50.0, 500.0, 200.0, 100.0)
430                .fill();
431
432            page.graphics()
433                .set_stroke_color(Color::rgb(0.2, 0.2, 0.8))
434                .set_line_width(2.0)
435                .move_to(50.0, 400.0)
436                .line_to(250.0, 400.0)
437                .stroke();
438
439            doc.add_page(page);
440
441            // Write and verify
442            let result = doc.save(&file_path);
443            assert!(result.is_ok());
444            assert!(file_path.exists());
445        }
446
447        #[test]
448        fn test_document_multiple_pages_integration() {
449            let temp_dir = TempDir::new().unwrap();
450            let file_path = temp_dir.path().join("multipage.pdf");
451
452            let mut doc = Document::new();
453            doc.set_title("Multi-page Integration Test");
454
455            // Create multiple pages with different content
456            for i in 1..=5 {
457                let mut page = Page::a4();
458
459                page.text()
460                    .set_font(Font::Helvetica, 16.0)
461                    .at(50.0, 750.0)
462                    .write(&format!("Page {}", i))
463                    .unwrap();
464
465                page.text()
466                    .set_font(Font::Helvetica, 12.0)
467                    .at(50.0, 700.0)
468                    .write(&format!("This is the content for page {}", i))
469                    .unwrap();
470
471                // Add unique graphics for each page
472                let color = match i % 3 {
473                    0 => Color::rgb(1.0, 0.0, 0.0),
474                    1 => Color::rgb(0.0, 1.0, 0.0),
475                    _ => Color::rgb(0.0, 0.0, 1.0),
476                };
477
478                page.graphics()
479                    .set_fill_color(color)
480                    .rectangle(50.0, 600.0, 100.0, 50.0)
481                    .fill();
482
483                doc.add_page(page);
484            }
485
486            // Write and verify
487            let result = doc.save(&file_path);
488            assert!(result.is_ok());
489            assert!(file_path.exists());
490
491            // Verify file size is reasonable for 5 pages
492            let metadata = fs::metadata(&file_path).unwrap();
493            assert!(metadata.len() > 1000); // Should be substantial
494        }
495
496        #[test]
497        fn test_document_metadata_persistence() {
498            let temp_dir = TempDir::new().unwrap();
499            let file_path = temp_dir.path().join("metadata.pdf");
500
501            let mut doc = Document::new();
502            doc.set_title("Metadata Persistence Test");
503            doc.set_author("Test Author");
504            doc.set_subject("Testing metadata preservation");
505            doc.set_keywords("metadata, persistence, test");
506
507            doc.add_page(Page::a4());
508
509            // Write to file
510            let result = doc.save(&file_path);
511            assert!(result.is_ok());
512
513            // Read file content to verify metadata is present
514            let content = fs::read(&file_path).unwrap();
515            let content_str = String::from_utf8_lossy(&content);
516
517            // Check that metadata appears in the PDF
518            assert!(content_str.contains("Metadata Persistence Test"));
519            assert!(content_str.contains("Test Author"));
520        }
521
522        #[test]
523        fn test_document_writer_error_handling() {
524            let mut doc = Document::new();
525            doc.add_page(Page::a4());
526
527            // Test writing to invalid path
528            let result = doc.save("/invalid/path/test.pdf");
529            assert!(result.is_err());
530        }
531
532        #[test]
533        fn test_document_object_management() {
534            let mut doc = Document::new();
535
536            // Add objects and verify they're managed properly
537            let obj1 = Object::Boolean(true);
538            let obj2 = Object::Integer(42);
539            let obj3 = Object::Real(3.14);
540
541            let id1 = doc.add_object(obj1.clone());
542            let id2 = doc.add_object(obj2.clone());
543            let id3 = doc.add_object(obj3.clone());
544
545            assert_eq!(id1.number(), 1);
546            assert_eq!(id2.number(), 2);
547            assert_eq!(id3.number(), 3);
548
549            assert_eq!(doc.objects.len(), 3);
550            assert!(doc.objects.contains_key(&id1));
551            assert!(doc.objects.contains_key(&id2));
552            assert!(doc.objects.contains_key(&id3));
553
554            // Verify objects are correct
555            assert_eq!(doc.objects.get(&id1), Some(&obj1));
556            assert_eq!(doc.objects.get(&id2), Some(&obj2));
557            assert_eq!(doc.objects.get(&id3), Some(&obj3));
558        }
559
560        #[test]
561        fn test_document_page_integration() {
562            let mut doc = Document::new();
563
564            // Test different page configurations
565            let page1 = Page::a4();
566            let page2 = Page::letter();
567            let mut page3 = Page::new(500.0, 400.0);
568
569            // Add content to custom page
570            page3
571                .text()
572                .set_font(Font::Helvetica, 10.0)
573                .at(25.0, 350.0)
574                .write("Custom size page")
575                .unwrap();
576
577            doc.add_page(page1);
578            doc.add_page(page2);
579            doc.add_page(page3);
580
581            assert_eq!(doc.pages.len(), 3);
582
583            // Verify pages maintain their properties (actual dimensions may vary)
584            assert!(doc.pages[0].width() > 500.0); // A4 width is reasonable
585            assert!(doc.pages[0].height() > 700.0); // A4 height is reasonable
586            assert!(doc.pages[1].width() > 500.0); // Letter width is reasonable
587            assert!(doc.pages[1].height() > 700.0); // Letter height is reasonable
588            assert_eq!(doc.pages[2].width(), 500.0); // Custom width
589            assert_eq!(doc.pages[2].height(), 400.0); // Custom height
590        }
591
592        #[test]
593        fn test_document_content_generation() {
594            let temp_dir = TempDir::new().unwrap();
595            let file_path = temp_dir.path().join("content.pdf");
596
597            let mut doc = Document::new();
598            doc.set_title("Content Generation Test");
599
600            let mut page = Page::a4();
601
602            // Generate content programmatically
603            for i in 0..10 {
604                let y_pos = 700.0 - (i as f64 * 30.0);
605                page.text()
606                    .set_font(Font::Helvetica, 12.0)
607                    .at(50.0, y_pos)
608                    .write(&format!("Generated line {}", i + 1))
609                    .unwrap();
610            }
611
612            doc.add_page(page);
613
614            // Write and verify
615            let result = doc.save(&file_path);
616            assert!(result.is_ok());
617            assert!(file_path.exists());
618
619            // Verify content was generated
620            let metadata = fs::metadata(&file_path).unwrap();
621            assert!(metadata.len() > 500); // Should contain substantial content
622        }
623
624        #[test]
625        fn test_document_buffer_vs_file_write() {
626            let temp_dir = TempDir::new().unwrap();
627            let file_path = temp_dir.path().join("buffer_vs_file.pdf");
628
629            let mut doc = Document::new();
630            doc.set_title("Buffer vs File Test");
631            doc.add_page(Page::a4());
632
633            // Write to buffer
634            let mut buffer = Vec::new();
635            let buffer_result = doc.write(&mut buffer);
636            assert!(buffer_result.is_ok());
637
638            // Write to file
639            let file_result = doc.save(&file_path);
640            assert!(file_result.is_ok());
641
642            // Read file back
643            let file_content = fs::read(&file_path).unwrap();
644
645            // Both should be valid PDFs with same structure (timestamps may differ)
646            assert!(buffer.starts_with(b"%PDF-1.7"));
647            assert!(file_content.starts_with(b"%PDF-1.7"));
648            assert!(buffer.ends_with(b"%%EOF\n"));
649            assert!(file_content.ends_with(b"%%EOF\n"));
650
651            // Both should contain the same title
652            let buffer_str = String::from_utf8_lossy(&buffer);
653            let file_str = String::from_utf8_lossy(&file_content);
654            assert!(buffer_str.contains("Buffer vs File Test"));
655            assert!(file_str.contains("Buffer vs File Test"));
656        }
657
658        #[test]
659        fn test_document_large_content_handling() {
660            let temp_dir = TempDir::new().unwrap();
661            let file_path = temp_dir.path().join("large_content.pdf");
662
663            let mut doc = Document::new();
664            doc.set_title("Large Content Test");
665
666            let mut page = Page::a4();
667
668            // Add large amount of text content - make it much larger
669            let large_text =
670                "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ".repeat(200);
671            page.text()
672                .set_font(Font::Helvetica, 10.0)
673                .at(50.0, 750.0)
674                .write(&large_text)
675                .unwrap();
676
677            doc.add_page(page);
678
679            // Write and verify
680            let result = doc.save(&file_path);
681            assert!(result.is_ok());
682            assert!(file_path.exists());
683
684            // Verify large content was handled properly - reduce expectation
685            let metadata = fs::metadata(&file_path).unwrap();
686            assert!(metadata.len() > 2000); // Should be substantial but realistic
687        }
688
689        #[test]
690        fn test_document_incremental_building() {
691            let temp_dir = TempDir::new().unwrap();
692            let file_path = temp_dir.path().join("incremental.pdf");
693
694            let mut doc = Document::new();
695
696            // Build document incrementally
697            doc.set_title("Incremental Building Test");
698
699            // Add first page
700            let mut page1 = Page::a4();
701            page1
702                .text()
703                .set_font(Font::Helvetica, 12.0)
704                .at(50.0, 750.0)
705                .write("First page content")
706                .unwrap();
707            doc.add_page(page1);
708
709            // Add metadata
710            doc.set_author("Incremental Author");
711            doc.set_subject("Incremental Subject");
712
713            // Add second page
714            let mut page2 = Page::a4();
715            page2
716                .text()
717                .set_font(Font::Helvetica, 12.0)
718                .at(50.0, 750.0)
719                .write("Second page content")
720                .unwrap();
721            doc.add_page(page2);
722
723            // Add more metadata
724            doc.set_keywords("incremental, building, test");
725
726            // Final write
727            let result = doc.save(&file_path);
728            assert!(result.is_ok());
729            assert!(file_path.exists());
730
731            // Verify final state
732            assert_eq!(doc.pages.len(), 2);
733            assert_eq!(
734                doc.metadata.title,
735                Some("Incremental Building Test".to_string())
736            );
737            assert_eq!(doc.metadata.author, Some("Incremental Author".to_string()));
738            assert_eq!(
739                doc.metadata.subject,
740                Some("Incremental Subject".to_string())
741            );
742            assert_eq!(
743                doc.metadata.keywords,
744                Some("incremental, building, test".to_string())
745            );
746        }
747
748        #[test]
749        fn test_document_concurrent_page_operations() {
750            let mut doc = Document::new();
751            doc.set_title("Concurrent Operations Test");
752
753            // Simulate concurrent-like operations
754            let mut pages = Vec::new();
755
756            // Create multiple pages
757            for i in 0..5 {
758                let mut page = Page::a4();
759                page.text()
760                    .set_font(Font::Helvetica, 12.0)
761                    .at(50.0, 750.0)
762                    .write(&format!("Concurrent page {}", i))
763                    .unwrap();
764                pages.push(page);
765            }
766
767            // Add all pages
768            for page in pages {
769                doc.add_page(page);
770            }
771
772            assert_eq!(doc.pages.len(), 5);
773
774            // Verify each page maintains its content
775            let temp_dir = TempDir::new().unwrap();
776            let file_path = temp_dir.path().join("concurrent.pdf");
777            let result = doc.save(&file_path);
778            assert!(result.is_ok());
779        }
780
781        #[test]
782        fn test_document_memory_efficiency() {
783            let mut doc = Document::new();
784            doc.set_title("Memory Efficiency Test");
785
786            // Add multiple pages with content
787            for i in 0..10 {
788                let mut page = Page::a4();
789                page.text()
790                    .set_font(Font::Helvetica, 12.0)
791                    .at(50.0, 700.0)
792                    .write(&format!("Memory test page {}", i))
793                    .unwrap();
794                doc.add_page(page);
795            }
796
797            // Write to buffer to test memory usage
798            let mut buffer = Vec::new();
799            let result = doc.write(&mut buffer);
800            assert!(result.is_ok());
801            assert!(!buffer.is_empty());
802
803            // Buffer should be reasonable size
804            assert!(buffer.len() < 1_000_000); // Should be less than 1MB for simple content
805        }
806
807        #[test]
808        fn test_document_creator_producer() {
809            let mut doc = Document::new();
810
811            // Default values
812            assert_eq!(doc.metadata.creator, Some("oxidize_pdf".to_string()));
813            assert!(doc
814                .metadata
815                .producer
816                .as_ref()
817                .unwrap()
818                .contains("oxidize_pdf"));
819
820            // Set custom values
821            doc.set_creator("My Application");
822            doc.set_producer("My PDF Library v1.0");
823
824            assert_eq!(doc.metadata.creator, Some("My Application".to_string()));
825            assert_eq!(
826                doc.metadata.producer,
827                Some("My PDF Library v1.0".to_string())
828            );
829        }
830
831        #[test]
832        fn test_document_dates() {
833            use chrono::{TimeZone, Utc};
834
835            let mut doc = Document::new();
836
837            // Check default dates are set
838            assert!(doc.metadata.creation_date.is_some());
839            assert!(doc.metadata.modification_date.is_some());
840
841            // Set specific dates
842            let creation_date = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
843            let mod_date = Utc.with_ymd_and_hms(2023, 6, 15, 18, 30, 0).unwrap();
844
845            doc.set_creation_date(creation_date);
846            doc.set_modification_date(mod_date);
847
848            assert_eq!(doc.metadata.creation_date, Some(creation_date));
849            assert_eq!(doc.metadata.modification_date, Some(mod_date));
850        }
851
852        #[test]
853        fn test_document_dates_local() {
854            use chrono::{Local, TimeZone};
855
856            let mut doc = Document::new();
857
858            // Test setting dates with local time
859            let local_date = Local.with_ymd_and_hms(2023, 12, 25, 10, 30, 0).unwrap();
860            doc.set_creation_date_local(local_date);
861
862            // Verify it was converted to UTC
863            assert!(doc.metadata.creation_date.is_some());
864            // Just verify the date was set, don't compare exact values due to timezone complexities
865            assert!(doc.metadata.creation_date.is_some());
866        }
867
868        #[test]
869        fn test_update_modification_date() {
870            let mut doc = Document::new();
871
872            let initial_mod_date = doc.metadata.modification_date;
873            assert!(initial_mod_date.is_some());
874
875            // Sleep briefly to ensure time difference
876            std::thread::sleep(std::time::Duration::from_millis(10));
877
878            doc.update_modification_date();
879
880            let new_mod_date = doc.metadata.modification_date;
881            assert!(new_mod_date.is_some());
882            assert!(new_mod_date.unwrap() > initial_mod_date.unwrap());
883        }
884
885        #[test]
886        fn test_document_save_updates_modification_date() {
887            let temp_dir = TempDir::new().unwrap();
888            let file_path = temp_dir.path().join("mod_date_test.pdf");
889
890            let mut doc = Document::new();
891            doc.add_page(Page::a4());
892
893            let initial_mod_date = doc.metadata.modification_date;
894
895            // Sleep briefly to ensure time difference
896            std::thread::sleep(std::time::Duration::from_millis(10));
897
898            doc.save(&file_path).unwrap();
899
900            // Modification date should be updated
901            assert!(doc.metadata.modification_date.unwrap() > initial_mod_date.unwrap());
902        }
903
904        #[test]
905        fn test_document_metadata_complete() {
906            let mut doc = Document::new();
907
908            // Set all metadata fields
909            doc.set_title("Complete Metadata Test");
910            doc.set_author("Test Author");
911            doc.set_subject("Testing all metadata fields");
912            doc.set_keywords("test, metadata, complete");
913            doc.set_creator("Test Application v1.0");
914            doc.set_producer("oxidize_pdf Test Suite");
915
916            // Verify all fields
917            assert_eq!(
918                doc.metadata.title,
919                Some("Complete Metadata Test".to_string())
920            );
921            assert_eq!(doc.metadata.author, Some("Test Author".to_string()));
922            assert_eq!(
923                doc.metadata.subject,
924                Some("Testing all metadata fields".to_string())
925            );
926            assert_eq!(
927                doc.metadata.keywords,
928                Some("test, metadata, complete".to_string())
929            );
930            assert_eq!(
931                doc.metadata.creator,
932                Some("Test Application v1.0".to_string())
933            );
934            assert_eq!(
935                doc.metadata.producer,
936                Some("oxidize_pdf Test Suite".to_string())
937            );
938            assert!(doc.metadata.creation_date.is_some());
939            assert!(doc.metadata.modification_date.is_some());
940        }
941    }
942}