Skip to main content

oxidize_pdf/
document.rs

1use crate::error::Result;
2use crate::fonts::{Font as CustomFont, FontCache};
3use crate::forms::{AcroForm, FormManager};
4use crate::page::Page;
5use crate::page_labels::PageLabelTree;
6use crate::semantic::{BoundingBox, EntityType, RelationType, SemanticEntity};
7use crate::structure::{NamedDestinations, OutlineTree, StructTree};
8// Alias to avoid collision with crate::fonts::FontMetrics (PDF font objects)
9use crate::text::metrics::{FontMetrics as TextMeasurementMetrics, FontMetricsStore};
10use crate::text::FontEncoding;
11use crate::writer::PdfWriter;
12use chrono::{DateTime, Local, Utc};
13use std::collections::{HashMap, HashSet};
14use std::sync::Arc;
15
16mod encryption;
17pub use encryption::{DocumentEncryption, EncryptionStrength};
18
19/// A PDF document that can contain multiple pages and metadata.
20///
21/// # Example
22///
23/// ```rust
24/// use oxidize_pdf::{Document, Page};
25///
26/// let mut doc = Document::new();
27/// doc.set_title("My Document");
28/// doc.set_author("John Doe");
29///
30/// let page = Page::a4();
31/// doc.add_page(page);
32///
33/// doc.save("output.pdf").unwrap();
34/// ```
35pub struct Document {
36    pub(crate) pages: Vec<Page>,
37    pub(crate) metadata: DocumentMetadata,
38    pub(crate) encryption: Option<DocumentEncryption>,
39    pub(crate) outline: Option<OutlineTree>,
40    pub(crate) named_destinations: Option<NamedDestinations>,
41    pub(crate) page_labels: Option<PageLabelTree>,
42    /// Default font encoding to use for fonts when no encoding is specified
43    pub(crate) default_font_encoding: Option<FontEncoding>,
44    /// Interactive form data (AcroForm)
45    pub(crate) acro_form: Option<AcroForm>,
46    /// Form manager for handling interactive forms
47    pub(crate) form_manager: Option<FormManager>,
48    /// Whether to compress streams when writing the PDF
49    pub(crate) compress: bool,
50    /// Whether to use compressed cross-reference streams (PDF 1.5+)
51    pub(crate) use_xref_streams: bool,
52    /// Cache for custom fonts
53    pub(crate) custom_fonts: FontCache,
54    /// Per-document font metrics store for text measurement (char widths)
55    pub(crate) font_metrics: FontMetricsStore,
56    /// Characters used in the document (for font subsetting)
57    /// Characters drawn in this document, bucketed by font name
58    /// (ISO 32000-1 §9.7.4 — only custom Type0/CID fonts need
59    /// subsetting; see issue #204). Populated by `add_page` from the
60    /// page's per-font accumulators.
61    pub(crate) used_characters_by_font: HashMap<String, HashSet<char>>,
62    /// Action to execute when the document is opened
63    pub(crate) open_action: Option<crate::actions::Action>,
64    /// Viewer preferences for controlling document display
65    pub(crate) viewer_preferences: Option<crate::viewer_preferences::ViewerPreferences>,
66    /// Semantic entities marked in the document for AI processing
67    pub(crate) semantic_entities: Vec<SemanticEntity>,
68    /// Document structure tree for Tagged PDF (accessibility)
69    pub(crate) struct_tree: Option<StructTree>,
70    /// CID-keyed fonts registered for positioned-glyph-run drawing (issue #358).
71    /// Each entry is the raw font bytes plus an explicit `CidMapping` (CID=GID,
72    /// CID→Unicode) supplied by the caller. Kept SEPARATE from `custom_fonts`
73    /// (the Unicode-keyed path) so a font object is never shared between the two
74    /// drawing modes — the CID semantics are incompatible. Embedded whole (no
75    /// subsetting in this iteration).
76    pub(crate) cid_keyed_fonts: HashMap<String, (Vec<u8>, crate::fonts::CidMapping)>,
77}
78
79/// Metadata for a PDF document.
80#[derive(Debug, Clone)]
81pub struct DocumentMetadata {
82    /// Document title
83    pub title: Option<String>,
84    /// Document author
85    pub author: Option<String>,
86    /// Document subject
87    pub subject: Option<String>,
88    /// Document keywords
89    pub keywords: Option<String>,
90    /// Software that created the original document
91    pub creator: Option<String>,
92    /// Software that produced the PDF
93    pub producer: Option<String>,
94    /// Date and time the document was created
95    pub creation_date: Option<DateTime<Utc>>,
96    /// Date and time the document was last modified
97    pub modification_date: Option<DateTime<Utc>>,
98}
99
100impl Default for DocumentMetadata {
101    fn default() -> Self {
102        let now = Utc::now();
103
104        let edition = "MIT";
105
106        Self {
107            title: None,
108            author: None,
109            subject: None,
110            keywords: None,
111            creator: Some("oxidize_pdf".to_string()),
112            producer: Some(format!(
113                "oxidize_pdf v{} ({})",
114                env!("CARGO_PKG_VERSION"),
115                edition
116            )),
117            creation_date: Some(now),
118            modification_date: Some(now),
119        }
120    }
121}
122
123impl Document {
124    /// Creates a new empty PDF document.
125    pub fn new() -> Self {
126        Self {
127            pages: Vec::new(),
128            metadata: DocumentMetadata::default(),
129            encryption: None,
130            outline: None,
131            named_destinations: None,
132            page_labels: None,
133            default_font_encoding: None,
134            acro_form: None,
135            form_manager: None,
136            compress: true,          // Enable compression by default
137            use_xref_streams: false, // Disabled by default for compatibility
138            custom_fonts: FontCache::new(),
139            font_metrics: FontMetricsStore::new(),
140            used_characters_by_font: HashMap::new(),
141            open_action: None,
142            viewer_preferences: None,
143            semantic_entities: Vec::new(),
144            struct_tree: None,
145            cid_keyed_fonts: HashMap::new(),
146        }
147    }
148
149    /// Adds a page to the document.
150    pub fn add_page(&mut self, mut page: Page) {
151        // Inject the Document's metrics store into the page if it does not
152        // already carry one. Pages constructed via Document::new_page_*()
153        // carry the store on BOTH `page.font_metrics_store` AND
154        // `page.text_context.font_metrics_store` from the factory, and are
155        // skipped here (preserves bindings to other Documents if a page is
156        // moved between them). Pages constructed via Page::a4() /
157        // Page::letter() / Page::new() start with both fields as None;
158        // both are set here so that subsequent measurements through the
159        // page's text context resolve custom fonts via the Document scope
160        // rather than the legacy global registry. The text context's
161        // accumulated ops (if the caller pushed any before add_page) are
162        // preserved — only the `font_metrics_store` field is mutated
163        // (issue #230 follow-up M1).
164        if page.font_metrics_store.is_none() {
165            page.font_metrics_store = Some(self.font_metrics.clone());
166            page.set_text_context_metrics_store(Some(self.font_metrics.clone()));
167        }
168        // Merge the page's per-font character accumulators into the
169        // document-wide map (issue #204 — each font gets subsetted with
170        // only its own characters later at write time).
171        for (font_name, chars) in page.get_used_characters_by_font() {
172            self.used_characters_by_font
173                .entry(font_name)
174                .or_default()
175                .extend(chars);
176        }
177        self.pages.push(page);
178    }
179
180    /// Returns the document's pages as a slice.
181    pub fn pages(&self) -> &[Page] {
182        &self.pages
183    }
184
185    /// Returns a reference to this Document's font metrics store.
186    ///
187    /// Public surface for external callers that need to thread the
188    /// per-Document scope into the `_with` measurement helpers
189    /// (`measure_text_with`, `measure_char_with`, `measure_text_block_with`).
190    /// `FontMetricsStore` uses interior mutability, so callers can also
191    /// `register` and `get` directly via this reference.
192    pub fn font_metrics(&self) -> &FontMetricsStore {
193        &self.font_metrics
194    }
195
196    /// Create a new A4 page already bound to this Document's font metrics store.
197    ///
198    /// Recommended over `Page::a4()` for code that uses custom fonts: the
199    /// returned page measures `Font::Custom(...)` against the Document's
200    /// per-instance metrics, avoiding the deprecated process-wide registry.
201    pub fn new_page_a4(&self) -> Page {
202        Page::a4_with_metrics(self.font_metrics.clone())
203    }
204
205    /// Create a new US Letter page bound to this Document's font metrics store.
206    pub fn new_page_letter(&self) -> Page {
207        Page::letter_with_metrics(self.font_metrics.clone())
208    }
209
210    /// Create a new page of arbitrary dimensions bound to this Document's
211    /// font metrics store.
212    pub fn new_page(&self, width: f64, height: f64) -> Page {
213        Page::new_with_metrics(width, height, self.font_metrics.clone())
214    }
215
216    /// Sets the document title.
217    pub fn set_title(&mut self, title: impl Into<String>) {
218        self.metadata.title = Some(title.into());
219    }
220
221    /// Sets the document author.
222    pub fn set_author(&mut self, author: impl Into<String>) {
223        self.metadata.author = Some(author.into());
224    }
225
226    /// Sets the form manager for the document.
227    pub fn set_form_manager(&mut self, form_manager: FormManager) {
228        self.form_manager = Some(form_manager);
229    }
230
231    /// Sets the document subject.
232    pub fn set_subject(&mut self, subject: impl Into<String>) {
233        self.metadata.subject = Some(subject.into());
234    }
235
236    /// Sets the document keywords.
237    pub fn set_keywords(&mut self, keywords: impl Into<String>) {
238        self.metadata.keywords = Some(keywords.into());
239    }
240
241    /// Set document encryption
242    pub fn set_encryption(&mut self, encryption: DocumentEncryption) {
243        self.encryption = Some(encryption);
244    }
245
246    /// Set simple encryption with passwords
247    pub fn encrypt_with_passwords(
248        &mut self,
249        user_password: impl Into<String>,
250        owner_password: impl Into<String>,
251    ) {
252        self.encryption = Some(DocumentEncryption::with_passwords(
253            user_password,
254            owner_password,
255        ));
256    }
257
258    /// Check if document is encrypted
259    pub fn is_encrypted(&self) -> bool {
260        self.encryption.is_some()
261    }
262
263    /// Set the action to execute when the document is opened
264    pub fn set_open_action(&mut self, action: crate::actions::Action) {
265        self.open_action = Some(action);
266    }
267
268    /// Get the document open action
269    pub fn open_action(&self) -> Option<&crate::actions::Action> {
270        self.open_action.as_ref()
271    }
272
273    /// Set viewer preferences for controlling document display
274    pub fn set_viewer_preferences(
275        &mut self,
276        preferences: crate::viewer_preferences::ViewerPreferences,
277    ) {
278        self.viewer_preferences = Some(preferences);
279    }
280
281    /// Get viewer preferences
282    pub fn viewer_preferences(&self) -> Option<&crate::viewer_preferences::ViewerPreferences> {
283        self.viewer_preferences.as_ref()
284    }
285
286    /// Set the document structure tree for Tagged PDF (accessibility)
287    ///
288    /// Tagged PDF provides semantic information about document content,
289    /// making PDFs accessible to screen readers and assistive technologies.
290    ///
291    /// # Example
292    ///
293    /// ```rust,no_run
294    /// use oxidize_pdf::{Document, structure::{StructTree, StructureElement, StandardStructureType}};
295    ///
296    /// let mut doc = Document::new();
297    /// let mut tree = StructTree::new();
298    ///
299    /// // Create document root
300    /// let doc_elem = StructureElement::new(StandardStructureType::Document);
301    /// let doc_idx = tree.set_root(doc_elem);
302    ///
303    /// // Add heading
304    /// let h1 = StructureElement::new(StandardStructureType::H1)
305    ///     .with_language("en-US")
306    ///     .with_actual_text("Welcome");
307    /// tree.add_child(doc_idx, h1).unwrap();
308    ///
309    /// doc.set_struct_tree(tree);
310    /// ```
311    pub fn set_struct_tree(&mut self, tree: StructTree) {
312        self.struct_tree = Some(tree);
313    }
314
315    /// Get a reference to the document structure tree
316    pub fn struct_tree(&self) -> Option<&StructTree> {
317        self.struct_tree.as_ref()
318    }
319
320    /// Get a mutable reference to the document structure tree
321    pub fn struct_tree_mut(&mut self) -> Option<&mut StructTree> {
322        self.struct_tree.as_mut()
323    }
324
325    /// Initialize a new structure tree if one doesn't exist and return a mutable reference
326    ///
327    /// This is a convenience method for adding Tagged PDF support.
328    ///
329    /// # Example
330    ///
331    /// ```rust,no_run
332    /// use oxidize_pdf::{Document, structure::{StructureElement, StandardStructureType}};
333    ///
334    /// let mut doc = Document::new();
335    /// let tree = doc.get_or_create_struct_tree();
336    ///
337    /// // Create document root
338    /// let doc_elem = StructureElement::new(StandardStructureType::Document);
339    /// tree.set_root(doc_elem);
340    /// ```
341    pub fn get_or_create_struct_tree(&mut self) -> &mut StructTree {
342        self.struct_tree.get_or_insert_with(StructTree::new)
343    }
344
345    /// Set document outline (bookmarks)
346    pub fn set_outline(&mut self, outline: OutlineTree) {
347        self.outline = Some(outline);
348    }
349
350    /// Get document outline
351    pub fn outline(&self) -> Option<&OutlineTree> {
352        self.outline.as_ref()
353    }
354
355    /// Get mutable document outline
356    pub fn outline_mut(&mut self) -> Option<&mut OutlineTree> {
357        self.outline.as_mut()
358    }
359
360    /// Set named destinations
361    pub fn set_named_destinations(&mut self, destinations: NamedDestinations) {
362        self.named_destinations = Some(destinations);
363    }
364
365    /// Get named destinations
366    pub fn named_destinations(&self) -> Option<&NamedDestinations> {
367        self.named_destinations.as_ref()
368    }
369
370    /// Get mutable named destinations
371    pub fn named_destinations_mut(&mut self) -> Option<&mut NamedDestinations> {
372        self.named_destinations.as_mut()
373    }
374
375    /// Set page labels
376    pub fn set_page_labels(&mut self, labels: PageLabelTree) {
377        self.page_labels = Some(labels);
378    }
379
380    /// Get page labels
381    pub fn page_labels(&self) -> Option<&PageLabelTree> {
382        self.page_labels.as_ref()
383    }
384
385    /// Get mutable page labels
386    pub fn page_labels_mut(&mut self) -> Option<&mut PageLabelTree> {
387        self.page_labels.as_mut()
388    }
389
390    /// Get page label for a specific page
391    pub fn get_page_label(&self, page_index: u32) -> String {
392        self.page_labels
393            .as_ref()
394            .and_then(|labels| labels.get_label(page_index))
395            .unwrap_or_else(|| (page_index + 1).to_string())
396    }
397
398    /// Get all page labels
399    pub fn get_all_page_labels(&self) -> Vec<String> {
400        let page_count = self.pages.len() as u32;
401        if let Some(labels) = &self.page_labels {
402            labels.get_all_labels(page_count)
403        } else {
404            (1..=page_count).map(|i| i.to_string()).collect()
405        }
406    }
407
408    /// Sets the document creator (software that created the original document).
409    pub fn set_creator(&mut self, creator: impl Into<String>) {
410        self.metadata.creator = Some(creator.into());
411    }
412
413    /// Sets the document producer (software that produced the PDF).
414    pub fn set_producer(&mut self, producer: impl Into<String>) {
415        self.metadata.producer = Some(producer.into());
416    }
417
418    /// Sets the document creation date.
419    pub fn set_creation_date(&mut self, date: DateTime<Utc>) {
420        self.metadata.creation_date = Some(date);
421    }
422
423    /// Sets the document creation date using local time.
424    pub fn set_creation_date_local(&mut self, date: DateTime<Local>) {
425        self.metadata.creation_date = Some(date.with_timezone(&Utc));
426    }
427
428    /// Sets the document modification date.
429    pub fn set_modification_date(&mut self, date: DateTime<Utc>) {
430        self.metadata.modification_date = Some(date);
431    }
432
433    /// Sets the document modification date using local time.
434    pub fn set_modification_date_local(&mut self, date: DateTime<Local>) {
435        self.metadata.modification_date = Some(date.with_timezone(&Utc));
436    }
437
438    /// Sets the modification date to the current time.
439    pub fn update_modification_date(&mut self) {
440        self.metadata.modification_date = Some(Utc::now());
441    }
442
443    /// Sets the default font encoding for fonts that don't specify an encoding.
444    ///
445    /// This encoding will be applied to fonts in the PDF font dictionary when
446    /// no explicit encoding is specified. Setting this to `None` (the default)
447    /// means no encoding metadata will be added to fonts unless explicitly specified.
448    ///
449    /// # Example
450    ///
451    /// ```rust
452    /// use oxidize_pdf::{Document, text::FontEncoding};
453    ///
454    /// let mut doc = Document::new();
455    /// doc.set_default_font_encoding(Some(FontEncoding::WinAnsiEncoding));
456    /// ```
457    pub fn set_default_font_encoding(&mut self, encoding: Option<FontEncoding>) {
458        self.default_font_encoding = encoding;
459    }
460
461    /// Gets the current default font encoding.
462    pub fn default_font_encoding(&self) -> Option<FontEncoding> {
463        self.default_font_encoding
464    }
465
466    /// Add a custom font from a file path
467    ///
468    /// # Example
469    ///
470    /// ```rust,no_run
471    /// use oxidize_pdf::Document;
472    ///
473    /// let mut doc = Document::new();
474    /// doc.add_font("MyFont", "path/to/font.ttf").unwrap();
475    /// ```
476    pub fn add_font(
477        &mut self,
478        name: impl Into<String>,
479        path: impl AsRef<std::path::Path>,
480    ) -> Result<()> {
481        let name = name.into();
482        let font = CustomFont::from_file(&name, path)?;
483        self.custom_fonts.add_font(name, font)?;
484        Ok(())
485    }
486
487    /// Get a registered embedded font by name, if present.
488    ///
489    /// Returns the embedding-layer [`crate::fonts::Font`] (not the
490    /// `oxidize_pdf::CustomFont` builder type). Useful for inspecting glyph
491    /// coverage via [`crate::fonts::Font::has_glyph`] or
492    /// [`crate::fonts::Font::missing_glyphs`] (issue #287).
493    pub fn embedded_font(&self, name: &str) -> Option<std::sync::Arc<CustomFont>> {
494        self.custom_fonts.get_font(name)
495    }
496
497    /// Characters in `text` that the named custom font cannot render because
498    /// its embedded glyph set has no glyph for them (they would appear as
499    /// `.notdef`, an empty box — issue #287). Deduplicated, first-seen order.
500    ///
501    /// Returns an empty vector when the font is not registered (nothing can be
502    /// determined). This lets callers detect coverage gaps before rendering,
503    /// e.g. to substitute a character or pick a different font.
504    pub fn font_missing_glyphs(&self, font_name: &str, text: &str) -> Vec<char> {
505        match self.custom_fonts.get_font(font_name) {
506            Some(font) => font.missing_glyphs(text),
507            None => Vec::new(),
508        }
509    }
510
511    /// Add a custom font from byte data
512    ///
513    /// # Example
514    ///
515    /// ```rust,no_run
516    /// use oxidize_pdf::Document;
517    ///
518    /// let mut doc = Document::new();
519    /// let font_data = vec![0; 1000]; // Your font data
520    /// doc.add_font_from_bytes("MyFont", font_data).unwrap();
521    /// ```
522    pub fn add_font_from_bytes(&mut self, name: impl Into<String>, data: Vec<u8>) -> Result<()> {
523        let name = name.into();
524        let font = CustomFont::from_bytes(&name, data)?;
525
526        // Extract glyph widths before moving font into the cache
527        // Convert from font units to 1/1000 em units used by text::metrics
528        let units_per_em = font.metrics.units_per_em as f64;
529        let char_width_map: std::collections::HashMap<char, u16> = font
530            .glyph_mapping
531            .char_widths_iter()
532            .map(|(ch, width_font_units)| {
533                let width_1000 = ((width_font_units as f64 * 1000.0) / units_per_em).round() as u16;
534                (ch, width_1000)
535            })
536            .collect();
537
538        // Add to font cache first — if this fails, no metrics are registered (consistent state)
539        self.custom_fonts.add_font(name.clone(), font)?;
540
541        // Register text measurement metrics only after successful cache insertion
542        if !char_width_map.is_empty() {
543            let sum: u32 = char_width_map.values().map(|&w| w as u32).sum();
544            let default_width = (sum / char_width_map.len() as u32) as u16;
545            let text_metrics = TextMeasurementMetrics::from_char_map(char_width_map, default_width);
546            self.font_metrics.register(name, text_metrics);
547        }
548
549        Ok(())
550    }
551
552    /// Register a CID-keyed font for positioned-glyph-run drawing (issue #358).
553    ///
554    /// Unlike [`add_font_from_bytes`](Self::add_font_from_bytes) (which is
555    /// Unicode-keyed: content-stream codes are interpreted as Unicode code
556    /// points), a CID-keyed font interprets the 2-byte codes drawn via
557    /// [`show_cid_array`](crate::text::TextContext::show_cid_array) as the CIDs
558    /// in `mapping`. With `CIDToGIDMap = Identity` (CID = GID) this lets a
559    /// caller draw a pre-shaped glyph run (e.g. from `rustybuzz`) directly by
560    /// glyph id, expressing ligatures and per-glyph positioning the
561    /// Unicode-keyed path cannot.
562    ///
563    /// `mapping` must carry `cid_to_gid` (which GID each CID renders) and
564    /// `cid_to_unicode` (so the text stays extractable via the emitted
565    /// `ToUnicode` CMap). The font is embedded whole — no subsetting in this
566    /// iteration. Registered separately from custom fonts so the two drawing
567    /// modes never share a font object.
568    ///
569    /// Only TrueType/SFNT fonts (`CIDFontType2`) are supported in this
570    /// iteration; an OpenType/CFF font returns an error.
571    pub fn add_cid_keyed_font(
572        &mut self,
573        name: impl Into<String>,
574        data: Vec<u8>,
575        mapping: crate::fonts::CidMapping,
576    ) -> Result<()> {
577        let name = name.into();
578        // Validate the bytes parse as a supported font up front, so registration
579        // fails fast rather than at write time. Reuses the standard loader.
580        let font = CustomFont::from_bytes(&name, data.clone())?;
581        if font.format != crate::fonts::FontFormat::TrueType {
582            return Err(crate::error::PdfError::InvalidStructure(format!(
583                "add_cid_keyed_font: only TrueType (CIDFontType2) fonts are supported \
584                 in this iteration; '{name}' is {:?}",
585                font.format
586            )));
587        }
588        self.cid_keyed_fonts.insert(name, (data, mapping));
589        Ok(())
590    }
591
592    /// CID-keyed fonts registered via [`add_cid_keyed_font`](Self::add_cid_keyed_font).
593    pub(crate) fn cid_keyed_fonts(&self) -> &HashMap<String, (Vec<u8>, crate::fonts::CidMapping)> {
594        &self.cid_keyed_fonts
595    }
596
597    /// Get a custom font by name
598    pub(crate) fn get_custom_font(&self, name: &str) -> Option<Arc<CustomFont>> {
599        self.custom_fonts.get_font(name)
600    }
601
602    /// Check if a custom font is loaded
603    pub fn has_custom_font(&self, name: &str) -> bool {
604        self.custom_fonts.has_font(name)
605    }
606
607    /// Get all loaded custom font names
608    pub fn custom_font_names(&self) -> Vec<String> {
609        self.custom_fonts.font_names()
610    }
611
612    /// Gets the number of pages in the document.
613    pub fn page_count(&self) -> usize {
614        self.pages.len()
615    }
616
617    /// Gets a reference to the page at `index`, or `None` if out of bounds.
618    pub fn page(&self, index: usize) -> Option<&Page> {
619        self.pages.get(index)
620    }
621
622    /// Gets a mutable reference to the page at `index`, or `None` if out of bounds.
623    pub fn page_mut(&mut self, index: usize) -> Option<&mut Page> {
624        self.pages.get_mut(index)
625    }
626
627    /// Gets a reference to the AcroForm (interactive form) if present.
628    pub fn acro_form(&self) -> Option<&AcroForm> {
629        self.acro_form.as_ref()
630    }
631
632    /// Gets a mutable reference to the AcroForm (interactive form) if present.
633    pub fn acro_form_mut(&mut self) -> Option<&mut AcroForm> {
634        self.acro_form.as_mut()
635    }
636
637    /// Enables interactive forms by creating a FormManager if not already present.
638    /// The FormManager handles both the AcroForm and the connection with page widgets.
639    pub fn enable_forms(&mut self) -> &mut FormManager {
640        if self.acro_form.is_none() {
641            self.acro_form = Some(AcroForm::new());
642        }
643        self.form_manager.get_or_insert_with(FormManager::new)
644    }
645
646    /// Disables interactive forms by removing both the AcroForm and FormManager.
647    pub fn disable_forms(&mut self) {
648        self.acro_form = None;
649        self.form_manager = None;
650    }
651
652    /// Fill an AcroForm field by name, updating `/V` and regenerating the
653    /// widget appearance stream(s) so the value is both machine-readable
654    /// (via `/V` on the field dictionary) and visually present in the PDF
655    /// (via `/AP/N` on each widget annotation).
656    ///
657    /// This implements ISO 32000-1 §12.7.3.3 Table 228 (`/V` on form fields)
658    /// plus §12.5.5 / §12.7.3.3 interplay: a viewer that honours
659    /// `/NeedAppearances true` may regenerate appearance streams on open,
660    /// but a compliant writer should still emit them so the PDF renders
661    /// correctly in readers that do not.
662    ///
663    /// # Arguments
664    ///
665    /// * `name` — the partial field name (`/T` on the field dictionary)
666    ///   assigned when the field was registered via `FormManager::add_*`.
667    /// * `value` — the new value. For text fields this becomes `/V` as a
668    ///   PDF string; it is also embedded verbatim into the regenerated
669    ///   appearance content stream (see `TextFieldAppearance`).
670    ///
671    /// # Errors
672    ///
673    /// * `PdfError::InvalidStructure` if the document has no `FormManager`
674    ///   attached (calling code must register fields before filling them).
675    /// * `PdfError::FieldNotFound` if no field with the given `name` exists
676    ///   in the `FormManager`.
677    ///
678    /// # Custom Type0/CID font dispatch (issue #212)
679    ///
680    /// Both `FieldType::Text` (TextField) and `FieldType::Choice` (ComboBox)
681    /// honour the field's typed `/DA` and dispatch to the correct emission
682    /// path:
683    ///
684    /// - `Font::Custom(name)` with the font registered via
685    ///   `add_font_from_bytes` → Type0/CID path. Hex-CID `<HHHH> Tj` in the
686    ///   appearance content stream and a `/Subtype /Type0` /
687    ///   `/Encoding /Identity-H` resource entry that the writer rewrites to
688    ///   an indirect Reference to the document-level CIDFontType0 object.
689    /// - Built-in font (Helvetica, Times, Courier) → WinAnsi-strict path.
690    ///   Returns `PdfError::EncodingError` for any character outside the
691    ///   WinAnsi repertoire.
692    /// - No `/DA` → Helvetica fallback, same WinAnsi-strict path.
693    ///
694    /// To use a custom font with a ComboBox, call
695    /// `ComboBox::with_default_appearance(Font::Custom("name"), size, color)`
696    /// before passing it to `FormManager::add_combo_box`. The same
697    /// constructor on `TextField` covers text fields. For PushButton labels
698    /// with custom fonts the resource dict is correct (Type0 placeholder)
699    /// but the label-render block is currently skipped; full hex-CID Tj for
700    /// push button labels remains a follow-up.
701    ///
702    /// # Path chosen (v2.5.6 Task 3)
703    ///
704    /// This method operates on an in-memory `Document` that was BUILT in
705    /// the current process (via `FormManager` + `Page::add_form_widget_with_ref`).
706    /// It does not re-parse an existing PDF; hydration of a parsed PDF
707    /// back into a mutable `Document` is out of scope for v2.5.6 Task 3
708    /// and tracked separately. The writer accepts the mutated document
709    /// and emits /V + /AP/N so the typical round-trip
710    /// "build → fill → save → reader sees filled value" is covered.
711    pub fn fill_field(&mut self, name: &str, value: impl Into<String>) -> Result<()> {
712        use crate::error::PdfError;
713        use crate::forms::FieldType;
714        use crate::objects::Object;
715
716        let value: String = value.into();
717
718        let form_manager = self.form_manager.as_mut().ok_or_else(|| {
719            PdfError::InvalidStructure(
720                "Document has no FormManager; register fields via enable_forms() or \
721                 set_form_manager() before calling fill_field"
722                    .to_string(),
723            )
724        })?;
725
726        // Capture the placeholder ref BEFORE taking a mutable borrow on the
727        // field; it lets us locate matching widget annotations below without
728        // a second lookup through `form_manager`.
729        let placeholder_ref = form_manager.field_ref(name);
730
731        let form_field = form_manager
732            .get_field_mut(name)
733            .ok_or_else(|| PdfError::FieldNotFound(name.to_string()))?;
734
735        // Resolve the field type from the field dict's `/FT` entry so the
736        // regenerated appearance matches the field's declared type (Tx, Btn,
737        // Ch, Sig). Default to `FieldType::Text` if absent — the FormManager
738        // always sets `/FT`, but defensive default keeps us robust.
739        let field_type = match form_field.field_dict.get("FT") {
740            Some(Object::Name(n)) => match n.as_str() {
741                "Btn" => FieldType::Button,
742                "Ch" => FieldType::Choice,
743                "Sig" => FieldType::Signature,
744                _ => FieldType::Text,
745            },
746            _ => FieldType::Text,
747        };
748
749        // 1) Update /V on the field dict. For text and choice fields
750        //    /V is a PDF string; for button fields it's a name, but the
751        //    `fill_field` contract (set textual value) is targeted at text
752        //    fields. Callers who need to toggle checkboxes should reach
753        //    through `FormManager::get_field_mut` directly.
754        form_field
755            .field_dict
756            .set("V", Object::String(value.clone()));
757
758        // 2) Regenerate the appearance stream(s) on each widget belonging
759        //    to this field. The regenerated /AP dictionary lives on the
760        //    widget struct inside the FormManager — but the `Annotation`
761        //    on the page was built at `add_form_widget_with_ref` time from
762        //    a clone of the widget's annotation dict, and therefore carries
763        //    its own (stale) /AP. Step 3 below refreshes that.
764        //
765        //    Font selection for the appearance follows the field's typed
766        //    `/DA` when present:
767        //      - `Font::Custom(name)` with a matching registered font →
768        //        Type0/CID path (hex-glyph Tj, subsetter covers the value's
769        //        chars). See issue #212.
770        //      - Built-in font (Helvetica/Times/Courier) → WinAnsi strict
771        //        encoding. Fails explicitly for non-WinAnsi values.
772        //      - No `/DA` → Helvetica fallback, same WinAnsi-strict path.
773        let typed_da = form_field.default_appearance.clone();
774        let custom_font_arc = match typed_da.as_ref().and_then(|da| match &da.font {
775            crate::text::Font::Custom(name) => Some(name.clone()),
776            _ => None,
777        }) {
778            Some(name) => self.get_custom_font(&name),
779            None => None,
780        };
781
782        // Re-fetch `form_field` mutably — `self.get_custom_font` borrowed
783        // `self` immutably so the earlier `form_manager.get_field_mut`
784        // borrow has already ended. The FormManager still owns the field.
785        let form_manager = self.form_manager.as_mut().ok_or_else(|| {
786            PdfError::InvalidStructure(
787                "FormManager vanished between steps of fill_field — unreachable in single-thread"
788                    .to_string(),
789            )
790        })?;
791        let form_field = form_manager
792            .get_field_mut(name)
793            .ok_or_else(|| PdfError::FieldNotFound(name.to_string()))?;
794
795        // Aggregated per-font chars from every widget on this field. Merged
796        // into `self.used_characters_by_font` below so the writer subsetter
797        // covers the value's chars on the custom font (issue #204 invariant).
798        let mut ap_used_chars_by_font: std::collections::HashMap<
799            String,
800            std::collections::HashSet<char>,
801        > = std::collections::HashMap::new();
802        // `CustomFont` is the type alias `Font as CustomFont` → the struct
803        // at `crate::fonts::Font`. `custom_font_arc.as_deref()` therefore
804        // yields `Option<&crate::fonts::Font>` — exactly what
805        // `generate_appearance_with_font` wants.
806        let custom_font_ref: Option<&crate::fonts::Font> = custom_font_arc.as_deref();
807        for widget in &mut form_field.widgets {
808            let used = widget.generate_appearance_with_font(
809                field_type,
810                Some(&value),
811                typed_da.as_ref(),
812                custom_font_ref,
813            )?;
814            for (font_name, chars) in used {
815                ap_used_chars_by_font
816                    .entry(font_name)
817                    .or_default()
818                    .extend(chars);
819            }
820        }
821        // Merge into the document-wide char tracker so the writer subsets
822        // this font with the appearance's chars included.
823        for (font_name, chars) in ap_used_chars_by_font {
824            self.used_characters_by_font
825                .entry(font_name)
826                .or_default()
827                .extend(chars);
828        }
829
830        // 3) For each page annotation whose `/Parent` matches this field's
831        //    placeholder ref, rewrite `properties.AP` with the freshly
832        //    generated appearance dict. We iterate all pages because the
833        //    API permits (and the .NET wrapper sometimes exercises) the
834        //    same field being referenced by widgets on multiple pages.
835        if let Some(placeholder) = placeholder_ref {
836            // Re-borrow after the mutable borrow on `form_field` ends.
837            let form_field = self
838                .form_manager
839                .as_ref()
840                .and_then(|fm| fm.get_field(name))
841                .ok_or_else(|| PdfError::FieldNotFound(name.to_string()))?;
842
843            // Use the first widget's appearance as the representative dict
844            // for the field. All widgets of a text field share content in
845            // this implementation (they differ only in geometry), so this
846            // avoids rebuilding per-page — the Widget→Annotation mapping
847            // below re-associates each annotation with its own widget via
848            // `field_parent` matching.
849            // Tolerance for widget ↔ annotation rect matching. PDF
850            // coordinates are serialised as decimal strings and may drift
851            // by a few ULPs through a write → parse round-trip or through
852            // caller-side float arithmetic; `f64::EPSILON` (~2.22e-16) is
853            // far too tight to absorb that drift, so we allow up to 1e-3
854            // points (~0.00035 mm — well below any physically meaningful
855            // distance on paper, and 10× tighter than the smallest PDF
856            // rendering unit) before declaring two rects distinct.
857            const RECT_MATCH_TOLERANCE: f64 = 1e-3;
858
859            // Tracks whether we had to clear any stale /AP below. If so,
860            // flip `/AcroForm/NeedAppearances` true so viewers know to
861            // regenerate the appearance client-side — otherwise readers
862            // that trust /AP would render nothing where we removed it.
863            let mut needs_need_appearances = false;
864
865            for page in self.pages.iter_mut() {
866                for annot in page.annotations_mut().iter_mut() {
867                    if annot.field_parent != Some(placeholder) {
868                        continue;
869                    }
870                    // Find the widget whose rect is within tolerance of
871                    // this annotation's rect. Widgets on a field are
872                    // distinguished only by geometry, so `Rect` is the
873                    // natural key.
874                    let matching_widget = form_field.widgets.iter().find(|w| {
875                        (w.rect.lower_left.x - annot.rect.lower_left.x).abs() < RECT_MATCH_TOLERANCE
876                            && (w.rect.lower_left.y - annot.rect.lower_left.y).abs()
877                                < RECT_MATCH_TOLERANCE
878                            && (w.rect.upper_right.x - annot.rect.upper_right.x).abs()
879                                < RECT_MATCH_TOLERANCE
880                            && (w.rect.upper_right.y - annot.rect.upper_right.y).abs()
881                                < RECT_MATCH_TOLERANCE
882                    });
883
884                    match matching_widget.and_then(|w| w.appearance_streams.as_ref()) {
885                        Some(app_dict) => {
886                            annot
887                                .properties
888                                .set("AP", Object::Dictionary(app_dict.to_dict()));
889                        }
890                        None => {
891                            // Either (a) no widget rect matches this
892                            // annotation's rect, or (b) the matched
893                            // widget has no regenerated appearance
894                            // stream. In BOTH cases we must NOT guess a
895                            // substitute /AP (the previous fallback to
896                            // `widgets[0]` was a silent-wrong-widget bug
897                            // for multi-widget fields — see code-review
898                            // SEC-F3 2026-04-23). Instead clear any
899                            // stale /AP left from a prior fill and flip
900                            // /NeedAppearances so viewers regenerate.
901                            if annot.properties.get("AP").is_some() {
902                                annot.properties.remove("AP");
903                                needs_need_appearances = true;
904                            } else {
905                                // No stale /AP to clear; still flip
906                                // /NeedAppearances so the new /V gets
907                                // a fresh appearance at open time.
908                                needs_need_appearances = true;
909                            }
910                        }
911                    }
912                }
913            }
914
915            if needs_need_appearances {
916                let acro_form = self.acro_form.get_or_insert_with(AcroForm::new);
917                acro_form.need_appearances = true;
918            }
919        }
920
921        Ok(())
922    }
923
924    /// Saves the document to a file.
925    ///
926    /// # Errors
927    ///
928    /// Returns an error if the file cannot be created or written.
929    pub fn save(&mut self, path: impl AsRef<std::path::Path>) -> Result<()> {
930        // Update modification date before saving
931        self.update_modification_date();
932
933        // Create writer config with document's compression setting
934        let config = crate::writer::WriterConfig {
935            use_xref_streams: self.use_xref_streams,
936            use_object_streams: false, // For now, keep object streams disabled by default
937            pdf_version: if self.use_xref_streams { "1.5" } else { "1.7" }.to_string(),
938            compress_streams: self.compress,
939            incremental_update: false,
940        };
941
942        use std::io::BufWriter;
943        let file = std::fs::File::create(path)?;
944        // Use 512KB buffer for better I/O performance (vs default 8KB)
945        // Reduces syscalls by ~98% for typical PDFs
946        let writer = BufWriter::with_capacity(512 * 1024, file);
947        let mut pdf_writer = PdfWriter::with_config(writer, config);
948
949        pdf_writer.write_document(self)?;
950        Ok(())
951    }
952
953    /// Saves the document to a file with custom writer configuration.
954    ///
955    /// # Errors
956    ///
957    /// Returns an error if the file cannot be created or written.
958    pub fn save_with_config(
959        &mut self,
960        path: impl AsRef<std::path::Path>,
961        config: crate::writer::WriterConfig,
962    ) -> Result<()> {
963        use std::io::BufWriter;
964
965        // Update modification date before saving
966        self.update_modification_date();
967
968        // Use the config as provided (don't override compress_streams)
969
970        let file = std::fs::File::create(path)?;
971        // Use 512KB buffer for better I/O performance (vs default 8KB)
972        let writer = BufWriter::with_capacity(512 * 1024, file);
973        let mut pdf_writer = PdfWriter::with_config(writer, config);
974        pdf_writer.write_document(self)?;
975        Ok(())
976    }
977
978    /// Saves the document to a file with custom values for headers/footers.
979    ///
980    /// This method processes all pages to replace custom placeholders in headers
981    /// and footers before saving the document.
982    ///
983    /// # Arguments
984    ///
985    /// * `path` - The path where the document should be saved
986    /// * `custom_values` - A map of placeholder names to their replacement values
987    ///
988    /// # Errors
989    ///
990    /// Returns an error if the file cannot be created or written.
991    pub fn save_with_custom_values(
992        &mut self,
993        path: impl AsRef<std::path::Path>,
994        custom_values: &std::collections::HashMap<String, String>,
995    ) -> Result<()> {
996        // Process all pages with custom values
997        let total_pages = self.pages.len();
998        for (index, page) in self.pages.iter_mut().enumerate() {
999            // Generate content with page info and custom values
1000            let page_content = page.generate_content_with_page_info(
1001                Some(index + 1),
1002                Some(total_pages),
1003                Some(custom_values),
1004            )?;
1005            // Update the page content
1006            page.set_content(page_content);
1007        }
1008
1009        // Save the document normally
1010        self.save(path)
1011    }
1012
1013    /// Writes the document to a buffer.
1014    ///
1015    /// # Errors
1016    ///
1017    /// Returns an error if the PDF cannot be generated.
1018    pub fn write(&mut self, buffer: &mut Vec<u8>) -> Result<()> {
1019        // Update modification date before writing
1020        self.update_modification_date();
1021
1022        let mut writer = PdfWriter::new_with_writer(buffer);
1023        writer.write_document(self)?;
1024        Ok(())
1025    }
1026
1027    /// Enables or disables compression for PDF streams.
1028    ///
1029    /// When compression is enabled (default), content streams and XRef streams are compressed
1030    /// using Flate/Zlib compression to reduce file size. When disabled, streams are written
1031    /// uncompressed, making the PDF larger but easier to debug.
1032    ///
1033    /// # Arguments
1034    ///
1035    /// * `compress` - Whether to enable compression
1036    ///
1037    /// # Example
1038    ///
1039    /// ```rust
1040    /// use oxidize_pdf::{Document, Page};
1041    ///
1042    /// let mut doc = Document::new();
1043    ///
1044    /// // Disable compression for debugging
1045    /// doc.set_compress(false);
1046    ///
1047    /// doc.set_title("My Document");
1048    /// doc.add_page(Page::a4());
1049    ///
1050    /// let pdf_bytes = doc.to_bytes().unwrap();
1051    /// println!("Uncompressed PDF size: {} bytes", pdf_bytes.len());
1052    /// ```
1053    pub fn set_compress(&mut self, compress: bool) {
1054        self.compress = compress;
1055    }
1056
1057    /// Enable or disable compressed cross-reference streams (PDF 1.5+).
1058    ///
1059    /// Cross-reference streams provide more compact representation of the cross-reference
1060    /// table and support additional features like compressed object streams.
1061    ///
1062    /// # Arguments
1063    ///
1064    /// * `enable` - Whether to enable compressed cross-reference streams
1065    ///
1066    /// # Example
1067    ///
1068    /// ```rust
1069    /// use oxidize_pdf::Document;
1070    ///
1071    /// let mut doc = Document::new();
1072    /// doc.enable_xref_streams(true);
1073    /// ```
1074    pub fn enable_xref_streams(&mut self, enable: bool) -> &mut Self {
1075        self.use_xref_streams = enable;
1076        self
1077    }
1078
1079    /// Gets the current compression setting.
1080    ///
1081    /// # Returns
1082    ///
1083    /// Returns `true` if compression is enabled, `false` otherwise.
1084    pub fn get_compress(&self) -> bool {
1085        self.compress
1086    }
1087
1088    /// Generates the PDF document as bytes in memory.
1089    ///
1090    /// This method provides in-memory PDF generation without requiring file I/O.
1091    /// The document is serialized to bytes and returned as a `Vec<u8>`.
1092    ///
1093    /// # Returns
1094    ///
1095    /// Returns the PDF document as bytes on success.
1096    ///
1097    /// # Errors
1098    ///
1099    /// Returns an error if the document cannot be serialized.
1100    ///
1101    /// # Example
1102    ///
1103    /// ```rust
1104    /// use oxidize_pdf::{Document, Page};
1105    ///
1106    /// let mut doc = Document::new();
1107    /// doc.set_title("My Document");
1108    ///
1109    /// let page = Page::a4();
1110    /// doc.add_page(page);
1111    ///
1112    /// let pdf_bytes = doc.to_bytes().unwrap();
1113    /// println!("Generated PDF size: {} bytes", pdf_bytes.len());
1114    /// ```
1115    pub fn to_bytes(&mut self) -> Result<Vec<u8>> {
1116        // Update modification date before serialization
1117        self.update_modification_date();
1118
1119        // Create a buffer to write the PDF data to
1120        let mut buffer = Vec::new();
1121
1122        // Create writer config with document's compression setting
1123        let config = crate::writer::WriterConfig {
1124            use_xref_streams: self.use_xref_streams,
1125            use_object_streams: false, // For now, keep object streams disabled by default
1126            pdf_version: if self.use_xref_streams { "1.5" } else { "1.7" }.to_string(),
1127            compress_streams: self.compress,
1128            incremental_update: false,
1129        };
1130
1131        // Use PdfWriter with the buffer as output and config
1132        let mut writer = PdfWriter::with_config(&mut buffer, config);
1133        writer.write_document(self)?;
1134
1135        Ok(buffer)
1136    }
1137
1138    /// Generates the PDF document as bytes with custom writer configuration.
1139    ///
1140    /// This method allows customizing the PDF output (e.g., using XRef streams)
1141    /// while still generating the document in memory.
1142    ///
1143    /// # Arguments
1144    ///
1145    /// * `config` - Writer configuration options
1146    ///
1147    /// # Returns
1148    ///
1149    /// Returns the PDF document as bytes on success.
1150    ///
1151    /// # Errors
1152    ///
1153    /// Returns an error if the document cannot be serialized.
1154    ///
1155    /// # Example
1156    ///
1157    /// ```rust
1158    /// use oxidize_pdf::{Document, Page};
1159    /// use oxidize_pdf::writer::WriterConfig;
1160    ///
1161    /// let mut doc = Document::new();
1162    /// doc.set_title("My Document");
1163    ///
1164    /// let page = Page::a4();
1165    /// doc.add_page(page);
1166    ///
1167    /// let config = WriterConfig {
1168    ///     use_xref_streams: true,
1169    ///     use_object_streams: false,
1170    ///     pdf_version: "1.5".to_string(),
1171    ///     compress_streams: true,
1172    ///     incremental_update: false,
1173    /// };
1174    ///
1175    /// let pdf_bytes = doc.to_bytes_with_config(config).unwrap();
1176    /// println!("Generated PDF size: {} bytes", pdf_bytes.len());
1177    /// ```
1178    pub fn to_bytes_with_config(&mut self, config: crate::writer::WriterConfig) -> Result<Vec<u8>> {
1179        // Update modification date before serialization
1180        self.update_modification_date();
1181
1182        // Use the config as provided (don't override compress_streams)
1183
1184        // Create a buffer to write the PDF data to
1185        let mut buffer = Vec::new();
1186
1187        // Use PdfWriter with the buffer as output and custom config
1188        let mut writer = PdfWriter::with_config(&mut buffer, config);
1189        writer.write_document(self)?;
1190
1191        Ok(buffer)
1192    }
1193
1194    // ==================== Semantic Entity Methods ====================
1195
1196    /// Mark a region of the PDF with semantic meaning for AI processing.
1197    ///
1198    /// This creates an AI-Ready PDF that contains machine-readable metadata
1199    /// alongside the visual content, enabling automated document processing.
1200    ///
1201    /// # Example
1202    ///
1203    /// ```rust
1204    /// use oxidize_pdf::{Document, semantic::{EntityType, BoundingBox}};
1205    ///
1206    /// let mut doc = Document::new();
1207    ///
1208    /// // Mark an invoice number region
1209    /// let entity_id = doc.mark_entity(
1210    ///     "invoice_001".to_string(),
1211    ///     EntityType::InvoiceNumber,
1212    ///     BoundingBox::new(100.0, 700.0, 150.0, 20.0, 1)
1213    /// );
1214    ///
1215    /// // Add content and metadata
1216    /// doc.set_entity_content(&entity_id, "INV-2024-001");
1217    /// doc.add_entity_metadata(&entity_id, "confidence", "0.98");
1218    /// ```
1219    pub fn mark_entity(
1220        &mut self,
1221        id: impl Into<String>,
1222        entity_type: EntityType,
1223        bounds: BoundingBox,
1224    ) -> String {
1225        let entity_id = id.into();
1226        let entity = SemanticEntity::new(entity_id.clone(), entity_type, bounds);
1227        self.semantic_entities.push(entity);
1228        entity_id
1229    }
1230
1231    /// Set the content text for an entity
1232    pub fn set_entity_content(&mut self, entity_id: &str, content: impl Into<String>) -> bool {
1233        if let Some(entity) = self
1234            .semantic_entities
1235            .iter_mut()
1236            .find(|e| e.id == entity_id)
1237        {
1238            entity.content = content.into();
1239            true
1240        } else {
1241            false
1242        }
1243    }
1244
1245    /// Add metadata to an entity
1246    pub fn add_entity_metadata(
1247        &mut self,
1248        entity_id: &str,
1249        key: impl Into<String>,
1250        value: impl Into<String>,
1251    ) -> bool {
1252        if let Some(entity) = self
1253            .semantic_entities
1254            .iter_mut()
1255            .find(|e| e.id == entity_id)
1256        {
1257            entity.metadata.properties.insert(key.into(), value.into());
1258            true
1259        } else {
1260            false
1261        }
1262    }
1263
1264    /// Set confidence score for an entity
1265    pub fn set_entity_confidence(&mut self, entity_id: &str, confidence: f32) -> bool {
1266        if let Some(entity) = self
1267            .semantic_entities
1268            .iter_mut()
1269            .find(|e| e.id == entity_id)
1270        {
1271            entity.metadata.confidence = Some(confidence.clamp(0.0, 1.0));
1272            true
1273        } else {
1274            false
1275        }
1276    }
1277
1278    /// Add a relationship between two entities
1279    pub fn relate_entities(
1280        &mut self,
1281        from_id: &str,
1282        to_id: &str,
1283        relation_type: RelationType,
1284    ) -> bool {
1285        // First check if target entity exists
1286        let target_exists = self.semantic_entities.iter().any(|e| e.id == to_id);
1287        if !target_exists {
1288            return false;
1289        }
1290
1291        // Then add the relationship
1292        if let Some(entity) = self.semantic_entities.iter_mut().find(|e| e.id == from_id) {
1293            entity.relationships.push(crate::semantic::EntityRelation {
1294                target_id: to_id.to_string(),
1295                relation_type,
1296            });
1297            true
1298        } else {
1299            false
1300        }
1301    }
1302
1303    /// Get all semantic entities in the document
1304    pub fn get_semantic_entities(&self) -> &[SemanticEntity] {
1305        &self.semantic_entities
1306    }
1307
1308    /// Get entities by type
1309    pub fn get_entities_by_type(&self, entity_type: EntityType) -> Vec<&SemanticEntity> {
1310        self.semantic_entities
1311            .iter()
1312            .filter(|e| e.entity_type == entity_type)
1313            .collect()
1314    }
1315
1316    /// Export semantic entities as JSON
1317    #[cfg(feature = "semantic")]
1318    pub fn export_semantic_entities_json(&self) -> Result<String> {
1319        serde_json::to_string_pretty(&self.semantic_entities)
1320            .map_err(|e| crate::error::PdfError::SerializationError(e.to_string()))
1321    }
1322
1323    /// Export semantic entities as JSON-LD with Schema.org context
1324    ///
1325    /// This creates a machine-readable export compatible with Schema.org vocabularies,
1326    /// making the PDF data accessible to AI/ML processing pipelines.
1327    ///
1328    /// # Example
1329    ///
1330    /// ```rust
1331    /// use oxidize_pdf::{Document, semantic::{EntityType, BoundingBox}};
1332    ///
1333    /// let mut doc = Document::new();
1334    ///
1335    /// // Mark an invoice
1336    /// let inv_id = doc.mark_entity(
1337    ///     "invoice_1".to_string(),
1338    ///     EntityType::Invoice,
1339    ///     BoundingBox::new(50.0, 50.0, 500.0, 700.0, 1)
1340    /// );
1341    /// doc.set_entity_content(&inv_id, "Invoice #INV-001");
1342    /// doc.add_entity_metadata(&inv_id, "totalPrice", "1234.56");
1343    ///
1344    /// // Export as JSON-LD
1345    /// let json_ld = doc.export_semantic_entities_json_ld().unwrap();
1346    /// println!("{}", json_ld);
1347    /// ```
1348    #[cfg(feature = "semantic")]
1349    pub fn export_semantic_entities_json_ld(&self) -> Result<String> {
1350        use crate::semantic::{Entity, EntityMap};
1351
1352        let mut entity_map = EntityMap::new();
1353
1354        // Convert SemanticEntity to Entity (backward compatibility)
1355        for sem_entity in &self.semantic_entities {
1356            let entity = Entity {
1357                id: sem_entity.id.clone(),
1358                entity_type: sem_entity.entity_type.clone(),
1359                bounds: (
1360                    sem_entity.bounds.x as f64,
1361                    sem_entity.bounds.y as f64,
1362                    sem_entity.bounds.width as f64,
1363                    sem_entity.bounds.height as f64,
1364                ),
1365                page: (sem_entity.bounds.page - 1) as usize, // Convert 1-indexed to 0-indexed
1366                metadata: sem_entity.metadata.clone(),
1367            };
1368            entity_map.add_entity(entity);
1369        }
1370
1371        // Add document metadata
1372        if let Some(title) = &self.metadata.title {
1373            entity_map
1374                .document_metadata
1375                .insert("name".to_string(), title.clone());
1376        }
1377        if let Some(author) = &self.metadata.author {
1378            entity_map
1379                .document_metadata
1380                .insert("author".to_string(), author.clone());
1381        }
1382
1383        entity_map
1384            .to_json_ld()
1385            .map_err(|e| crate::error::PdfError::SerializationError(e.to_string()))
1386    }
1387
1388    /// Find an entity by ID
1389    pub fn find_entity(&self, entity_id: &str) -> Option<&SemanticEntity> {
1390        self.semantic_entities.iter().find(|e| e.id == entity_id)
1391    }
1392
1393    /// Remove an entity by ID
1394    pub fn remove_entity(&mut self, entity_id: &str) -> bool {
1395        if let Some(pos) = self
1396            .semantic_entities
1397            .iter()
1398            .position(|e| e.id == entity_id)
1399        {
1400            self.semantic_entities.remove(pos);
1401            // Also remove any relationships pointing to this entity
1402            for entity in &mut self.semantic_entities {
1403                entity.relationships.retain(|r| r.target_id != entity_id);
1404            }
1405            true
1406        } else {
1407            false
1408        }
1409    }
1410
1411    /// Get the count of semantic entities
1412    pub fn semantic_entity_count(&self) -> usize {
1413        self.semantic_entities.len()
1414    }
1415
1416    /// Create XMP metadata from document metadata
1417    ///
1418    /// Generates an XMP metadata object from the document's metadata.
1419    /// The XMP metadata can be serialized and embedded in the PDF.
1420    ///
1421    /// # Returns
1422    /// XMP metadata object populated with document information
1423    pub fn create_xmp_metadata(&self) -> crate::metadata::XmpMetadata {
1424        let mut xmp = crate::metadata::XmpMetadata::new();
1425
1426        // Add Dublin Core metadata
1427        if let Some(title) = &self.metadata.title {
1428            xmp.set_text(crate::metadata::XmpNamespace::DublinCore, "title", title);
1429        }
1430        if let Some(author) = &self.metadata.author {
1431            xmp.set_text(crate::metadata::XmpNamespace::DublinCore, "creator", author);
1432        }
1433        if let Some(subject) = &self.metadata.subject {
1434            xmp.set_text(
1435                crate::metadata::XmpNamespace::DublinCore,
1436                "description",
1437                subject,
1438            );
1439        }
1440
1441        // Add XMP Basic metadata
1442        if let Some(creator) = &self.metadata.creator {
1443            xmp.set_text(
1444                crate::metadata::XmpNamespace::XmpBasic,
1445                "CreatorTool",
1446                creator,
1447            );
1448        }
1449        if let Some(creation_date) = &self.metadata.creation_date {
1450            xmp.set_date(
1451                crate::metadata::XmpNamespace::XmpBasic,
1452                "CreateDate",
1453                creation_date.to_rfc3339(),
1454            );
1455        }
1456        if let Some(mod_date) = &self.metadata.modification_date {
1457            xmp.set_date(
1458                crate::metadata::XmpNamespace::XmpBasic,
1459                "ModifyDate",
1460                mod_date.to_rfc3339(),
1461            );
1462        }
1463
1464        // Add PDF specific metadata
1465        if let Some(producer) = &self.metadata.producer {
1466            xmp.set_text(crate::metadata::XmpNamespace::Pdf, "Producer", producer);
1467        }
1468
1469        xmp
1470    }
1471
1472    /// Get XMP packet as string
1473    ///
1474    /// Returns the XMP metadata packet that can be embedded in the PDF.
1475    /// This is a convenience method that creates XMP from document metadata
1476    /// and serializes it to XML.
1477    ///
1478    /// # Returns
1479    /// XMP packet as XML string
1480    pub fn get_xmp_packet(&self) -> String {
1481        self.create_xmp_metadata().to_xmp_packet()
1482    }
1483
1484    /// Extract text content from all pages (placeholder implementation)
1485    pub fn extract_text(&self) -> Result<String> {
1486        // Placeholder implementation - in a real PDF reader this would
1487        // parse content streams and extract text operators
1488        let mut text = String::new();
1489        for (i, _page) in self.pages.iter().enumerate() {
1490            text.push_str(&format!("Text from page {} (placeholder)\n", i + 1));
1491        }
1492        Ok(text)
1493    }
1494
1495    /// Extract text content from a specific page (placeholder implementation)
1496    pub fn extract_page_text(&self, page_index: usize) -> Result<String> {
1497        if page_index < self.pages.len() {
1498            Ok(format!("Text from page {} (placeholder)", page_index + 1))
1499        } else {
1500            Err(crate::error::PdfError::InvalidReference(format!(
1501                "Page index {} out of bounds",
1502                page_index
1503            )))
1504        }
1505    }
1506}
1507
1508impl Default for Document {
1509    fn default() -> Self {
1510        Self::new()
1511    }
1512}
1513
1514#[cfg(test)]
1515mod tests {
1516    use super::*;
1517
1518    #[test]
1519    fn test_document_new() {
1520        let doc = Document::new();
1521        assert!(doc.pages.is_empty());
1522        assert!(doc.metadata.title.is_none());
1523        assert!(doc.metadata.author.is_none());
1524        assert!(doc.metadata.subject.is_none());
1525        assert!(doc.metadata.keywords.is_none());
1526        assert_eq!(doc.metadata.creator, Some("oxidize_pdf".to_string()));
1527        assert!(doc
1528            .metadata
1529            .producer
1530            .as_ref()
1531            .unwrap()
1532            .starts_with("oxidize_pdf"));
1533    }
1534
1535    #[test]
1536    fn test_document_default() {
1537        let doc = Document::default();
1538        assert!(doc.pages.is_empty());
1539    }
1540
1541    #[test]
1542    fn test_add_page() {
1543        let mut doc = Document::new();
1544        let page1 = Page::a4();
1545        let page2 = Page::letter();
1546
1547        doc.add_page(page1);
1548        assert_eq!(doc.pages.len(), 1);
1549
1550        doc.add_page(page2);
1551        assert_eq!(doc.pages.len(), 2);
1552    }
1553
1554    #[test]
1555    fn test_set_title() {
1556        let mut doc = Document::new();
1557        assert!(doc.metadata.title.is_none());
1558
1559        doc.set_title("Test Document");
1560        assert_eq!(doc.metadata.title, Some("Test Document".to_string()));
1561
1562        doc.set_title(String::from("Another Title"));
1563        assert_eq!(doc.metadata.title, Some("Another Title".to_string()));
1564    }
1565
1566    #[test]
1567    fn test_set_author() {
1568        let mut doc = Document::new();
1569        assert!(doc.metadata.author.is_none());
1570
1571        doc.set_author("John Doe");
1572        assert_eq!(doc.metadata.author, Some("John Doe".to_string()));
1573    }
1574
1575    #[test]
1576    fn test_set_subject() {
1577        let mut doc = Document::new();
1578        assert!(doc.metadata.subject.is_none());
1579
1580        doc.set_subject("Test Subject");
1581        assert_eq!(doc.metadata.subject, Some("Test Subject".to_string()));
1582    }
1583
1584    #[test]
1585    fn test_set_keywords() {
1586        let mut doc = Document::new();
1587        assert!(doc.metadata.keywords.is_none());
1588
1589        doc.set_keywords("test, pdf, rust");
1590        assert_eq!(doc.metadata.keywords, Some("test, pdf, rust".to_string()));
1591    }
1592
1593    #[test]
1594    fn test_metadata_default() {
1595        let metadata = DocumentMetadata::default();
1596        assert!(metadata.title.is_none());
1597        assert!(metadata.author.is_none());
1598        assert!(metadata.subject.is_none());
1599        assert!(metadata.keywords.is_none());
1600        assert_eq!(metadata.creator, Some("oxidize_pdf".to_string()));
1601        assert!(metadata
1602            .producer
1603            .as_ref()
1604            .unwrap()
1605            .starts_with("oxidize_pdf"));
1606    }
1607
1608    #[test]
1609    fn test_write_to_buffer() {
1610        let mut doc = Document::new();
1611        doc.set_title("Buffer Test");
1612        doc.add_page(Page::a4());
1613
1614        let mut buffer = Vec::new();
1615        let result = doc.write(&mut buffer);
1616
1617        assert!(result.is_ok());
1618        assert!(!buffer.is_empty());
1619        assert!(buffer.starts_with(b"%PDF-1.7"));
1620    }
1621
1622    #[test]
1623    fn test_document_with_multiple_pages() {
1624        let mut doc = Document::new();
1625        doc.set_title("Multi-page Document");
1626        doc.set_author("Test Author");
1627        doc.set_subject("Testing multiple pages");
1628        doc.set_keywords("test, multiple, pages");
1629
1630        for _ in 0..5 {
1631            doc.add_page(Page::a4());
1632        }
1633
1634        assert_eq!(doc.pages.len(), 5);
1635        assert_eq!(doc.metadata.title, Some("Multi-page Document".to_string()));
1636        assert_eq!(doc.metadata.author, Some("Test Author".to_string()));
1637    }
1638
1639    #[test]
1640    fn test_empty_document_write() {
1641        let mut doc = Document::new();
1642        let mut buffer = Vec::new();
1643
1644        // Empty document should still produce valid PDF
1645        let result = doc.write(&mut buffer);
1646        assert!(result.is_ok());
1647        assert!(!buffer.is_empty());
1648        assert!(buffer.starts_with(b"%PDF-1.7"));
1649    }
1650
1651    // Integration tests for Document ↔ Writer ↔ Parser interactions
1652    mod integration_tests {
1653        use super::*;
1654        use crate::graphics::Color;
1655        use crate::text::Font;
1656        use std::fs;
1657        use tempfile::TempDir;
1658
1659        #[test]
1660        fn test_document_writer_roundtrip() {
1661            let temp_dir = TempDir::new().unwrap();
1662            let file_path = temp_dir.path().join("test.pdf");
1663
1664            // Create document with content
1665            let mut doc = Document::new();
1666            doc.set_title("Integration Test");
1667            doc.set_author("Test Author");
1668            doc.set_subject("Writer Integration");
1669            doc.set_keywords("test, writer, integration");
1670
1671            let mut page = Page::a4();
1672            page.text()
1673                .set_font(Font::Helvetica, 12.0)
1674                .at(100.0, 700.0)
1675                .write("Integration Test Content")
1676                .unwrap();
1677
1678            doc.add_page(page);
1679
1680            // Write to file
1681            let result = doc.save(&file_path);
1682            assert!(result.is_ok());
1683
1684            // Verify file exists and has content
1685            assert!(file_path.exists());
1686            let metadata = fs::metadata(&file_path).unwrap();
1687            assert!(metadata.len() > 0);
1688
1689            // Read file back to verify PDF format
1690            let content = fs::read(&file_path).unwrap();
1691            assert!(content.starts_with(b"%PDF-1.7"));
1692            // Check for %%EOF with or without newline
1693            assert!(content.ends_with(b"%%EOF\n") || content.ends_with(b"%%EOF"));
1694        }
1695
1696        #[test]
1697        fn test_document_with_complex_content() {
1698            let temp_dir = TempDir::new().unwrap();
1699            let file_path = temp_dir.path().join("complex.pdf");
1700
1701            let mut doc = Document::new();
1702            doc.set_title("Complex Content Test");
1703
1704            // Create page with mixed content
1705            let mut page = Page::a4();
1706
1707            // Add text
1708            page.text()
1709                .set_font(Font::Helvetica, 14.0)
1710                .at(50.0, 750.0)
1711                .write("Complex Content Test")
1712                .unwrap();
1713
1714            // Add graphics
1715            page.graphics()
1716                .set_fill_color(Color::rgb(0.8, 0.2, 0.2))
1717                .rectangle(50.0, 500.0, 200.0, 100.0)
1718                .fill();
1719
1720            page.graphics()
1721                .set_stroke_color(Color::rgb(0.2, 0.2, 0.8))
1722                .set_line_width(2.0)
1723                .move_to(50.0, 400.0)
1724                .line_to(250.0, 400.0)
1725                .stroke();
1726
1727            doc.add_page(page);
1728
1729            // Write and verify
1730            let result = doc.save(&file_path);
1731            assert!(result.is_ok());
1732            assert!(file_path.exists());
1733        }
1734
1735        #[test]
1736        fn test_document_multiple_pages_integration() {
1737            let temp_dir = TempDir::new().unwrap();
1738            let file_path = temp_dir.path().join("multipage.pdf");
1739
1740            let mut doc = Document::new();
1741            doc.set_title("Multi-page Integration Test");
1742
1743            // Create multiple pages with different content
1744            for i in 1..=5 {
1745                let mut page = Page::a4();
1746
1747                page.text()
1748                    .set_font(Font::Helvetica, 16.0)
1749                    .at(50.0, 750.0)
1750                    .write(&format!("Page {i}"))
1751                    .unwrap();
1752
1753                page.text()
1754                    .set_font(Font::Helvetica, 12.0)
1755                    .at(50.0, 700.0)
1756                    .write(&format!("This is the content for page {i}"))
1757                    .unwrap();
1758
1759                // Add unique graphics for each page
1760                let color = match i % 3 {
1761                    0 => Color::rgb(1.0, 0.0, 0.0),
1762                    1 => Color::rgb(0.0, 1.0, 0.0),
1763                    _ => Color::rgb(0.0, 0.0, 1.0),
1764                };
1765
1766                page.graphics()
1767                    .set_fill_color(color)
1768                    .rectangle(50.0, 600.0, 100.0, 50.0)
1769                    .fill();
1770
1771                doc.add_page(page);
1772            }
1773
1774            // Write and verify
1775            let result = doc.save(&file_path);
1776            assert!(result.is_ok());
1777            assert!(file_path.exists());
1778
1779            // Verify file size is reasonable for 5 pages
1780            let metadata = fs::metadata(&file_path).unwrap();
1781            assert!(metadata.len() > 1000); // Should be substantial
1782        }
1783
1784        #[test]
1785        fn test_document_metadata_persistence() {
1786            let temp_dir = TempDir::new().unwrap();
1787            let file_path = temp_dir.path().join("metadata.pdf");
1788
1789            let mut doc = Document::new();
1790            doc.set_title("Metadata Persistence Test");
1791            doc.set_author("Test Author");
1792            doc.set_subject("Testing metadata preservation");
1793            doc.set_keywords("metadata, persistence, test");
1794
1795            doc.add_page(Page::a4());
1796
1797            // Write to file
1798            let result = doc.save(&file_path);
1799            assert!(result.is_ok());
1800
1801            // Read file content to verify metadata is present
1802            let content = fs::read(&file_path).unwrap();
1803            let content_str = String::from_utf8_lossy(&content);
1804
1805            // Check that metadata appears in the PDF
1806            assert!(content_str.contains("Metadata Persistence Test"));
1807            assert!(content_str.contains("Test Author"));
1808        }
1809
1810        #[test]
1811        fn test_document_writer_error_handling() {
1812            let mut doc = Document::new();
1813            doc.add_page(Page::a4());
1814
1815            // Test writing to invalid path
1816            let result = doc.save("/invalid/path/test.pdf");
1817            assert!(result.is_err());
1818        }
1819
1820        #[test]
1821        fn test_document_page_integration() {
1822            let mut doc = Document::new();
1823
1824            // Test different page configurations
1825            let page1 = Page::a4();
1826            let page2 = Page::letter();
1827            let mut page3 = Page::new(500.0, 400.0);
1828
1829            // Add content to custom page
1830            page3
1831                .text()
1832                .set_font(Font::Helvetica, 10.0)
1833                .at(25.0, 350.0)
1834                .write("Custom size page")
1835                .unwrap();
1836
1837            doc.add_page(page1);
1838            doc.add_page(page2);
1839            doc.add_page(page3);
1840
1841            assert_eq!(doc.pages.len(), 3);
1842
1843            // Verify pages maintain their properties (actual dimensions may vary)
1844            assert!(doc.pages[0].width() > 500.0); // A4 width is reasonable
1845            assert!(doc.pages[0].height() > 700.0); // A4 height is reasonable
1846            assert!(doc.pages[1].width() > 500.0); // Letter width is reasonable
1847            assert!(doc.pages[1].height() > 700.0); // Letter height is reasonable
1848            assert_eq!(doc.pages[2].width(), 500.0); // Custom width
1849            assert_eq!(doc.pages[2].height(), 400.0); // Custom height
1850        }
1851
1852        #[test]
1853        fn test_document_content_generation() {
1854            let temp_dir = TempDir::new().unwrap();
1855            let file_path = temp_dir.path().join("content.pdf");
1856
1857            let mut doc = Document::new();
1858            doc.set_title("Content Generation Test");
1859
1860            let mut page = Page::a4();
1861
1862            // Generate content programmatically
1863            for i in 0..10 {
1864                let y_pos = 700.0 - (i as f64 * 30.0);
1865                page.text()
1866                    .set_font(Font::Helvetica, 12.0)
1867                    .at(50.0, y_pos)
1868                    .write(&format!("Generated line {}", i + 1))
1869                    .unwrap();
1870            }
1871
1872            doc.add_page(page);
1873
1874            // Write and verify
1875            let result = doc.save(&file_path);
1876            assert!(result.is_ok());
1877            assert!(file_path.exists());
1878
1879            // Verify content was generated
1880            let metadata = fs::metadata(&file_path).unwrap();
1881            assert!(metadata.len() > 500); // Should contain substantial content
1882        }
1883
1884        #[test]
1885        fn test_document_buffer_vs_file_write() {
1886            let temp_dir = TempDir::new().unwrap();
1887            let file_path = temp_dir.path().join("buffer_vs_file.pdf");
1888
1889            let mut doc = Document::new();
1890            doc.set_title("Buffer vs File Test");
1891            doc.add_page(Page::a4());
1892
1893            // Write to buffer
1894            let mut buffer = Vec::new();
1895            let buffer_result = doc.write(&mut buffer);
1896            assert!(buffer_result.is_ok());
1897
1898            // Write to file
1899            let file_result = doc.save(&file_path);
1900            assert!(file_result.is_ok());
1901
1902            // Read file back
1903            let file_content = fs::read(&file_path).unwrap();
1904
1905            // Both should be valid PDFs with same structure (timestamps may differ)
1906            assert!(buffer.starts_with(b"%PDF-1.7"));
1907            assert!(file_content.starts_with(b"%PDF-1.7"));
1908            assert!(buffer.ends_with(b"%%EOF\n"));
1909            assert!(file_content.ends_with(b"%%EOF\n"));
1910
1911            // Both should contain the same title
1912            let buffer_str = String::from_utf8_lossy(&buffer);
1913            let file_str = String::from_utf8_lossy(&file_content);
1914            assert!(buffer_str.contains("Buffer vs File Test"));
1915            assert!(file_str.contains("Buffer vs File Test"));
1916        }
1917
1918        #[test]
1919        fn test_document_large_content_handling() {
1920            let temp_dir = TempDir::new().unwrap();
1921            let file_path = temp_dir.path().join("large_content.pdf");
1922
1923            let mut doc = Document::new();
1924            doc.set_title("Large Content Test");
1925
1926            let mut page = Page::a4();
1927
1928            // Add large amount of text content - make it much larger
1929            let large_text =
1930                "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ".repeat(200);
1931            page.text()
1932                .set_font(Font::Helvetica, 10.0)
1933                .at(50.0, 750.0)
1934                .write(&large_text)
1935                .unwrap();
1936
1937            doc.add_page(page);
1938
1939            // Write and verify
1940            let result = doc.save(&file_path);
1941            assert!(result.is_ok());
1942            assert!(file_path.exists());
1943
1944            // Verify large content was handled properly - reduce expectation
1945            let metadata = fs::metadata(&file_path).unwrap();
1946            assert!(metadata.len() > 500); // Should be substantial but realistic
1947        }
1948
1949        #[test]
1950        fn test_document_incremental_building() {
1951            let temp_dir = TempDir::new().unwrap();
1952            let file_path = temp_dir.path().join("incremental.pdf");
1953
1954            let mut doc = Document::new();
1955
1956            // Build document incrementally
1957            doc.set_title("Incremental Building Test");
1958
1959            // Add first page
1960            let mut page1 = Page::a4();
1961            page1
1962                .text()
1963                .set_font(Font::Helvetica, 12.0)
1964                .at(50.0, 750.0)
1965                .write("First page content")
1966                .unwrap();
1967            doc.add_page(page1);
1968
1969            // Add metadata
1970            doc.set_author("Incremental Author");
1971            doc.set_subject("Incremental Subject");
1972
1973            // Add second page
1974            let mut page2 = Page::a4();
1975            page2
1976                .text()
1977                .set_font(Font::Helvetica, 12.0)
1978                .at(50.0, 750.0)
1979                .write("Second page content")
1980                .unwrap();
1981            doc.add_page(page2);
1982
1983            // Add more metadata
1984            doc.set_keywords("incremental, building, test");
1985
1986            // Final write
1987            let result = doc.save(&file_path);
1988            assert!(result.is_ok());
1989            assert!(file_path.exists());
1990
1991            // Verify final state
1992            assert_eq!(doc.pages.len(), 2);
1993            assert_eq!(
1994                doc.metadata.title,
1995                Some("Incremental Building Test".to_string())
1996            );
1997            assert_eq!(doc.metadata.author, Some("Incremental Author".to_string()));
1998            assert_eq!(
1999                doc.metadata.subject,
2000                Some("Incremental Subject".to_string())
2001            );
2002            assert_eq!(
2003                doc.metadata.keywords,
2004                Some("incremental, building, test".to_string())
2005            );
2006        }
2007
2008        #[test]
2009        fn test_document_concurrent_page_operations() {
2010            let mut doc = Document::new();
2011            doc.set_title("Concurrent Operations Test");
2012
2013            // Simulate concurrent-like operations
2014            let mut pages = Vec::new();
2015
2016            // Create multiple pages
2017            for i in 0..5 {
2018                let mut page = Page::a4();
2019                page.text()
2020                    .set_font(Font::Helvetica, 12.0)
2021                    .at(50.0, 750.0)
2022                    .write(&format!("Concurrent page {i}"))
2023                    .unwrap();
2024                pages.push(page);
2025            }
2026
2027            // Add all pages
2028            for page in pages {
2029                doc.add_page(page);
2030            }
2031
2032            assert_eq!(doc.pages.len(), 5);
2033
2034            // Verify each page maintains its content
2035            let temp_dir = TempDir::new().unwrap();
2036            let file_path = temp_dir.path().join("concurrent.pdf");
2037            let result = doc.save(&file_path);
2038            assert!(result.is_ok());
2039        }
2040
2041        #[test]
2042        fn test_document_memory_efficiency() {
2043            let mut doc = Document::new();
2044            doc.set_title("Memory Efficiency Test");
2045
2046            // Add multiple pages with content
2047            for i in 0..10 {
2048                let mut page = Page::a4();
2049                page.text()
2050                    .set_font(Font::Helvetica, 12.0)
2051                    .at(50.0, 700.0)
2052                    .write(&format!("Memory test page {i}"))
2053                    .unwrap();
2054                doc.add_page(page);
2055            }
2056
2057            // Write to buffer to test memory usage
2058            let mut buffer = Vec::new();
2059            let result = doc.write(&mut buffer);
2060            assert!(result.is_ok());
2061            assert!(!buffer.is_empty());
2062
2063            // Buffer should be reasonable size
2064            assert!(buffer.len() < 1_000_000); // Should be less than 1MB for simple content
2065        }
2066
2067        #[test]
2068        fn test_document_creator_producer() {
2069            let mut doc = Document::new();
2070
2071            // Default values
2072            assert_eq!(doc.metadata.creator, Some("oxidize_pdf".to_string()));
2073            assert!(doc
2074                .metadata
2075                .producer
2076                .as_ref()
2077                .unwrap()
2078                .contains("oxidize_pdf"));
2079
2080            // Set custom values
2081            doc.set_creator("My Application");
2082            doc.set_producer("My PDF Library v1.0");
2083
2084            assert_eq!(doc.metadata.creator, Some("My Application".to_string()));
2085            assert_eq!(
2086                doc.metadata.producer,
2087                Some("My PDF Library v1.0".to_string())
2088            );
2089        }
2090
2091        #[test]
2092        fn test_document_dates() {
2093            use chrono::{TimeZone, Utc};
2094
2095            let mut doc = Document::new();
2096
2097            // Check default dates are set
2098            assert!(doc.metadata.creation_date.is_some());
2099            assert!(doc.metadata.modification_date.is_some());
2100
2101            // Set specific dates
2102            let creation_date = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
2103            let mod_date = Utc.with_ymd_and_hms(2023, 6, 15, 18, 30, 0).unwrap();
2104
2105            doc.set_creation_date(creation_date);
2106            doc.set_modification_date(mod_date);
2107
2108            assert_eq!(doc.metadata.creation_date, Some(creation_date));
2109            assert_eq!(doc.metadata.modification_date, Some(mod_date));
2110        }
2111
2112        #[test]
2113        fn test_document_dates_local() {
2114            use chrono::{Local, TimeZone};
2115
2116            let mut doc = Document::new();
2117
2118            // Test setting dates with local time
2119            let local_date = Local.with_ymd_and_hms(2023, 12, 25, 10, 30, 0).unwrap();
2120            doc.set_creation_date_local(local_date);
2121
2122            // Verify it was converted to UTC
2123            assert!(doc.metadata.creation_date.is_some());
2124            // Just verify the date was set, don't compare exact values due to timezone complexities
2125            assert!(doc.metadata.creation_date.is_some());
2126        }
2127
2128        #[test]
2129        fn test_update_modification_date() {
2130            let mut doc = Document::new();
2131
2132            let initial_mod_date = doc.metadata.modification_date;
2133            assert!(initial_mod_date.is_some());
2134
2135            // Sleep briefly to ensure time difference
2136            std::thread::sleep(std::time::Duration::from_millis(10));
2137
2138            doc.update_modification_date();
2139
2140            let new_mod_date = doc.metadata.modification_date;
2141            assert!(new_mod_date.is_some());
2142            assert!(new_mod_date.unwrap() > initial_mod_date.unwrap());
2143        }
2144
2145        #[test]
2146        fn test_document_save_updates_modification_date() {
2147            let temp_dir = TempDir::new().unwrap();
2148            let file_path = temp_dir.path().join("mod_date_test.pdf");
2149
2150            let mut doc = Document::new();
2151            doc.add_page(Page::a4());
2152
2153            let initial_mod_date = doc.metadata.modification_date;
2154
2155            // Sleep briefly to ensure time difference
2156            std::thread::sleep(std::time::Duration::from_millis(10));
2157
2158            doc.save(&file_path).unwrap();
2159
2160            // Modification date should be updated
2161            assert!(doc.metadata.modification_date.unwrap() > initial_mod_date.unwrap());
2162        }
2163
2164        #[test]
2165        fn test_document_metadata_complete() {
2166            let mut doc = Document::new();
2167
2168            // Set all metadata fields
2169            doc.set_title("Complete Metadata Test");
2170            doc.set_author("Test Author");
2171            doc.set_subject("Testing all metadata fields");
2172            doc.set_keywords("test, metadata, complete");
2173            doc.set_creator("Test Application v1.0");
2174            doc.set_producer("oxidize_pdf Test Suite");
2175
2176            // Verify all fields
2177            assert_eq!(
2178                doc.metadata.title,
2179                Some("Complete Metadata Test".to_string())
2180            );
2181            assert_eq!(doc.metadata.author, Some("Test Author".to_string()));
2182            assert_eq!(
2183                doc.metadata.subject,
2184                Some("Testing all metadata fields".to_string())
2185            );
2186            assert_eq!(
2187                doc.metadata.keywords,
2188                Some("test, metadata, complete".to_string())
2189            );
2190            assert_eq!(
2191                doc.metadata.creator,
2192                Some("Test Application v1.0".to_string())
2193            );
2194            assert_eq!(
2195                doc.metadata.producer,
2196                Some("oxidize_pdf Test Suite".to_string())
2197            );
2198            assert!(doc.metadata.creation_date.is_some());
2199            assert!(doc.metadata.modification_date.is_some());
2200        }
2201
2202        #[test]
2203        fn test_document_to_bytes() {
2204            let mut doc = Document::new();
2205            doc.set_title("Test Document");
2206            doc.set_author("Test Author");
2207
2208            let page = Page::a4();
2209            doc.add_page(page);
2210
2211            // Generate PDF as bytes
2212            let pdf_bytes = doc.to_bytes().unwrap();
2213
2214            // Basic validation
2215            assert!(!pdf_bytes.is_empty());
2216            assert!(pdf_bytes.len() > 100); // Should be reasonable size
2217
2218            // Check PDF header
2219            let header = &pdf_bytes[0..5];
2220            assert_eq!(header, b"%PDF-");
2221
2222            // Check for some basic PDF structure
2223            let pdf_str = String::from_utf8_lossy(&pdf_bytes);
2224            assert!(pdf_str.contains("Test Document"));
2225            assert!(pdf_str.contains("Test Author"));
2226        }
2227
2228        #[test]
2229        fn test_document_to_bytes_with_config() {
2230            let mut doc = Document::new();
2231            doc.set_title("Test Document XRef");
2232
2233            let page = Page::a4();
2234            doc.add_page(page);
2235
2236            let config = crate::writer::WriterConfig {
2237                use_xref_streams: true,
2238                use_object_streams: false,
2239                pdf_version: "1.5".to_string(),
2240                compress_streams: true,
2241                incremental_update: false,
2242            };
2243
2244            // Generate PDF with custom config
2245            let pdf_bytes = doc.to_bytes_with_config(config).unwrap();
2246
2247            // Basic validation
2248            assert!(!pdf_bytes.is_empty());
2249            assert!(pdf_bytes.len() > 100);
2250
2251            // Check PDF header with correct version
2252            let header = String::from_utf8_lossy(&pdf_bytes[0..8]);
2253            assert!(header.contains("PDF-1.5"));
2254        }
2255
2256        #[test]
2257        fn test_to_bytes_vs_save_equivalence() {
2258            use std::fs;
2259            use tempfile::NamedTempFile;
2260
2261            // Create two identical documents
2262            let mut doc1 = Document::new();
2263            doc1.set_title("Equivalence Test");
2264            doc1.add_page(Page::a4());
2265
2266            let mut doc2 = Document::new();
2267            doc2.set_title("Equivalence Test");
2268            doc2.add_page(Page::a4());
2269
2270            // Generate bytes
2271            let pdf_bytes = doc1.to_bytes().unwrap();
2272
2273            // Save to file
2274            let temp_file = NamedTempFile::new().unwrap();
2275            doc2.save(temp_file.path()).unwrap();
2276            let file_bytes = fs::read(temp_file.path()).unwrap();
2277
2278            // Both should generate similar structure (lengths may vary due to timestamps)
2279            assert!(!pdf_bytes.is_empty());
2280            assert!(!file_bytes.is_empty());
2281            assert_eq!(&pdf_bytes[0..5], &file_bytes[0..5]); // PDF headers should match
2282        }
2283
2284        #[test]
2285        fn test_document_set_compress() {
2286            let mut doc = Document::new();
2287            doc.set_title("Compression Test");
2288            doc.add_page(Page::a4());
2289
2290            // Default should be compressed
2291            assert!(doc.get_compress());
2292
2293            // Test with compression enabled
2294            doc.set_compress(true);
2295            let compressed_bytes = doc.to_bytes().unwrap();
2296
2297            // Test with compression disabled
2298            doc.set_compress(false);
2299            let uncompressed_bytes = doc.to_bytes().unwrap();
2300
2301            // Uncompressed should generally be larger (though not always guaranteed)
2302            assert!(!compressed_bytes.is_empty());
2303            assert!(!uncompressed_bytes.is_empty());
2304
2305            // Both should be valid PDFs
2306            assert_eq!(&compressed_bytes[0..5], b"%PDF-");
2307            assert_eq!(&uncompressed_bytes[0..5], b"%PDF-");
2308        }
2309
2310        #[test]
2311        fn test_document_compression_config_inheritance() {
2312            let mut doc = Document::new();
2313            doc.set_title("Config Inheritance Test");
2314            doc.add_page(Page::a4());
2315
2316            // Set document compression to false
2317            doc.set_compress(false);
2318
2319            // Create config with compression true (should be overridden)
2320            let config = crate::writer::WriterConfig {
2321                use_xref_streams: false,
2322                use_object_streams: false,
2323                pdf_version: "1.7".to_string(),
2324                compress_streams: true,
2325                incremental_update: false,
2326            };
2327
2328            // Document setting should take precedence
2329            let pdf_bytes = doc.to_bytes_with_config(config).unwrap();
2330
2331            // Should be valid PDF
2332            assert!(!pdf_bytes.is_empty());
2333            assert_eq!(&pdf_bytes[0..5], b"%PDF-");
2334        }
2335
2336        #[test]
2337        fn test_document_metadata_all_fields() {
2338            let mut doc = Document::new();
2339
2340            // Set all metadata fields
2341            doc.set_title("Test Document");
2342            doc.set_author("John Doe");
2343            doc.set_subject("Testing PDF metadata");
2344            doc.set_keywords("test, pdf, metadata");
2345            doc.set_creator("Test Suite");
2346            doc.set_producer("oxidize_pdf tests");
2347
2348            // Verify all fields are set
2349            assert_eq!(doc.metadata.title.as_deref(), Some("Test Document"));
2350            assert_eq!(doc.metadata.author.as_deref(), Some("John Doe"));
2351            assert_eq!(
2352                doc.metadata.subject.as_deref(),
2353                Some("Testing PDF metadata")
2354            );
2355            assert_eq!(
2356                doc.metadata.keywords.as_deref(),
2357                Some("test, pdf, metadata")
2358            );
2359            assert_eq!(doc.metadata.creator.as_deref(), Some("Test Suite"));
2360            assert_eq!(doc.metadata.producer.as_deref(), Some("oxidize_pdf tests"));
2361            assert!(doc.metadata.creation_date.is_some());
2362            assert!(doc.metadata.modification_date.is_some());
2363        }
2364
2365        #[test]
2366        fn test_document_add_pages() {
2367            let mut doc = Document::new();
2368
2369            // Initially empty
2370            assert_eq!(doc.page_count(), 0);
2371
2372            // Add pages
2373            let page1 = Page::a4();
2374            let page2 = Page::letter();
2375            let page3 = Page::legal();
2376
2377            doc.add_page(page1);
2378            assert_eq!(doc.page_count(), 1);
2379
2380            doc.add_page(page2);
2381            assert_eq!(doc.page_count(), 2);
2382
2383            doc.add_page(page3);
2384            assert_eq!(doc.page_count(), 3);
2385
2386            // Verify we can convert to PDF with multiple pages
2387            let result = doc.to_bytes();
2388            assert!(result.is_ok());
2389        }
2390
2391        #[test]
2392        fn test_document_default_font_encoding() {
2393            let mut doc = Document::new();
2394
2395            // Initially no default encoding
2396            assert!(doc.default_font_encoding.is_none());
2397
2398            // Set default encoding
2399            doc.set_default_font_encoding(Some(FontEncoding::WinAnsiEncoding));
2400            assert_eq!(
2401                doc.default_font_encoding(),
2402                Some(FontEncoding::WinAnsiEncoding)
2403            );
2404
2405            // Change encoding
2406            doc.set_default_font_encoding(Some(FontEncoding::MacRomanEncoding));
2407            assert_eq!(
2408                doc.default_font_encoding(),
2409                Some(FontEncoding::MacRomanEncoding)
2410            );
2411        }
2412
2413        #[test]
2414        fn test_document_compression_setting() {
2415            let mut doc = Document::new();
2416
2417            // Default should compress
2418            assert!(doc.compress);
2419
2420            // Disable compression
2421            doc.set_compress(false);
2422            assert!(!doc.compress);
2423
2424            // Re-enable compression
2425            doc.set_compress(true);
2426            assert!(doc.compress);
2427        }
2428
2429        #[test]
2430        fn test_document_with_empty_pages() {
2431            let mut doc = Document::new();
2432
2433            // Add empty page
2434            doc.add_page(Page::a4());
2435
2436            // Should be able to convert to bytes
2437            let result = doc.to_bytes();
2438            assert!(result.is_ok());
2439
2440            let pdf_bytes = result.unwrap();
2441            assert!(!pdf_bytes.is_empty());
2442            assert!(pdf_bytes.starts_with(b"%PDF-"));
2443        }
2444
2445        #[test]
2446        fn test_document_with_multiple_page_sizes() {
2447            let mut doc = Document::new();
2448
2449            // Add pages with different sizes
2450            doc.add_page(Page::a4()); // 595 x 842
2451            doc.add_page(Page::letter()); // 612 x 792
2452            doc.add_page(Page::legal()); // 612 x 1008
2453            doc.add_page(Page::a4()); // Another A4
2454            doc.add_page(Page::new(200.0, 300.0)); // Custom size
2455
2456            assert_eq!(doc.page_count(), 5);
2457
2458            // Verify we have 5 pages
2459            // Note: Direct page access is not available in public API
2460            // We verify by successful PDF generation
2461            let result = doc.to_bytes();
2462            assert!(result.is_ok());
2463        }
2464
2465        #[test]
2466        fn test_document_metadata_dates() {
2467            use chrono::Duration;
2468
2469            let doc = Document::new();
2470
2471            // Should have creation and modification dates
2472            assert!(doc.metadata.creation_date.is_some());
2473            assert!(doc.metadata.modification_date.is_some());
2474
2475            if let (Some(created), Some(modified)) =
2476                (doc.metadata.creation_date, doc.metadata.modification_date)
2477            {
2478                // Dates should be very close (created during construction)
2479                let diff = modified - created;
2480                assert!(diff < Duration::seconds(1));
2481            }
2482        }
2483
2484        #[test]
2485        fn test_document_builder_pattern() {
2486            // Test fluent API style
2487            let mut doc = Document::new();
2488            doc.set_title("Fluent");
2489            doc.set_author("Builder");
2490            doc.set_compress(true);
2491
2492            assert_eq!(doc.metadata.title.as_deref(), Some("Fluent"));
2493            assert_eq!(doc.metadata.author.as_deref(), Some("Builder"));
2494            assert!(doc.compress);
2495        }
2496
2497        #[test]
2498        fn test_xref_streams_functionality() {
2499            use crate::{Document, Font, Page};
2500
2501            // Test with xref streams disabled (default)
2502            let mut doc = Document::new();
2503            assert!(!doc.use_xref_streams);
2504
2505            let mut page = Page::a4();
2506            page.text()
2507                .set_font(Font::Helvetica, 12.0)
2508                .at(100.0, 700.0)
2509                .write("Testing XRef Streams")
2510                .unwrap();
2511
2512            doc.add_page(page);
2513
2514            // Generate PDF without xref streams
2515            let pdf_without_xref = doc.to_bytes().unwrap();
2516
2517            // Verify traditional xref is used
2518            let pdf_str = String::from_utf8_lossy(&pdf_without_xref);
2519            assert!(pdf_str.contains("xref"), "Traditional xref table not found");
2520            assert!(
2521                !pdf_str.contains("/Type /XRef"),
2522                "XRef stream found when it shouldn't be"
2523            );
2524
2525            // Test with xref streams enabled
2526            doc.enable_xref_streams(true);
2527            assert!(doc.use_xref_streams);
2528
2529            // Generate PDF with xref streams
2530            let pdf_with_xref = doc.to_bytes().unwrap();
2531
2532            // Verify xref streams are used
2533            let pdf_str = String::from_utf8_lossy(&pdf_with_xref);
2534            // XRef streams replace traditional xref tables in PDF 1.5+
2535            assert!(
2536                pdf_str.contains("/Type /XRef") || pdf_str.contains("stream"),
2537                "XRef stream not found when enabled"
2538            );
2539
2540            // Verify PDF version is set correctly
2541            assert!(
2542                pdf_str.contains("PDF-1.5"),
2543                "PDF version not set to 1.5 for xref streams"
2544            );
2545
2546            // Test fluent interface
2547            let mut doc2 = Document::new();
2548            doc2.enable_xref_streams(true);
2549            doc2.set_title("XRef Streams Test");
2550            doc2.set_author("oxidize-pdf");
2551
2552            assert!(doc2.use_xref_streams);
2553            assert_eq!(doc2.metadata.title.as_deref(), Some("XRef Streams Test"));
2554            assert_eq!(doc2.metadata.author.as_deref(), Some("oxidize-pdf"));
2555        }
2556
2557        #[test]
2558        fn test_document_save_to_vec() {
2559            let mut doc = Document::new();
2560            doc.set_title("Test Save");
2561            doc.add_page(Page::a4());
2562
2563            // Test to_bytes
2564            let bytes_result = doc.to_bytes();
2565            assert!(bytes_result.is_ok());
2566
2567            let bytes = bytes_result.unwrap();
2568            assert!(!bytes.is_empty());
2569            assert!(bytes.starts_with(b"%PDF-"));
2570            assert!(bytes.ends_with(b"%%EOF") || bytes.ends_with(b"%%EOF\n"));
2571        }
2572
2573        #[test]
2574        fn test_document_unicode_metadata() {
2575            let mut doc = Document::new();
2576
2577            // Set metadata with Unicode characters
2578            doc.set_title("日本語のタイトル");
2579            doc.set_author("作者名 😀");
2580            doc.set_subject("Тема документа");
2581            doc.set_keywords("كلمات, מפתח, 关键词");
2582
2583            assert_eq!(doc.metadata.title.as_deref(), Some("日本語のタイトル"));
2584            assert_eq!(doc.metadata.author.as_deref(), Some("作者名 😀"));
2585            assert_eq!(doc.metadata.subject.as_deref(), Some("Тема документа"));
2586            assert_eq!(
2587                doc.metadata.keywords.as_deref(),
2588                Some("كلمات, מפתח, 关键词")
2589            );
2590        }
2591
2592        #[test]
2593        fn test_document_page_iteration() {
2594            let mut doc = Document::new();
2595
2596            // Add multiple pages
2597            for i in 0..5 {
2598                let mut page = Page::a4();
2599                let gc = page.graphics();
2600                gc.begin_text();
2601                let _ = gc.show_text(&format!("Page {}", i + 1));
2602                gc.end_text();
2603                doc.add_page(page);
2604            }
2605
2606            // Verify page count
2607            assert_eq!(doc.page_count(), 5);
2608
2609            // Verify we can generate PDF with all pages
2610            let result = doc.to_bytes();
2611            assert!(result.is_ok());
2612        }
2613
2614        #[test]
2615        fn test_document_with_graphics_content() {
2616            let mut doc = Document::new();
2617
2618            let mut page = Page::a4();
2619            {
2620                let gc = page.graphics();
2621
2622                // Add various graphics operations
2623                gc.save_state();
2624
2625                // Draw rectangle
2626                gc.rectangle(100.0, 100.0, 200.0, 150.0);
2627                gc.stroke();
2628
2629                // Draw circle (approximated)
2630                gc.move_to(300.0, 300.0);
2631                gc.circle(300.0, 300.0, 50.0);
2632                gc.fill();
2633
2634                // Add text
2635                gc.begin_text();
2636                gc.set_text_position(100.0, 500.0);
2637                let _ = gc.show_text("Graphics Test");
2638                gc.end_text();
2639
2640                gc.restore_state();
2641            }
2642
2643            doc.add_page(page);
2644
2645            // Should produce valid PDF
2646            let result = doc.to_bytes();
2647            assert!(result.is_ok());
2648        }
2649
2650        #[test]
2651        fn test_document_producer_version() {
2652            let doc = Document::new();
2653
2654            // Producer should contain version
2655            assert!(doc.metadata.producer.is_some());
2656            if let Some(producer) = &doc.metadata.producer {
2657                assert!(producer.contains("oxidize_pdf"));
2658                assert!(producer.contains(env!("CARGO_PKG_VERSION")));
2659            }
2660        }
2661
2662        #[test]
2663        fn test_document_empty_metadata_fields() {
2664            let mut doc = Document::new();
2665
2666            // Set empty strings
2667            doc.set_title("");
2668            doc.set_author("");
2669            doc.set_subject("");
2670            doc.set_keywords("");
2671
2672            // Empty strings should be stored as Some("")
2673            assert_eq!(doc.metadata.title.as_deref(), Some(""));
2674            assert_eq!(doc.metadata.author.as_deref(), Some(""));
2675            assert_eq!(doc.metadata.subject.as_deref(), Some(""));
2676            assert_eq!(doc.metadata.keywords.as_deref(), Some(""));
2677        }
2678
2679        #[test]
2680        fn test_document_very_long_metadata() {
2681            let mut doc = Document::new();
2682
2683            // Create very long strings
2684            let long_title = "A".repeat(1000);
2685            let long_author = "B".repeat(500);
2686            let long_keywords = vec!["keyword"; 100].join(", ");
2687
2688            doc.set_title(&long_title);
2689            doc.set_author(&long_author);
2690            doc.set_keywords(&long_keywords);
2691
2692            assert_eq!(doc.metadata.title.as_deref(), Some(long_title.as_str()));
2693            assert_eq!(doc.metadata.author.as_deref(), Some(long_author.as_str()));
2694            assert!(doc.metadata.keywords.as_ref().unwrap().len() > 500);
2695        }
2696    }
2697
2698    #[test]
2699    fn test_add_font_from_bytes_writes_to_per_document_store_not_global() {
2700        // Use a unique font name so this test does not collide with parallel tests.
2701        let unique = format!("PerDocTask9_{}", std::process::id());
2702        // Capture global size before.
2703        // get_custom_font_metrics is deprecated by Task 12 of #230 (v2.8.0).
2704        // #[allow(deprecated)] is applied now to avoid churn when the attribute lands.
2705        #[allow(deprecated)]
2706        let before = crate::text::metrics::get_custom_font_metrics(&unique);
2707        assert!(before.is_none(), "precondition: name not in global");
2708
2709        // Construct a Document and register a synthetic font under this name.
2710        // We bypass the TTF parser by going through the metrics store directly
2711        // — the public API requires real TTF bytes, which is exercised in the
2712        // integration suite (Task 14). This unit test focuses on the routing.
2713        let doc = Document::new();
2714        doc.font_metrics
2715            .register(unique.clone(), crate::text::metrics::FontMetrics::new(500));
2716
2717        // The Document store contains the entry.
2718        assert!(doc.font_metrics.get(&unique).is_some());
2719
2720        // The legacy global was untouched.
2721        #[allow(deprecated)]
2722        let after = crate::text::metrics::get_custom_font_metrics(&unique);
2723        assert!(after.is_none(), "global must remain untouched");
2724    }
2725
2726    #[test]
2727    fn test_new_page_a4_returns_page_bound_to_document_store() {
2728        let doc = Document::new();
2729        doc.font_metrics
2730            .register("Sentinel", crate::text::metrics::FontMetrics::new(400));
2731
2732        let page = doc.new_page_a4();
2733        assert!(page.font_metrics_store.is_some());
2734        let store = page.font_metrics_store.as_ref().unwrap();
2735        assert!(
2736            store.get("Sentinel").is_some(),
2737            "store must share with Document"
2738        );
2739    }
2740
2741    #[test]
2742    fn test_new_page_letter_and_new_page_carry_store() {
2743        let doc = Document::new();
2744        doc.font_metrics
2745            .register("S", crate::text::metrics::FontMetrics::new(400));
2746        assert!(doc.new_page_letter().font_metrics_store.is_some());
2747        assert!(doc.new_page(400.0, 600.0).font_metrics_store.is_some());
2748    }
2749
2750    #[test]
2751    fn test_add_page_injects_store_into_legacy_page() {
2752        let mut doc = Document::new();
2753        doc.font_metrics
2754            .register("Inj", crate::text::metrics::FontMetrics::new(400));
2755
2756        let page = Page::a4(); // legacy ctor → store = None
2757        assert!(page.font_metrics_store.is_none());
2758
2759        doc.add_page(page);
2760
2761        let stored_page = doc.pages.last().expect("page added");
2762        assert!(
2763            stored_page.font_metrics_store.is_some(),
2764            "add_page must inject the Document store when page has none"
2765        );
2766        assert!(
2767            stored_page
2768                .font_metrics_store
2769                .as_ref()
2770                .unwrap()
2771                .get("Inj")
2772                .is_some(),
2773            "injected store must share state with the Document"
2774        );
2775    }
2776
2777    #[test]
2778    fn test_add_page_does_not_overwrite_existing_store() {
2779        let doc_a = Document::new();
2780        doc_a
2781            .font_metrics
2782            .register("FromA", crate::text::metrics::FontMetrics::new(400));
2783        let page = doc_a.new_page_a4(); // bound to doc_a's store
2784
2785        let mut doc_b = Document::new();
2786        doc_b
2787            .font_metrics
2788            .register("FromB", crate::text::metrics::FontMetrics::new(500));
2789        doc_b.add_page(page);
2790
2791        let stored_page = doc_b.pages.last().expect("page added");
2792        let store = stored_page.font_metrics_store.as_ref().unwrap();
2793        assert!(store.get("FromA").is_some(), "page kept doc_a's store");
2794        assert!(store.get("FromB").is_none(), "doc_b did not overwrite");
2795    }
2796}