oxidize_pdf/
document.rs

1use crate::error::Result;
2use crate::fonts::{Font as CustomFont, FontCache};
3use crate::forms::{AcroForm, FormManager};
4use crate::objects::{Object, ObjectId};
5use crate::page::Page;
6use crate::page_labels::PageLabelTree;
7use crate::structure::{NamedDestinations, OutlineTree, PageTree};
8use crate::text::{FontEncoding, FontWithEncoding};
9use crate::writer::PdfWriter;
10use chrono::{DateTime, Local, Utc};
11use std::collections::{HashMap, HashSet};
12use std::sync::Arc;
13
14mod encryption;
15pub use encryption::{DocumentEncryption, EncryptionStrength};
16
17/// A PDF document that can contain multiple pages and metadata.
18///
19/// # Example
20///
21/// ```rust
22/// use oxidize_pdf::{Document, Page};
23///
24/// let mut doc = Document::new();
25/// doc.set_title("My Document");
26/// doc.set_author("John Doe");
27///
28/// let page = Page::a4();
29/// doc.add_page(page);
30///
31/// doc.save("output.pdf").unwrap();
32/// ```
33pub struct Document {
34    pub(crate) pages: Vec<Page>,
35    #[allow(dead_code)]
36    pub(crate) objects: HashMap<ObjectId, Object>,
37    #[allow(dead_code)]
38    pub(crate) next_object_id: u32,
39    pub(crate) metadata: DocumentMetadata,
40    pub(crate) encryption: Option<DocumentEncryption>,
41    pub(crate) outline: Option<OutlineTree>,
42    pub(crate) named_destinations: Option<NamedDestinations>,
43    #[allow(dead_code)]
44    pub(crate) page_tree: Option<PageTree>,
45    pub(crate) page_labels: Option<PageLabelTree>,
46    /// Default font encoding to use for fonts when no encoding is specified
47    pub(crate) default_font_encoding: Option<FontEncoding>,
48    /// Interactive form data (AcroForm)
49    pub(crate) acro_form: Option<AcroForm>,
50    /// Form manager for handling interactive forms
51    pub(crate) form_manager: Option<FormManager>,
52    /// Whether to compress streams when writing the PDF
53    pub(crate) compress: bool,
54    /// Whether to use compressed cross-reference streams (PDF 1.5+)
55    pub(crate) use_xref_streams: bool,
56    /// Cache for custom fonts
57    pub(crate) custom_fonts: FontCache,
58    /// Map from font name to embedded font object ID
59    #[allow(dead_code)]
60    pub(crate) embedded_fonts: HashMap<String, ObjectId>,
61    /// Characters used in the document (for font subsetting)
62    pub(crate) used_characters: HashSet<char>,
63    /// Action to execute when the document is opened
64    pub(crate) open_action: Option<crate::actions::Action>,
65    /// Viewer preferences for controlling document display
66    pub(crate) viewer_preferences: Option<crate::viewer_preferences::ViewerPreferences>,
67}
68
69/// Metadata for a PDF document.
70#[derive(Debug, Clone)]
71pub struct DocumentMetadata {
72    /// Document title
73    pub title: Option<String>,
74    /// Document author
75    pub author: Option<String>,
76    /// Document subject
77    pub subject: Option<String>,
78    /// Document keywords
79    pub keywords: Option<String>,
80    /// Software that created the original document
81    pub creator: Option<String>,
82    /// Software that produced the PDF
83    pub producer: Option<String>,
84    /// Date and time the document was created
85    pub creation_date: Option<DateTime<Utc>>,
86    /// Date and time the document was last modified
87    pub modification_date: Option<DateTime<Utc>>,
88}
89
90impl Default for DocumentMetadata {
91    fn default() -> Self {
92        let now = Utc::now();
93        Self {
94            title: None,
95            author: None,
96            subject: None,
97            keywords: None,
98            creator: Some("oxidize_pdf".to_string()),
99            producer: Some(format!("oxidize_pdf v{}", env!("CARGO_PKG_VERSION"))),
100            creation_date: Some(now),
101            modification_date: Some(now),
102        }
103    }
104}
105
106impl Document {
107    /// Creates a new empty PDF document.
108    pub fn new() -> Self {
109        Self {
110            pages: Vec::new(),
111            objects: HashMap::new(),
112            next_object_id: 1,
113            metadata: DocumentMetadata::default(),
114            encryption: None,
115            outline: None,
116            named_destinations: None,
117            page_tree: None,
118            page_labels: None,
119            default_font_encoding: None,
120            acro_form: None,
121            form_manager: None,
122            compress: true,          // Enable compression by default
123            use_xref_streams: false, // Disabled by default for compatibility
124            custom_fonts: FontCache::new(),
125            embedded_fonts: HashMap::new(),
126            used_characters: HashSet::new(),
127            open_action: None,
128            viewer_preferences: None,
129        }
130    }
131
132    /// Adds a page to the document.
133    pub fn add_page(&mut self, page: Page) {
134        // Collect used characters from the page
135        if let Some(used_chars) = page.get_used_characters() {
136            self.used_characters.extend(used_chars);
137        }
138        self.pages.push(page);
139    }
140
141    /// Sets the document title.
142    pub fn set_title(&mut self, title: impl Into<String>) {
143        self.metadata.title = Some(title.into());
144    }
145
146    /// Sets the document author.
147    pub fn set_author(&mut self, author: impl Into<String>) {
148        self.metadata.author = Some(author.into());
149    }
150
151    /// Sets the form manager for the document.
152    pub fn set_form_manager(&mut self, form_manager: FormManager) {
153        self.form_manager = Some(form_manager);
154    }
155
156    /// Sets the document subject.
157    pub fn set_subject(&mut self, subject: impl Into<String>) {
158        self.metadata.subject = Some(subject.into());
159    }
160
161    /// Sets the document keywords.
162    pub fn set_keywords(&mut self, keywords: impl Into<String>) {
163        self.metadata.keywords = Some(keywords.into());
164    }
165
166    /// Set document encryption
167    pub fn set_encryption(&mut self, encryption: DocumentEncryption) {
168        self.encryption = Some(encryption);
169    }
170
171    /// Set simple encryption with passwords
172    pub fn encrypt_with_passwords(
173        &mut self,
174        user_password: impl Into<String>,
175        owner_password: impl Into<String>,
176    ) {
177        self.encryption = Some(DocumentEncryption::with_passwords(
178            user_password,
179            owner_password,
180        ));
181    }
182
183    /// Check if document is encrypted
184    pub fn is_encrypted(&self) -> bool {
185        self.encryption.is_some()
186    }
187
188    /// Set the action to execute when the document is opened
189    pub fn set_open_action(&mut self, action: crate::actions::Action) {
190        self.open_action = Some(action);
191    }
192
193    /// Get the document open action
194    pub fn open_action(&self) -> Option<&crate::actions::Action> {
195        self.open_action.as_ref()
196    }
197
198    /// Set viewer preferences for controlling document display
199    pub fn set_viewer_preferences(
200        &mut self,
201        preferences: crate::viewer_preferences::ViewerPreferences,
202    ) {
203        self.viewer_preferences = Some(preferences);
204    }
205
206    /// Get viewer preferences
207    pub fn viewer_preferences(&self) -> Option<&crate::viewer_preferences::ViewerPreferences> {
208        self.viewer_preferences.as_ref()
209    }
210
211    /// Set document outline (bookmarks)
212    pub fn set_outline(&mut self, outline: OutlineTree) {
213        self.outline = Some(outline);
214    }
215
216    /// Get document outline
217    pub fn outline(&self) -> Option<&OutlineTree> {
218        self.outline.as_ref()
219    }
220
221    /// Get mutable document outline
222    pub fn outline_mut(&mut self) -> Option<&mut OutlineTree> {
223        self.outline.as_mut()
224    }
225
226    /// Set named destinations
227    pub fn set_named_destinations(&mut self, destinations: NamedDestinations) {
228        self.named_destinations = Some(destinations);
229    }
230
231    /// Get named destinations
232    pub fn named_destinations(&self) -> Option<&NamedDestinations> {
233        self.named_destinations.as_ref()
234    }
235
236    /// Get mutable named destinations
237    pub fn named_destinations_mut(&mut self) -> Option<&mut NamedDestinations> {
238        self.named_destinations.as_mut()
239    }
240
241    /// Set page labels
242    pub fn set_page_labels(&mut self, labels: PageLabelTree) {
243        self.page_labels = Some(labels);
244    }
245
246    /// Get page labels
247    pub fn page_labels(&self) -> Option<&PageLabelTree> {
248        self.page_labels.as_ref()
249    }
250
251    /// Get mutable page labels
252    pub fn page_labels_mut(&mut self) -> Option<&mut PageLabelTree> {
253        self.page_labels.as_mut()
254    }
255
256    /// Get page label for a specific page
257    pub fn get_page_label(&self, page_index: u32) -> String {
258        self.page_labels
259            .as_ref()
260            .and_then(|labels| labels.get_label(page_index))
261            .unwrap_or_else(|| (page_index + 1).to_string())
262    }
263
264    /// Get all page labels
265    pub fn get_all_page_labels(&self) -> Vec<String> {
266        let page_count = self.pages.len() as u32;
267        if let Some(labels) = &self.page_labels {
268            labels.get_all_labels(page_count)
269        } else {
270            (1..=page_count).map(|i| i.to_string()).collect()
271        }
272    }
273
274    /// Sets the document creator (software that created the original document).
275    pub fn set_creator(&mut self, creator: impl Into<String>) {
276        self.metadata.creator = Some(creator.into());
277    }
278
279    /// Sets the document producer (software that produced the PDF).
280    pub fn set_producer(&mut self, producer: impl Into<String>) {
281        self.metadata.producer = Some(producer.into());
282    }
283
284    /// Sets the document creation date.
285    pub fn set_creation_date(&mut self, date: DateTime<Utc>) {
286        self.metadata.creation_date = Some(date);
287    }
288
289    /// Sets the document creation date using local time.
290    pub fn set_creation_date_local(&mut self, date: DateTime<Local>) {
291        self.metadata.creation_date = Some(date.with_timezone(&Utc));
292    }
293
294    /// Sets the document modification date.
295    pub fn set_modification_date(&mut self, date: DateTime<Utc>) {
296        self.metadata.modification_date = Some(date);
297    }
298
299    /// Sets the document modification date using local time.
300    pub fn set_modification_date_local(&mut self, date: DateTime<Local>) {
301        self.metadata.modification_date = Some(date.with_timezone(&Utc));
302    }
303
304    /// Sets the modification date to the current time.
305    pub fn update_modification_date(&mut self) {
306        self.metadata.modification_date = Some(Utc::now());
307    }
308
309    /// Sets the default font encoding for fonts that don't specify an encoding.
310    ///
311    /// This encoding will be applied to fonts in the PDF font dictionary when
312    /// no explicit encoding is specified. Setting this to `None` (the default)
313    /// means no encoding metadata will be added to fonts unless explicitly specified.
314    ///
315    /// # Example
316    ///
317    /// ```rust
318    /// use oxidize_pdf::{Document, text::FontEncoding};
319    ///
320    /// let mut doc = Document::new();
321    /// doc.set_default_font_encoding(Some(FontEncoding::WinAnsiEncoding));
322    /// ```
323    pub fn set_default_font_encoding(&mut self, encoding: Option<FontEncoding>) {
324        self.default_font_encoding = encoding;
325    }
326
327    /// Gets the current default font encoding.
328    pub fn default_font_encoding(&self) -> Option<FontEncoding> {
329        self.default_font_encoding
330    }
331
332    /// Gets all fonts used in the document with their encodings.
333    ///
334    /// This scans all pages and collects the unique fonts used, applying
335    /// the default encoding where no explicit encoding is specified.
336    #[allow(dead_code)]
337    pub(crate) fn get_fonts_with_encodings(&self) -> Vec<FontWithEncoding> {
338        let mut fonts_used = HashSet::new();
339
340        // Collect fonts from all pages
341        for page in &self.pages {
342            // Get fonts from text content
343            for font in page.get_used_fonts() {
344                let font_with_encoding = match self.default_font_encoding {
345                    Some(default_encoding) => FontWithEncoding::new(font, Some(default_encoding)),
346                    None => FontWithEncoding::without_encoding(font),
347                };
348                fonts_used.insert(font_with_encoding);
349            }
350        }
351
352        fonts_used.into_iter().collect()
353    }
354
355    /// Add a custom font from a file path
356    ///
357    /// # Example
358    ///
359    /// ```rust,no_run
360    /// use oxidize_pdf::Document;
361    ///
362    /// let mut doc = Document::new();
363    /// doc.add_font("MyFont", "path/to/font.ttf").unwrap();
364    /// ```
365    pub fn add_font(
366        &mut self,
367        name: impl Into<String>,
368        path: impl AsRef<std::path::Path>,
369    ) -> Result<()> {
370        let name = name.into();
371        let font = CustomFont::from_file(&name, path)?;
372        self.custom_fonts.add_font(name, font)?;
373        Ok(())
374    }
375
376    /// Add a custom font from byte data
377    ///
378    /// # Example
379    ///
380    /// ```rust,no_run
381    /// use oxidize_pdf::Document;
382    ///
383    /// let mut doc = Document::new();
384    /// let font_data = vec![0; 1000]; // Your font data
385    /// doc.add_font_from_bytes("MyFont", font_data).unwrap();
386    /// ```
387    pub fn add_font_from_bytes(&mut self, name: impl Into<String>, data: Vec<u8>) -> Result<()> {
388        let name = name.into();
389        let font = CustomFont::from_bytes(&name, data)?;
390        self.custom_fonts.add_font(name, font)?;
391        Ok(())
392    }
393
394    /// Get a custom font by name
395    #[allow(dead_code)]
396    pub(crate) fn get_custom_font(&self, name: &str) -> Option<Arc<CustomFont>> {
397        self.custom_fonts.get_font(name)
398    }
399
400    /// Check if a custom font is loaded
401    pub fn has_custom_font(&self, name: &str) -> bool {
402        self.custom_fonts.has_font(name)
403    }
404
405    /// Get all loaded custom font names
406    pub fn custom_font_names(&self) -> Vec<String> {
407        self.custom_fonts.font_names()
408    }
409
410    /// Gets the number of pages in the document.
411    pub fn page_count(&self) -> usize {
412        self.pages.len()
413    }
414
415    /// Gets a reference to the AcroForm (interactive form) if present.
416    pub fn acro_form(&self) -> Option<&AcroForm> {
417        self.acro_form.as_ref()
418    }
419
420    /// Gets a mutable reference to the AcroForm (interactive form) if present.
421    pub fn acro_form_mut(&mut self) -> Option<&mut AcroForm> {
422        self.acro_form.as_mut()
423    }
424
425    /// Enables interactive forms by creating a FormManager if not already present.
426    /// The FormManager handles both the AcroForm and the connection with page widgets.
427    pub fn enable_forms(&mut self) -> &mut FormManager {
428        if self.form_manager.is_none() {
429            self.form_manager = Some(FormManager::new());
430        }
431        if self.acro_form.is_none() {
432            self.acro_form = Some(AcroForm::new());
433        }
434        // This should always succeed since we just ensured form_manager exists
435        self.form_manager
436            .as_mut()
437            .expect("FormManager should exist after initialization")
438    }
439
440    /// Disables interactive forms by removing both the AcroForm and FormManager.
441    pub fn disable_forms(&mut self) {
442        self.acro_form = None;
443        self.form_manager = None;
444    }
445
446    /// Saves the document to a file.
447    ///
448    /// # Errors
449    ///
450    /// Returns an error if the file cannot be created or written.
451    pub fn save(&mut self, path: impl AsRef<std::path::Path>) -> Result<()> {
452        // Update modification date before saving
453        self.update_modification_date();
454
455        // Create writer config with document's compression setting
456        let config = crate::writer::WriterConfig {
457            use_xref_streams: self.use_xref_streams,
458            pdf_version: if self.use_xref_streams { "1.5" } else { "1.7" }.to_string(),
459            compress_streams: self.compress,
460        };
461
462        use std::io::BufWriter;
463        let file = std::fs::File::create(path)?;
464        let writer = BufWriter::new(file);
465        let mut pdf_writer = PdfWriter::with_config(writer, config);
466
467        pdf_writer.write_document(self)?;
468        Ok(())
469    }
470
471    /// Saves the document to a file with custom writer configuration.
472    ///
473    /// # Errors
474    ///
475    /// Returns an error if the file cannot be created or written.
476    pub fn save_with_config(
477        &mut self,
478        path: impl AsRef<std::path::Path>,
479        config: crate::writer::WriterConfig,
480    ) -> Result<()> {
481        use std::io::BufWriter;
482
483        // Update modification date before saving
484        self.update_modification_date();
485
486        // Use the config as provided (don't override compress_streams)
487
488        let file = std::fs::File::create(path)?;
489        let writer = BufWriter::new(file);
490        let mut pdf_writer = PdfWriter::with_config(writer, config);
491        pdf_writer.write_document(self)?;
492        Ok(())
493    }
494
495    /// Saves the document to a file with custom values for headers/footers.
496    ///
497    /// This method processes all pages to replace custom placeholders in headers
498    /// and footers before saving the document.
499    ///
500    /// # Arguments
501    ///
502    /// * `path` - The path where the document should be saved
503    /// * `custom_values` - A map of placeholder names to their replacement values
504    ///
505    /// # Errors
506    ///
507    /// Returns an error if the file cannot be created or written.
508    pub fn save_with_custom_values(
509        &mut self,
510        path: impl AsRef<std::path::Path>,
511        custom_values: &std::collections::HashMap<String, String>,
512    ) -> Result<()> {
513        // Process all pages with custom values
514        let total_pages = self.pages.len();
515        for (index, page) in self.pages.iter_mut().enumerate() {
516            // Generate content with page info and custom values
517            let page_content = page.generate_content_with_page_info(
518                Some(index + 1),
519                Some(total_pages),
520                Some(custom_values),
521            )?;
522            // Update the page content
523            page.set_content(page_content);
524        }
525
526        // Save the document normally
527        self.save(path)
528    }
529
530    /// Writes the document to a buffer.
531    ///
532    /// # Errors
533    ///
534    /// Returns an error if the PDF cannot be generated.
535    pub fn write(&mut self, buffer: &mut Vec<u8>) -> Result<()> {
536        // Update modification date before writing
537        self.update_modification_date();
538
539        let mut writer = PdfWriter::new_with_writer(buffer);
540        writer.write_document(self)?;
541        Ok(())
542    }
543
544    #[allow(dead_code)]
545    pub(crate) fn allocate_object_id(&mut self) -> ObjectId {
546        let id = ObjectId::new(self.next_object_id, 0);
547        self.next_object_id += 1;
548        id
549    }
550
551    #[allow(dead_code)]
552    pub(crate) fn add_object(&mut self, obj: Object) -> ObjectId {
553        let id = self.allocate_object_id();
554        self.objects.insert(id, obj);
555        id
556    }
557
558    /// Enables or disables compression for PDF streams.
559    ///
560    /// When compression is enabled (default), content streams and XRef streams are compressed
561    /// using Flate/Zlib compression to reduce file size. When disabled, streams are written
562    /// uncompressed, making the PDF larger but easier to debug.
563    ///
564    /// # Arguments
565    ///
566    /// * `compress` - Whether to enable compression
567    ///
568    /// # Example
569    ///
570    /// ```rust
571    /// use oxidize_pdf::{Document, Page};
572    ///
573    /// let mut doc = Document::new();
574    ///
575    /// // Disable compression for debugging
576    /// doc.set_compress(false);
577    ///
578    /// doc.set_title("My Document");
579    /// doc.add_page(Page::a4());
580    ///
581    /// let pdf_bytes = doc.to_bytes().unwrap();
582    /// println!("Uncompressed PDF size: {} bytes", pdf_bytes.len());
583    /// ```
584    pub fn set_compress(&mut self, compress: bool) {
585        self.compress = compress;
586    }
587
588    /// Enable or disable compressed cross-reference streams (PDF 1.5+).
589    ///
590    /// Cross-reference streams provide more compact representation of the cross-reference
591    /// table and support additional features like compressed object streams.
592    ///
593    /// # Arguments
594    ///
595    /// * `enable` - Whether to enable compressed cross-reference streams
596    ///
597    /// # Example
598    ///
599    /// ```rust
600    /// use oxidize_pdf::Document;
601    ///
602    /// let mut doc = Document::new();
603    /// doc.enable_xref_streams(true);
604    /// ```
605    pub fn enable_xref_streams(&mut self, enable: bool) -> &mut Self {
606        self.use_xref_streams = enable;
607        self
608    }
609
610    /// Gets the current compression setting.
611    ///
612    /// # Returns
613    ///
614    /// Returns `true` if compression is enabled, `false` otherwise.
615    pub fn get_compress(&self) -> bool {
616        self.compress
617    }
618
619    /// Generates the PDF document as bytes in memory.
620    ///
621    /// This method provides in-memory PDF generation without requiring file I/O.
622    /// The document is serialized to bytes and returned as a `Vec<u8>`.
623    ///
624    /// # Returns
625    ///
626    /// Returns the PDF document as bytes on success.
627    ///
628    /// # Errors
629    ///
630    /// Returns an error if the document cannot be serialized.
631    ///
632    /// # Example
633    ///
634    /// ```rust
635    /// use oxidize_pdf::{Document, Page};
636    ///
637    /// let mut doc = Document::new();
638    /// doc.set_title("My Document");
639    ///
640    /// let page = Page::a4();
641    /// doc.add_page(page);
642    ///
643    /// let pdf_bytes = doc.to_bytes().unwrap();
644    /// println!("Generated PDF size: {} bytes", pdf_bytes.len());
645    /// ```
646    pub fn to_bytes(&mut self) -> Result<Vec<u8>> {
647        // Update modification date before serialization
648        self.update_modification_date();
649
650        // Create a buffer to write the PDF data to
651        let mut buffer = Vec::new();
652
653        // Create writer config with document's compression setting
654        let config = crate::writer::WriterConfig {
655            use_xref_streams: self.use_xref_streams,
656            pdf_version: if self.use_xref_streams { "1.5" } else { "1.7" }.to_string(),
657            compress_streams: self.compress,
658        };
659
660        // Use PdfWriter with the buffer as output and config
661        let mut writer = PdfWriter::with_config(&mut buffer, config);
662        writer.write_document(self)?;
663
664        Ok(buffer)
665    }
666
667    /// Generates the PDF document as bytes with custom writer configuration.
668    ///
669    /// This method allows customizing the PDF output (e.g., using XRef streams)
670    /// while still generating the document in memory.
671    ///
672    /// # Arguments
673    ///
674    /// * `config` - Writer configuration options
675    ///
676    /// # Returns
677    ///
678    /// Returns the PDF document as bytes on success.
679    ///
680    /// # Errors
681    ///
682    /// Returns an error if the document cannot be serialized.
683    ///
684    /// # Example
685    ///
686    /// ```rust
687    /// use oxidize_pdf::{Document, Page};
688    /// use oxidize_pdf::writer::WriterConfig;
689    ///
690    /// let mut doc = Document::new();
691    /// doc.set_title("My Document");
692    ///
693    /// let page = Page::a4();
694    /// doc.add_page(page);
695    ///
696    /// let config = WriterConfig {
697    ///     use_xref_streams: true,
698    ///     pdf_version: "1.5".to_string(),
699    ///     compress_streams: true,
700    /// };
701    ///
702    /// let pdf_bytes = doc.to_bytes_with_config(config).unwrap();
703    /// println!("Generated PDF size: {} bytes", pdf_bytes.len());
704    /// ```
705    pub fn to_bytes_with_config(&mut self, config: crate::writer::WriterConfig) -> Result<Vec<u8>> {
706        // Update modification date before serialization
707        self.update_modification_date();
708
709        // Use the config as provided (don't override compress_streams)
710
711        // Create a buffer to write the PDF data to
712        let mut buffer = Vec::new();
713
714        // Use PdfWriter with the buffer as output and custom config
715        let mut writer = PdfWriter::with_config(&mut buffer, config);
716        writer.write_document(self)?;
717
718        Ok(buffer)
719    }
720}
721
722impl Default for Document {
723    fn default() -> Self {
724        Self::new()
725    }
726}
727
728#[cfg(test)]
729mod tests {
730    use super::*;
731
732    #[test]
733    fn test_document_new() {
734        let doc = Document::new();
735        assert!(doc.pages.is_empty());
736        assert!(doc.objects.is_empty());
737        assert_eq!(doc.next_object_id, 1);
738        assert!(doc.metadata.title.is_none());
739        assert!(doc.metadata.author.is_none());
740        assert!(doc.metadata.subject.is_none());
741        assert!(doc.metadata.keywords.is_none());
742        assert_eq!(doc.metadata.creator, Some("oxidize_pdf".to_string()));
743        assert!(doc
744            .metadata
745            .producer
746            .as_ref()
747            .unwrap()
748            .starts_with("oxidize_pdf"));
749    }
750
751    #[test]
752    fn test_document_default() {
753        let doc = Document::default();
754        assert!(doc.pages.is_empty());
755        assert_eq!(doc.next_object_id, 1);
756    }
757
758    #[test]
759    fn test_add_page() {
760        let mut doc = Document::new();
761        let page1 = Page::a4();
762        let page2 = Page::letter();
763
764        doc.add_page(page1);
765        assert_eq!(doc.pages.len(), 1);
766
767        doc.add_page(page2);
768        assert_eq!(doc.pages.len(), 2);
769    }
770
771    #[test]
772    fn test_set_title() {
773        let mut doc = Document::new();
774        assert!(doc.metadata.title.is_none());
775
776        doc.set_title("Test Document");
777        assert_eq!(doc.metadata.title, Some("Test Document".to_string()));
778
779        doc.set_title(String::from("Another Title"));
780        assert_eq!(doc.metadata.title, Some("Another Title".to_string()));
781    }
782
783    #[test]
784    fn test_set_author() {
785        let mut doc = Document::new();
786        assert!(doc.metadata.author.is_none());
787
788        doc.set_author("John Doe");
789        assert_eq!(doc.metadata.author, Some("John Doe".to_string()));
790    }
791
792    #[test]
793    fn test_set_subject() {
794        let mut doc = Document::new();
795        assert!(doc.metadata.subject.is_none());
796
797        doc.set_subject("Test Subject");
798        assert_eq!(doc.metadata.subject, Some("Test Subject".to_string()));
799    }
800
801    #[test]
802    fn test_set_keywords() {
803        let mut doc = Document::new();
804        assert!(doc.metadata.keywords.is_none());
805
806        doc.set_keywords("test, pdf, rust");
807        assert_eq!(doc.metadata.keywords, Some("test, pdf, rust".to_string()));
808    }
809
810    #[test]
811    fn test_metadata_default() {
812        let metadata = DocumentMetadata::default();
813        assert!(metadata.title.is_none());
814        assert!(metadata.author.is_none());
815        assert!(metadata.subject.is_none());
816        assert!(metadata.keywords.is_none());
817        assert_eq!(metadata.creator, Some("oxidize_pdf".to_string()));
818        assert!(metadata
819            .producer
820            .as_ref()
821            .unwrap()
822            .starts_with("oxidize_pdf"));
823    }
824
825    #[test]
826    fn test_allocate_object_id() {
827        let mut doc = Document::new();
828
829        let id1 = doc.allocate_object_id();
830        assert_eq!(id1.number(), 1);
831        assert_eq!(id1.generation(), 0);
832        assert_eq!(doc.next_object_id, 2);
833
834        let id2 = doc.allocate_object_id();
835        assert_eq!(id2.number(), 2);
836        assert_eq!(id2.generation(), 0);
837        assert_eq!(doc.next_object_id, 3);
838    }
839
840    #[test]
841    fn test_add_object() {
842        let mut doc = Document::new();
843        assert!(doc.objects.is_empty());
844
845        let obj = Object::Boolean(true);
846        let id = doc.add_object(obj.clone());
847
848        assert_eq!(id.number(), 1);
849        assert_eq!(doc.objects.len(), 1);
850        assert!(doc.objects.contains_key(&id));
851    }
852
853    #[test]
854    fn test_write_to_buffer() {
855        let mut doc = Document::new();
856        doc.set_title("Buffer Test");
857        doc.add_page(Page::a4());
858
859        let mut buffer = Vec::new();
860        let result = doc.write(&mut buffer);
861
862        assert!(result.is_ok());
863        assert!(!buffer.is_empty());
864        assert!(buffer.starts_with(b"%PDF-1.7"));
865    }
866
867    #[test]
868    fn test_document_with_multiple_pages() {
869        let mut doc = Document::new();
870        doc.set_title("Multi-page Document");
871        doc.set_author("Test Author");
872        doc.set_subject("Testing multiple pages");
873        doc.set_keywords("test, multiple, pages");
874
875        for _ in 0..5 {
876            doc.add_page(Page::a4());
877        }
878
879        assert_eq!(doc.pages.len(), 5);
880        assert_eq!(doc.metadata.title, Some("Multi-page Document".to_string()));
881        assert_eq!(doc.metadata.author, Some("Test Author".to_string()));
882    }
883
884    #[test]
885    fn test_empty_document_write() {
886        let mut doc = Document::new();
887        let mut buffer = Vec::new();
888
889        // Empty document should still produce valid PDF
890        let result = doc.write(&mut buffer);
891        assert!(result.is_ok());
892        assert!(!buffer.is_empty());
893        assert!(buffer.starts_with(b"%PDF-1.7"));
894    }
895
896    // Integration tests for Document ↔ Writer ↔ Parser interactions
897    mod integration_tests {
898        use super::*;
899        use crate::graphics::Color;
900        use crate::text::Font;
901        use std::fs;
902        use tempfile::TempDir;
903
904        #[test]
905        fn test_document_writer_roundtrip() {
906            let temp_dir = TempDir::new().unwrap();
907            let file_path = temp_dir.path().join("test.pdf");
908
909            // Create document with content
910            let mut doc = Document::new();
911            doc.set_title("Integration Test");
912            doc.set_author("Test Author");
913            doc.set_subject("Writer Integration");
914            doc.set_keywords("test, writer, integration");
915
916            let mut page = Page::a4();
917            page.text()
918                .set_font(Font::Helvetica, 12.0)
919                .at(100.0, 700.0)
920                .write("Integration Test Content")
921                .unwrap();
922
923            doc.add_page(page);
924
925            // Write to file
926            let result = doc.save(&file_path);
927            assert!(result.is_ok());
928
929            // Verify file exists and has content
930            assert!(file_path.exists());
931            let metadata = fs::metadata(&file_path).unwrap();
932            assert!(metadata.len() > 0);
933
934            // Read file back to verify PDF format
935            let content = fs::read(&file_path).unwrap();
936            assert!(content.starts_with(b"%PDF-1.7"));
937            // Check for %%EOF with or without newline
938            assert!(content.ends_with(b"%%EOF\n") || content.ends_with(b"%%EOF"));
939        }
940
941        #[test]
942        fn test_document_with_complex_content() {
943            let temp_dir = TempDir::new().unwrap();
944            let file_path = temp_dir.path().join("complex.pdf");
945
946            let mut doc = Document::new();
947            doc.set_title("Complex Content Test");
948
949            // Create page with mixed content
950            let mut page = Page::a4();
951
952            // Add text
953            page.text()
954                .set_font(Font::Helvetica, 14.0)
955                .at(50.0, 750.0)
956                .write("Complex Content Test")
957                .unwrap();
958
959            // Add graphics
960            page.graphics()
961                .set_fill_color(Color::rgb(0.8, 0.2, 0.2))
962                .rectangle(50.0, 500.0, 200.0, 100.0)
963                .fill();
964
965            page.graphics()
966                .set_stroke_color(Color::rgb(0.2, 0.2, 0.8))
967                .set_line_width(2.0)
968                .move_to(50.0, 400.0)
969                .line_to(250.0, 400.0)
970                .stroke();
971
972            doc.add_page(page);
973
974            // Write and verify
975            let result = doc.save(&file_path);
976            assert!(result.is_ok());
977            assert!(file_path.exists());
978        }
979
980        #[test]
981        fn test_document_multiple_pages_integration() {
982            let temp_dir = TempDir::new().unwrap();
983            let file_path = temp_dir.path().join("multipage.pdf");
984
985            let mut doc = Document::new();
986            doc.set_title("Multi-page Integration Test");
987
988            // Create multiple pages with different content
989            for i in 1..=5 {
990                let mut page = Page::a4();
991
992                page.text()
993                    .set_font(Font::Helvetica, 16.0)
994                    .at(50.0, 750.0)
995                    .write(&format!("Page {i}"))
996                    .unwrap();
997
998                page.text()
999                    .set_font(Font::Helvetica, 12.0)
1000                    .at(50.0, 700.0)
1001                    .write(&format!("This is the content for page {i}"))
1002                    .unwrap();
1003
1004                // Add unique graphics for each page
1005                let color = match i % 3 {
1006                    0 => Color::rgb(1.0, 0.0, 0.0),
1007                    1 => Color::rgb(0.0, 1.0, 0.0),
1008                    _ => Color::rgb(0.0, 0.0, 1.0),
1009                };
1010
1011                page.graphics()
1012                    .set_fill_color(color)
1013                    .rectangle(50.0, 600.0, 100.0, 50.0)
1014                    .fill();
1015
1016                doc.add_page(page);
1017            }
1018
1019            // Write and verify
1020            let result = doc.save(&file_path);
1021            assert!(result.is_ok());
1022            assert!(file_path.exists());
1023
1024            // Verify file size is reasonable for 5 pages
1025            let metadata = fs::metadata(&file_path).unwrap();
1026            assert!(metadata.len() > 1000); // Should be substantial
1027        }
1028
1029        #[test]
1030        fn test_document_metadata_persistence() {
1031            let temp_dir = TempDir::new().unwrap();
1032            let file_path = temp_dir.path().join("metadata.pdf");
1033
1034            let mut doc = Document::new();
1035            doc.set_title("Metadata Persistence Test");
1036            doc.set_author("Test Author");
1037            doc.set_subject("Testing metadata preservation");
1038            doc.set_keywords("metadata, persistence, test");
1039
1040            doc.add_page(Page::a4());
1041
1042            // Write to file
1043            let result = doc.save(&file_path);
1044            assert!(result.is_ok());
1045
1046            // Read file content to verify metadata is present
1047            let content = fs::read(&file_path).unwrap();
1048            let content_str = String::from_utf8_lossy(&content);
1049
1050            // Check that metadata appears in the PDF
1051            assert!(content_str.contains("Metadata Persistence Test"));
1052            assert!(content_str.contains("Test Author"));
1053        }
1054
1055        #[test]
1056        fn test_document_writer_error_handling() {
1057            let mut doc = Document::new();
1058            doc.add_page(Page::a4());
1059
1060            // Test writing to invalid path
1061            let result = doc.save("/invalid/path/test.pdf");
1062            assert!(result.is_err());
1063        }
1064
1065        #[test]
1066        fn test_document_object_management() {
1067            let mut doc = Document::new();
1068
1069            // Add objects and verify they're managed properly
1070            let obj1 = Object::Boolean(true);
1071            let obj2 = Object::Integer(42);
1072            let obj3 = Object::Real(std::f64::consts::PI);
1073
1074            let id1 = doc.add_object(obj1.clone());
1075            let id2 = doc.add_object(obj2.clone());
1076            let id3 = doc.add_object(obj3.clone());
1077
1078            assert_eq!(id1.number(), 1);
1079            assert_eq!(id2.number(), 2);
1080            assert_eq!(id3.number(), 3);
1081
1082            assert_eq!(doc.objects.len(), 3);
1083            assert!(doc.objects.contains_key(&id1));
1084            assert!(doc.objects.contains_key(&id2));
1085            assert!(doc.objects.contains_key(&id3));
1086
1087            // Verify objects are correct
1088            assert_eq!(doc.objects.get(&id1), Some(&obj1));
1089            assert_eq!(doc.objects.get(&id2), Some(&obj2));
1090            assert_eq!(doc.objects.get(&id3), Some(&obj3));
1091        }
1092
1093        #[test]
1094        fn test_document_page_integration() {
1095            let mut doc = Document::new();
1096
1097            // Test different page configurations
1098            let page1 = Page::a4();
1099            let page2 = Page::letter();
1100            let mut page3 = Page::new(500.0, 400.0);
1101
1102            // Add content to custom page
1103            page3
1104                .text()
1105                .set_font(Font::Helvetica, 10.0)
1106                .at(25.0, 350.0)
1107                .write("Custom size page")
1108                .unwrap();
1109
1110            doc.add_page(page1);
1111            doc.add_page(page2);
1112            doc.add_page(page3);
1113
1114            assert_eq!(doc.pages.len(), 3);
1115
1116            // Verify pages maintain their properties (actual dimensions may vary)
1117            assert!(doc.pages[0].width() > 500.0); // A4 width is reasonable
1118            assert!(doc.pages[0].height() > 700.0); // A4 height is reasonable
1119            assert!(doc.pages[1].width() > 500.0); // Letter width is reasonable
1120            assert!(doc.pages[1].height() > 700.0); // Letter height is reasonable
1121            assert_eq!(doc.pages[2].width(), 500.0); // Custom width
1122            assert_eq!(doc.pages[2].height(), 400.0); // Custom height
1123        }
1124
1125        #[test]
1126        fn test_document_content_generation() {
1127            let temp_dir = TempDir::new().unwrap();
1128            let file_path = temp_dir.path().join("content.pdf");
1129
1130            let mut doc = Document::new();
1131            doc.set_title("Content Generation Test");
1132
1133            let mut page = Page::a4();
1134
1135            // Generate content programmatically
1136            for i in 0..10 {
1137                let y_pos = 700.0 - (i as f64 * 30.0);
1138                page.text()
1139                    .set_font(Font::Helvetica, 12.0)
1140                    .at(50.0, y_pos)
1141                    .write(&format!("Generated line {}", i + 1))
1142                    .unwrap();
1143            }
1144
1145            doc.add_page(page);
1146
1147            // Write and verify
1148            let result = doc.save(&file_path);
1149            assert!(result.is_ok());
1150            assert!(file_path.exists());
1151
1152            // Verify content was generated
1153            let metadata = fs::metadata(&file_path).unwrap();
1154            assert!(metadata.len() > 500); // Should contain substantial content
1155        }
1156
1157        #[test]
1158        fn test_document_buffer_vs_file_write() {
1159            let temp_dir = TempDir::new().unwrap();
1160            let file_path = temp_dir.path().join("buffer_vs_file.pdf");
1161
1162            let mut doc = Document::new();
1163            doc.set_title("Buffer vs File Test");
1164            doc.add_page(Page::a4());
1165
1166            // Write to buffer
1167            let mut buffer = Vec::new();
1168            let buffer_result = doc.write(&mut buffer);
1169            assert!(buffer_result.is_ok());
1170
1171            // Write to file
1172            let file_result = doc.save(&file_path);
1173            assert!(file_result.is_ok());
1174
1175            // Read file back
1176            let file_content = fs::read(&file_path).unwrap();
1177
1178            // Both should be valid PDFs with same structure (timestamps may differ)
1179            assert!(buffer.starts_with(b"%PDF-1.7"));
1180            assert!(file_content.starts_with(b"%PDF-1.7"));
1181            assert!(buffer.ends_with(b"%%EOF\n"));
1182            assert!(file_content.ends_with(b"%%EOF\n"));
1183
1184            // Both should contain the same title
1185            let buffer_str = String::from_utf8_lossy(&buffer);
1186            let file_str = String::from_utf8_lossy(&file_content);
1187            assert!(buffer_str.contains("Buffer vs File Test"));
1188            assert!(file_str.contains("Buffer vs File Test"));
1189        }
1190
1191        #[test]
1192        fn test_document_large_content_handling() {
1193            let temp_dir = TempDir::new().unwrap();
1194            let file_path = temp_dir.path().join("large_content.pdf");
1195
1196            let mut doc = Document::new();
1197            doc.set_title("Large Content Test");
1198
1199            let mut page = Page::a4();
1200
1201            // Add large amount of text content - make it much larger
1202            let large_text =
1203                "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ".repeat(200);
1204            page.text()
1205                .set_font(Font::Helvetica, 10.0)
1206                .at(50.0, 750.0)
1207                .write(&large_text)
1208                .unwrap();
1209
1210            doc.add_page(page);
1211
1212            // Write and verify
1213            let result = doc.save(&file_path);
1214            assert!(result.is_ok());
1215            assert!(file_path.exists());
1216
1217            // Verify large content was handled properly - reduce expectation
1218            let metadata = fs::metadata(&file_path).unwrap();
1219            assert!(metadata.len() > 500); // Should be substantial but realistic
1220        }
1221
1222        #[test]
1223        fn test_document_incremental_building() {
1224            let temp_dir = TempDir::new().unwrap();
1225            let file_path = temp_dir.path().join("incremental.pdf");
1226
1227            let mut doc = Document::new();
1228
1229            // Build document incrementally
1230            doc.set_title("Incremental Building Test");
1231
1232            // Add first page
1233            let mut page1 = Page::a4();
1234            page1
1235                .text()
1236                .set_font(Font::Helvetica, 12.0)
1237                .at(50.0, 750.0)
1238                .write("First page content")
1239                .unwrap();
1240            doc.add_page(page1);
1241
1242            // Add metadata
1243            doc.set_author("Incremental Author");
1244            doc.set_subject("Incremental Subject");
1245
1246            // Add second page
1247            let mut page2 = Page::a4();
1248            page2
1249                .text()
1250                .set_font(Font::Helvetica, 12.0)
1251                .at(50.0, 750.0)
1252                .write("Second page content")
1253                .unwrap();
1254            doc.add_page(page2);
1255
1256            // Add more metadata
1257            doc.set_keywords("incremental, building, test");
1258
1259            // Final write
1260            let result = doc.save(&file_path);
1261            assert!(result.is_ok());
1262            assert!(file_path.exists());
1263
1264            // Verify final state
1265            assert_eq!(doc.pages.len(), 2);
1266            assert_eq!(
1267                doc.metadata.title,
1268                Some("Incremental Building Test".to_string())
1269            );
1270            assert_eq!(doc.metadata.author, Some("Incremental Author".to_string()));
1271            assert_eq!(
1272                doc.metadata.subject,
1273                Some("Incremental Subject".to_string())
1274            );
1275            assert_eq!(
1276                doc.metadata.keywords,
1277                Some("incremental, building, test".to_string())
1278            );
1279        }
1280
1281        #[test]
1282        fn test_document_concurrent_page_operations() {
1283            let mut doc = Document::new();
1284            doc.set_title("Concurrent Operations Test");
1285
1286            // Simulate concurrent-like operations
1287            let mut pages = Vec::new();
1288
1289            // Create multiple pages
1290            for i in 0..5 {
1291                let mut page = Page::a4();
1292                page.text()
1293                    .set_font(Font::Helvetica, 12.0)
1294                    .at(50.0, 750.0)
1295                    .write(&format!("Concurrent page {i}"))
1296                    .unwrap();
1297                pages.push(page);
1298            }
1299
1300            // Add all pages
1301            for page in pages {
1302                doc.add_page(page);
1303            }
1304
1305            assert_eq!(doc.pages.len(), 5);
1306
1307            // Verify each page maintains its content
1308            let temp_dir = TempDir::new().unwrap();
1309            let file_path = temp_dir.path().join("concurrent.pdf");
1310            let result = doc.save(&file_path);
1311            assert!(result.is_ok());
1312        }
1313
1314        #[test]
1315        fn test_document_memory_efficiency() {
1316            let mut doc = Document::new();
1317            doc.set_title("Memory Efficiency Test");
1318
1319            // Add multiple pages with content
1320            for i in 0..10 {
1321                let mut page = Page::a4();
1322                page.text()
1323                    .set_font(Font::Helvetica, 12.0)
1324                    .at(50.0, 700.0)
1325                    .write(&format!("Memory test page {i}"))
1326                    .unwrap();
1327                doc.add_page(page);
1328            }
1329
1330            // Write to buffer to test memory usage
1331            let mut buffer = Vec::new();
1332            let result = doc.write(&mut buffer);
1333            assert!(result.is_ok());
1334            assert!(!buffer.is_empty());
1335
1336            // Buffer should be reasonable size
1337            assert!(buffer.len() < 1_000_000); // Should be less than 1MB for simple content
1338        }
1339
1340        #[test]
1341        fn test_document_creator_producer() {
1342            let mut doc = Document::new();
1343
1344            // Default values
1345            assert_eq!(doc.metadata.creator, Some("oxidize_pdf".to_string()));
1346            assert!(doc
1347                .metadata
1348                .producer
1349                .as_ref()
1350                .unwrap()
1351                .contains("oxidize_pdf"));
1352
1353            // Set custom values
1354            doc.set_creator("My Application");
1355            doc.set_producer("My PDF Library v1.0");
1356
1357            assert_eq!(doc.metadata.creator, Some("My Application".to_string()));
1358            assert_eq!(
1359                doc.metadata.producer,
1360                Some("My PDF Library v1.0".to_string())
1361            );
1362        }
1363
1364        #[test]
1365        fn test_document_dates() {
1366            use chrono::{TimeZone, Utc};
1367
1368            let mut doc = Document::new();
1369
1370            // Check default dates are set
1371            assert!(doc.metadata.creation_date.is_some());
1372            assert!(doc.metadata.modification_date.is_some());
1373
1374            // Set specific dates
1375            let creation_date = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
1376            let mod_date = Utc.with_ymd_and_hms(2023, 6, 15, 18, 30, 0).unwrap();
1377
1378            doc.set_creation_date(creation_date);
1379            doc.set_modification_date(mod_date);
1380
1381            assert_eq!(doc.metadata.creation_date, Some(creation_date));
1382            assert_eq!(doc.metadata.modification_date, Some(mod_date));
1383        }
1384
1385        #[test]
1386        fn test_document_dates_local() {
1387            use chrono::{Local, TimeZone};
1388
1389            let mut doc = Document::new();
1390
1391            // Test setting dates with local time
1392            let local_date = Local.with_ymd_and_hms(2023, 12, 25, 10, 30, 0).unwrap();
1393            doc.set_creation_date_local(local_date);
1394
1395            // Verify it was converted to UTC
1396            assert!(doc.metadata.creation_date.is_some());
1397            // Just verify the date was set, don't compare exact values due to timezone complexities
1398            assert!(doc.metadata.creation_date.is_some());
1399        }
1400
1401        #[test]
1402        fn test_update_modification_date() {
1403            let mut doc = Document::new();
1404
1405            let initial_mod_date = doc.metadata.modification_date;
1406            assert!(initial_mod_date.is_some());
1407
1408            // Sleep briefly to ensure time difference
1409            std::thread::sleep(std::time::Duration::from_millis(10));
1410
1411            doc.update_modification_date();
1412
1413            let new_mod_date = doc.metadata.modification_date;
1414            assert!(new_mod_date.is_some());
1415            assert!(new_mod_date.unwrap() > initial_mod_date.unwrap());
1416        }
1417
1418        #[test]
1419        fn test_document_save_updates_modification_date() {
1420            let temp_dir = TempDir::new().unwrap();
1421            let file_path = temp_dir.path().join("mod_date_test.pdf");
1422
1423            let mut doc = Document::new();
1424            doc.add_page(Page::a4());
1425
1426            let initial_mod_date = doc.metadata.modification_date;
1427
1428            // Sleep briefly to ensure time difference
1429            std::thread::sleep(std::time::Duration::from_millis(10));
1430
1431            doc.save(&file_path).unwrap();
1432
1433            // Modification date should be updated
1434            assert!(doc.metadata.modification_date.unwrap() > initial_mod_date.unwrap());
1435        }
1436
1437        #[test]
1438        fn test_document_metadata_complete() {
1439            let mut doc = Document::new();
1440
1441            // Set all metadata fields
1442            doc.set_title("Complete Metadata Test");
1443            doc.set_author("Test Author");
1444            doc.set_subject("Testing all metadata fields");
1445            doc.set_keywords("test, metadata, complete");
1446            doc.set_creator("Test Application v1.0");
1447            doc.set_producer("oxidize_pdf Test Suite");
1448
1449            // Verify all fields
1450            assert_eq!(
1451                doc.metadata.title,
1452                Some("Complete Metadata Test".to_string())
1453            );
1454            assert_eq!(doc.metadata.author, Some("Test Author".to_string()));
1455            assert_eq!(
1456                doc.metadata.subject,
1457                Some("Testing all metadata fields".to_string())
1458            );
1459            assert_eq!(
1460                doc.metadata.keywords,
1461                Some("test, metadata, complete".to_string())
1462            );
1463            assert_eq!(
1464                doc.metadata.creator,
1465                Some("Test Application v1.0".to_string())
1466            );
1467            assert_eq!(
1468                doc.metadata.producer,
1469                Some("oxidize_pdf Test Suite".to_string())
1470            );
1471            assert!(doc.metadata.creation_date.is_some());
1472            assert!(doc.metadata.modification_date.is_some());
1473        }
1474
1475        #[test]
1476        fn test_document_to_bytes() {
1477            let mut doc = Document::new();
1478            doc.set_title("Test Document");
1479            doc.set_author("Test Author");
1480
1481            let page = Page::a4();
1482            doc.add_page(page);
1483
1484            // Generate PDF as bytes
1485            let pdf_bytes = doc.to_bytes().unwrap();
1486
1487            // Basic validation
1488            assert!(!pdf_bytes.is_empty());
1489            assert!(pdf_bytes.len() > 100); // Should be reasonable size
1490
1491            // Check PDF header
1492            let header = &pdf_bytes[0..5];
1493            assert_eq!(header, b"%PDF-");
1494
1495            // Check for some basic PDF structure
1496            let pdf_str = String::from_utf8_lossy(&pdf_bytes);
1497            assert!(pdf_str.contains("Test Document"));
1498            assert!(pdf_str.contains("Test Author"));
1499        }
1500
1501        #[test]
1502        fn test_document_to_bytes_with_config() {
1503            let mut doc = Document::new();
1504            doc.set_title("Test Document XRef");
1505
1506            let page = Page::a4();
1507            doc.add_page(page);
1508
1509            let config = crate::writer::WriterConfig {
1510                use_xref_streams: true,
1511                pdf_version: "1.5".to_string(),
1512                compress_streams: true,
1513            };
1514
1515            // Generate PDF with custom config
1516            let pdf_bytes = doc.to_bytes_with_config(config).unwrap();
1517
1518            // Basic validation
1519            assert!(!pdf_bytes.is_empty());
1520            assert!(pdf_bytes.len() > 100);
1521
1522            // Check PDF header with correct version
1523            let header = String::from_utf8_lossy(&pdf_bytes[0..8]);
1524            assert!(header.contains("PDF-1.5"));
1525        }
1526
1527        #[test]
1528        fn test_to_bytes_vs_save_equivalence() {
1529            use std::fs;
1530            use tempfile::NamedTempFile;
1531
1532            // Create two identical documents
1533            let mut doc1 = Document::new();
1534            doc1.set_title("Equivalence Test");
1535            doc1.add_page(Page::a4());
1536
1537            let mut doc2 = Document::new();
1538            doc2.set_title("Equivalence Test");
1539            doc2.add_page(Page::a4());
1540
1541            // Generate bytes
1542            let pdf_bytes = doc1.to_bytes().unwrap();
1543
1544            // Save to file
1545            let temp_file = NamedTempFile::new().unwrap();
1546            doc2.save(temp_file.path()).unwrap();
1547            let file_bytes = fs::read(temp_file.path()).unwrap();
1548
1549            // Both should generate similar structure (lengths may vary due to timestamps)
1550            assert!(!pdf_bytes.is_empty());
1551            assert!(!file_bytes.is_empty());
1552            assert_eq!(&pdf_bytes[0..5], &file_bytes[0..5]); // PDF headers should match
1553        }
1554
1555        #[test]
1556        fn test_document_set_compress() {
1557            let mut doc = Document::new();
1558            doc.set_title("Compression Test");
1559            doc.add_page(Page::a4());
1560
1561            // Default should be compressed
1562            assert!(doc.get_compress());
1563
1564            // Test with compression enabled
1565            doc.set_compress(true);
1566            let compressed_bytes = doc.to_bytes().unwrap();
1567
1568            // Test with compression disabled
1569            doc.set_compress(false);
1570            let uncompressed_bytes = doc.to_bytes().unwrap();
1571
1572            // Uncompressed should generally be larger (though not always guaranteed)
1573            assert!(!compressed_bytes.is_empty());
1574            assert!(!uncompressed_bytes.is_empty());
1575
1576            // Both should be valid PDFs
1577            assert_eq!(&compressed_bytes[0..5], b"%PDF-");
1578            assert_eq!(&uncompressed_bytes[0..5], b"%PDF-");
1579        }
1580
1581        #[test]
1582        fn test_document_compression_config_inheritance() {
1583            let mut doc = Document::new();
1584            doc.set_title("Config Inheritance Test");
1585            doc.add_page(Page::a4());
1586
1587            // Set document compression to false
1588            doc.set_compress(false);
1589
1590            // Create config with compression true (should be overridden)
1591            let config = crate::writer::WriterConfig {
1592                use_xref_streams: false,
1593                pdf_version: "1.7".to_string(),
1594                compress_streams: true,
1595            };
1596
1597            // Document setting should take precedence
1598            let pdf_bytes = doc.to_bytes_with_config(config).unwrap();
1599
1600            // Should be valid PDF
1601            assert!(!pdf_bytes.is_empty());
1602            assert_eq!(&pdf_bytes[0..5], b"%PDF-");
1603        }
1604
1605        #[test]
1606        fn test_document_metadata_all_fields() {
1607            let mut doc = Document::new();
1608
1609            // Set all metadata fields
1610            doc.set_title("Test Document");
1611            doc.set_author("John Doe");
1612            doc.set_subject("Testing PDF metadata");
1613            doc.set_keywords("test, pdf, metadata");
1614            doc.set_creator("Test Suite");
1615            doc.set_producer("oxidize_pdf tests");
1616
1617            // Verify all fields are set
1618            assert_eq!(doc.metadata.title.as_deref(), Some("Test Document"));
1619            assert_eq!(doc.metadata.author.as_deref(), Some("John Doe"));
1620            assert_eq!(
1621                doc.metadata.subject.as_deref(),
1622                Some("Testing PDF metadata")
1623            );
1624            assert_eq!(
1625                doc.metadata.keywords.as_deref(),
1626                Some("test, pdf, metadata")
1627            );
1628            assert_eq!(doc.metadata.creator.as_deref(), Some("Test Suite"));
1629            assert_eq!(doc.metadata.producer.as_deref(), Some("oxidize_pdf tests"));
1630            assert!(doc.metadata.creation_date.is_some());
1631            assert!(doc.metadata.modification_date.is_some());
1632        }
1633
1634        #[test]
1635        fn test_document_add_pages() {
1636            let mut doc = Document::new();
1637
1638            // Initially empty
1639            assert_eq!(doc.page_count(), 0);
1640
1641            // Add pages
1642            let page1 = Page::a4();
1643            let page2 = Page::letter();
1644            let page3 = Page::legal();
1645
1646            doc.add_page(page1);
1647            assert_eq!(doc.page_count(), 1);
1648
1649            doc.add_page(page2);
1650            assert_eq!(doc.page_count(), 2);
1651
1652            doc.add_page(page3);
1653            assert_eq!(doc.page_count(), 3);
1654
1655            // Verify we can convert to PDF with multiple pages
1656            let result = doc.to_bytes();
1657            assert!(result.is_ok());
1658        }
1659
1660        #[test]
1661        fn test_document_default_font_encoding() {
1662            let mut doc = Document::new();
1663
1664            // Initially no default encoding
1665            assert!(doc.default_font_encoding.is_none());
1666
1667            // Set default encoding
1668            doc.set_default_font_encoding(Some(FontEncoding::WinAnsiEncoding));
1669            assert_eq!(
1670                doc.default_font_encoding(),
1671                Some(FontEncoding::WinAnsiEncoding)
1672            );
1673
1674            // Change encoding
1675            doc.set_default_font_encoding(Some(FontEncoding::MacRomanEncoding));
1676            assert_eq!(
1677                doc.default_font_encoding(),
1678                Some(FontEncoding::MacRomanEncoding)
1679            );
1680        }
1681
1682        #[test]
1683        fn test_document_compression_setting() {
1684            let mut doc = Document::new();
1685
1686            // Default should compress
1687            assert!(doc.compress);
1688
1689            // Disable compression
1690            doc.set_compress(false);
1691            assert!(!doc.compress);
1692
1693            // Re-enable compression
1694            doc.set_compress(true);
1695            assert!(doc.compress);
1696        }
1697
1698        #[test]
1699        fn test_document_with_empty_pages() {
1700            let mut doc = Document::new();
1701
1702            // Add empty page
1703            doc.add_page(Page::a4());
1704
1705            // Should be able to convert to bytes
1706            let result = doc.to_bytes();
1707            assert!(result.is_ok());
1708
1709            let pdf_bytes = result.unwrap();
1710            assert!(!pdf_bytes.is_empty());
1711            assert!(pdf_bytes.starts_with(b"%PDF-"));
1712        }
1713
1714        #[test]
1715        fn test_document_with_multiple_page_sizes() {
1716            let mut doc = Document::new();
1717
1718            // Add pages with different sizes
1719            doc.add_page(Page::a4()); // 595 x 842
1720            doc.add_page(Page::letter()); // 612 x 792
1721            doc.add_page(Page::legal()); // 612 x 1008
1722            doc.add_page(Page::a4()); // Another A4
1723            doc.add_page(Page::new(200.0, 300.0)); // Custom size
1724
1725            assert_eq!(doc.page_count(), 5);
1726
1727            // Verify we have 5 pages
1728            // Note: Direct page access is not available in public API
1729            // We verify by successful PDF generation
1730            let result = doc.to_bytes();
1731            assert!(result.is_ok());
1732        }
1733
1734        #[test]
1735        fn test_document_metadata_dates() {
1736            use chrono::Duration;
1737
1738            let doc = Document::new();
1739
1740            // Should have creation and modification dates
1741            assert!(doc.metadata.creation_date.is_some());
1742            assert!(doc.metadata.modification_date.is_some());
1743
1744            if let (Some(created), Some(modified)) =
1745                (doc.metadata.creation_date, doc.metadata.modification_date)
1746            {
1747                // Dates should be very close (created during construction)
1748                let diff = modified - created;
1749                assert!(diff < Duration::seconds(1));
1750            }
1751        }
1752
1753        #[test]
1754        fn test_document_builder_pattern() {
1755            // Test fluent API style
1756            let mut doc = Document::new();
1757            doc.set_title("Fluent");
1758            doc.set_author("Builder");
1759            doc.set_compress(true);
1760
1761            assert_eq!(doc.metadata.title.as_deref(), Some("Fluent"));
1762            assert_eq!(doc.metadata.author.as_deref(), Some("Builder"));
1763            assert!(doc.compress);
1764        }
1765
1766        #[test]
1767        fn test_xref_streams_functionality() {
1768            use crate::{Document, Font, Page};
1769
1770            // Test with xref streams disabled (default)
1771            let mut doc = Document::new();
1772            assert!(!doc.use_xref_streams);
1773
1774            let mut page = Page::a4();
1775            page.text()
1776                .set_font(Font::Helvetica, 12.0)
1777                .at(100.0, 700.0)
1778                .write("Testing XRef Streams")
1779                .unwrap();
1780
1781            doc.add_page(page);
1782
1783            // Generate PDF without xref streams
1784            let pdf_without_xref = doc.to_bytes().unwrap();
1785
1786            // Verify traditional xref is used
1787            let pdf_str = String::from_utf8_lossy(&pdf_without_xref);
1788            assert!(pdf_str.contains("xref"), "Traditional xref table not found");
1789            assert!(
1790                !pdf_str.contains("/Type /XRef"),
1791                "XRef stream found when it shouldn't be"
1792            );
1793
1794            // Test with xref streams enabled
1795            doc.enable_xref_streams(true);
1796            assert!(doc.use_xref_streams);
1797
1798            // Generate PDF with xref streams
1799            let pdf_with_xref = doc.to_bytes().unwrap();
1800
1801            // Verify xref streams are used
1802            let pdf_str = String::from_utf8_lossy(&pdf_with_xref);
1803            // XRef streams replace traditional xref tables in PDF 1.5+
1804            assert!(
1805                pdf_str.contains("/Type /XRef") || pdf_str.contains("stream"),
1806                "XRef stream not found when enabled"
1807            );
1808
1809            // Verify PDF version is set correctly
1810            assert!(
1811                pdf_str.contains("PDF-1.5"),
1812                "PDF version not set to 1.5 for xref streams"
1813            );
1814
1815            // Test fluent interface
1816            let mut doc2 = Document::new();
1817            doc2.enable_xref_streams(true);
1818            doc2.set_title("XRef Streams Test");
1819            doc2.set_author("oxidize-pdf");
1820
1821            assert!(doc2.use_xref_streams);
1822            assert_eq!(doc2.metadata.title.as_deref(), Some("XRef Streams Test"));
1823            assert_eq!(doc2.metadata.author.as_deref(), Some("oxidize-pdf"));
1824        }
1825
1826        #[test]
1827        fn test_document_save_to_vec() {
1828            let mut doc = Document::new();
1829            doc.set_title("Test Save");
1830            doc.add_page(Page::a4());
1831
1832            // Test to_bytes
1833            let bytes_result = doc.to_bytes();
1834            assert!(bytes_result.is_ok());
1835
1836            let bytes = bytes_result.unwrap();
1837            assert!(!bytes.is_empty());
1838            assert!(bytes.starts_with(b"%PDF-"));
1839            assert!(bytes.ends_with(b"%%EOF") || bytes.ends_with(b"%%EOF\n"));
1840        }
1841
1842        #[test]
1843        fn test_document_unicode_metadata() {
1844            let mut doc = Document::new();
1845
1846            // Set metadata with Unicode characters
1847            doc.set_title("日本語のタイトル");
1848            doc.set_author("作者名 😀");
1849            doc.set_subject("Тема документа");
1850            doc.set_keywords("كلمات, מפתח, 关键词");
1851
1852            assert_eq!(doc.metadata.title.as_deref(), Some("日本語のタイトル"));
1853            assert_eq!(doc.metadata.author.as_deref(), Some("作者名 😀"));
1854            assert_eq!(doc.metadata.subject.as_deref(), Some("Тема документа"));
1855            assert_eq!(
1856                doc.metadata.keywords.as_deref(),
1857                Some("كلمات, מפתח, 关键词")
1858            );
1859        }
1860
1861        #[test]
1862        fn test_document_page_iteration() {
1863            let mut doc = Document::new();
1864
1865            // Add multiple pages
1866            for i in 0..5 {
1867                let mut page = Page::a4();
1868                let gc = page.graphics();
1869                gc.begin_text();
1870                let _ = gc.show_text(&format!("Page {}", i + 1));
1871                gc.end_text();
1872                doc.add_page(page);
1873            }
1874
1875            // Verify page count
1876            assert_eq!(doc.page_count(), 5);
1877
1878            // Verify we can generate PDF with all pages
1879            let result = doc.to_bytes();
1880            assert!(result.is_ok());
1881        }
1882
1883        #[test]
1884        fn test_document_with_graphics_content() {
1885            let mut doc = Document::new();
1886
1887            let mut page = Page::a4();
1888            {
1889                let gc = page.graphics();
1890
1891                // Add various graphics operations
1892                gc.save_state();
1893
1894                // Draw rectangle
1895                gc.rectangle(100.0, 100.0, 200.0, 150.0);
1896                gc.stroke();
1897
1898                // Draw circle (approximated)
1899                gc.move_to(300.0, 300.0);
1900                gc.circle(300.0, 300.0, 50.0);
1901                gc.fill();
1902
1903                // Add text
1904                gc.begin_text();
1905                gc.set_text_position(100.0, 500.0);
1906                let _ = gc.show_text("Graphics Test");
1907                gc.end_text();
1908
1909                gc.restore_state();
1910            }
1911
1912            doc.add_page(page);
1913
1914            // Should produce valid PDF
1915            let result = doc.to_bytes();
1916            assert!(result.is_ok());
1917        }
1918
1919        #[test]
1920        fn test_document_producer_version() {
1921            let doc = Document::new();
1922
1923            // Producer should contain version
1924            assert!(doc.metadata.producer.is_some());
1925            if let Some(producer) = &doc.metadata.producer {
1926                assert!(producer.contains("oxidize_pdf"));
1927                assert!(producer.contains(env!("CARGO_PKG_VERSION")));
1928            }
1929        }
1930
1931        #[test]
1932        fn test_document_empty_metadata_fields() {
1933            let mut doc = Document::new();
1934
1935            // Set empty strings
1936            doc.set_title("");
1937            doc.set_author("");
1938            doc.set_subject("");
1939            doc.set_keywords("");
1940
1941            // Empty strings should be stored as Some("")
1942            assert_eq!(doc.metadata.title.as_deref(), Some(""));
1943            assert_eq!(doc.metadata.author.as_deref(), Some(""));
1944            assert_eq!(doc.metadata.subject.as_deref(), Some(""));
1945            assert_eq!(doc.metadata.keywords.as_deref(), Some(""));
1946        }
1947
1948        #[test]
1949        fn test_document_very_long_metadata() {
1950            let mut doc = Document::new();
1951
1952            // Create very long strings
1953            let long_title = "A".repeat(1000);
1954            let long_author = "B".repeat(500);
1955            let long_keywords = vec!["keyword"; 100].join(", ");
1956
1957            doc.set_title(&long_title);
1958            doc.set_author(&long_author);
1959            doc.set_keywords(&long_keywords);
1960
1961            assert_eq!(doc.metadata.title.as_deref(), Some(long_title.as_str()));
1962            assert_eq!(doc.metadata.author.as_deref(), Some(long_author.as_str()));
1963            assert!(doc.metadata.keywords.as_ref().unwrap().len() > 500);
1964        }
1965    }
1966}