oxidize_pdf/
document.rs

1use crate::error::Result;
2use crate::objects::{Object, ObjectId};
3use crate::page::Page;
4use crate::writer::PdfWriter;
5use std::collections::HashMap;
6
7/// A PDF document that can contain multiple pages and metadata.
8///
9/// # Example
10///
11/// ```rust
12/// use oxidize_pdf::{Document, Page};
13///
14/// let mut doc = Document::new();
15/// doc.set_title("My Document");
16/// doc.set_author("John Doe");
17///
18/// let page = Page::a4();
19/// doc.add_page(page);
20///
21/// doc.save("output.pdf").unwrap();
22/// ```
23pub struct Document {
24    pub(crate) pages: Vec<Page>,
25    #[allow(dead_code)]
26    pub(crate) objects: HashMap<ObjectId, Object>,
27    #[allow(dead_code)]
28    pub(crate) next_object_id: u32,
29    pub(crate) metadata: DocumentMetadata,
30}
31
32/// Metadata for a PDF document.
33#[derive(Debug, Clone)]
34pub struct DocumentMetadata {
35    /// Document title
36    pub title: Option<String>,
37    /// Document author
38    pub author: Option<String>,
39    /// Document subject
40    pub subject: Option<String>,
41    /// Document keywords
42    pub keywords: Option<String>,
43    /// Software that created the original document
44    pub creator: Option<String>,
45    /// Software that produced the PDF
46    pub producer: Option<String>,
47}
48
49impl Default for DocumentMetadata {
50    fn default() -> Self {
51        Self {
52            title: None,
53            author: None,
54            subject: None,
55            keywords: None,
56            creator: Some("oxidize_pdf".to_string()),
57            producer: Some("oxidize_pdf".to_string()),
58        }
59    }
60}
61
62impl Document {
63    /// Creates a new empty PDF document.
64    pub fn new() -> Self {
65        Self {
66            pages: Vec::new(),
67            objects: HashMap::new(),
68            next_object_id: 1,
69            metadata: DocumentMetadata::default(),
70        }
71    }
72
73    /// Adds a page to the document.
74    pub fn add_page(&mut self, page: Page) {
75        self.pages.push(page);
76    }
77
78    /// Sets the document title.
79    pub fn set_title(&mut self, title: impl Into<String>) {
80        self.metadata.title = Some(title.into());
81    }
82
83    /// Sets the document author.
84    pub fn set_author(&mut self, author: impl Into<String>) {
85        self.metadata.author = Some(author.into());
86    }
87
88    /// Sets the document subject.
89    pub fn set_subject(&mut self, subject: impl Into<String>) {
90        self.metadata.subject = Some(subject.into());
91    }
92
93    /// Sets the document keywords.
94    pub fn set_keywords(&mut self, keywords: impl Into<String>) {
95        self.metadata.keywords = Some(keywords.into());
96    }
97
98    /// Saves the document to a file.
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if the file cannot be created or written.
103    pub fn save(&mut self, path: impl AsRef<std::path::Path>) -> Result<()> {
104        let mut writer = PdfWriter::new(path)?;
105        writer.write_document(self)?;
106        Ok(())
107    }
108
109    /// Writes the document to a buffer.
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if the PDF cannot be generated.
114    pub fn write(&mut self, buffer: &mut Vec<u8>) -> Result<()> {
115        let mut writer = PdfWriter::new_with_writer(buffer);
116        writer.write_document(self)?;
117        Ok(())
118    }
119
120    #[allow(dead_code)]
121    pub(crate) fn allocate_object_id(&mut self) -> ObjectId {
122        let id = ObjectId::new(self.next_object_id, 0);
123        self.next_object_id += 1;
124        id
125    }
126
127    #[allow(dead_code)]
128    pub(crate) fn add_object(&mut self, obj: Object) -> ObjectId {
129        let id = self.allocate_object_id();
130        self.objects.insert(id, obj);
131        id
132    }
133}
134
135impl Default for Document {
136    fn default() -> Self {
137        Self::new()
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn test_document_new() {
147        let doc = Document::new();
148        assert!(doc.pages.is_empty());
149        assert!(doc.objects.is_empty());
150        assert_eq!(doc.next_object_id, 1);
151        assert!(doc.metadata.title.is_none());
152        assert!(doc.metadata.author.is_none());
153        assert!(doc.metadata.subject.is_none());
154        assert!(doc.metadata.keywords.is_none());
155        assert_eq!(doc.metadata.creator, Some("oxidize_pdf".to_string()));
156        assert_eq!(doc.metadata.producer, Some("oxidize_pdf".to_string()));
157    }
158
159    #[test]
160    fn test_document_default() {
161        let doc = Document::default();
162        assert!(doc.pages.is_empty());
163        assert_eq!(doc.next_object_id, 1);
164    }
165
166    #[test]
167    fn test_add_page() {
168        let mut doc = Document::new();
169        let page1 = Page::a4();
170        let page2 = Page::letter();
171        
172        doc.add_page(page1);
173        assert_eq!(doc.pages.len(), 1);
174        
175        doc.add_page(page2);
176        assert_eq!(doc.pages.len(), 2);
177    }
178
179    #[test]
180    fn test_set_title() {
181        let mut doc = Document::new();
182        assert!(doc.metadata.title.is_none());
183        
184        doc.set_title("Test Document");
185        assert_eq!(doc.metadata.title, Some("Test Document".to_string()));
186        
187        doc.set_title(String::from("Another Title"));
188        assert_eq!(doc.metadata.title, Some("Another Title".to_string()));
189    }
190
191    #[test]
192    fn test_set_author() {
193        let mut doc = Document::new();
194        assert!(doc.metadata.author.is_none());
195        
196        doc.set_author("John Doe");
197        assert_eq!(doc.metadata.author, Some("John Doe".to_string()));
198    }
199
200    #[test]
201    fn test_set_subject() {
202        let mut doc = Document::new();
203        assert!(doc.metadata.subject.is_none());
204        
205        doc.set_subject("Test Subject");
206        assert_eq!(doc.metadata.subject, Some("Test Subject".to_string()));
207    }
208
209    #[test]
210    fn test_set_keywords() {
211        let mut doc = Document::new();
212        assert!(doc.metadata.keywords.is_none());
213        
214        doc.set_keywords("test, pdf, rust");
215        assert_eq!(doc.metadata.keywords, Some("test, pdf, rust".to_string()));
216    }
217
218    #[test]
219    fn test_metadata_default() {
220        let metadata = DocumentMetadata::default();
221        assert!(metadata.title.is_none());
222        assert!(metadata.author.is_none());
223        assert!(metadata.subject.is_none());
224        assert!(metadata.keywords.is_none());
225        assert_eq!(metadata.creator, Some("oxidize_pdf".to_string()));
226        assert_eq!(metadata.producer, Some("oxidize_pdf".to_string()));
227    }
228
229    #[test]
230    fn test_allocate_object_id() {
231        let mut doc = Document::new();
232        
233        let id1 = doc.allocate_object_id();
234        assert_eq!(id1.number(), 1);
235        assert_eq!(id1.generation(), 0);
236        assert_eq!(doc.next_object_id, 2);
237        
238        let id2 = doc.allocate_object_id();
239        assert_eq!(id2.number(), 2);
240        assert_eq!(id2.generation(), 0);
241        assert_eq!(doc.next_object_id, 3);
242    }
243
244    #[test]
245    fn test_add_object() {
246        let mut doc = Document::new();
247        assert!(doc.objects.is_empty());
248        
249        let obj = Object::Boolean(true);
250        let id = doc.add_object(obj.clone());
251        
252        assert_eq!(id.number(), 1);
253        assert_eq!(doc.objects.len(), 1);
254        assert!(doc.objects.contains_key(&id));
255    }
256
257    #[test]
258    fn test_write_to_buffer() {
259        let mut doc = Document::new();
260        doc.set_title("Buffer Test");
261        doc.add_page(Page::a4());
262        
263        let mut buffer = Vec::new();
264        let result = doc.write(&mut buffer);
265        
266        assert!(result.is_ok());
267        assert!(!buffer.is_empty());
268        assert!(buffer.starts_with(b"%PDF-1.7"));
269    }
270
271    #[test]
272    fn test_document_with_multiple_pages() {
273        let mut doc = Document::new();
274        doc.set_title("Multi-page Document");
275        doc.set_author("Test Author");
276        doc.set_subject("Testing multiple pages");
277        doc.set_keywords("test, multiple, pages");
278        
279        for _ in 0..5 {
280            doc.add_page(Page::a4());
281        }
282        
283        assert_eq!(doc.pages.len(), 5);
284        assert_eq!(doc.metadata.title, Some("Multi-page Document".to_string()));
285        assert_eq!(doc.metadata.author, Some("Test Author".to_string()));
286    }
287
288    #[test]
289    fn test_empty_document_write() {
290        let mut doc = Document::new();
291        let mut buffer = Vec::new();
292        
293        // Empty document should still produce valid PDF
294        let result = doc.write(&mut buffer);
295        assert!(result.is_ok());
296        assert!(!buffer.is_empty());
297        assert!(buffer.starts_with(b"%PDF-1.7"));
298    }
299
300    // Integration tests for Document ↔ Writer ↔ Parser interactions
301    mod integration_tests {
302        use super::*;
303        use crate::text::Font;
304        use crate::graphics::Color;
305        use tempfile::TempDir;
306        use std::fs;
307
308        #[test]
309        fn test_document_writer_roundtrip() {
310            let temp_dir = TempDir::new().unwrap();
311            let file_path = temp_dir.path().join("test.pdf");
312
313            // Create document with content
314            let mut doc = Document::new();
315            doc.set_title("Integration Test");
316            doc.set_author("Test Author");
317            doc.set_subject("Writer Integration");
318            doc.set_keywords("test, writer, integration");
319
320            let mut page = Page::a4();
321            page.text()
322                .set_font(Font::Helvetica, 12.0)
323                .at(100.0, 700.0)
324                .write("Integration Test Content")
325                .unwrap();
326
327            doc.add_page(page);
328
329            // Write to file
330            let result = doc.save(&file_path);
331            assert!(result.is_ok());
332
333            // Verify file exists and has content
334            assert!(file_path.exists());
335            let metadata = fs::metadata(&file_path).unwrap();
336            assert!(metadata.len() > 0);
337
338            // Read file back to verify PDF format
339            let content = fs::read(&file_path).unwrap();
340            assert!(content.starts_with(b"%PDF-1.7"));
341            // Check for %%EOF with or without newline
342            assert!(content.ends_with(b"%%EOF\n") || content.ends_with(b"%%EOF"));
343        }
344
345        #[test]
346        fn test_document_with_complex_content() {
347            let temp_dir = TempDir::new().unwrap();
348            let file_path = temp_dir.path().join("complex.pdf");
349
350            let mut doc = Document::new();
351            doc.set_title("Complex Content Test");
352
353            // Create page with mixed content
354            let mut page = Page::a4();
355            
356            // Add text
357            page.text()
358                .set_font(Font::Helvetica, 14.0)
359                .at(50.0, 750.0)
360                .write("Complex Content Test")
361                .unwrap();
362
363            // Add graphics
364            page.graphics()
365                .set_fill_color(Color::rgb(0.8, 0.2, 0.2))
366                .rectangle(50.0, 500.0, 200.0, 100.0)
367                .fill();
368
369            page.graphics()
370                .set_stroke_color(Color::rgb(0.2, 0.2, 0.8))
371                .set_line_width(2.0)
372                .move_to(50.0, 400.0)
373                .line_to(250.0, 400.0)
374                .stroke();
375
376            doc.add_page(page);
377
378            // Write and verify
379            let result = doc.save(&file_path);
380            assert!(result.is_ok());
381            assert!(file_path.exists());
382        }
383
384        #[test]
385        fn test_document_multiple_pages_integration() {
386            let temp_dir = TempDir::new().unwrap();
387            let file_path = temp_dir.path().join("multipage.pdf");
388
389            let mut doc = Document::new();
390            doc.set_title("Multi-page Integration Test");
391
392            // Create multiple pages with different content
393            for i in 1..=5 {
394                let mut page = Page::a4();
395                
396                page.text()
397                    .set_font(Font::Helvetica, 16.0)
398                    .at(50.0, 750.0)
399                    .write(&format!("Page {}", i))
400                    .unwrap();
401
402                page.text()
403                    .set_font(Font::Helvetica, 12.0)
404                    .at(50.0, 700.0)
405                    .write(&format!("This is the content for page {}", i))
406                    .unwrap();
407
408                // Add unique graphics for each page
409                let color = match i % 3 {
410                    0 => Color::rgb(1.0, 0.0, 0.0),
411                    1 => Color::rgb(0.0, 1.0, 0.0),
412                    _ => Color::rgb(0.0, 0.0, 1.0),
413                };
414
415                page.graphics()
416                    .set_fill_color(color)
417                    .rectangle(50.0, 600.0, 100.0, 50.0)
418                    .fill();
419
420                doc.add_page(page);
421            }
422
423            // Write and verify
424            let result = doc.save(&file_path);
425            assert!(result.is_ok());
426            assert!(file_path.exists());
427
428            // Verify file size is reasonable for 5 pages
429            let metadata = fs::metadata(&file_path).unwrap();
430            assert!(metadata.len() > 1000); // Should be substantial
431        }
432
433        #[test]
434        fn test_document_metadata_persistence() {
435            let temp_dir = TempDir::new().unwrap();
436            let file_path = temp_dir.path().join("metadata.pdf");
437
438            let mut doc = Document::new();
439            doc.set_title("Metadata Persistence Test");
440            doc.set_author("Test Author");
441            doc.set_subject("Testing metadata preservation");
442            doc.set_keywords("metadata, persistence, test");
443
444            doc.add_page(Page::a4());
445
446            // Write to file
447            let result = doc.save(&file_path);
448            assert!(result.is_ok());
449
450            // Read file content to verify metadata is present
451            let content = fs::read(&file_path).unwrap();
452            let content_str = String::from_utf8_lossy(&content);
453            
454            // Check that metadata appears in the PDF
455            assert!(content_str.contains("Metadata Persistence Test"));
456            assert!(content_str.contains("Test Author"));
457        }
458
459        #[test]
460        fn test_document_writer_error_handling() {
461            let mut doc = Document::new();
462            doc.add_page(Page::a4());
463
464            // Test writing to invalid path
465            let result = doc.save("/invalid/path/test.pdf");
466            assert!(result.is_err());
467        }
468
469        #[test]
470        fn test_document_object_management() {
471            let mut doc = Document::new();
472            
473            // Add objects and verify they're managed properly
474            let obj1 = Object::Boolean(true);
475            let obj2 = Object::Integer(42);
476            let obj3 = Object::Real(3.14);
477
478            let id1 = doc.add_object(obj1.clone());
479            let id2 = doc.add_object(obj2.clone());
480            let id3 = doc.add_object(obj3.clone());
481
482            assert_eq!(id1.number(), 1);
483            assert_eq!(id2.number(), 2);
484            assert_eq!(id3.number(), 3);
485
486            assert_eq!(doc.objects.len(), 3);
487            assert!(doc.objects.contains_key(&id1));
488            assert!(doc.objects.contains_key(&id2));
489            assert!(doc.objects.contains_key(&id3));
490
491            // Verify objects are correct
492            assert_eq!(doc.objects.get(&id1), Some(&obj1));
493            assert_eq!(doc.objects.get(&id2), Some(&obj2));
494            assert_eq!(doc.objects.get(&id3), Some(&obj3));
495        }
496
497        #[test]
498        fn test_document_page_integration() {
499            let mut doc = Document::new();
500            
501            // Test different page configurations
502            let page1 = Page::a4();
503            let page2 = Page::letter();
504            let mut page3 = Page::new(500.0, 400.0);
505            
506            // Add content to custom page
507            page3.text()
508                .set_font(Font::Helvetica, 10.0)
509                .at(25.0, 350.0)
510                .write("Custom size page")
511                .unwrap();
512
513            doc.add_page(page1);
514            doc.add_page(page2);
515            doc.add_page(page3);
516
517            assert_eq!(doc.pages.len(), 3);
518
519            // Verify pages maintain their properties (actual dimensions may vary)
520            assert!(doc.pages[0].width() > 500.0); // A4 width is reasonable
521            assert!(doc.pages[0].height() > 700.0); // A4 height is reasonable
522            assert!(doc.pages[1].width() > 500.0); // Letter width is reasonable
523            assert!(doc.pages[1].height() > 700.0); // Letter height is reasonable
524            assert_eq!(doc.pages[2].width(), 500.0); // Custom width
525            assert_eq!(doc.pages[2].height(), 400.0); // Custom height
526        }
527
528        #[test]
529        fn test_document_content_generation() {
530            let temp_dir = TempDir::new().unwrap();
531            let file_path = temp_dir.path().join("content.pdf");
532
533            let mut doc = Document::new();
534            doc.set_title("Content Generation Test");
535
536            let mut page = Page::a4();
537            
538            // Generate content programmatically
539            for i in 0..10 {
540                let y_pos = 700.0 - (i as f64 * 30.0);
541                page.text()
542                    .set_font(Font::Helvetica, 12.0)
543                    .at(50.0, y_pos)
544                    .write(&format!("Generated line {}", i + 1))
545                    .unwrap();
546            }
547
548            doc.add_page(page);
549
550            // Write and verify
551            let result = doc.save(&file_path);
552            assert!(result.is_ok());
553            assert!(file_path.exists());
554
555            // Verify content was generated
556            let metadata = fs::metadata(&file_path).unwrap();
557            assert!(metadata.len() > 500); // Should contain substantial content
558        }
559
560        #[test]
561        fn test_document_buffer_vs_file_write() {
562            let temp_dir = TempDir::new().unwrap();
563            let file_path = temp_dir.path().join("buffer_vs_file.pdf");
564
565            let mut doc = Document::new();
566            doc.set_title("Buffer vs File Test");
567            doc.add_page(Page::a4());
568
569            // Write to buffer
570            let mut buffer = Vec::new();
571            let buffer_result = doc.write(&mut buffer);
572            assert!(buffer_result.is_ok());
573
574            // Write to file
575            let file_result = doc.save(&file_path);
576            assert!(file_result.is_ok());
577
578            // Read file back
579            let file_content = fs::read(&file_path).unwrap();
580
581            // Both should be valid PDFs with same structure (timestamps may differ)
582            assert!(buffer.starts_with(b"%PDF-1.7"));
583            assert!(file_content.starts_with(b"%PDF-1.7"));
584            assert!(buffer.ends_with(b"%%EOF\n"));
585            assert!(file_content.ends_with(b"%%EOF\n"));
586            
587            // Both should contain the same title
588            let buffer_str = String::from_utf8_lossy(&buffer);
589            let file_str = String::from_utf8_lossy(&file_content);
590            assert!(buffer_str.contains("Buffer vs File Test"));
591            assert!(file_str.contains("Buffer vs File Test"));
592        }
593
594        #[test]
595        fn test_document_large_content_handling() {
596            let temp_dir = TempDir::new().unwrap();
597            let file_path = temp_dir.path().join("large_content.pdf");
598
599            let mut doc = Document::new();
600            doc.set_title("Large Content Test");
601
602            let mut page = Page::a4();
603            
604            // Add large amount of text content - make it much larger
605            let large_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ".repeat(200);
606            page.text()
607                .set_font(Font::Helvetica, 10.0)
608                .at(50.0, 750.0)
609                .write(&large_text)
610                .unwrap();
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 large content was handled properly - reduce expectation
620            let metadata = fs::metadata(&file_path).unwrap();
621            assert!(metadata.len() > 2000); // Should be substantial but realistic
622        }
623
624        #[test]
625        fn test_document_incremental_building() {
626            let temp_dir = TempDir::new().unwrap();
627            let file_path = temp_dir.path().join("incremental.pdf");
628
629            let mut doc = Document::new();
630            
631            // Build document incrementally
632            doc.set_title("Incremental Building Test");
633            
634            // Add first page
635            let mut page1 = Page::a4();
636            page1.text()
637                .set_font(Font::Helvetica, 12.0)
638                .at(50.0, 750.0)
639                .write("First page content")
640                .unwrap();
641            doc.add_page(page1);
642
643            // Add metadata
644            doc.set_author("Incremental Author");
645            doc.set_subject("Incremental Subject");
646
647            // Add second page
648            let mut page2 = Page::a4();
649            page2.text()
650                .set_font(Font::Helvetica, 12.0)
651                .at(50.0, 750.0)
652                .write("Second page content")
653                .unwrap();
654            doc.add_page(page2);
655
656            // Add more metadata
657            doc.set_keywords("incremental, building, test");
658
659            // Final write
660            let result = doc.save(&file_path);
661            assert!(result.is_ok());
662            assert!(file_path.exists());
663
664            // Verify final state
665            assert_eq!(doc.pages.len(), 2);
666            assert_eq!(doc.metadata.title, Some("Incremental Building Test".to_string()));
667            assert_eq!(doc.metadata.author, Some("Incremental Author".to_string()));
668            assert_eq!(doc.metadata.subject, Some("Incremental Subject".to_string()));
669            assert_eq!(doc.metadata.keywords, Some("incremental, building, test".to_string()));
670        }
671
672        #[test]
673        fn test_document_concurrent_page_operations() {
674            let mut doc = Document::new();
675            doc.set_title("Concurrent Operations Test");
676
677            // Simulate concurrent-like operations
678            let mut pages = Vec::new();
679            
680            // Create multiple pages
681            for i in 0..5 {
682                let mut page = Page::a4();
683                page.text()
684                    .set_font(Font::Helvetica, 12.0)
685                    .at(50.0, 750.0)
686                    .write(&format!("Concurrent page {}", i))
687                    .unwrap();
688                pages.push(page);
689            }
690
691            // Add all pages
692            for page in pages {
693                doc.add_page(page);
694            }
695
696            assert_eq!(doc.pages.len(), 5);
697
698            // Verify each page maintains its content
699            let temp_dir = TempDir::new().unwrap();
700            let file_path = temp_dir.path().join("concurrent.pdf");
701            let result = doc.save(&file_path);
702            assert!(result.is_ok());
703        }
704
705        #[test]
706        fn test_document_memory_efficiency() {
707            let mut doc = Document::new();
708            doc.set_title("Memory Efficiency Test");
709
710            // Add multiple pages with content
711            for i in 0..10 {
712                let mut page = Page::a4();
713                page.text()
714                    .set_font(Font::Helvetica, 12.0)
715                    .at(50.0, 700.0)
716                    .write(&format!("Memory test page {}", i))
717                    .unwrap();
718                doc.add_page(page);
719            }
720
721            // Write to buffer to test memory usage
722            let mut buffer = Vec::new();
723            let result = doc.write(&mut buffer);
724            assert!(result.is_ok());
725            assert!(!buffer.is_empty());
726
727            // Buffer should be reasonable size
728            assert!(buffer.len() < 1_000_000); // Should be less than 1MB for simple content
729        }
730    }
731}