Skip to main content

oxidize_pdf/
page.rs

1use crate::annotations::Annotation;
2use crate::error::Result;
3use crate::fonts::type0_parsing::{detect_type0_font, resolve_type0_hierarchy};
4use crate::forms::Widget;
5use crate::graphics::{GraphicsContext, Image};
6use crate::objects::{Array, Dictionary, Object, ObjectReference};
7use crate::text::metrics::FontMetricsStore;
8use crate::text::{HeaderFooter, Table, TextContext, TextFlowContext};
9use std::collections::{HashMap, HashSet};
10
11/// Page margins in points (1/72 inch).
12#[derive(Clone, Debug)]
13pub struct Margins {
14    /// Left margin
15    pub left: f64,
16    /// Right margin
17    pub right: f64,
18    /// Top margin
19    pub top: f64,
20    /// Bottom margin
21    pub bottom: f64,
22}
23
24impl Default for Margins {
25    fn default() -> Self {
26        Self {
27            left: 72.0,   // 1 inch
28            right: 72.0,  // 1 inch
29            top: 72.0,    // 1 inch
30            bottom: 72.0, // 1 inch
31        }
32    }
33}
34
35/// A single page in a PDF document.
36///
37/// Pages have a size (width and height in points), margins, and can contain
38/// graphics, text, and images.
39///
40/// # Example
41///
42/// ```rust
43/// use oxidize_pdf::{Page, Font, Color};
44///
45/// let mut page = Page::a4();
46///
47/// // Add text
48/// page.text()
49///     .set_font(Font::Helvetica, 12.0)
50///     .at(100.0, 700.0)
51///     .write("Hello World")?;
52///
53/// // Add graphics
54/// page.graphics()
55///     .set_fill_color(Color::red())
56///     .rect(100.0, 100.0, 200.0, 150.0)
57///     .fill();
58/// # Ok::<(), oxidize_pdf::PdfError>(())
59/// ```
60/// Validates that `name` is a well-formed PDF resource name per
61/// ISO 32000-1 §7.3.5 (Name Objects).
62///
63/// A resource name is a `Name` token written as `/<name>` inside a
64/// dictionary. The spec forbids:
65///   * empty names (must have at least one regular character);
66///   * whitespace characters (NUL, HT, LF, FF, CR, SP);
67///   * delimiter characters `( ) < > [ ] { } / %`;
68///   * the `#` character, which is reserved for the 2-digit hex escape
69///     form (`#NN`) — accepting raw `#` would require callers to think
70///     about hex escaping, which we decline to do at this layer.
71///
72/// We reject at the public API boundary so a caller cannot — even
73/// unintentionally — smuggle a dict-closing token into a resource name
74/// and produce a PDF where the emitted `/<name>` splits the resource
75/// dict into pieces.
76fn validate_pdf_resource_name(name: &str) -> Result<()> {
77    use crate::error::PdfError;
78
79    if name.is_empty() {
80        return Err(PdfError::InvalidStructure(
81            "PDF resource name must not be empty (ISO 32000-1 §7.3.5)".to_string(),
82        ));
83    }
84
85    for (idx, byte) in name.as_bytes().iter().enumerate() {
86        // Whitespace per Table 1 (§7.2.3).
87        let is_whitespace = matches!(*byte, 0x00 | 0x09 | 0x0A | 0x0C | 0x0D | 0x20);
88        // Delimiters per Table 2 (§7.2.3).
89        let is_delimiter = matches!(
90            *byte,
91            b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
92        );
93        // Hex-escape introducer (§7.3.5): legal only as `#NN`, but we
94        // reject outright rather than trust callers to escape correctly.
95        let is_hash = *byte == b'#';
96
97        if is_whitespace || is_delimiter || is_hash {
98            return Err(PdfError::InvalidStructure(format!(
99                "invalid PDF resource name {name:?}: byte 0x{byte:02X} at position {idx} \
100                 is not allowed per ISO 32000-1 §7.3.5 (whitespace, delimiter, or `#`)"
101            )));
102        }
103    }
104    Ok(())
105}
106
107#[derive(Clone)]
108pub struct Page {
109    width: f64,
110    height: f64,
111    margins: Margins,
112    content: Vec<u8>,
113    graphics_context: GraphicsContext,
114    text_context: TextContext,
115    images: HashMap<String, Image>,
116    form_xobjects: HashMap<String, crate::graphics::FormXObject>,
117    /// Registered colour spaces, emitted under `/Resources/ColorSpace`
118    /// per ISO 32000-1 §8.6, Table 62. Values are typed via
119    /// [`crate::graphics::PageColorSpace`] — see that enum for the two
120    /// supported wire-format shapes (device alias vs. parameterised
121    /// `[/<family> <<params>>]`).
122    color_spaces: HashMap<String, crate::graphics::PageColorSpace>,
123    /// Registered tiling patterns, emitted as indirect stream objects
124    /// referenced from `/Resources/Pattern` per ISO 32000-1 §8.7.3.
125    patterns: HashMap<String, crate::graphics::TilingPattern>,
126    /// Registered shadings, emitted as indirect dictionary objects
127    /// referenced from `/Resources/Shading` per ISO 32000-1 §8.7.4.
128    shadings: HashMap<String, crate::graphics::ShadingDefinition>,
129    /// Registered mesh (Type 4) and conic (Type 1) shadings, emitted into the
130    /// same `/Resources/Shading` dict as `shadings` but kept in a separate map
131    /// so the public `ShadingDefinition` enum stays unchanged (#407).
132    advanced_shadings: HashMap<String, crate::graphics::AdvancedShading>,
133    header: Option<HeaderFooter>,
134    footer: Option<HeaderFooter>,
135    annotations: Vec<Annotation>,
136    coordinate_system: crate::coordinate_system::CoordinateSystem,
137    rotation: i32, // Page rotation in degrees (0, 90, 180, 270)
138    /// Next MCID (Marked Content ID) for tagged PDF
139    next_mcid: u32,
140    /// Currently open marked content tags (for nesting validation)
141    marked_content_stack: Vec<String>,
142    /// Preserved resources from original PDF (for overlay operations)
143    /// Contains fonts, XObjects, ColorSpaces, etc. from parsed pages
144    preserved_resources: Option<crate::pdf_objects::Dictionary>,
145    /// Aggregated content-stream operators in caller-defined order
146    /// (issue #227). Each call to [`Page::graphics`] or [`Page::text`]
147    /// flushes the buffer of the *opposite* context here before
148    /// returning the borrow, so PDF painter-model call order is
149    /// preserved across context switches. The remaining tail in
150    /// either context's own buffer is appended at flush time
151    /// (`generate_content_with_page_info`).
152    page_ops: Vec<crate::graphics::ops::Op>,
153    /// Optional per-document font metrics store (issue #230 / v2.8.0).
154    /// `None` on pages created via `Page::a4()` / `letter()` / `new()`.
155    /// Populated by `Page::a4_with_metrics` and friends, or injected by
156    /// `Document::add_page()` in Task 11.
157    pub(crate) font_metrics_store: Option<FontMetricsStore>,
158    /// Collision-only rename map for preserved fonts (issue #395), stamped by
159    /// the writer just before content generation. Maps an original preserved
160    /// `/Font` key to its disambiguated key when (and only when) that key
161    /// collided with a writer-injected/overlay font. Empty for pages with no
162    /// preserved fonts or no collisions — in which case the preserved content
163    /// is emitted verbatim.
164    pub(crate) preserved_font_rewrite_map: HashMap<String, String>,
165}
166
167impl Page {
168    /// Creates a new page with the specified width and height in points.
169    ///
170    /// Points are 1/72 of an inch.
171    pub fn new(width: f64, height: f64) -> Self {
172        Self {
173            width,
174            height,
175            margins: Margins::default(),
176            content: Vec::new(),
177            graphics_context: GraphicsContext::new(),
178            text_context: TextContext::new(),
179            images: HashMap::new(),
180            form_xobjects: HashMap::new(),
181            color_spaces: HashMap::new(),
182            patterns: HashMap::new(),
183            shadings: HashMap::new(),
184            advanced_shadings: HashMap::new(),
185            header: None,
186            footer: None,
187            annotations: Vec::new(),
188            coordinate_system: crate::coordinate_system::CoordinateSystem::PdfStandard,
189            rotation: 0, // Default to no rotation
190            next_mcid: 0,
191            marked_content_stack: Vec::new(),
192            preserved_resources: None,
193            page_ops: Vec::new(),
194            font_metrics_store: None,
195            preserved_font_rewrite_map: HashMap::new(),
196        }
197    }
198
199    /// Creates a writable Page from a parsed page dictionary.
200    ///
201    /// This method bridges the gap between the parser (read-only) and writer (writable)
202    /// by converting a parsed page into a Page structure that can be modified and saved.
203    ///
204    /// **IMPORTANT**: This method preserves the existing content stream and resources,
205    /// allowing you to overlay new content on top of the existing page content without
206    /// manual recreation.
207    ///
208    /// # Arguments
209    ///
210    /// * `parsed_page` - Reference to a parsed page from the PDF parser
211    ///
212    /// # Returns
213    ///
214    /// A writable `Page` with existing content preserved, ready for modification
215    ///
216    /// # Example
217    ///
218    /// ```rust,no_run
219    /// use oxidize_pdf::parser::{PdfReader, PdfDocument};
220    /// use oxidize_pdf::Page;
221    ///
222    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
223    /// // Load existing PDF
224    /// let reader = PdfReader::open("input.pdf")?;
225    /// let document = PdfDocument::new(reader);
226    /// let parsed_page = document.get_page(0)?;
227    ///
228    /// // Convert to writable page
229    /// let mut page = Page::from_parsed(&parsed_page)?;
230    ///
231    /// // Now you can add content on top of existing content
232    /// page.text()
233    ///     .set_font(oxidize_pdf::text::Font::Helvetica, 12.0)
234    ///     .at(100.0, 100.0)
235    ///     .write("Overlaid text")?;
236    /// # Ok(())
237    /// # }
238    /// ```
239    pub fn from_parsed(parsed_page: &crate::parser::page_tree::ParsedPage) -> Result<Self> {
240        // Extract dimensions from MediaBox
241        let media_box = parsed_page.media_box;
242        let width = media_box[2] - media_box[0];
243        let height = media_box[3] - media_box[1];
244
245        // Extract rotation
246        let rotation = parsed_page.rotation;
247
248        // Create base page
249        let mut page = Self::new(width, height);
250        page.rotation = rotation;
251
252        // TODO: Extract and preserve Resources (fonts, images, XObjects)
253        // This requires deeper integration with the parser's resource manager
254
255        // Extract and preserve existing content streams
256        // Note: This requires a PdfDocument reference to resolve content streams
257        // For now, we mark the content field to indicate it should be preserved
258        // The actual content stream extraction will be done when we have access to the reader
259
260        Ok(page)
261    }
262
263    /// Creates a writable Page from a parsed page with content stream preservation.
264    ///
265    /// This is an extended version of `from_parsed()` that requires access to the
266    /// PdfDocument to extract and preserve the original content streams.
267    ///
268    /// # Arguments
269    ///
270    /// * `parsed_page` - Reference to a parsed page
271    /// * `document` - Reference to the PDF document (for content stream resolution)
272    ///
273    /// # Returns
274    ///
275    /// A writable `Page` with original content streams preserved
276    ///
277    /// # Example
278    ///
279    /// ```rust,no_run
280    /// use oxidize_pdf::parser::{PdfReader, PdfDocument};
281    /// use oxidize_pdf::Page;
282    ///
283    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
284    /// let reader = PdfReader::open("input.pdf")?;
285    /// let document = PdfDocument::new(reader);
286    /// let parsed_page = document.get_page(0)?;
287    ///
288    /// // Convert with content preservation
289    /// let mut page = Page::from_parsed_with_content(&parsed_page, &document)?;
290    ///
291    /// // Original content is preserved, overlay will be added on top
292    /// page.text()
293    ///     .set_font(oxidize_pdf::text::Font::Helvetica, 12.0)
294    ///     .at(100.0, 100.0)
295    ///     .write("Overlay text")?;
296    /// # Ok(())
297    /// # }
298    /// ```
299    pub fn from_parsed_with_content<R: std::io::Read + std::io::Seek>(
300        parsed_page: &crate::parser::page_tree::ParsedPage,
301        document: &crate::parser::document::PdfDocument<R>,
302    ) -> Result<Self> {
303        // Extract dimensions from MediaBox
304        let media_box = parsed_page.media_box;
305        let width = media_box[2] - media_box[0];
306        let height = media_box[3] - media_box[1];
307
308        // Extract rotation
309        let rotation = parsed_page.rotation;
310
311        // Create base page
312        let mut page = Self::new(width, height);
313        page.rotation = rotation;
314
315        // Extract and preserve existing content streams
316        let content_streams = parsed_page.content_streams_with_document(document)?;
317
318        // Concatenate all content streams
319        let mut preserved_content = Vec::new();
320        for stream in content_streams {
321            preserved_content.extend_from_slice(&stream);
322            // Add a newline between streams for safety
323            preserved_content.push(b'\n');
324        }
325
326        // Store the original content
327        // We'll need to wrap it with q/Q to isolate it when overlaying
328        page.content = preserved_content;
329
330        // Extract and preserve Resources (fonts, images, XObjects, etc.)
331        if let Some(resources) = parsed_page.get_resources() {
332            let mut unified_resources = Self::convert_parser_dict_to_unified(resources);
333
334            // Phase 3.2: Resolve embedded font streams
335            // For each font in resources, resolve FontDescriptor and font stream references
336            // and embed the stream data directly so writer doesn't need to resolve references
337            // The `/Font` entry may itself be an indirect reference
338            // (`/Font 1 0 R`) instead of an inline dictionary. Resolve it here
339            // so the loop below sees the underlying font dictionary. The owned
340            // resolved object is held in `resolved_font` to outlive the borrow:
341            // returning `&convert(..)` directly fails on the MSRV (1.88) with
342            // E0716, as the temporary is dropped at the end of the statement.
343            let resolved_font: crate::pdf_objects::Object;
344            let font_resource = match unified_resources.get("Font") {
345                Some(crate::pdf_objects::Object::Reference(id)) => {
346                    match document.get_object(id.number(), id.generation()) {
347                        Ok(resolved_obj) => {
348                            resolved_font = Self::convert_parser_object_to_unified(&resolved_obj);
349                            Some(&resolved_font)
350                        }
351                        _ => None,
352                    }
353                }
354                other => other,
355            };
356
357            if let Some(crate::pdf_objects::Object::Dictionary(fonts)) = font_resource {
358                let fonts_clone = fonts.clone();
359                let mut resolved_fonts = crate::pdf_objects::Dictionary::new();
360
361                for (font_name, font_obj) in fonts_clone.iter() {
362                    // Step 1: Resolve reference if needed to get actual font dictionary
363                    let font_dict = match font_obj {
364                        crate::pdf_objects::Object::Reference(id) => {
365                            // Resolve reference to get actual font dictionary from document
366                            match document.get_object(id.number(), id.generation()) {
367                                Ok(resolved_obj) => {
368                                    // Convert parser object to unified format
369                                    match Self::convert_parser_object_to_unified(&resolved_obj) {
370                                        crate::pdf_objects::Object::Dictionary(dict) => dict,
371                                        _ => {
372                                            // Not a dictionary, keep original reference
373                                            resolved_fonts.set(font_name.clone(), font_obj.clone());
374                                            continue;
375                                        }
376                                    }
377                                }
378                                Err(_) => {
379                                    // Resolution failed, keep original reference
380                                    resolved_fonts.set(font_name.clone(), font_obj.clone());
381                                    continue;
382                                }
383                            }
384                        }
385                        crate::pdf_objects::Object::Dictionary(dict) => dict.clone(),
386                        _ => {
387                            // Neither reference nor dictionary, keep as-is
388                            resolved_fonts.set(font_name.clone(), font_obj.clone());
389                            continue;
390                        }
391                    };
392
393                    // Step 2: Now font_dict is guaranteed to be a Dictionary, resolve embedded streams
394                    match Self::resolve_font_streams(&font_dict, document) {
395                        Ok(resolved_dict) => {
396                            resolved_fonts.set(
397                                font_name.clone(),
398                                crate::pdf_objects::Object::Dictionary(resolved_dict),
399                            );
400                        }
401                        Err(_) => {
402                            // If stream resolution fails, keep the resolved dictionary without streams
403                            resolved_fonts.set(
404                                font_name.clone(),
405                                crate::pdf_objects::Object::Dictionary(font_dict),
406                            );
407                        }
408                    }
409                }
410
411                // Replace Font dictionary with resolved version
412                unified_resources.set(
413                    "Font",
414                    crate::pdf_objects::Object::Dictionary(resolved_fonts),
415                );
416            }
417
418            // Phase 3.5: Resolve XObject streams (images, forms)
419            // XObjects are critical for PDF content - unresolved references cause blank PDFs
420            if let Some(crate::pdf_objects::Object::Dictionary(xobjects)) =
421                unified_resources.get("XObject")
422            {
423                let xobjects_clone = xobjects.clone();
424                let mut resolved_xobjects = crate::pdf_objects::Dictionary::new();
425
426                for (xobj_name, xobj_obj) in xobjects_clone.iter() {
427                    let resolved = match xobj_obj {
428                        crate::pdf_objects::Object::Reference(id) => {
429                            // Resolve reference to get actual XObject stream
430                            match document.get_object(id.number(), id.generation()) {
431                                Ok(resolved_obj) => {
432                                    Self::convert_parser_object_to_unified(&resolved_obj)
433                                }
434                                Err(_) => {
435                                    // Resolution failed, keep reference
436                                    xobj_obj.clone()
437                                }
438                            }
439                        }
440                        _ => xobj_obj.clone(),
441                    };
442                    resolved_xobjects.set(xobj_name.clone(), resolved);
443                }
444
445                unified_resources.set(
446                    "XObject",
447                    crate::pdf_objects::Object::Dictionary(resolved_xobjects),
448                );
449            }
450
451            // Phase 3.5: Resolve ExtGState (graphics state parameters)
452            if let Some(crate::pdf_objects::Object::Dictionary(extgstates)) =
453                unified_resources.get("ExtGState")
454            {
455                let extgstates_clone = extgstates.clone();
456                let mut resolved_extgstates = crate::pdf_objects::Dictionary::new();
457
458                for (gs_name, gs_obj) in extgstates_clone.iter() {
459                    let resolved = match gs_obj {
460                        crate::pdf_objects::Object::Reference(id) => {
461                            match document.get_object(id.number(), id.generation()) {
462                                Ok(resolved_obj) => {
463                                    Self::convert_parser_object_to_unified(&resolved_obj)
464                                }
465                                Err(_) => gs_obj.clone(),
466                            }
467                        }
468                        _ => gs_obj.clone(),
469                    };
470                    resolved_extgstates.set(gs_name.clone(), resolved);
471                }
472
473                unified_resources.set(
474                    "ExtGState",
475                    crate::pdf_objects::Object::Dictionary(resolved_extgstates),
476                );
477            }
478
479            // Phase 3.5: Resolve ColorSpace references
480            if let Some(crate::pdf_objects::Object::Dictionary(colorspaces)) =
481                unified_resources.get("ColorSpace")
482            {
483                let colorspaces_clone = colorspaces.clone();
484                let mut resolved_colorspaces = crate::pdf_objects::Dictionary::new();
485
486                for (cs_name, cs_obj) in colorspaces_clone.iter() {
487                    let resolved = match cs_obj {
488                        crate::pdf_objects::Object::Reference(id) => {
489                            match document.get_object(id.number(), id.generation()) {
490                                Ok(resolved_obj) => {
491                                    Self::convert_parser_object_to_unified(&resolved_obj)
492                                }
493                                Err(_) => cs_obj.clone(),
494                            }
495                        }
496                        _ => cs_obj.clone(),
497                    };
498                    resolved_colorspaces.set(cs_name.clone(), resolved);
499                }
500
501                unified_resources.set(
502                    "ColorSpace",
503                    crate::pdf_objects::Object::Dictionary(resolved_colorspaces),
504                );
505            }
506
507            // Phase 3.5: Resolve Pattern references
508            if let Some(crate::pdf_objects::Object::Dictionary(patterns)) =
509                unified_resources.get("Pattern")
510            {
511                let patterns_clone = patterns.clone();
512                let mut resolved_patterns = crate::pdf_objects::Dictionary::new();
513
514                for (pat_name, pat_obj) in patterns_clone.iter() {
515                    let resolved = match pat_obj {
516                        crate::pdf_objects::Object::Reference(id) => {
517                            match document.get_object(id.number(), id.generation()) {
518                                Ok(resolved_obj) => {
519                                    Self::convert_parser_object_to_unified(&resolved_obj)
520                                }
521                                Err(_) => pat_obj.clone(),
522                            }
523                        }
524                        _ => pat_obj.clone(),
525                    };
526                    resolved_patterns.set(pat_name.clone(), resolved);
527                }
528
529                unified_resources.set(
530                    "Pattern",
531                    crate::pdf_objects::Object::Dictionary(resolved_patterns),
532                );
533            }
534
535            // Phase 3.5: Resolve Shading references
536            if let Some(crate::pdf_objects::Object::Dictionary(shadings)) =
537                unified_resources.get("Shading")
538            {
539                let shadings_clone = shadings.clone();
540                let mut resolved_shadings = crate::pdf_objects::Dictionary::new();
541
542                for (sh_name, sh_obj) in shadings_clone.iter() {
543                    let resolved = match sh_obj {
544                        crate::pdf_objects::Object::Reference(id) => {
545                            match document.get_object(id.number(), id.generation()) {
546                                Ok(resolved_obj) => {
547                                    Self::convert_parser_object_to_unified(&resolved_obj)
548                                }
549                                Err(_) => sh_obj.clone(),
550                            }
551                        }
552                        _ => sh_obj.clone(),
553                    };
554                    resolved_shadings.set(sh_name.clone(), resolved);
555                }
556
557                unified_resources.set(
558                    "Shading",
559                    crate::pdf_objects::Object::Dictionary(resolved_shadings),
560                );
561            }
562
563            page.preserved_resources = Some(unified_resources);
564        }
565
566        Ok(page)
567    }
568
569    /// Creates a new A4 page (595 x 842 points).
570    pub fn a4() -> Self {
571        Self::new(595.0, 842.0)
572    }
573
574    /// Creates a new A4 landscape page (842 x 595 points).
575    pub fn a4_landscape() -> Self {
576        Self::new(842.0, 595.0)
577    }
578
579    /// Creates a new US Letter page (612 x 792 points).
580    pub fn letter() -> Self {
581        Self::new(612.0, 792.0)
582    }
583
584    /// Creates a new US Letter landscape page (792 x 612 points).
585    pub fn letter_landscape() -> Self {
586        Self::new(792.0, 612.0)
587    }
588
589    /// Returns the `FontMetricsStore` bound to this page, if any (issue #230).
590    ///
591    /// Pages constructed via `Document::new_page_*()` carry the Document's
592    /// store; pages constructed via `Page::a4()` / `Page::letter()` /
593    /// `Page::new()` are bound at `Document::add_page` time. Returns `None`
594    /// for pages that have not yet been attached to a Document.
595    pub fn font_metrics_store(&self) -> Option<&FontMetricsStore> {
596        self.font_metrics_store.as_ref()
597    }
598
599    /// Returns the `FontMetricsStore` bound to this page's `text_context`,
600    /// if any. Exposed for integration tests verifying that
601    /// `Document::add_page` injects the per-Document store into the
602    /// text context as well as into the page itself (issue #230 follow-up).
603    ///
604    /// Gated behind the `internal-testing` feature so it does not enter the
605    /// production ABI. Lib unit tests get it via `cfg(test)`.
606    #[cfg(any(test, feature = "internal-testing"))]
607    #[doc(hidden)]
608    pub fn text_context_metrics_store_for_test(
609        &self,
610    ) -> Option<&crate::text::metrics::FontMetricsStore> {
611        self.text_context.font_metrics_store.as_ref()
612    }
613
614    /// Returns the number of operations accumulated in the page's
615    /// `text_context`. Exposed for integration tests verifying that
616    /// `Document::add_page` does not erase ops while injecting the
617    /// per-Document `FontMetricsStore` (issue #230 follow-up).
618    ///
619    /// Gated behind the `internal-testing` feature so it does not enter the
620    /// production ABI. Lib unit tests get it via `cfg(test)`.
621    #[cfg(any(test, feature = "internal-testing"))]
622    #[doc(hidden)]
623    pub fn text_context_ops_count_for_test(&self) -> usize {
624        self.text_context.ops_slice().len()
625    }
626
627    /// Injects or replaces the `FontMetricsStore` on this page's text
628    /// context. Called by `Document::add_page` to wire the Document scope
629    /// into pages constructed via `Page::a4() / Page::letter() / Page::new()`
630    /// (issue #230 follow-up). Accumulated ops are preserved.
631    pub(crate) fn set_text_context_metrics_store(
632        &mut self,
633        store: Option<crate::text::metrics::FontMetricsStore>,
634    ) {
635        self.text_context.set_metrics_store(store);
636    }
637
638    /// Creates a new A4 page pre-loaded with a `FontMetricsStore` (issue #230).
639    ///
640    /// `Page::a4()` has `font_metrics_store: None`; this variant is used by
641    /// `Document::new_page_a4()` to bind the document-level store.
642    pub(crate) fn a4_with_metrics(store: FontMetricsStore) -> Self {
643        let mut p = Self::a4();
644        p.font_metrics_store = Some(store.clone());
645        p.text_context = TextContext::with_metrics_store(Some(store));
646        p
647    }
648
649    /// Creates a new US Letter page pre-loaded with a `FontMetricsStore` (issue #230).
650    pub(crate) fn letter_with_metrics(store: FontMetricsStore) -> Self {
651        let mut p = Self::letter();
652        p.font_metrics_store = Some(store.clone());
653        p.text_context = TextContext::with_metrics_store(Some(store));
654        p
655    }
656
657    /// Creates a new page of custom size pre-loaded with a `FontMetricsStore` (issue #230).
658    pub(crate) fn new_with_metrics(width: f64, height: f64, store: FontMetricsStore) -> Self {
659        let mut p = Self::new(width, height);
660        p.font_metrics_store = Some(store.clone());
661        p.text_context = TextContext::with_metrics_store(Some(store));
662        p
663    }
664
665    /// Creates a new US Legal page (612 x 1008 points).
666    pub fn legal() -> Self {
667        Self::new(612.0, 1008.0)
668    }
669
670    /// Creates a new US Legal landscape page (1008 x 612 points).
671    pub fn legal_landscape() -> Self {
672        Self::new(1008.0, 612.0)
673    }
674
675    /// Returns a mutable reference to the graphics context for drawing shapes.
676    ///
677    /// As of v2.7.0, this also flushes any pending text-context operations
678    /// into the page-level ordered buffer (`page_ops`) so that subsequent
679    /// graphics calls are emitted *after* the text that preceded them in
680    /// call order — preserving the PDF painter model across context
681    /// switches (issue #227).
682    pub fn graphics(&mut self) -> &mut GraphicsContext {
683        if !self.text_context.ops_slice().is_empty() {
684            let drained = self.text_context.drain_ops();
685            self.page_ops.extend(drained);
686        }
687        &mut self.graphics_context
688    }
689
690    /// Returns the accumulated content-stream operators for this page.
691    ///
692    /// Read-only counterpart to [`Page::graphics`]. The returned string is
693    /// the union of:
694    /// - operators already flushed to the page-level ordered buffer
695    ///   (`page_ops`) — i.e. graphics ops drawn before any
696    ///   `Page::text()` / `Page::add_text_flow()` switch — and
697    /// - operators still pending in the active `GraphicsContext` tail.
698    ///
699    /// Pre-2.7.0 this returned only the tail; the union is the correct
700    /// answer for callers inspecting "what has been drawn so far"
701    /// because the tail alone is incomplete after the first context
702    /// switch (review finding).
703    pub fn graphics_operations(&self) -> String {
704        let mut buf = Vec::new();
705        crate::graphics::ops::serialize_ops(&mut buf, &self.page_ops);
706        let tail = self.graphics_context.operations();
707        let mut out =
708            String::from_utf8(buf).expect("serialize_ops emits ASCII content-stream tokens");
709        out.push_str(&tail);
710        out
711    }
712
713    /// Returns a mutable reference to the text context for adding text.
714    ///
715    /// As of v2.7.0, this also flushes any pending graphics-context
716    /// operations into the page-level ordered buffer (`page_ops`) so
717    /// that subsequent text calls are emitted *after* the graphics that
718    /// preceded them in call order (issue #227).
719    ///
720    /// As of issue #239, this also propagates the current graphics-state
721    /// non-stroking colour into the text context when the caller has not
722    /// set an explicit text fill colour. Per ISO 32000-1 §8.6.8, `rg` is
723    /// a graphics-state operator that applies both to path fills and to
724    /// glyph fills at text rendering mode 0 (default). Splitting the
725    /// fill-colour slot between `GraphicsContext` and `TextContext`
726    /// without this handoff produced a stream where the text was painted
727    /// in whatever colour the previous path fill left active, never the
728    /// colour the caller intended for the text. An explicit
729    /// `text().set_fill_color(...)` still overrides the inherited value.
730    ///
731    /// **Side effect on the returned `TextContext`:** when the handoff
732    /// fires, this call mutates `text_context.fill_color` from `None`
733    /// to `Some(<graphics-state colour>)` BEFORE returning the
734    /// reference. Code that probes `fill_color()` as a signal of
735    /// "user has set a colour" will see `Some(...)` even when the user
736    /// never called `set_fill_color`, because the graphics state is now
737    /// considered the source of truth for the non-stroking colour.
738    /// After `clear()`, the next `text()` call will repeat the handoff.
739    pub fn text(&mut self) -> &mut TextContext {
740        if !self.graphics_context.ops_slice().is_empty() {
741            let drained = self.graphics_context.drain_ops();
742            self.page_ops.extend(drained);
743        }
744        if self.text_context.fill_color().is_none() {
745            let inherited = self.graphics_context.fill_color();
746            self.text_context.set_fill_color(inherited);
747        }
748        &mut self.text_context
749    }
750
751    pub fn set_margins(&mut self, left: f64, right: f64, top: f64, bottom: f64) {
752        self.margins = Margins {
753            left,
754            right,
755            top,
756            bottom,
757        };
758    }
759
760    pub fn margins(&self) -> &Margins {
761        &self.margins
762    }
763
764    pub fn content_width(&self) -> f64 {
765        self.width - self.margins.left - self.margins.right
766    }
767
768    pub fn content_height(&self) -> f64 {
769        self.height - self.margins.top - self.margins.bottom
770    }
771
772    pub fn content_area(&self) -> (f64, f64, f64, f64) {
773        (
774            self.margins.left,
775            self.margins.bottom,
776            self.width - self.margins.right,
777            self.height - self.margins.top,
778        )
779    }
780
781    pub fn width(&self) -> f64 {
782        self.width
783    }
784
785    pub fn height(&self) -> f64 {
786        self.height
787    }
788
789    /// Get the current coordinate system for this page
790    pub fn coordinate_system(&self) -> crate::coordinate_system::CoordinateSystem {
791        self.coordinate_system
792    }
793
794    /// Set the coordinate system for this page
795    pub fn set_coordinate_system(
796        &mut self,
797        coordinate_system: crate::coordinate_system::CoordinateSystem,
798    ) -> &mut Self {
799        self.coordinate_system = coordinate_system;
800        self
801    }
802
803    /// Sets the page rotation in degrees.
804    /// Valid values are 0, 90, 180, and 270.
805    /// Other values will be normalized to the nearest valid rotation.
806    pub fn set_rotation(&mut self, rotation: i32) {
807        // Normalize rotation to valid values (0, 90, 180, 270)
808        let normalized = rotation.rem_euclid(360); // Ensure positive
809        self.rotation = match normalized {
810            0..=44 | 316..=360 => 0,
811            45..=134 => 90,
812            135..=224 => 180,
813            225..=315 => 270,
814            _ => 0, // Should not happen, but default to 0
815        };
816    }
817
818    /// Converts a parser Dictionary to unified pdf_objects Dictionary
819    fn convert_parser_dict_to_unified(
820        parser_dict: &crate::parser::objects::PdfDictionary,
821    ) -> crate::pdf_objects::Dictionary {
822        use crate::pdf_objects::{Dictionary, Name};
823
824        let mut unified_dict = Dictionary::new();
825
826        for (key, value) in &parser_dict.0 {
827            let unified_key = Name::new(key.as_str());
828            let unified_value = Self::convert_parser_object_to_unified(value);
829            unified_dict.set(unified_key, unified_value);
830        }
831
832        unified_dict
833    }
834
835    /// Converts a parser PdfObject to unified Object
836    fn convert_parser_object_to_unified(
837        parser_obj: &crate::parser::objects::PdfObject,
838    ) -> crate::pdf_objects::Object {
839        use crate::parser::objects::PdfObject;
840        use crate::pdf_objects::{Array, BinaryString, Name, Object, ObjectId, Stream};
841
842        match parser_obj {
843            PdfObject::Null => Object::Null,
844            PdfObject::Boolean(b) => Object::Boolean(*b),
845            PdfObject::Integer(i) => Object::Integer(*i),
846            PdfObject::Real(f) => Object::Real(*f),
847            PdfObject::String(s) => Object::String(BinaryString::new(s.as_bytes().to_vec())),
848            PdfObject::Name(n) => Object::Name(Name::new(n.as_str())),
849            PdfObject::Array(arr) => {
850                let mut unified_arr = Array::new();
851                for item in &arr.0 {
852                    unified_arr.push(Self::convert_parser_object_to_unified(item));
853                }
854                Object::Array(unified_arr)
855            }
856            PdfObject::Dictionary(dict) => {
857                Object::Dictionary(Self::convert_parser_dict_to_unified(dict))
858            }
859            PdfObject::Stream(stream) => {
860                let dict = Self::convert_parser_dict_to_unified(&stream.dict);
861                let data = stream.data.clone();
862                Object::Stream(Stream::new(dict, data))
863            }
864            PdfObject::Reference(num, gen) => Object::Reference(ObjectId::new(*num, *gen)),
865        }
866    }
867
868    /// Resolves embedded font streams from a font dictionary (Phase 3.2 + Phase 3.4)
869    ///
870    /// Takes a font dictionary and resolves any FontDescriptor + FontFile references,
871    /// embedding the stream data directly so the writer doesn't need to resolve references.
872    ///
873    /// For Type0 (composite) fonts, this also resolves the complete hierarchy:
874    /// Type0 → DescendantFonts → CIDFont → FontDescriptor → FontFile2/FontFile3
875    ///
876    /// # Returns
877    /// Font dictionary with embedded streams (if font has embedded data),
878    /// or original dictionary (if standard font or resolution fails)
879    fn resolve_font_streams<R: std::io::Read + std::io::Seek>(
880        font_dict: &crate::pdf_objects::Dictionary,
881        document: &crate::parser::document::PdfDocument<R>,
882    ) -> Result<crate::pdf_objects::Dictionary> {
883        use crate::pdf_objects::Object;
884
885        let mut resolved_dict = font_dict.clone();
886
887        // Phase 3.4: Check if this is a Type0 (composite) font
888        if detect_type0_font(font_dict) {
889            // Create a resolver closure that converts parser objects to unified format
890            let resolver =
891                |id: crate::pdf_objects::ObjectId| -> Option<crate::pdf_objects::Object> {
892                    match document.get_object(id.number(), id.generation()) {
893                        Ok(parser_obj) => Some(Self::convert_parser_object_to_unified(&parser_obj)),
894                        Err(_) => None,
895                    }
896                };
897
898            // Resolve the complete Type0 hierarchy
899            if let Some(info) = resolve_type0_hierarchy(font_dict, resolver) {
900                // Embed the resolved CIDFont as DescendantFonts
901                if let Some(cidfont) = info.cidfont_dict {
902                    let mut resolved_cidfont = cidfont;
903
904                    // Embed FontDescriptor with resolved font stream
905                    if let Some(descriptor) = info.font_descriptor {
906                        let mut resolved_descriptor = descriptor;
907
908                        // Embed the font stream directly in FontDescriptor
909                        if let Some(stream) = info.font_stream {
910                            // Determine which key to use based on font_file_type
911                            let key = match info.font_file_type {
912                                Some(crate::fonts::type0_parsing::FontFileType::TrueType) => {
913                                    "FontFile2"
914                                }
915                                Some(crate::fonts::type0_parsing::FontFileType::CFF) => "FontFile3",
916                                Some(crate::fonts::type0_parsing::FontFileType::Type1) => {
917                                    "FontFile"
918                                }
919                                None => "FontFile2", // Default for CIDFontType2
920                            };
921                            resolved_descriptor.set(key, Object::Stream(stream));
922                        }
923
924                        resolved_cidfont
925                            .set("FontDescriptor", Object::Dictionary(resolved_descriptor));
926                    }
927
928                    // Replace DescendantFonts array with resolved CIDFont
929                    let mut descendants = crate::pdf_objects::Array::new();
930                    descendants.push(Object::Dictionary(resolved_cidfont));
931                    resolved_dict.set("DescendantFonts", Object::Array(descendants));
932                }
933
934                // Embed ToUnicode stream if present
935                if let Some(tounicode) = info.tounicode_stream {
936                    resolved_dict.set("ToUnicode", Object::Stream(tounicode));
937                }
938            }
939
940            return Ok(resolved_dict);
941        }
942
943        // Original Phase 3.2 logic for simple fonts (Type1, TrueType, etc.)
944        // Check if font has a FontDescriptor
945        if let Some(Object::Reference(descriptor_id)) = font_dict.get("FontDescriptor") {
946            // Resolve FontDescriptor from document
947            let descriptor_obj =
948                document.get_object(descriptor_id.number(), descriptor_id.generation())?;
949
950            // Convert to unified format
951            let descriptor_unified = Self::convert_parser_object_to_unified(&descriptor_obj);
952
953            if let Object::Dictionary(mut descriptor_dict) = descriptor_unified {
954                // Check for embedded font streams (FontFile, FontFile2, FontFile3)
955                let font_file_keys = ["FontFile", "FontFile2", "FontFile3"];
956                let mut stream_resolved = false;
957
958                for key in &font_file_keys {
959                    if let Some(Object::Reference(stream_id)) = descriptor_dict.get(*key) {
960                        // Resolve font stream from document
961                        match document.get_object(stream_id.number(), stream_id.generation()) {
962                            Ok(stream_obj) => {
963                                // Convert to unified format (includes stream data)
964                                let stream_unified =
965                                    Self::convert_parser_object_to_unified(&stream_obj);
966
967                                // Replace reference with actual stream object
968                                descriptor_dict.set(*key, stream_unified);
969                                stream_resolved = true;
970                            }
971                            Err(_) => {
972                                // Resolution failed, keep reference as-is
973                                continue;
974                            }
975                        }
976                    }
977                }
978
979                // If we resolved any streams, update FontDescriptor in font dictionary
980                if stream_resolved {
981                    resolved_dict.set("FontDescriptor", Object::Dictionary(descriptor_dict));
982                }
983            }
984        }
985
986        Ok(resolved_dict)
987    }
988
989    /// Gets the preserved resources from the original PDF (if any)
990    pub fn get_preserved_resources(&self) -> Option<&crate::pdf_objects::Dictionary> {
991        self.preserved_resources.as_ref()
992    }
993
994    /// Gets the current page rotation in degrees.
995    pub fn get_rotation(&self) -> i32 {
996        self.rotation
997    }
998
999    /// Gets the effective width considering rotation.
1000    /// For 90° and 270° rotations, returns the height.
1001    pub fn effective_width(&self) -> f64 {
1002        match self.rotation {
1003            90 | 270 => self.height,
1004            _ => self.width,
1005        }
1006    }
1007
1008    /// Gets the effective height considering rotation.
1009    /// For 90° and 270° rotations, returns the width.
1010    pub fn effective_height(&self) -> f64 {
1011        match self.rotation {
1012            90 | 270 => self.width,
1013            _ => self.height,
1014        }
1015    }
1016
1017    pub fn text_flow(&self) -> TextFlowContext {
1018        // Issue #216: inherit the page-level text state (font, size, and
1019        // fill colour) so callers that rely on `set_font` / `set_text_color`
1020        // before invoking flow helpers (notably `text_flow_at` in the Python
1021        // and .NET wrappers) get the formatting they configured.
1022        //
1023        // Issue #222 (Phase 6 of the v2.7.0 IR refactor): the remaining
1024        // seven text-state parameters (character spacing, word spacing,
1025        // horizontal scaling, leading, text rise, rendering mode, stroke
1026        // colour) are now propagated as well. An explicit setter call on
1027        // the returned `TextFlowContext` still overrides the inherited
1028        // value, so this is a strict superset of the previous behaviour.
1029        let mut ctx = TextFlowContext::with_metrics_store(
1030            self.width,
1031            self.height,
1032            self.margins.clone(),
1033            self.font_metrics_store.clone(),
1034        );
1035        ctx.set_font(
1036            self.text_context.current_font().clone(),
1037            self.text_context.font_size(),
1038        );
1039        // Issue #239: when no explicit text fill colour was set, fall
1040        // back to the graphics-state non-stroking colour so the flow
1041        // honours `graphics().set_fill_color(...)` per ISO 32000-1
1042        // §8.6.8. Symmetric with the handoff in `Page::text()`.
1043        let effective_fill = self
1044            .text_context
1045            .fill_color()
1046            .unwrap_or_else(|| self.graphics_context.fill_color());
1047        ctx.set_fill_color(effective_fill);
1048        if let Some(spacing) = self.text_context.character_spacing() {
1049            ctx.set_character_spacing(spacing);
1050        }
1051        if let Some(spacing) = self.text_context.word_spacing() {
1052            ctx.set_word_spacing(spacing);
1053        }
1054        if let Some(scale) = self.text_context.horizontal_scaling() {
1055            ctx.set_horizontal_scaling(scale);
1056        }
1057        if let Some(leading) = self.text_context.leading() {
1058            ctx.set_leading(leading);
1059        }
1060        if let Some(rise) = self.text_context.text_rise() {
1061            ctx.set_text_rise(rise);
1062        }
1063        if let Some(mode) = self.text_context.rendering_mode() {
1064            ctx.set_rendering_mode(mode as u8);
1065        }
1066        if let Some(color) = self.text_context.stroke_color() {
1067            ctx.set_stroke_color(color);
1068        }
1069        ctx
1070    }
1071
1072    pub fn add_text_flow(&mut self, text_flow: &TextFlowContext) {
1073        // Route the flow's serialised content into the page-level
1074        // ordered buffer so its position respects PDF painter-model
1075        // call order across `Page::graphics()` / `Page::text()` /
1076        // `Page::add_text_flow()` interleaving (issue #227, residual
1077        // gap surfaced by the v2.7.0 review). Drain both context tails
1078        // first so the page_ops timeline stays monotonic by call.
1079        self.flush_pending_contexts();
1080        let operations = text_flow.generate_operations();
1081        if !operations.is_empty() {
1082            self.page_ops
1083                .push(crate::graphics::ops::Op::Raw(operations));
1084        }
1085        // Absorb the text flow's per-font character tracking into the
1086        // page's graphics-context accumulator so the writer can subset
1087        // each custom font referenced by the flow (issue #204). Pre-fix
1088        // this merge did not happen, so a `TextFlowContext` with a
1089        // `Font::Custom` produced a content stream that referenced a
1090        // font whose subset was driven by whatever other page drew
1091        // with the global `used_characters` — and after the fix, the
1092        // unused-font skip would remove the font entirely.
1093        self.graphics_context
1094            .merge_font_usage(text_flow.get_used_characters_by_font());
1095    }
1096
1097    /// Drain whatever ops are currently buffered in the per-context
1098    /// `operations` vectors into `page_ops`, preserving call order.
1099    /// Used by APIs that emit content directly to `page_ops`
1100    /// (`add_text_flow`, `append_raw_content`) so the timeline does
1101    /// not skip over pending tails.
1102    fn flush_pending_contexts(&mut self) {
1103        if !self.graphics_context.ops_slice().is_empty() {
1104            let drained = self.graphics_context.drain_ops();
1105            self.page_ops.extend(drained);
1106        }
1107        if !self.text_context.ops_slice().is_empty() {
1108            let drained = self.text_context.drain_ops();
1109            self.page_ops.extend(drained);
1110        }
1111    }
1112
1113    pub fn add_image(&mut self, name: impl Into<String>, image: Image) {
1114        self.images.insert(name.into(), image);
1115    }
1116
1117    pub fn draw_image(
1118        &mut self,
1119        name: &str,
1120        x: f64,
1121        y: f64,
1122        width: f64,
1123        height: f64,
1124    ) -> Result<()> {
1125        if self.images.contains_key(name) {
1126            // Draw the image using the graphics context
1127            self.graphics_context.draw_image(name, x, y, width, height);
1128            Ok(())
1129        } else {
1130            Err(crate::PdfError::InvalidReference(format!(
1131                "Image '{name}' not found"
1132            )))
1133        }
1134    }
1135
1136    pub(crate) fn images(&self) -> &HashMap<String, Image> {
1137        &self.images
1138    }
1139
1140    /// Adds a Form XObject resource to this page (public as of v2.5.6).
1141    ///
1142    /// `name` is the key under which the Form XObject is exposed in the
1143    /// page's `/Resources/XObject` dictionary — typically `F1`, `Fm0`, etc.
1144    /// Use this when embedding reusable graphical content, overlays, stamps
1145    /// or template page fragments composed with the `FormXObject` /
1146    /// `FormXObjectBuilder` APIs.
1147    ///
1148    /// Duplicate names overwrite silently (the same behaviour as inserting
1149    /// into the underlying map); if you need idempotent registration,
1150    /// inspect [`Page::form_xobjects`] before calling.
1151    ///
1152    /// # Errors
1153    ///
1154    /// Returns [`PdfError::InvalidStructure`] if `name` is empty or
1155    /// contains any character forbidden by ISO 32000-1 §7.3.5 (delimiter
1156    /// characters `( ) < > [ ] { } / %`, whitespace, or the `#` escape
1157    /// introducer). Emitting such a name verbatim would close the
1158    /// resource dict early and allow dict-level injection.
1159    ///
1160    /// ```rust
1161    /// use oxidize_pdf::geometry::Rectangle;
1162    /// use oxidize_pdf::graphics::FormXObject;
1163    /// use oxidize_pdf::Page;
1164    ///
1165    /// let mut page = Page::a4();
1166    /// let bbox = Rectangle::from_position_and_size(0.0, 0.0, 100.0, 100.0);
1167    /// page.add_form_xobject("F1", FormXObject::new(bbox)).unwrap();
1168    /// assert!(page.form_xobjects().contains_key("F1"));
1169    /// ```
1170    pub fn add_form_xobject(
1171        &mut self,
1172        name: impl Into<String>,
1173        form: crate::graphics::FormXObject,
1174    ) -> Result<()> {
1175        let name = name.into();
1176        validate_pdf_resource_name(&name)?;
1177        self.form_xobjects.insert(name, form);
1178        Ok(())
1179    }
1180
1181    /// Returns all Form XObjects registered on this page (public as of v2.5.6).
1182    ///
1183    /// Keys correspond to `/Resources/XObject` entries emitted by the
1184    /// writer. The returned map is read-only; to mutate, use
1185    /// [`Page::add_form_xobject`].
1186    pub fn form_xobjects(&self) -> &HashMap<String, crate::graphics::FormXObject> {
1187        &self.form_xobjects
1188    }
1189
1190    /// Registers a colour space under `name` (ISO 32000-1 §8.6).
1191    ///
1192    /// `cs` is a typed [`crate::graphics::PageColorSpace`] — either a
1193    /// [`PageColorSpace::DeviceAlias`] wrapping one of the four device
1194    /// spaces (`/DeviceGray`, `/DeviceRGB`, `/DeviceCMYK`, `/Pattern`),
1195    /// or a [`PageColorSpace::Parameterised`] entry producing
1196    /// `[/<family> <<params>>]` for the calibrated families (`CalGray`,
1197    /// `CalRGB`, `Lab`, `ICCBased`). Indexed / Separation / DeviceN
1198    /// shapes are out of scope for this wrapper at v2.5.6; see the
1199    /// [`page_color_space`] module docs for the rationale.
1200    ///
1201    /// The writer emits the value under `/Resources/ColorSpace/<name>`,
1202    /// converting the enum to its concrete wire format at serialization
1203    /// time.
1204    ///
1205    /// [`page_color_space`]: crate::graphics::page_color_space
1206    /// [`PageColorSpace::DeviceAlias`]: crate::graphics::PageColorSpace::DeviceAlias
1207    /// [`PageColorSpace::Parameterised`]: crate::graphics::PageColorSpace::Parameterised
1208    ///
1209    /// # Errors
1210    ///
1211    /// Returns [`PdfError::InvalidStructure`] if `name` is not a valid
1212    /// PDF resource name per ISO 32000-1 §7.3.5 (see
1213    /// [`Page::add_form_xobject`] for the full rule).
1214    pub fn add_color_space(
1215        &mut self,
1216        name: impl Into<String>,
1217        cs: crate::graphics::PageColorSpace,
1218    ) -> Result<()> {
1219        let name = name.into();
1220        validate_pdf_resource_name(&name)?;
1221        self.color_spaces.insert(name, cs);
1222        Ok(())
1223    }
1224
1225    /// Register an ICC-profile-backed colour space under `name`, embedding the
1226    /// profile bytes so the writer emits a conformant `/ICCBased` **stream**
1227    /// `[/ICCBased <ref>]` (ISO 32000-1 §8.6.5.5).
1228    ///
1229    /// This is the ergonomic entry point for ICC colour: it bridges an
1230    /// [`IccProfile`](crate::graphics::IccProfile) into the stream-backed
1231    /// [`PageColorSpace::IccStream`](crate::graphics::PageColorSpace) variant.
1232    /// Unlike registering `PageColorSpace::Parameterised` with the `IccBased`
1233    /// family — which can only express an inline dictionary and drops the
1234    /// profile data — this path preserves the profile in the output.
1235    ///
1236    /// # Errors
1237    ///
1238    /// Returns [`PdfError::InvalidStructure`] if `name` is not a valid PDF
1239    /// resource name per ISO 32000-1 §7.3.5.
1240    pub fn add_icc_color_space(
1241        &mut self,
1242        name: impl Into<String>,
1243        profile: &crate::graphics::IccProfile,
1244    ) -> Result<()> {
1245        self.add_color_space(name, crate::graphics::PageColorSpace::from(profile))
1246    }
1247
1248    /// Returns all colour spaces registered on this page.
1249    ///
1250    /// Map values are typed as [`crate::graphics::PageColorSpace`]; the
1251    /// writer converts to the concrete PDF `Object` shape at emit time.
1252    pub fn color_spaces(&self) -> &HashMap<String, crate::graphics::PageColorSpace> {
1253        &self.color_spaces
1254    }
1255
1256    /// Registers a tiling pattern under `name` (ISO 32000-1 §8.7.3).
1257    ///
1258    /// The writer emits each pattern as an indirect stream object (patterns
1259    /// are streams per §7.3.8.1) and references it from
1260    /// `/Resources/Pattern/<name>`. Inside the content stream, paint the
1261    /// pattern with `/<name> scn` / `/<name> SCN` after selecting the
1262    /// /Pattern colour space.
1263    ///
1264    /// # Errors
1265    ///
1266    /// Returns [`PdfError::InvalidStructure`] if `name` is not a valid
1267    /// PDF resource name per ISO 32000-1 §7.3.5.
1268    pub fn add_pattern(
1269        &mut self,
1270        name: impl Into<String>,
1271        pattern: crate::graphics::TilingPattern,
1272    ) -> Result<()> {
1273        let name = name.into();
1274        validate_pdf_resource_name(&name)?;
1275        self.patterns.insert(name, pattern);
1276        Ok(())
1277    }
1278
1279    /// Returns all tiling patterns registered on this page.
1280    pub fn patterns(&self) -> &HashMap<String, crate::graphics::TilingPattern> {
1281        &self.patterns
1282    }
1283
1284    /// Registers a shading under `name` (ISO 32000-1 §8.7.4).
1285    ///
1286    /// The writer emits the shading as an indirect dictionary object and
1287    /// references it from `/Resources/Shading/<name>`. Paint with the
1288    /// `sh` operator (`/<name> sh`) or via a type-2 Pattern.
1289    ///
1290    /// # Errors
1291    ///
1292    /// Returns [`PdfError::InvalidStructure`] if `name` is not a valid
1293    /// PDF resource name per ISO 32000-1 §7.3.5.
1294    pub fn add_shading(
1295        &mut self,
1296        name: impl Into<String>,
1297        shading: crate::graphics::ShadingDefinition,
1298    ) -> Result<()> {
1299        let name = name.into();
1300        validate_pdf_resource_name(&name)?;
1301        self.shadings.insert(name, shading);
1302        Ok(())
1303    }
1304
1305    /// Returns all shadings registered on this page.
1306    pub fn shadings(&self) -> &HashMap<String, crate::graphics::ShadingDefinition> {
1307        &self.shadings
1308    }
1309
1310    /// Register a Type 4 free-form Gouraud mesh shading (#407), referenced
1311    /// from `/Resources/Shading/<name>` and painted with `/<name> sh`. The
1312    /// mesh is emitted as a stream object.
1313    ///
1314    /// # Errors
1315    ///
1316    /// Returns [`PdfError::InvalidStructure`] if `name` is not a valid PDF
1317    /// resource name (ISO 32000-1 §7.3.5) or the mesh fails validation.
1318    pub fn add_mesh_shading(
1319        &mut self,
1320        name: impl Into<String>,
1321        shading: crate::graphics::FreeFormGouraudShading,
1322    ) -> Result<()> {
1323        let name = name.into();
1324        validate_pdf_resource_name(&name)?;
1325        shading.validate()?;
1326        self.advanced_shadings
1327            .insert(name, crate::graphics::AdvancedShading::Mesh(shading));
1328        Ok(())
1329    }
1330
1331    /// Register an exact conic (angular) gradient as a Type 1 function-based
1332    /// shading (#407), referenced from `/Resources/Shading/<name>`.
1333    ///
1334    /// # Errors
1335    ///
1336    /// Returns [`PdfError::InvalidStructure`] if `name` is not a valid PDF
1337    /// resource name (ISO 32000-1 §7.3.5) or the shading fails validation.
1338    pub fn add_conic_shading(
1339        &mut self,
1340        name: impl Into<String>,
1341        shading: crate::graphics::ConicShading,
1342    ) -> Result<()> {
1343        let name = name.into();
1344        validate_pdf_resource_name(&name)?;
1345        shading.validate()?;
1346        self.advanced_shadings
1347            .insert(name, crate::graphics::AdvancedShading::Conic(shading));
1348        Ok(())
1349    }
1350
1351    /// Returns the mesh/conic shadings registered on this page (crate-internal;
1352    /// consumed by the writer).
1353    pub(crate) fn advanced_shadings(&self) -> &HashMap<String, crate::graphics::AdvancedShading> {
1354        &self.advanced_shadings
1355    }
1356
1357    /// Append raw PDF operators to the content stream and record which
1358    /// fonts each character was drawn with (issue #204).
1359    ///
1360    /// `font_usage` MUST account for every `Tj`/`TJ` emitted inside
1361    /// `data` — the caller is the only actor that has ground truth
1362    /// about which font was active when the operator was generated.
1363    /// Pre-fix this method took just `data`; the resulting silent
1364    /// failure mode (unused fonts being subsetted with someone else's
1365    /// characters, or — post-fix — missing entirely from the emitted
1366    /// PDF because no per-font bucket existed) is exactly what the
1367    /// type-gate here prevents. Future content builders must return
1368    /// their usage map alongside their bytes.
1369    ///
1370    /// Content appended here renders AFTER the existing page content
1371    /// (on top).
1372    pub(crate) fn append_raw_content(
1373        &mut self,
1374        data: &[u8],
1375        font_usage: &HashMap<String, HashSet<char>>,
1376    ) {
1377        // Mirror of `add_text_flow`: route through `page_ops` so this
1378        // content respects painter-model call order against
1379        // `graphics()` / `text()` / `add_text_flow()` calls
1380        // (issue #227 residual). Drain pending tails first.
1381        self.flush_pending_contexts();
1382        if !data.is_empty() {
1383            self.page_ops
1384                .push(crate::graphics::ops::Op::Raw(data.to_vec()));
1385        }
1386        self.graphics_context.merge_font_usage(font_usage);
1387    }
1388
1389    /// Add a table to the page.
1390    ///
1391    /// This method renders a table at the specified position using the current
1392    /// graphics context. The table will be drawn with borders, text, and any
1393    /// configured styling options.
1394    ///
1395    /// # Arguments
1396    ///
1397    /// * `table` - The table to render on the page
1398    ///
1399    /// # Example
1400    ///
1401    /// ```rust
1402    /// use oxidize_pdf::{Page, text::{Table, TableOptions}};
1403    ///
1404    /// let mut page = Page::a4();
1405    ///
1406    /// // Create a table with 3 columns
1407    /// let mut table = Table::new(vec![100.0, 150.0, 100.0]);
1408    /// table.set_position(50.0, 700.0);
1409    ///
1410    /// // Add header row
1411    /// table.add_header_row(vec![
1412    ///     "Name".to_string(),
1413    ///     "Description".to_string(),
1414    ///     "Price".to_string()
1415    /// ])?;
1416    ///
1417    /// // Add data rows
1418    /// table.add_row(vec![
1419    ///     "Item 1".to_string(),
1420    ///     "First item description".to_string(),
1421    ///     "$10.00".to_string()
1422    /// ])?;
1423    ///
1424    /// // Render the table on the page
1425    /// page.add_table(&table)?;
1426    /// # Ok::<(), oxidize_pdf::PdfError>(())
1427    /// ```
1428    pub fn add_table(&mut self, table: &Table) -> Result<()> {
1429        self.graphics_context.render_table(table)
1430    }
1431
1432    /// Get ExtGState resources from the graphics context
1433    pub fn get_extgstate_resources(
1434        &self,
1435    ) -> Option<&std::collections::HashMap<String, crate::graphics::ExtGState>> {
1436        if self.graphics_context.has_extgstates() {
1437            Some(self.graphics_context.extgstate_manager().states())
1438        } else {
1439            None
1440        }
1441    }
1442
1443    /// Adds an annotation to this page
1444    pub fn add_annotation(&mut self, annotation: Annotation) {
1445        self.annotations.push(annotation);
1446    }
1447
1448    /// Returns a reference to the annotations
1449    pub fn annotations(&self) -> &[Annotation] {
1450        &self.annotations
1451    }
1452
1453    /// Returns a mutable reference to the annotations  
1454    pub fn annotations_mut(&mut self) -> &mut Vec<Annotation> {
1455        &mut self.annotations
1456    }
1457
1458    /// Add a form field widget to the page.
1459    ///
1460    /// This method adds a widget annotation and returns the reference ID that
1461    /// should be used to link the widget to its corresponding form field.
1462    ///
1463    /// # Arguments
1464    ///
1465    /// * `widget` - The widget to add to the page
1466    ///
1467    /// # Returns
1468    ///
1469    /// An ObjectReference that should be used to link this widget to a form field
1470    ///
1471    /// # Example
1472    ///
1473    /// ```rust,no_run
1474    /// use oxidize_pdf::{Page, forms::Widget, geometry::{Rectangle, Point}};
1475    ///
1476    /// let mut page = Page::a4();
1477    /// let widget = Widget::new(
1478    ///     Rectangle::new(Point::new(100.0, 700.0), Point::new(300.0, 720.0))
1479    /// );
1480    /// let widget_ref = page.add_form_widget(widget);
1481    /// ```
1482    pub fn add_form_widget(&mut self, widget: Widget) -> ObjectReference {
1483        // Create a placeholder object reference for this widget
1484        // The actual ObjectId will be assigned by the document writer
1485        // We use a placeholder ID that doesn't conflict with real ObjectIds
1486        let widget_ref = ObjectReference::new(
1487            0, // Placeholder ID - writer will assign the real ID
1488            0,
1489        );
1490
1491        // Convert widget to annotation
1492        let mut annot = Annotation::new(crate::annotations::AnnotationType::Widget, widget.rect);
1493
1494        // Add widget-specific properties
1495        for (key, value) in widget.to_annotation_dict().iter() {
1496            annot.properties.set(key, value.clone());
1497        }
1498
1499        // Add to page's annotations
1500        self.annotations.push(annot);
1501
1502        widget_ref
1503    }
1504
1505    /// Add a form field widget to the page and link it to an existing AcroForm field.
1506    ///
1507    /// Unlike [`Page::add_form_widget`], which expects the writer to track the
1508    /// relationship implicitly via shared field names, this variant records
1509    /// the `field_ref` as the widget's `/Parent` so the resulting PDF carries
1510    /// an explicit widget→field link per ISO 32000-1 §12.7.3.1.
1511    ///
1512    /// This is the recommended path when the field itself was created via
1513    /// [`crate::forms::FormManager`] (whose `add_text_field`, `add_combo_box`,
1514    /// etc. return the field's `ObjectReference`): the field object is
1515    /// serialized as an indirect object, and every widget placed on a page
1516    /// points back to it through `/Parent`.
1517    ///
1518    /// # Arguments
1519    ///
1520    /// * `widget` - The widget appearance/geometry to place on the page.
1521    /// * `field_ref` - The `ObjectReference` of the AcroForm field this
1522    ///   widget belongs to (as returned by `FormManager::add_*`).
1523    ///
1524    /// # Returns
1525    ///
1526    /// `Ok(())` on success. The method currently cannot fail; the `Result`
1527    /// return type is reserved for future validation.
1528    pub fn add_form_widget_with_ref(
1529        &mut self,
1530        widget: Widget,
1531        field_ref: ObjectReference,
1532    ) -> crate::error::Result<()> {
1533        // Convert widget to annotation, same as add_form_widget.
1534        let mut annot = Annotation::new(crate::annotations::AnnotationType::Widget, widget.rect);
1535
1536        for (key, value) in widget.to_annotation_dict().iter() {
1537            annot.properties.set(key, value.clone());
1538        }
1539
1540        // Record the parent field reference so the writer emits
1541        // `/Parent <field_ref>` in the widget annotation dictionary.
1542        // Goes through the setter so the Widget-annotation-type invariant
1543        // is enforced (debug_assert! in debug builds) — `field_parent` is
1544        // `pub(crate)` precisely to prevent external callers bypassing it.
1545        annot.set_field_parent(field_ref);
1546
1547        self.annotations.push(annot);
1548        Ok(())
1549    }
1550
1551    /// Sets the header for this page.
1552    ///
1553    /// # Example
1554    ///
1555    /// ```rust
1556    /// use oxidize_pdf::{Page, text::HeaderFooter};
1557    ///
1558    /// let mut page = Page::a4();
1559    /// page.set_header(HeaderFooter::new_header("Company Report 2024"));
1560    /// ```
1561    pub fn set_header(&mut self, header: HeaderFooter) {
1562        self.register_header_footer_font_usage(&header);
1563        self.header = Some(header);
1564    }
1565
1566    /// Sets the footer for this page.
1567    ///
1568    /// # Example
1569    ///
1570    /// ```rust
1571    /// use oxidize_pdf::{Page, text::HeaderFooter};
1572    ///
1573    /// let mut page = Page::a4();
1574    /// page.set_footer(HeaderFooter::new_footer("Page {{page_number}} of {{total_pages}}"));
1575    /// ```
1576    pub fn set_footer(&mut self, footer: HeaderFooter) {
1577        self.register_header_footer_font_usage(&footer);
1578        self.footer = Some(footer);
1579    }
1580
1581    /// Eagerly record the font + estimated character set of a header or
1582    /// footer on the page's graphics-context accumulator (gap R5 of
1583    /// issue #204).
1584    ///
1585    /// Header/footer content is rendered at serialization time — inside
1586    /// `generate_content_with_page_info` — which runs AFTER the writer
1587    /// has snapshotted the document's per-font map into its own
1588    /// `document_used_chars_by_font`. Any characters drawn at render
1589    /// time therefore arrive too late to be included in the font
1590    /// subset. We compensate here by expanding the template with
1591    /// canonical sample values (the numeric placeholders become the
1592    /// digit set; `{{date}}`, `{{time}}`, `{{month}}`, etc. expand to
1593    /// the current locale's rendering) and registering those chars
1594    /// plus the template's literal text up front.
1595    ///
1596    /// **Known limitation**: user-supplied `custom_values` passed later
1597    /// to the writer may introduce characters not in the template
1598    /// literal. Those chars will NOT appear in the embedded subset and
1599    /// will render as `.notdef`. Callers who need runtime-defined
1600    /// custom strings in a custom-font header should also draw a
1601    /// `page.text()` with the same font somewhere (even invisibly)
1602    /// so the chars reach the per-font bucket through the standard
1603    /// path.
1604    fn register_header_footer_font_usage(&mut self, hf: &HeaderFooter) {
1605        let font_name = hf.options().font.pdf_name();
1606
1607        // Render the template with canonical sample values to capture
1608        // both the literal text and whatever the locale-dependent
1609        // placeholders expand to (month name, date format, etc.).
1610        // page_number=1 / total_pages=999 covers the digit 9 which is
1611        // the upper boundary of common PDFs; a further safety net adds
1612        // the full digit set below.
1613        let sampled = hf.render(1, 999, None);
1614
1615        let mut chars: HashSet<char> = sampled.chars().collect();
1616        // Digits 0-9 are guaranteed to appear once the template is
1617        // rendered with real page numbers (the sample above only
1618        // covered {1, 9} digits). Include the rest so page 2..8 don't
1619        // render as .notdef on the last page of a >10-page document.
1620        chars.extend('0'..='9');
1621
1622        self.graphics_context
1623            .merge_font_usage(&std::iter::once((font_name, chars)).collect());
1624    }
1625
1626    /// Gets a reference to the header if set.
1627    pub fn header(&self) -> Option<&HeaderFooter> {
1628        self.header.as_ref()
1629    }
1630
1631    /// Gets a reference to the footer if set.
1632    pub fn footer(&self) -> Option<&HeaderFooter> {
1633        self.footer.as_ref()
1634    }
1635
1636    /// Sets the page content directly.
1637    ///
1638    /// This is used internally when processing headers and footers.
1639    pub(crate) fn set_content(&mut self, content: Vec<u8>) {
1640        self.content = content;
1641    }
1642
1643    pub(crate) fn generate_content(&mut self) -> Result<Vec<u8>> {
1644        // Generate content with no page info (used for simple pages without headers/footers)
1645        self.generate_content_with_page_info(None, None, None)
1646    }
1647
1648    /// Generates page content with header/footer support.
1649    ///
1650    /// This method is used internally by the writer to render pages with
1651    /// proper page numbering in headers and footers.
1652    pub(crate) fn generate_content_with_page_info(
1653        &mut self,
1654        page_number: Option<usize>,
1655        total_pages: Option<usize>,
1656        custom_values: Option<&HashMap<String, String>>,
1657    ) -> Result<Vec<u8>> {
1658        let mut final_content = Vec::new();
1659
1660        // Render header if present
1661        if let Some(header) = &self.header {
1662            if let (Some(page_num), Some(total)) = (page_number, total_pages) {
1663                let header_content =
1664                    self.render_header_footer(header, page_num, total, custom_values)?;
1665                final_content.extend_from_slice(&header_content);
1666            }
1667        }
1668
1669        // Painter-model preservation (issue #227): emit operators in
1670        // caller call order, not in fixed `graphics-then-text` category
1671        // order.
1672        //
1673        // `page_ops` already holds the operators flushed at every
1674        // context switch (see `Page::graphics` / `Page::text`). Whatever
1675        // remains in either context's own buffer is the *tail* — the
1676        // operators emitted after the last switch — and is appended in
1677        // its natural ordering. Only one of the two tails can be
1678        // non-empty at any given time (because the other was drained on
1679        // the most recent switch), so the relative order of the two
1680        // appends below is irrelevant.
1681        crate::graphics::ops::serialize_ops(&mut final_content, &self.page_ops);
1682        let gfx_tail = self.graphics_context.generate_operations()?;
1683        final_content.extend_from_slice(&gfx_tail);
1684        let text_tail = self.text_context.generate_operations()?;
1685        final_content.extend_from_slice(&text_tail);
1686
1687        // Add preserved original content. Issue #395: rewrite only the font
1688        // references the writer disambiguated (collision-only). When no font
1689        // collided, `preserved_font_rewrite_map` is empty and the preserved
1690        // content is emitted verbatim — the common case (incl. all-non-base-14
1691        // inputs like `testi.pdf`), which avoids ever touching the stream.
1692        let content_to_add = if self.preserved_font_rewrite_map.is_empty()
1693            || self.content.is_empty()
1694        {
1695            self.content.clone()
1696        } else {
1697            crate::writer::rewrite_font_references(&self.content, &self.preserved_font_rewrite_map)
1698        };
1699
1700        final_content.extend_from_slice(&content_to_add);
1701
1702        // Render footer if present
1703        if let Some(footer) = &self.footer {
1704            if let (Some(page_num), Some(total)) = (page_number, total_pages) {
1705                let footer_content =
1706                    self.render_header_footer(footer, page_num, total, custom_values)?;
1707                final_content.extend_from_slice(&footer_content);
1708            }
1709        }
1710
1711        Ok(final_content)
1712    }
1713
1714    /// Renders a header or footer with the given page information.
1715    fn render_header_footer(
1716        &self,
1717        header_footer: &HeaderFooter,
1718        page_number: usize,
1719        total_pages: usize,
1720        custom_values: Option<&HashMap<String, String>>,
1721    ) -> Result<Vec<u8>> {
1722        use crate::text::measure_text;
1723
1724        // Render the content with placeholders replaced
1725        let content = header_footer.render(page_number, total_pages, custom_values);
1726
1727        // Calculate text width for alignment
1728        let text_width = measure_text(
1729            &content,
1730            &header_footer.options().font,
1731            header_footer.options().font_size,
1732        );
1733
1734        // Calculate positions
1735        let x = header_footer.calculate_x_position(self.width, text_width);
1736        let y = header_footer.calculate_y_position(self.height);
1737
1738        // Create a temporary text context for the header/footer
1739        let mut text_ctx = TextContext::new();
1740        text_ctx
1741            .set_font(
1742                header_footer.options().font.clone(),
1743                header_footer.options().font_size,
1744            )
1745            .at(x, y)
1746            .write(&content)?;
1747
1748        text_ctx.generate_operations()
1749    }
1750
1751    /// Convert page to dictionary for PDF structure
1752    pub(crate) fn to_dict(&self) -> Dictionary {
1753        let mut dict = Dictionary::new();
1754
1755        // MediaBox
1756        let media_box = Array::from(vec![
1757            Object::Real(0.0),
1758            Object::Real(0.0),
1759            Object::Real(self.width),
1760            Object::Real(self.height),
1761        ]);
1762        dict.set("MediaBox", Object::Array(media_box.into()));
1763
1764        // Add rotation if not zero
1765        if self.rotation != 0 {
1766            dict.set("Rotate", Object::Integer(self.rotation as i64));
1767        }
1768
1769        // Resources (empty for now, would include fonts, images, etc.)
1770        let resources = Dictionary::new();
1771        dict.set("Resources", Object::Dictionary(resources));
1772
1773        // Annotations - will be added by the writer with proper object references
1774        // The Page struct holds the annotation data, but the writer is responsible
1775        // for creating object references and writing the annotation objects
1776        //
1777        // NOTE: We don't add Annots array here anymore because the writer
1778        // will handle this properly with sequential ObjectIds. The temporary
1779        // ObjectIds (1000+) were causing invalid references in the final PDF.
1780        // The writer now handles all ObjectId allocation and writing.
1781
1782        // Contents would be added by the writer
1783
1784        dict
1785    }
1786
1787    /// Gets all characters used in this page, bucketed by font name
1788    /// (issue #204).
1789    ///
1790    /// Merges the per-font maps from `graphics_context` and
1791    /// `text_context` — either or both may contain entries for the
1792    /// same font (e.g. a caller mixed `page.text()` with direct
1793    /// graphics-context `draw_text`). Both builtin and custom fonts
1794    /// appear as keys; the writer filters to the registered custom
1795    /// fonts at subsetting time (builtin fonts don't need subsetting).
1796    pub(crate) fn get_used_characters_by_font(&self) -> HashMap<String, HashSet<char>> {
1797        let mut merged: HashMap<String, HashSet<char>> = HashMap::new();
1798        for (name, chars) in self.graphics_context.get_used_characters_by_font() {
1799            merged.entry(name.clone()).or_default().extend(chars);
1800        }
1801        for (name, chars) in self.text_context.get_used_characters_by_font() {
1802            merged.entry(name.clone()).or_default().extend(chars);
1803        }
1804        merged
1805    }
1806
1807    /// Back-compat accessor used by the legacy issue-#97 tests: returns
1808    /// all characters drawn on this page merged across fonts. Prefer
1809    /// [`Page::get_used_characters_by_font`] for new callers — the
1810    /// writer depends on per-font accuracy to avoid bundling the
1811    /// active font's coverage into unused fonts (issue #204).
1812    #[cfg(test)]
1813    pub(crate) fn get_used_characters(&self) -> Option<HashSet<char>> {
1814        let merged: HashSet<char> = self
1815            .get_used_characters_by_font()
1816            .into_values()
1817            .flatten()
1818            .collect();
1819        if merged.is_empty() {
1820            None
1821        } else {
1822            Some(merged)
1823        }
1824    }
1825
1826    // ==================== Tagged PDF / Marked Content Support ====================
1827
1828    /// Begins a marked content sequence for Tagged PDF
1829    ///
1830    /// This adds a BDC (Begin Marked Content with Properties) operator to the content stream
1831    /// with an MCID (Marked Content ID) property. The MCID connects the content to a
1832    /// structure element in the structure tree.
1833    ///
1834    /// # Returns
1835    ///
1836    /// Returns the assigned MCID, which should be added to the corresponding StructureElement
1837    /// via `StructureElement::add_mcid(page_index, mcid)`.
1838    ///
1839    /// # Example
1840    ///
1841    /// ```rust,no_run
1842    /// use oxidize_pdf::{Page, structure::{StructTree, StructureElement, StandardStructureType}};
1843    ///
1844    /// let mut page = Page::a4();
1845    /// let mut tree = StructTree::new();
1846    ///
1847    /// // Create structure
1848    /// let doc = StructureElement::new(StandardStructureType::Document);
1849    /// let doc_idx = tree.set_root(doc);
1850    /// let mut para = StructureElement::new(StandardStructureType::P);
1851    ///
1852    /// // Begin marked content for paragraph
1853    /// let mcid = page.begin_marked_content("P")?;
1854    ///
1855    /// // Add content
1856    /// page.text().write("Hello, Tagged PDF!")?;
1857    ///
1858    /// // End marked content
1859    /// page.end_marked_content()?;
1860    ///
1861    /// // Connect MCID to structure element
1862    /// para.add_mcid(0, mcid);  // page_index=0, mcid from above
1863    /// tree.add_child(doc_idx, para).map_err(|e| oxidize_pdf::PdfError::InvalidOperation(e))?;
1864    /// # Ok::<(), oxidize_pdf::PdfError>(())
1865    /// ```
1866    pub fn begin_marked_content(&mut self, tag: &str) -> Result<u32> {
1867        let mcid = self.next_mcid;
1868        self.next_mcid += 1;
1869
1870        // Add BDC operator with MCID property to text context
1871        // Format: /Tag <</MCID mcid>> BDC
1872        let bdc_op = format!("/{} <</MCID {}>> BDC\n", tag, mcid);
1873        self.text_context.append_raw_operation(&bdc_op);
1874
1875        self.marked_content_stack.push(tag.to_string());
1876
1877        Ok(mcid)
1878    }
1879
1880    /// Ends the current marked content sequence
1881    ///
1882    /// This adds an EMC (End Marked Content) operator to close the most recently
1883    /// opened marked content sequence.
1884    ///
1885    /// # Errors
1886    ///
1887    /// Returns an error if there is no open marked content sequence.
1888    pub fn end_marked_content(&mut self) -> Result<()> {
1889        if self.marked_content_stack.is_empty() {
1890            return Err(crate::PdfError::InvalidOperation(
1891                "No marked content sequence to end (EMC without BDC)".to_string(),
1892            ));
1893        }
1894
1895        self.marked_content_stack.pop();
1896
1897        // Add EMC operator to text context
1898        self.text_context.append_raw_operation("EMC\n");
1899
1900        Ok(())
1901    }
1902
1903    /// Returns the next MCID that will be assigned
1904    ///
1905    /// This is useful for pre-allocating structure elements before adding content.
1906    pub fn next_mcid(&self) -> u32 {
1907        self.next_mcid
1908    }
1909
1910    /// Returns the current depth of nested marked content
1911    pub fn marked_content_depth(&self) -> usize {
1912        self.marked_content_stack.len()
1913    }
1914}
1915
1916#[cfg(test)]
1917mod tests {
1918    use super::*;
1919    use crate::graphics::Color;
1920    use crate::text::Font;
1921
1922    #[test]
1923    fn test_page_new() {
1924        let page = Page::new(100.0, 200.0);
1925        assert_eq!(page.width(), 100.0);
1926        assert_eq!(page.height(), 200.0);
1927        assert_eq!(page.margins().left, 72.0);
1928        assert_eq!(page.margins().right, 72.0);
1929        assert_eq!(page.margins().top, 72.0);
1930        assert_eq!(page.margins().bottom, 72.0);
1931    }
1932
1933    #[test]
1934    fn test_page_a4() {
1935        let page = Page::a4();
1936        assert_eq!(page.width(), 595.0);
1937        assert_eq!(page.height(), 842.0);
1938    }
1939
1940    #[test]
1941    fn test_page_letter() {
1942        let page = Page::letter();
1943        assert_eq!(page.width(), 612.0);
1944        assert_eq!(page.height(), 792.0);
1945    }
1946
1947    #[test]
1948    fn test_set_margins() {
1949        let mut page = Page::a4();
1950        page.set_margins(10.0, 20.0, 30.0, 40.0);
1951
1952        assert_eq!(page.margins().left, 10.0);
1953        assert_eq!(page.margins().right, 20.0);
1954        assert_eq!(page.margins().top, 30.0);
1955        assert_eq!(page.margins().bottom, 40.0);
1956    }
1957
1958    #[test]
1959    fn test_content_dimensions() {
1960        let mut page = Page::new(300.0, 400.0);
1961        page.set_margins(50.0, 50.0, 50.0, 50.0);
1962
1963        assert_eq!(page.content_width(), 200.0);
1964        assert_eq!(page.content_height(), 300.0);
1965    }
1966
1967    #[test]
1968    fn test_content_area() {
1969        let mut page = Page::new(300.0, 400.0);
1970        page.set_margins(10.0, 20.0, 30.0, 40.0);
1971
1972        let (left, bottom, right, top) = page.content_area();
1973        assert_eq!(left, 10.0);
1974        assert_eq!(bottom, 40.0);
1975        assert_eq!(right, 280.0);
1976        assert_eq!(top, 370.0);
1977    }
1978
1979    #[test]
1980    fn test_graphics_context() {
1981        let mut page = Page::a4();
1982        let graphics = page.graphics();
1983        graphics.set_fill_color(Color::red());
1984        graphics.rect(100.0, 100.0, 200.0, 150.0);
1985        graphics.fill();
1986
1987        // Graphics context should be accessible and modifiable
1988        assert!(page.generate_content().is_ok());
1989    }
1990
1991    #[test]
1992    fn test_text_context() {
1993        let mut page = Page::a4();
1994        let text = page.text();
1995        text.set_font(Font::Helvetica, 12.0);
1996        text.at(100.0, 700.0);
1997        text.write("Hello World").unwrap();
1998
1999        // Text context should be accessible and modifiable
2000        assert!(page.generate_content().is_ok());
2001    }
2002
2003    #[test]
2004    fn test_text_flow() {
2005        let page = Page::a4();
2006        let text_flow = page.text_flow();
2007
2008        // Text flow should be created with page dimensions and margins
2009        // Just verify it can be created
2010        drop(text_flow);
2011    }
2012
2013    #[test]
2014    fn test_add_text_flow() {
2015        let mut page = Page::a4();
2016        let mut text_flow = page.text_flow();
2017        text_flow.at(100.0, 700.0);
2018        text_flow.set_font(Font::TimesRoman, 14.0);
2019        text_flow.write_wrapped("Test text flow").unwrap();
2020
2021        page.add_text_flow(&text_flow);
2022
2023        let content = page.generate_content().unwrap();
2024        assert!(!content.is_empty());
2025    }
2026
2027    #[test]
2028    fn test_add_image() {
2029        let mut page = Page::a4();
2030        // Create a minimal valid JPEG with SOF0 header
2031        let jpeg_data = vec![
2032            0xFF, 0xD8, // SOI marker
2033            0xFF, 0xC0, // SOF0 marker
2034            0x00, 0x11, // Length (17 bytes)
2035            0x08, // Precision (8 bits)
2036            0x00, 0x64, // Height (100)
2037            0x00, 0xC8, // Width (200)
2038            0x03, // Components (3 = RGB)
2039            0xFF, 0xD9, // EOI marker
2040        ];
2041        let image = Image::from_jpeg_data(jpeg_data).unwrap();
2042
2043        page.add_image("test_image", image);
2044        assert!(page.images().contains_key("test_image"));
2045        assert_eq!(page.images().len(), 1);
2046    }
2047
2048    #[test]
2049    fn test_draw_image() {
2050        let mut page = Page::a4();
2051        // Create a minimal valid JPEG with SOF0 header
2052        let jpeg_data = vec![
2053            0xFF, 0xD8, // SOI marker
2054            0xFF, 0xC0, // SOF0 marker
2055            0x00, 0x11, // Length (17 bytes)
2056            0x08, // Precision (8 bits)
2057            0x00, 0x64, // Height (100)
2058            0x00, 0xC8, // Width (200)
2059            0x03, // Components (3 = RGB)
2060            0xFF, 0xD9, // EOI marker
2061        ];
2062        let image = Image::from_jpeg_data(jpeg_data).unwrap();
2063
2064        page.add_image("test_image", image);
2065        let result = page.draw_image("test_image", 50.0, 50.0, 200.0, 200.0);
2066        assert!(result.is_ok());
2067    }
2068
2069    #[test]
2070    fn test_draw_nonexistent_image() {
2071        let mut page = Page::a4();
2072        let result = page.draw_image("nonexistent", 50.0, 50.0, 200.0, 200.0);
2073        assert!(result.is_err());
2074    }
2075
2076    #[test]
2077    fn test_generate_content() {
2078        let mut page = Page::a4();
2079
2080        // Add some graphics
2081        page.graphics()
2082            .set_fill_color(Color::blue())
2083            .circle(200.0, 400.0, 50.0)
2084            .fill();
2085
2086        // Add some text
2087        page.text()
2088            .set_font(Font::Courier, 10.0)
2089            .at(50.0, 650.0)
2090            .write("Test content")
2091            .unwrap();
2092
2093        let content = page.generate_content().unwrap();
2094        assert!(!content.is_empty());
2095    }
2096
2097    #[test]
2098    fn test_margins_default() {
2099        let margins = Margins::default();
2100        assert_eq!(margins.left, 72.0);
2101        assert_eq!(margins.right, 72.0);
2102        assert_eq!(margins.top, 72.0);
2103        assert_eq!(margins.bottom, 72.0);
2104    }
2105
2106    #[test]
2107    fn test_page_clone() {
2108        let mut page1 = Page::a4();
2109        page1.set_margins(10.0, 20.0, 30.0, 40.0);
2110        // Create a minimal valid JPEG with SOF0 header
2111        let jpeg_data = vec![
2112            0xFF, 0xD8, // SOI marker
2113            0xFF, 0xC0, // SOF0 marker
2114            0x00, 0x11, // Length (17 bytes)
2115            0x08, // Precision (8 bits)
2116            0x00, 0x32, // Height (50)
2117            0x00, 0x32, // Width (50)
2118            0x03, // Components (3 = RGB)
2119            0xFF, 0xD9, // EOI marker
2120        ];
2121        let image = Image::from_jpeg_data(jpeg_data).unwrap();
2122        page1.add_image("img1", image);
2123
2124        let page2 = page1.clone();
2125        assert_eq!(page2.width(), page1.width());
2126        assert_eq!(page2.height(), page1.height());
2127        assert_eq!(page2.margins().left, page1.margins().left);
2128        assert_eq!(page2.images().len(), page1.images().len());
2129    }
2130
2131    #[test]
2132    fn test_header_footer_basic() {
2133        use crate::text::HeaderFooter;
2134
2135        let mut page = Page::a4();
2136
2137        let header = HeaderFooter::new_header("Test Header");
2138        let footer = HeaderFooter::new_footer("Test Footer");
2139
2140        page.set_header(header);
2141        page.set_footer(footer);
2142
2143        assert!(page.header().is_some());
2144        assert!(page.footer().is_some());
2145        assert_eq!(page.header().unwrap().content(), "Test Header");
2146        assert_eq!(page.footer().unwrap().content(), "Test Footer");
2147    }
2148
2149    #[test]
2150    fn test_header_footer_with_page_numbers() {
2151        use crate::text::HeaderFooter;
2152
2153        let mut page = Page::a4();
2154
2155        let footer = HeaderFooter::new_footer("Page {{page_number}} of {{total_pages}}");
2156        page.set_footer(footer);
2157
2158        // Generate content with page info
2159        let content = page
2160            .generate_content_with_page_info(Some(3), Some(10), None)
2161            .unwrap();
2162        assert!(!content.is_empty());
2163
2164        // The content should contain the rendered footer
2165        let content_str = String::from_utf8_lossy(&content);
2166        assert!(content_str.contains("Page 3 of 10"));
2167    }
2168
2169    #[test]
2170    fn test_page_content_with_headers_footers() {
2171        use crate::text::{HeaderFooter, TextAlign};
2172
2173        let mut page = Page::a4();
2174
2175        // Add header
2176        let header = HeaderFooter::new_header("Document Title")
2177            .with_font(Font::HelveticaBold, 14.0)
2178            .with_alignment(TextAlign::Center);
2179        page.set_header(header);
2180
2181        // Add footer
2182        let footer = HeaderFooter::new_footer("Page {{page_number}}")
2183            .with_font(Font::Helvetica, 10.0)
2184            .with_alignment(TextAlign::Right);
2185        page.set_footer(footer);
2186
2187        // Add main content
2188        page.text()
2189            .set_font(Font::TimesRoman, 12.0)
2190            .at(100.0, 700.0)
2191            .write("Main content here")
2192            .unwrap();
2193
2194        // Generate with page info
2195        let content = page
2196            .generate_content_with_page_info(Some(1), Some(5), None)
2197            .unwrap();
2198        assert!(!content.is_empty());
2199
2200        // Verify that content was generated (it includes header, main content, and footer)
2201        // Note: We generate raw PDF content streams here, not the full PDF
2202        // The content may be in PDF format with operators like BT/ET, Tj, etc.
2203        assert!(content.len() > 100); // Should have substantial content
2204    }
2205
2206    #[test]
2207    fn test_no_headers_footers() {
2208        let mut page = Page::a4();
2209
2210        // No headers/footers set
2211        assert!(page.header().is_none());
2212        assert!(page.footer().is_none());
2213
2214        // Content generation should work without headers/footers
2215        let content = page
2216            .generate_content_with_page_info(Some(1), Some(1), None)
2217            .unwrap();
2218        assert!(content.is_empty() || !content.is_empty()); // May be empty or contain default content
2219    }
2220
2221    #[test]
2222    fn test_header_footer_custom_values() {
2223        use crate::text::HeaderFooter;
2224        use std::collections::HashMap;
2225
2226        let mut page = Page::a4();
2227
2228        let header = HeaderFooter::new_header("{{company}} - {{title}}");
2229        page.set_header(header);
2230
2231        let mut custom_values = HashMap::new();
2232        custom_values.insert("company".to_string(), "ACME Corp".to_string());
2233        custom_values.insert("title".to_string(), "Annual Report".to_string());
2234
2235        let content = page
2236            .generate_content_with_page_info(Some(1), Some(1), Some(&custom_values))
2237            .unwrap();
2238        let content_str = String::from_utf8_lossy(&content);
2239        assert!(content_str.contains("ACME Corp - Annual Report"));
2240    }
2241
2242    // Integration tests for Page ↔ Document ↔ Writer interactions
2243    mod integration_tests {
2244        use super::*;
2245        use crate::document::Document;
2246        use crate::writer::PdfWriter;
2247        use std::fs;
2248        use tempfile::TempDir;
2249
2250        #[test]
2251        fn test_page_document_integration() {
2252            let mut doc = Document::new();
2253            doc.set_title("Page Integration Test");
2254
2255            // Create pages with different sizes
2256            let page1 = Page::a4();
2257            let page2 = Page::letter();
2258            let mut page3 = Page::new(400.0, 600.0);
2259
2260            // Add content to custom page
2261            page3.set_margins(20.0, 20.0, 20.0, 20.0);
2262            page3
2263                .text()
2264                .set_font(Font::Helvetica, 14.0)
2265                .at(50.0, 550.0)
2266                .write("Custom page content")
2267                .unwrap();
2268
2269            doc.add_page(page1);
2270            doc.add_page(page2);
2271            doc.add_page(page3);
2272
2273            assert_eq!(doc.pages.len(), 3);
2274
2275            // Verify page properties are preserved
2276            assert_eq!(doc.pages[0].width(), 595.0); // A4
2277            assert_eq!(doc.pages[1].width(), 612.0); // Letter
2278            assert_eq!(doc.pages[2].width(), 400.0); // Custom
2279
2280            // Verify content generation works
2281            let mut page_copy = doc.pages[2].clone();
2282            let content = page_copy.generate_content().unwrap();
2283            assert!(!content.is_empty());
2284        }
2285
2286        #[test]
2287        fn test_page_writer_integration() {
2288            let temp_dir = TempDir::new().unwrap();
2289            let file_path = temp_dir.path().join("page_writer_test.pdf");
2290
2291            let mut doc = Document::new();
2292            doc.set_title("Page Writer Integration");
2293
2294            // Create a page with complex content
2295            let mut page = Page::a4();
2296            page.set_margins(50.0, 50.0, 50.0, 50.0);
2297
2298            // Add text content
2299            page.text()
2300                .set_font(Font::Helvetica, 16.0)
2301                .at(100.0, 750.0)
2302                .write("Integration Test Header")
2303                .unwrap();
2304
2305            page.text()
2306                .set_font(Font::TimesRoman, 12.0)
2307                .at(100.0, 700.0)
2308                .write("This is body text for the integration test.")
2309                .unwrap();
2310
2311            // Add graphics content
2312            page.graphics()
2313                .set_fill_color(Color::rgb(0.2, 0.6, 0.9))
2314                .rect(100.0, 600.0, 200.0, 50.0)
2315                .fill();
2316
2317            page.graphics()
2318                .set_stroke_color(Color::rgb(0.8, 0.2, 0.2))
2319                .set_line_width(3.0)
2320                .circle(300.0, 500.0, 40.0)
2321                .stroke();
2322
2323            doc.add_page(page);
2324
2325            // Write to file
2326            let mut writer = PdfWriter::new(&file_path).unwrap();
2327            writer.write_document(&mut doc).unwrap();
2328
2329            // Verify file was created and has content
2330            assert!(file_path.exists());
2331            let metadata = fs::metadata(&file_path).unwrap();
2332            assert!(metadata.len() > 1000); // Should be substantial
2333
2334            // Verify PDF structure (text may be compressed, so check for basic structure)
2335            let content = fs::read(&file_path).unwrap();
2336            let content_str = String::from_utf8_lossy(&content);
2337            assert!(content_str.contains("obj")); // Should contain PDF objects
2338            assert!(content_str.contains("stream")); // Should contain content streams
2339        }
2340
2341        #[test]
2342        fn test_page_margins_integration() {
2343            let temp_dir = TempDir::new().unwrap();
2344            let file_path = temp_dir.path().join("margins_test.pdf");
2345
2346            let mut doc = Document::new();
2347            doc.set_title("Margins Integration Test");
2348
2349            // Test different margin configurations
2350            let mut page1 = Page::a4();
2351            page1.set_margins(10.0, 20.0, 30.0, 40.0);
2352
2353            let mut page2 = Page::letter();
2354            page2.set_margins(72.0, 72.0, 72.0, 72.0); // 1 inch margins
2355
2356            let mut page3 = Page::new(500.0, 700.0);
2357            page3.set_margins(0.0, 0.0, 0.0, 0.0); // No margins
2358
2359            // Add content that uses margin information
2360            for (i, page) in [&mut page1, &mut page2, &mut page3].iter_mut().enumerate() {
2361                let (left, bottom, right, top) = page.content_area();
2362
2363                // Place text at content area boundaries
2364                page.text()
2365                    .set_font(Font::Helvetica, 10.0)
2366                    .at(left, top - 20.0)
2367                    .write(&format!(
2368                        "Page {} - Content area: ({:.1}, {:.1}, {:.1}, {:.1})",
2369                        i + 1,
2370                        left,
2371                        bottom,
2372                        right,
2373                        top
2374                    ))
2375                    .unwrap();
2376
2377                // Draw border around content area
2378                page.graphics()
2379                    .set_stroke_color(Color::rgb(0.5, 0.5, 0.5))
2380                    .set_line_width(1.0)
2381                    .rect(left, bottom, right - left, top - bottom)
2382                    .stroke();
2383            }
2384
2385            doc.add_page(page1);
2386            doc.add_page(page2);
2387            doc.add_page(page3);
2388
2389            // Write and verify
2390            let mut writer = PdfWriter::new(&file_path).unwrap();
2391            writer.write_document(&mut doc).unwrap();
2392
2393            assert!(file_path.exists());
2394            let metadata = fs::metadata(&file_path).unwrap();
2395            assert!(metadata.len() > 500); // Should contain substantial content
2396        }
2397
2398        #[test]
2399        fn test_page_image_integration() {
2400            let temp_dir = TempDir::new().unwrap();
2401            let file_path = temp_dir.path().join("image_test.pdf");
2402
2403            let mut doc = Document::new();
2404            doc.set_title("Image Integration Test");
2405
2406            let mut page = Page::a4();
2407
2408            // Create test images
2409            let jpeg_data1 = vec![
2410                0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x64, 0x00, 0xC8, 0x03, 0xFF, 0xD9,
2411            ];
2412            let image1 = Image::from_jpeg_data(jpeg_data1).unwrap();
2413
2414            let jpeg_data2 = vec![
2415                0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x32, 0x00, 0x32, 0x01, 0xFF, 0xD9,
2416            ];
2417            let image2 = Image::from_jpeg_data(jpeg_data2).unwrap();
2418
2419            // Add images to page
2420            page.add_image("image1", image1);
2421            page.add_image("image2", image2);
2422
2423            // Draw images at different positions
2424            page.draw_image("image1", 100.0, 600.0, 200.0, 100.0)
2425                .unwrap();
2426            page.draw_image("image2", 350.0, 600.0, 50.0, 50.0).unwrap();
2427
2428            // Add text labels
2429            page.text()
2430                .set_font(Font::Helvetica, 12.0)
2431                .at(100.0, 580.0)
2432                .write("Image 1 (200x100)")
2433                .unwrap();
2434
2435            page.text()
2436                .set_font(Font::Helvetica, 12.0)
2437                .at(350.0, 580.0)
2438                .write("Image 2 (50x50)")
2439                .unwrap();
2440
2441            // Verify images were added before moving page
2442            assert_eq!(page.images().len(), 2, "Two images should be added to page");
2443
2444            doc.add_page(page);
2445
2446            // Write and verify
2447            let mut writer = PdfWriter::new(&file_path).unwrap();
2448            writer.write_document(&mut doc).unwrap();
2449
2450            assert!(file_path.exists());
2451            let metadata = fs::metadata(&file_path).unwrap();
2452            assert!(metadata.len() > 500); // Should contain images and text
2453
2454            // Verify XObject references in PDF
2455            let content = fs::read(&file_path).unwrap();
2456            let content_str = String::from_utf8_lossy(&content);
2457
2458            // Debug: print what we're looking for
2459            tracing::debug!("PDF size: {} bytes", content.len());
2460            tracing::debug!("Contains 'XObject': {}", content_str.contains("XObject"));
2461            tracing::debug!("Contains '/XObject': {}", content_str.contains("/XObject"));
2462
2463            // Check for image-related content
2464            if content_str.contains("/Type /Image") || content_str.contains("DCTDecode") {
2465                tracing::debug!("Found image-related content but no XObject dictionary");
2466            }
2467
2468            // Verify XObject is properly written
2469            assert!(content_str.contains("XObject"));
2470        }
2471
2472        #[test]
2473        fn test_page_text_flow_integration() {
2474            let temp_dir = TempDir::new().unwrap();
2475            let file_path = temp_dir.path().join("text_flow_test.pdf");
2476
2477            let mut doc = Document::new();
2478            doc.set_title("Text Flow Integration Test");
2479
2480            let mut page = Page::a4();
2481            page.set_margins(50.0, 50.0, 50.0, 50.0);
2482
2483            // Create text flow with long content
2484            let mut text_flow = page.text_flow();
2485            text_flow.set_font(Font::TimesRoman, 12.0);
2486            text_flow.at(100.0, 700.0);
2487
2488            let long_text =
2489                "This is a long paragraph that should demonstrate text flow capabilities. "
2490                    .repeat(10);
2491            text_flow.write_wrapped(&long_text).unwrap();
2492
2493            // Add the text flow to the page
2494            page.add_text_flow(&text_flow);
2495
2496            // Also add regular text
2497            page.text()
2498                .set_font(Font::Helvetica, 14.0)
2499                .at(100.0, 750.0)
2500                .write("Regular Text Above Text Flow")
2501                .unwrap();
2502
2503            doc.add_page(page);
2504
2505            // Write and verify
2506            let mut writer = PdfWriter::new(&file_path).unwrap();
2507            writer.write_document(&mut doc).unwrap();
2508
2509            assert!(file_path.exists());
2510            let metadata = fs::metadata(&file_path).unwrap();
2511            assert!(metadata.len() > 1000); // Should contain text content
2512
2513            // Verify text structure appears in PDF
2514            let content = fs::read(&file_path).unwrap();
2515            let content_str = String::from_utf8_lossy(&content);
2516            assert!(content_str.contains("obj")); // Should contain PDF objects
2517            assert!(content_str.contains("stream")); // Should contain content streams
2518        }
2519
2520        #[test]
2521        fn test_page_complex_content_integration() {
2522            let temp_dir = TempDir::new().unwrap();
2523            let file_path = temp_dir.path().join("complex_content_test.pdf");
2524
2525            let mut doc = Document::new();
2526            doc.set_title("Complex Content Integration Test");
2527
2528            let mut page = Page::a4();
2529            page.set_margins(40.0, 40.0, 40.0, 40.0);
2530
2531            // Create complex layered content
2532
2533            // Background graphics
2534            page.graphics()
2535                .set_fill_color(Color::rgb(0.95, 0.95, 0.95))
2536                .rect(50.0, 50.0, 495.0, 742.0)
2537                .fill();
2538
2539            // Header section
2540            page.graphics()
2541                .set_fill_color(Color::rgb(0.2, 0.4, 0.8))
2542                .rect(50.0, 750.0, 495.0, 42.0)
2543                .fill();
2544
2545            page.text()
2546                .set_font(Font::HelveticaBold, 18.0)
2547                .at(60.0, 765.0)
2548                .write("Complex Content Integration Test")
2549                .unwrap();
2550
2551            // Content sections with mixed elements
2552            let mut y_pos = 700.0;
2553            for i in 1..=3 {
2554                // Section header
2555                page.graphics()
2556                    .set_fill_color(Color::rgb(0.8, 0.8, 0.9))
2557                    .rect(60.0, y_pos, 475.0, 20.0)
2558                    .fill();
2559
2560                page.text()
2561                    .set_font(Font::HelveticaBold, 12.0)
2562                    .at(70.0, y_pos + 5.0)
2563                    .write(&format!("Section {i}"))
2564                    .unwrap();
2565
2566                y_pos -= 30.0;
2567
2568                // Section content
2569                page.text()
2570                    .set_font(Font::TimesRoman, 10.0)
2571                    .at(70.0, y_pos)
2572                    .write(&format!(
2573                        "This is the content for section {i}. It demonstrates mixed content."
2574                    ))
2575                    .unwrap();
2576
2577                // Section graphics
2578                page.graphics()
2579                    .set_stroke_color(Color::rgb(0.6, 0.2, 0.2))
2580                    .set_line_width(2.0)
2581                    .move_to(70.0, y_pos - 10.0)
2582                    .line_to(530.0, y_pos - 10.0)
2583                    .stroke();
2584
2585                y_pos -= 50.0;
2586            }
2587
2588            // Footer
2589            page.graphics()
2590                .set_fill_color(Color::rgb(0.3, 0.3, 0.3))
2591                .rect(50.0, 50.0, 495.0, 30.0)
2592                .fill();
2593
2594            page.text()
2595                .set_font(Font::Helvetica, 10.0)
2596                .at(60.0, 60.0)
2597                .write("Generated by oxidize-pdf integration test")
2598                .unwrap();
2599
2600            doc.add_page(page);
2601
2602            // Write and verify
2603            let mut writer = PdfWriter::new(&file_path).unwrap();
2604            writer.write_document(&mut doc).unwrap();
2605
2606            assert!(file_path.exists());
2607            let metadata = fs::metadata(&file_path).unwrap();
2608            assert!(metadata.len() > 500); // Should contain substantial content
2609
2610            // Verify content structure (text may be compressed, so check for basic structure)
2611            let content = fs::read(&file_path).unwrap();
2612            let content_str = String::from_utf8_lossy(&content);
2613            assert!(content_str.contains("obj")); // Should contain PDF objects
2614            assert!(content_str.contains("stream")); // Should contain content streams
2615            assert!(content_str.contains("endobj")); // Should contain object endings
2616        }
2617
2618        #[test]
2619        fn test_page_content_generation_performance() {
2620            let mut page = Page::a4();
2621
2622            // Add many elements to test performance
2623            for i in 0..100 {
2624                let y = 800.0 - (i as f64 * 7.0);
2625                if y > 50.0 {
2626                    page.text()
2627                        .set_font(Font::Helvetica, 8.0)
2628                        .at(50.0, y)
2629                        .write(&format!("Performance test line {i}"))
2630                        .unwrap();
2631                }
2632            }
2633
2634            // Add graphics elements
2635            for i in 0..50 {
2636                let x = 50.0 + (i as f64 * 10.0);
2637                if x < 550.0 {
2638                    page.graphics()
2639                        .set_fill_color(Color::rgb(0.5, 0.5, 0.8))
2640                        .rect(x, 400.0, 8.0, 8.0)
2641                        .fill();
2642                }
2643            }
2644
2645            // Content generation should complete in reasonable time
2646            let start = std::time::Instant::now();
2647            let content = page.generate_content().unwrap();
2648            let duration = start.elapsed();
2649
2650            assert!(!content.is_empty());
2651            assert!(duration.as_millis() < 1000); // Should complete within 1 second
2652        }
2653
2654        #[test]
2655        fn test_page_error_handling() {
2656            let mut page = Page::a4();
2657
2658            // Test drawing non-existent image
2659            let result = page.draw_image("nonexistent", 100.0, 100.0, 50.0, 50.0);
2660            assert!(result.is_err());
2661
2662            // Test with invalid parameters - should still work
2663            let result = page.draw_image("still_nonexistent", -100.0, -100.0, 0.0, 0.0);
2664            assert!(result.is_err());
2665
2666            // Add an image and test valid drawing
2667            let jpeg_data = vec![
2668                0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x32, 0x00, 0x32, 0x01, 0xFF, 0xD9,
2669            ];
2670            let image = Image::from_jpeg_data(jpeg_data).unwrap();
2671            page.add_image("valid_image", image);
2672
2673            let result = page.draw_image("valid_image", 100.0, 100.0, 50.0, 50.0);
2674            assert!(result.is_ok());
2675        }
2676
2677        #[test]
2678        fn test_page_memory_management() {
2679            let mut pages = Vec::new();
2680
2681            // Create many pages to test memory usage
2682            for i in 0..100 {
2683                let mut page = Page::a4();
2684                page.set_margins(i as f64, i as f64, i as f64, i as f64);
2685
2686                page.text()
2687                    .set_font(Font::Helvetica, 12.0)
2688                    .at(100.0, 700.0)
2689                    .write(&format!("Page {i}"))
2690                    .unwrap();
2691
2692                pages.push(page);
2693            }
2694
2695            // All pages should be valid
2696            assert_eq!(pages.len(), 100);
2697
2698            // Content generation should work for all pages
2699            for page in pages.iter_mut() {
2700                let content = page.generate_content().unwrap();
2701                assert!(!content.is_empty());
2702            }
2703        }
2704
2705        #[test]
2706        fn test_page_standard_sizes() {
2707            let a4 = Page::a4();
2708            let letter = Page::letter();
2709            let custom = Page::new(200.0, 300.0);
2710
2711            // Test standard dimensions
2712            assert_eq!(a4.width(), 595.0);
2713            assert_eq!(a4.height(), 842.0);
2714            assert_eq!(letter.width(), 612.0);
2715            assert_eq!(letter.height(), 792.0);
2716            assert_eq!(custom.width(), 200.0);
2717            assert_eq!(custom.height(), 300.0);
2718
2719            // Test content areas with default margins
2720            let a4_content_width = a4.content_width();
2721            let letter_content_width = letter.content_width();
2722            let custom_content_width = custom.content_width();
2723
2724            assert_eq!(a4_content_width, 595.0 - 144.0); // 595 - 2*72
2725            assert_eq!(letter_content_width, 612.0 - 144.0); // 612 - 2*72
2726            assert_eq!(custom_content_width, 200.0 - 144.0); // 200 - 2*72
2727        }
2728
2729        #[test]
2730        fn test_header_footer_document_integration() {
2731            use crate::text::{HeaderFooter, TextAlign};
2732
2733            let temp_dir = TempDir::new().unwrap();
2734            let file_path = temp_dir.path().join("header_footer_test.pdf");
2735
2736            let mut doc = Document::new();
2737            doc.set_title("Header Footer Integration Test");
2738
2739            // Create multiple pages with headers and footers
2740            for i in 1..=3 {
2741                let mut page = Page::a4();
2742
2743                // Set header
2744                let header = HeaderFooter::new_header(format!("Chapter {i}"))
2745                    .with_font(Font::HelveticaBold, 16.0)
2746                    .with_alignment(TextAlign::Center);
2747                page.set_header(header);
2748
2749                // Set footer with page numbers
2750                let footer = HeaderFooter::new_footer("Page {{page_number}} of {{total_pages}}")
2751                    .with_font(Font::Helvetica, 10.0)
2752                    .with_alignment(TextAlign::Center);
2753                page.set_footer(footer);
2754
2755                // Add content
2756                page.text()
2757                    .set_font(Font::TimesRoman, 12.0)
2758                    .at(100.0, 700.0)
2759                    .write(&format!("This is the content of chapter {i}"))
2760                    .unwrap();
2761
2762                doc.add_page(page);
2763            }
2764
2765            // Write to file
2766            let mut writer = PdfWriter::new(&file_path).unwrap();
2767            writer.write_document(&mut doc).unwrap();
2768
2769            // Verify file was created
2770            assert!(file_path.exists());
2771            let metadata = fs::metadata(&file_path).unwrap();
2772            assert!(metadata.len() > 1000);
2773
2774            // Read and verify content
2775            let content = fs::read(&file_path).unwrap();
2776            let content_str = String::from_utf8_lossy(&content);
2777
2778            // PDF was created successfully and has substantial content
2779            assert!(content.len() > 2000);
2780            // Verify basic PDF structure
2781            assert!(content_str.contains("%PDF"));
2782            assert!(content_str.contains("endobj"));
2783
2784            // Note: Content may be compressed, so we can't directly check for text strings
2785            // The important thing is that the PDF was generated without errors
2786        }
2787
2788        #[test]
2789        fn test_header_footer_alignment_integration() {
2790            use crate::text::{HeaderFooter, TextAlign};
2791
2792            let temp_dir = TempDir::new().unwrap();
2793            let file_path = temp_dir.path().join("alignment_test.pdf");
2794
2795            let mut doc = Document::new();
2796
2797            let mut page = Page::a4();
2798
2799            // Left-aligned header
2800            let header = HeaderFooter::new_header("Left Header")
2801                .with_font(Font::Helvetica, 12.0)
2802                .with_alignment(TextAlign::Left)
2803                .with_margin(50.0);
2804            page.set_header(header);
2805
2806            // Right-aligned footer
2807            let footer = HeaderFooter::new_footer("Right Footer - Page {{page_number}}")
2808                .with_font(Font::Helvetica, 10.0)
2809                .with_alignment(TextAlign::Right)
2810                .with_margin(50.0);
2811            page.set_footer(footer);
2812
2813            doc.add_page(page);
2814
2815            // Write to file
2816            let mut writer = PdfWriter::new(&file_path).unwrap();
2817            writer.write_document(&mut doc).unwrap();
2818
2819            assert!(file_path.exists());
2820        }
2821
2822        #[test]
2823        fn test_header_footer_date_time_integration() {
2824            use crate::text::HeaderFooter;
2825
2826            let temp_dir = TempDir::new().unwrap();
2827            let file_path = temp_dir.path().join("date_time_test.pdf");
2828
2829            let mut doc = Document::new();
2830
2831            let mut page = Page::a4();
2832
2833            // Header with date/time
2834            let header = HeaderFooter::new_header("Report generated on {{date}} at {{time}}")
2835                .with_font(Font::Helvetica, 11.0);
2836            page.set_header(header);
2837
2838            // Footer with year
2839            let footer =
2840                HeaderFooter::new_footer("© {{year}} Company Name").with_font(Font::Helvetica, 9.0);
2841            page.set_footer(footer);
2842
2843            doc.add_page(page);
2844
2845            // Write to file
2846            let mut writer = PdfWriter::new(&file_path).unwrap();
2847            writer.write_document(&mut doc).unwrap();
2848
2849            assert!(file_path.exists());
2850
2851            // Verify the file was created successfully
2852            let content = fs::read(&file_path).unwrap();
2853            assert!(content.len() > 500);
2854
2855            // Verify basic PDF structure
2856            let content_str = String::from_utf8_lossy(&content);
2857            assert!(content_str.contains("%PDF"));
2858            assert!(content_str.contains("endobj"));
2859
2860            // Note: We can't check for specific text content as it may be compressed
2861            // The test validates that headers/footers with date placeholders don't cause errors
2862        }
2863    }
2864
2865    // ── Task 8: FontMetricsStore threading through Page ──────────────────────
2866
2867    #[test]
2868    fn test_page_a4_default_has_no_metrics_store() {
2869        let page = Page::a4();
2870        assert!(
2871            page.font_metrics_store.is_none(),
2872            "Page::a4() must not bind a store; binding happens via Document"
2873        );
2874    }
2875
2876    #[test]
2877    fn test_page_a4_with_metrics_carries_store() {
2878        use crate::text::metrics::FontMetricsStore;
2879        let store = FontMetricsStore::new();
2880        let page = Page::a4_with_metrics(store);
2881        assert!(page.font_metrics_store.is_some());
2882    }
2883
2884    #[test]
2885    fn test_page_text_flow_propagates_store() {
2886        use crate::text::metrics::FontMetricsStore;
2887        let store = FontMetricsStore::new();
2888        let page = Page::a4_with_metrics(store);
2889        let flow = page.text_flow();
2890        assert!(
2891            flow.font_metrics_store.is_some(),
2892            "page.text_flow() must propagate the store handle"
2893        );
2894    }
2895}
2896
2897#[cfg(test)]
2898mod unit_tests {
2899    use super::*;
2900    use crate::graphics::Color;
2901    use crate::text::Font;
2902
2903    // ============= Constructor Tests =============
2904
2905    #[test]
2906    fn test_new_page_dimensions() {
2907        let page = Page::new(100.0, 200.0);
2908        assert_eq!(page.width(), 100.0);
2909        assert_eq!(page.height(), 200.0);
2910    }
2911
2912    #[test]
2913    fn test_a4_page_dimensions() {
2914        let page = Page::a4();
2915        assert_eq!(page.width(), 595.0);
2916        assert_eq!(page.height(), 842.0);
2917    }
2918
2919    #[test]
2920    fn test_letter_page_dimensions() {
2921        let page = Page::letter();
2922        assert_eq!(page.width(), 612.0);
2923        assert_eq!(page.height(), 792.0);
2924    }
2925
2926    #[test]
2927    fn test_legal_page_dimensions() {
2928        let page = Page::legal();
2929        assert_eq!(page.width(), 612.0);
2930        assert_eq!(page.height(), 1008.0);
2931    }
2932
2933    // ============= Margins Tests =============
2934
2935    #[test]
2936    fn test_default_margins() {
2937        let page = Page::a4();
2938        let margins = page.margins();
2939        assert_eq!(margins.left, 72.0);
2940        assert_eq!(margins.right, 72.0);
2941        assert_eq!(margins.top, 72.0);
2942        assert_eq!(margins.bottom, 72.0);
2943    }
2944
2945    #[test]
2946    fn test_set_margins() {
2947        let mut page = Page::a4();
2948        page.set_margins(10.0, 20.0, 30.0, 40.0);
2949
2950        let margins = page.margins();
2951        assert_eq!(margins.left, 10.0);
2952        assert_eq!(margins.right, 20.0);
2953        assert_eq!(margins.top, 30.0);
2954        assert_eq!(margins.bottom, 40.0);
2955    }
2956
2957    #[test]
2958    fn test_content_width() {
2959        let mut page = Page::new(600.0, 800.0);
2960        page.set_margins(50.0, 50.0, 0.0, 0.0);
2961        assert_eq!(page.content_width(), 500.0);
2962    }
2963
2964    #[test]
2965    fn test_content_height() {
2966        let mut page = Page::new(600.0, 800.0);
2967        page.set_margins(0.0, 0.0, 100.0, 100.0);
2968        assert_eq!(page.content_height(), 600.0);
2969    }
2970
2971    #[test]
2972    fn test_content_area() {
2973        let mut page = Page::new(600.0, 800.0);
2974        page.set_margins(50.0, 60.0, 70.0, 80.0);
2975
2976        let (x, y, right, top) = page.content_area();
2977        assert_eq!(x, 50.0); // left margin
2978        assert_eq!(y, 80.0); // bottom margin
2979        assert_eq!(right, 540.0); // page width (600) - right margin (60)
2980        assert_eq!(top, 730.0); // page height (800) - top margin (70)
2981    }
2982
2983    // ============= Graphics Context Tests =============
2984
2985    #[test]
2986    fn test_graphics_context_access() {
2987        let mut page = Page::a4();
2988        let gc = page.graphics();
2989
2990        // Test that we can perform basic operations
2991        gc.move_to(0.0, 0.0);
2992        gc.line_to(100.0, 100.0);
2993
2994        // Operations should be recorded
2995        let ops = gc.get_operations();
2996        assert!(!ops.is_empty());
2997    }
2998
2999    #[test]
3000    fn test_graphics_operations_chain() {
3001        let mut page = Page::a4();
3002
3003        page.graphics()
3004            .set_fill_color(Color::red())
3005            .rectangle(10.0, 10.0, 100.0, 50.0)
3006            .fill();
3007
3008        let ops = page.graphics().get_operations();
3009        assert!(ops.contains("re")); // rectangle operator
3010        assert!(ops.contains("f")); // fill operator
3011    }
3012
3013    // ============= Text Context Tests =============
3014
3015    #[test]
3016    fn test_text_context_access() {
3017        let mut page = Page::a4();
3018        let tc = page.text();
3019
3020        tc.set_font(Font::Helvetica, 12.0);
3021        tc.at(100.0, 100.0);
3022
3023        // Should be able to write text without error
3024        let result = tc.write("Test text");
3025        assert!(result.is_ok());
3026    }
3027
3028    // Issue #97: Test that get_used_characters combines both contexts
3029    #[test]
3030    fn test_get_used_characters_from_text_context() {
3031        let mut page = Page::a4();
3032
3033        // Write text via text_context
3034        page.text().write("ABC").unwrap();
3035
3036        // Should capture characters from text_context
3037        let chars = page.get_used_characters();
3038        assert!(chars.is_some());
3039        let chars = chars.unwrap();
3040        assert!(chars.contains(&'A'));
3041        assert!(chars.contains(&'B'));
3042        assert!(chars.contains(&'C'));
3043    }
3044
3045    #[test]
3046    fn test_get_used_characters_combines_both_contexts() {
3047        let mut page = Page::a4();
3048
3049        // Write text via text_context
3050        page.text().write("AB").unwrap();
3051
3052        // Write text via graphics_context
3053        let _ = page.graphics().draw_text("CD", 100.0, 100.0);
3054
3055        // Should capture characters from both contexts
3056        let chars = page.get_used_characters();
3057        assert!(chars.is_some());
3058        let chars = chars.unwrap();
3059        assert!(chars.contains(&'A'));
3060        assert!(chars.contains(&'B'));
3061        assert!(chars.contains(&'C'));
3062        assert!(chars.contains(&'D'));
3063    }
3064
3065    #[test]
3066    fn test_get_used_characters_cjk_via_text_context() {
3067        let mut page = Page::a4();
3068
3069        // Write CJK text via text_context (the bug scenario from issue #97)
3070        page.text()
3071            .set_font(Font::Custom("NotoSansCJK".to_string()), 12.0);
3072        page.text().write("中文").unwrap();
3073
3074        let chars = page.get_used_characters();
3075        assert!(chars.is_some());
3076        let chars = chars.unwrap();
3077        assert!(chars.contains(&'中'));
3078        assert!(chars.contains(&'文'));
3079    }
3080
3081    #[test]
3082    fn test_text_flow_creation() {
3083        let page = Page::a4();
3084        let text_flow = page.text_flow();
3085
3086        // Test that text flow is created without panic
3087        // TextFlowContext doesn't expose its internal state
3088        // but we can verify it's created correctly
3089        let _ = text_flow; // Just ensure it can be created
3090    }
3091
3092    // ============= Image Tests =============
3093
3094    #[test]
3095    fn test_add_image() {
3096        let mut page = Page::a4();
3097
3098        // Create a minimal JPEG image
3099        let image_data = vec![
3100            0xFF, 0xD8, // SOI marker
3101            0xFF, 0xC0, // SOF0 marker
3102            0x00, 0x11, // Length
3103            0x08, // Precision
3104            0x00, 0x10, // Height
3105            0x00, 0x10, // Width
3106            0x03, // Components
3107            0x01, 0x11, 0x00, // Component 1
3108            0x02, 0x11, 0x00, // Component 2
3109            0x03, 0x11, 0x00, // Component 3
3110            0xFF, 0xD9, // EOI marker
3111        ];
3112
3113        let image = Image::from_jpeg_data(image_data).unwrap();
3114        page.add_image("test_image", image);
3115
3116        // Image should be stored
3117        assert!(page.images.contains_key("test_image"));
3118    }
3119
3120    #[test]
3121    fn test_draw_image_simple() {
3122        let mut page = Page::a4();
3123
3124        // Create and add image
3125        let image_data = vec![
3126            0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x10, 0x00, 0x10, 0x03, 0x01, 0x11,
3127            0x00, 0x02, 0x11, 0x00, 0x03, 0x11, 0x00, 0xFF, 0xD9,
3128        ];
3129
3130        let image = Image::from_jpeg_data(image_data).unwrap();
3131        page.add_image("img1", image);
3132
3133        // Draw the image
3134        let result = page.draw_image("img1", 100.0, 100.0, 200.0, 200.0);
3135        assert!(result.is_ok());
3136    }
3137
3138    // ============= Annotations Tests =============
3139
3140    #[test]
3141    fn test_add_annotation() {
3142        use crate::annotations::{Annotation, AnnotationType};
3143        use crate::geometry::{Point, Rectangle};
3144
3145        let mut page = Page::a4();
3146        let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 150.0));
3147        let annotation = Annotation::new(AnnotationType::Text, rect);
3148
3149        page.add_annotation(annotation);
3150        assert_eq!(page.annotations().len(), 1);
3151    }
3152
3153    #[test]
3154    fn test_annotations_mut() {
3155        use crate::annotations::{Annotation, AnnotationType};
3156        use crate::geometry::{Point, Rectangle};
3157
3158        let mut page = Page::a4();
3159        let _rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 150.0));
3160
3161        // Add multiple annotations
3162        for i in 0..3 {
3163            let annotation = Annotation::new(
3164                AnnotationType::Text,
3165                Rectangle::new(
3166                    Point::new(100.0 + i as f64 * 10.0, 100.0),
3167                    Point::new(200.0 + i as f64 * 10.0, 150.0),
3168                ),
3169            );
3170            page.add_annotation(annotation);
3171        }
3172
3173        // Modify annotations
3174        let annotations = page.annotations_mut();
3175        annotations.clear();
3176        assert_eq!(page.annotations().len(), 0);
3177    }
3178
3179    // ============= Form Widget Tests =============
3180
3181    #[test]
3182    fn test_add_form_widget() {
3183        use crate::forms::Widget;
3184        use crate::geometry::{Point, Rectangle};
3185
3186        let mut page = Page::a4();
3187        let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 120.0));
3188        let widget = Widget::new(rect);
3189
3190        let obj_ref = page.add_form_widget(widget);
3191        assert_eq!(obj_ref.number(), 0);
3192        assert_eq!(obj_ref.generation(), 0);
3193
3194        // Annotations should include the widget
3195        assert_eq!(page.annotations().len(), 1);
3196    }
3197
3198    // ============= Header/Footer Tests =============
3199
3200    #[test]
3201    fn test_set_header() {
3202        use crate::text::HeaderFooter;
3203
3204        let mut page = Page::a4();
3205        let header = HeaderFooter::new_header("Test Header");
3206
3207        page.set_header(header);
3208        assert!(page.header().is_some());
3209
3210        if let Some(h) = page.header() {
3211            assert_eq!(h.content(), "Test Header");
3212        }
3213    }
3214
3215    #[test]
3216    fn test_set_footer() {
3217        use crate::text::HeaderFooter;
3218
3219        let mut page = Page::a4();
3220        let footer = HeaderFooter::new_footer("Page {{page}} of {{total}}");
3221
3222        page.set_footer(footer);
3223        assert!(page.footer().is_some());
3224
3225        if let Some(f) = page.footer() {
3226            assert_eq!(f.content(), "Page {{page}} of {{total}}");
3227        }
3228    }
3229
3230    #[test]
3231    fn test_header_footer_rendering() {
3232        use crate::text::HeaderFooter;
3233
3234        let mut page = Page::a4();
3235
3236        // Set both header and footer
3237        page.set_header(HeaderFooter::new_header("Header"));
3238        page.set_footer(HeaderFooter::new_footer("Footer"));
3239
3240        // Generate content with header/footer
3241        let result = page.generate_content_with_page_info(Some(1), Some(1), None);
3242        assert!(result.is_ok());
3243
3244        let content = result.unwrap();
3245        assert!(!content.is_empty());
3246    }
3247
3248    // ============= Table Tests =============
3249
3250    #[test]
3251    fn test_add_table() {
3252        use crate::text::Table;
3253
3254        let mut page = Page::a4();
3255        let mut table = Table::with_equal_columns(2, 200.0);
3256
3257        // Add some rows
3258        table
3259            .add_row(vec!["Cell 1".to_string(), "Cell 2".to_string()])
3260            .unwrap();
3261        table
3262            .add_row(vec!["Cell 3".to_string(), "Cell 4".to_string()])
3263            .unwrap();
3264
3265        let result = page.add_table(&table);
3266        assert!(result.is_ok());
3267    }
3268
3269    // ============= Content Generation Tests =============
3270
3271    #[test]
3272    fn test_generate_operations_empty() {
3273        let page = Page::a4();
3274        // Page doesn't have generate_operations, use graphics_context
3275        let ops = page.graphics_context.generate_operations();
3276
3277        // Even empty page should have valid PDF operations
3278        assert!(ops.is_ok());
3279    }
3280
3281    #[test]
3282    fn test_generate_operations_with_graphics() {
3283        let mut page = Page::a4();
3284
3285        page.graphics().rectangle(50.0, 50.0, 100.0, 100.0).fill();
3286
3287        // Page doesn't have generate_operations, use graphics_context
3288        let ops = page.graphics_context.generate_operations();
3289        assert!(ops.is_ok());
3290
3291        let content = ops.unwrap();
3292        let content_str = String::from_utf8_lossy(&content);
3293        assert!(content_str.contains("re")); // rectangle
3294        assert!(content_str.contains("f")); // fill
3295    }
3296
3297    #[test]
3298    fn test_generate_operations_with_text() {
3299        let mut page = Page::a4();
3300
3301        page.text()
3302            .set_font(Font::Helvetica, 12.0)
3303            .at(100.0, 700.0)
3304            .write("Hello")
3305            .unwrap();
3306
3307        // Text operations are in text_context, not graphics_context
3308        let ops = page.text_context.generate_operations();
3309        assert!(ops.is_ok());
3310
3311        let content = ops.unwrap();
3312        let content_str = String::from_utf8_lossy(&content);
3313        assert!(content_str.contains("BT")); // Begin text
3314        assert!(content_str.contains("ET")); // End text
3315    }
3316
3317    // ============= Edge Cases and Error Handling =============
3318
3319    #[test]
3320    fn test_negative_margins() {
3321        let mut page = Page::a4();
3322        page.set_margins(-10.0, -20.0, -30.0, -40.0);
3323
3324        // Negative margins should still work (might be intentional)
3325        let margins = page.margins();
3326        assert_eq!(margins.left, -10.0);
3327        assert_eq!(margins.right, -20.0);
3328    }
3329
3330    #[test]
3331    fn test_zero_dimensions() {
3332        let page = Page::new(0.0, 0.0);
3333        assert_eq!(page.width(), 0.0);
3334        assert_eq!(page.height(), 0.0);
3335
3336        // Content area with default margins would be negative
3337        let (_, _, width, height) = page.content_area();
3338        assert!(width < 0.0);
3339        assert!(height < 0.0);
3340    }
3341
3342    #[test]
3343    fn test_huge_dimensions() {
3344        let page = Page::new(1_000_000.0, 1_000_000.0);
3345        assert_eq!(page.width(), 1_000_000.0);
3346        assert_eq!(page.height(), 1_000_000.0);
3347    }
3348
3349    #[test]
3350    fn test_draw_nonexistent_image() {
3351        let mut page = Page::a4();
3352
3353        // Try to draw an image that wasn't added
3354        let result = page.draw_image("nonexistent", 100.0, 100.0, 200.0, 200.0);
3355
3356        // Should fail gracefully
3357        assert!(result.is_err());
3358    }
3359
3360    #[test]
3361    fn test_clone_page() {
3362        let mut page = Page::a4();
3363        page.set_margins(10.0, 20.0, 30.0, 40.0);
3364
3365        page.graphics().rectangle(50.0, 50.0, 100.0, 100.0).fill();
3366
3367        let cloned = page.clone();
3368        assert_eq!(cloned.width(), page.width());
3369        assert_eq!(cloned.height(), page.height());
3370        assert_eq!(cloned.margins().left, page.margins().left);
3371    }
3372
3373    #[test]
3374    fn test_page_from_parsed_basic() {
3375        use crate::parser::objects::PdfDictionary;
3376        use crate::parser::page_tree::ParsedPage;
3377
3378        // Create a test parsed page
3379        let parsed_page = ParsedPage {
3380            obj_ref: (1, 0),
3381            dict: PdfDictionary::new(),
3382            inherited_resources: None,
3383            media_box: [0.0, 0.0, 612.0, 792.0], // US Letter
3384            crop_box: None,
3385            rotation: 0,
3386            annotations: None,
3387        };
3388
3389        // Convert to writable page
3390        let page = Page::from_parsed(&parsed_page).unwrap();
3391
3392        // Verify dimensions
3393        assert_eq!(page.width(), 612.0);
3394        assert_eq!(page.height(), 792.0);
3395        assert_eq!(page.get_rotation(), 0);
3396    }
3397
3398    #[test]
3399    fn test_page_from_parsed_with_rotation() {
3400        use crate::parser::objects::PdfDictionary;
3401        use crate::parser::page_tree::ParsedPage;
3402
3403        // Create a test parsed page with 90-degree rotation
3404        let parsed_page = ParsedPage {
3405            obj_ref: (1, 0),
3406            dict: PdfDictionary::new(),
3407            inherited_resources: None,
3408            media_box: [0.0, 0.0, 595.0, 842.0], // A4
3409            crop_box: None,
3410            rotation: 90,
3411            annotations: None,
3412        };
3413
3414        // Convert to writable page
3415        let page = Page::from_parsed(&parsed_page).unwrap();
3416
3417        // Verify rotation was preserved
3418        assert_eq!(page.get_rotation(), 90);
3419        assert_eq!(page.width(), 595.0);
3420        assert_eq!(page.height(), 842.0);
3421
3422        // Verify effective dimensions (rotated)
3423        assert_eq!(page.effective_width(), 842.0);
3424        assert_eq!(page.effective_height(), 595.0);
3425    }
3426
3427    #[test]
3428    fn test_page_from_parsed_with_cropbox() {
3429        use crate::parser::objects::PdfDictionary;
3430        use crate::parser::page_tree::ParsedPage;
3431
3432        // Create a test parsed page with CropBox
3433        let parsed_page = ParsedPage {
3434            obj_ref: (1, 0),
3435            dict: PdfDictionary::new(),
3436            inherited_resources: None,
3437            media_box: [0.0, 0.0, 612.0, 792.0],
3438            crop_box: Some([10.0, 10.0, 602.0, 782.0]),
3439            rotation: 0,
3440            annotations: None,
3441        };
3442
3443        // Convert to writable page
3444        let page = Page::from_parsed(&parsed_page).unwrap();
3445
3446        // CropBox doesn't affect page dimensions (only visible area)
3447        assert_eq!(page.width(), 612.0);
3448        assert_eq!(page.height(), 792.0);
3449    }
3450
3451    #[test]
3452    fn test_page_from_parsed_small_mediabox() {
3453        use crate::parser::objects::PdfDictionary;
3454        use crate::parser::page_tree::ParsedPage;
3455
3456        // Create a test parsed page with custom small dimensions
3457        let parsed_page = ParsedPage {
3458            obj_ref: (1, 0),
3459            dict: PdfDictionary::new(),
3460            inherited_resources: None,
3461            media_box: [0.0, 0.0, 200.0, 300.0],
3462            crop_box: None,
3463            rotation: 0,
3464            annotations: None,
3465        };
3466
3467        // Convert to writable page
3468        let page = Page::from_parsed(&parsed_page).unwrap();
3469
3470        assert_eq!(page.width(), 200.0);
3471        assert_eq!(page.height(), 300.0);
3472    }
3473
3474    #[test]
3475    fn test_page_from_parsed_non_zero_origin() {
3476        use crate::parser::objects::PdfDictionary;
3477        use crate::parser::page_tree::ParsedPage;
3478
3479        // Create a test parsed page with non-zero origin
3480        let parsed_page = ParsedPage {
3481            obj_ref: (1, 0),
3482            dict: PdfDictionary::new(),
3483            inherited_resources: None,
3484            media_box: [10.0, 20.0, 610.0, 820.0], // Offset origin
3485            crop_box: None,
3486            rotation: 0,
3487            annotations: None,
3488        };
3489
3490        // Convert to writable page
3491        let page = Page::from_parsed(&parsed_page).unwrap();
3492
3493        // Width and height should be calculated correctly
3494        assert_eq!(page.width(), 600.0); // 610 - 10
3495        assert_eq!(page.height(), 800.0); // 820 - 20
3496    }
3497
3498    #[test]
3499    fn test_page_rotation() {
3500        let mut page = Page::a4();
3501
3502        // Test default rotation
3503        assert_eq!(page.get_rotation(), 0);
3504
3505        // Test setting valid rotations
3506        page.set_rotation(90);
3507        assert_eq!(page.get_rotation(), 90);
3508
3509        page.set_rotation(180);
3510        assert_eq!(page.get_rotation(), 180);
3511
3512        page.set_rotation(270);
3513        assert_eq!(page.get_rotation(), 270);
3514
3515        page.set_rotation(360);
3516        assert_eq!(page.get_rotation(), 0);
3517
3518        // Test rotation normalization
3519        page.set_rotation(45);
3520        assert_eq!(page.get_rotation(), 90);
3521
3522        page.set_rotation(135);
3523        assert_eq!(page.get_rotation(), 180);
3524
3525        page.set_rotation(-90);
3526        assert_eq!(page.get_rotation(), 270);
3527    }
3528
3529    #[test]
3530    fn test_effective_dimensions() {
3531        let mut page = Page::new(600.0, 800.0);
3532
3533        // No rotation - same dimensions
3534        assert_eq!(page.effective_width(), 600.0);
3535        assert_eq!(page.effective_height(), 800.0);
3536
3537        // 90 degree rotation - swapped dimensions
3538        page.set_rotation(90);
3539        assert_eq!(page.effective_width(), 800.0);
3540        assert_eq!(page.effective_height(), 600.0);
3541
3542        // 180 degree rotation - same dimensions
3543        page.set_rotation(180);
3544        assert_eq!(page.effective_width(), 600.0);
3545        assert_eq!(page.effective_height(), 800.0);
3546
3547        // 270 degree rotation - swapped dimensions
3548        page.set_rotation(270);
3549        assert_eq!(page.effective_width(), 800.0);
3550        assert_eq!(page.effective_height(), 600.0);
3551    }
3552
3553    #[test]
3554    fn test_rotation_in_pdf_dict() {
3555        let mut page = Page::a4();
3556
3557        // No rotation should not include Rotate field
3558        let dict = page.to_dict();
3559        assert!(dict.get("Rotate").is_none());
3560
3561        // With rotation should include Rotate field
3562        page.set_rotation(90);
3563        let dict = page.to_dict();
3564        assert_eq!(dict.get("Rotate"), Some(&Object::Integer(90)));
3565
3566        page.set_rotation(270);
3567        let dict = page.to_dict();
3568        assert_eq!(dict.get("Rotate"), Some(&Object::Integer(270)));
3569    }
3570}
3571
3572/// Layout manager for intelligent positioning of elements on a page
3573///
3574/// This manager handles automatic positioning of tables, images, and other elements
3575/// using different coordinate systems while preventing overlaps and managing page flow.
3576#[derive(Debug)]
3577pub struct LayoutManager {
3578    /// Coordinate system being used
3579    pub coordinate_system: crate::coordinate_system::CoordinateSystem,
3580    /// Current Y position for next element
3581    pub current_y: f64,
3582    /// Page dimensions
3583    pub page_width: f64,
3584    pub page_height: f64,
3585    /// Page margins
3586    pub margins: Margins,
3587    /// Spacing between elements
3588    pub element_spacing: f64,
3589}
3590
3591impl LayoutManager {
3592    /// Create a new layout manager for the given page
3593    pub fn new(page: &Page, coordinate_system: crate::coordinate_system::CoordinateSystem) -> Self {
3594        let current_y = match coordinate_system {
3595            crate::coordinate_system::CoordinateSystem::PdfStandard => {
3596                // PDF coordinates: start from top (high Y value)
3597                page.height() - page.margins().top
3598            }
3599            crate::coordinate_system::CoordinateSystem::ScreenSpace => {
3600                // Screen coordinates: start from top (low Y value)
3601                page.margins().top
3602            }
3603            crate::coordinate_system::CoordinateSystem::Custom(_) => {
3604                // For custom systems, start conservatively in the middle
3605                page.height() / 2.0
3606            }
3607        };
3608
3609        Self {
3610            coordinate_system,
3611            current_y,
3612            page_width: page.width(),
3613            page_height: page.height(),
3614            margins: page.margins().clone(),
3615            element_spacing: 10.0,
3616        }
3617    }
3618
3619    /// Set custom spacing between elements
3620    pub fn with_element_spacing(mut self, spacing: f64) -> Self {
3621        self.element_spacing = spacing;
3622        self
3623    }
3624
3625    /// Check if an element of given height will fit on the current page
3626    pub fn will_fit(&self, element_height: f64) -> bool {
3627        let required_space = element_height + self.element_spacing;
3628
3629        match self.coordinate_system {
3630            crate::coordinate_system::CoordinateSystem::PdfStandard => {
3631                // In PDF coords, we subtract height and check against bottom margin
3632                self.current_y - required_space >= self.margins.bottom
3633            }
3634            crate::coordinate_system::CoordinateSystem::ScreenSpace => {
3635                // In screen coords, we add height and check against page height
3636                self.current_y + required_space <= self.page_height - self.margins.bottom
3637            }
3638            crate::coordinate_system::CoordinateSystem::Custom(_) => {
3639                // Conservative check for custom coordinate systems
3640                required_space <= (self.page_height - self.margins.top - self.margins.bottom) / 2.0
3641            }
3642        }
3643    }
3644
3645    /// Get the current available space remaining on the page
3646    pub fn remaining_space(&self) -> f64 {
3647        match self.coordinate_system {
3648            crate::coordinate_system::CoordinateSystem::PdfStandard => {
3649                (self.current_y - self.margins.bottom).max(0.0)
3650            }
3651            crate::coordinate_system::CoordinateSystem::ScreenSpace => {
3652                (self.page_height - self.margins.bottom - self.current_y).max(0.0)
3653            }
3654            crate::coordinate_system::CoordinateSystem::Custom(_) => {
3655                self.page_height / 2.0 // Conservative estimate
3656            }
3657        }
3658    }
3659
3660    /// Reserve space for an element and return its Y position
3661    ///
3662    /// Returns `None` if the element doesn't fit on the current page.
3663    /// If it fits, returns the Y coordinate where the element should be placed
3664    /// and updates the internal current_y for the next element.
3665    pub fn add_element(&mut self, element_height: f64) -> Option<f64> {
3666        if !self.will_fit(element_height) {
3667            return None;
3668        }
3669
3670        let position_y = match self.coordinate_system {
3671            crate::coordinate_system::CoordinateSystem::PdfStandard => {
3672                // Position element at current_y (top of element)
3673                // Then move current_y down by element height + spacing
3674                let y_position = self.current_y - element_height;
3675                self.current_y = y_position - self.element_spacing;
3676                self.current_y + element_height // Return the bottom Y of the element area
3677            }
3678            crate::coordinate_system::CoordinateSystem::ScreenSpace => {
3679                // Position element at current_y (top of element)
3680                // Then move current_y down by element height + spacing
3681                let y_position = self.current_y;
3682                self.current_y += element_height + self.element_spacing;
3683                y_position
3684            }
3685            crate::coordinate_system::CoordinateSystem::Custom(_) => {
3686                // Simple implementation for custom coordinate systems
3687                let y_position = self.current_y;
3688                self.current_y -= element_height + self.element_spacing;
3689                y_position
3690            }
3691        };
3692
3693        Some(position_y)
3694    }
3695
3696    /// Reset the layout manager for a new page
3697    pub fn new_page(&mut self) {
3698        self.current_y = match self.coordinate_system {
3699            crate::coordinate_system::CoordinateSystem::PdfStandard => {
3700                self.page_height - self.margins.top
3701            }
3702            crate::coordinate_system::CoordinateSystem::ScreenSpace => self.margins.top,
3703            crate::coordinate_system::CoordinateSystem::Custom(_) => self.page_height / 2.0,
3704        };
3705    }
3706
3707    /// Get the X position for centering an element of given width
3708    pub fn center_x(&self, element_width: f64) -> f64 {
3709        let available_width = self.page_width - self.margins.left - self.margins.right;
3710        self.margins.left + (available_width - element_width) / 2.0
3711    }
3712
3713    /// Get the left margin X position
3714    pub fn left_x(&self) -> f64 {
3715        self.margins.left
3716    }
3717
3718    /// Get the right margin X position minus element width
3719    pub fn right_x(&self, element_width: f64) -> f64 {
3720        self.page_width - self.margins.right - element_width
3721    }
3722}
3723
3724#[cfg(test)]
3725mod layout_manager_tests {
3726    use super::*;
3727    use crate::coordinate_system::CoordinateSystem;
3728
3729    #[test]
3730    fn test_layout_manager_pdf_standard() {
3731        let page = Page::a4(); // 595 x 842
3732        let mut layout = LayoutManager::new(&page, CoordinateSystem::PdfStandard);
3733
3734        // Check initial position (should be near top in PDF coords)
3735        // A4 height is 842, with default margin of 72, so current_y should be 842 - 72 = 770
3736        assert!(layout.current_y > 750.0); // Near top of A4, adjusted for actual margins
3737
3738        // Add an element
3739        let element_height = 100.0;
3740        let position = layout.add_element(element_height);
3741
3742        assert!(position.is_some());
3743        let y_pos = position.unwrap();
3744        assert!(y_pos > 700.0); // Should be positioned high up
3745
3746        // Current Y should have moved down
3747        assert!(layout.current_y < y_pos);
3748    }
3749
3750    #[test]
3751    fn test_layout_manager_screen_space() {
3752        let page = Page::a4();
3753        let mut layout = LayoutManager::new(&page, CoordinateSystem::ScreenSpace);
3754
3755        // Check initial position (should be near top in screen coords)
3756        assert!(layout.current_y < 100.0); // Near top margin
3757
3758        // Add an element
3759        let element_height = 100.0;
3760        let position = layout.add_element(element_height);
3761
3762        assert!(position.is_some());
3763        let y_pos = position.unwrap();
3764        assert!(y_pos < 100.0); // Should be positioned near top
3765
3766        // Current Y should have moved down (increased)
3767        assert!(layout.current_y > y_pos);
3768    }
3769
3770    #[test]
3771    fn test_layout_manager_overflow() {
3772        let page = Page::a4();
3773        let mut layout = LayoutManager::new(&page, CoordinateSystem::PdfStandard);
3774
3775        // Try to add an element that's too large
3776        let huge_element = 900.0; // Larger than page height
3777        let position = layout.add_element(huge_element);
3778
3779        assert!(position.is_none()); // Should not fit
3780
3781        // Fill the page with smaller elements
3782        let mut count = 0;
3783        while layout.add_element(50.0).is_some() {
3784            count += 1;
3785            if count > 100 {
3786                break;
3787            } // Safety valve
3788        }
3789
3790        // Should have added multiple elements
3791        assert!(count > 5);
3792
3793        // Next element should not fit
3794        assert!(layout.add_element(50.0).is_none());
3795    }
3796
3797    #[test]
3798    fn test_layout_manager_centering() {
3799        let page = Page::a4();
3800        let layout = LayoutManager::new(&page, CoordinateSystem::PdfStandard);
3801
3802        let element_width = 200.0;
3803        let center_x = layout.center_x(element_width);
3804
3805        // Should be centered considering margins
3806        let expected_center = page.margins().left
3807            + (page.width() - page.margins().left - page.margins().right - element_width) / 2.0;
3808        assert_eq!(center_x, expected_center);
3809    }
3810}