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::{HeaderFooter, Table, TextContext, TextFlowContext};
8use std::collections::{HashMap, HashSet};
9
10/// Page margins in points (1/72 inch).
11#[derive(Clone, Debug)]
12pub struct Margins {
13    /// Left margin
14    pub left: f64,
15    /// Right margin
16    pub right: f64,
17    /// Top margin
18    pub top: f64,
19    /// Bottom margin
20    pub bottom: f64,
21}
22
23impl Default for Margins {
24    fn default() -> Self {
25        Self {
26            left: 72.0,   // 1 inch
27            right: 72.0,  // 1 inch
28            top: 72.0,    // 1 inch
29            bottom: 72.0, // 1 inch
30        }
31    }
32}
33
34/// A single page in a PDF document.
35///
36/// Pages have a size (width and height in points), margins, and can contain
37/// graphics, text, and images.
38///
39/// # Example
40///
41/// ```rust
42/// use oxidize_pdf::{Page, Font, Color};
43///
44/// let mut page = Page::a4();
45///
46/// // Add text
47/// page.text()
48///     .set_font(Font::Helvetica, 12.0)
49///     .at(100.0, 700.0)
50///     .write("Hello World")?;
51///
52/// // Add graphics
53/// page.graphics()
54///     .set_fill_color(Color::red())
55///     .rect(100.0, 100.0, 200.0, 150.0)
56///     .fill();
57/// # Ok::<(), oxidize_pdf::PdfError>(())
58/// ```
59#[derive(Clone)]
60pub struct Page {
61    width: f64,
62    height: f64,
63    margins: Margins,
64    content: Vec<u8>,
65    graphics_context: GraphicsContext,
66    text_context: TextContext,
67    images: HashMap<String, Image>,
68    header: Option<HeaderFooter>,
69    footer: Option<HeaderFooter>,
70    annotations: Vec<Annotation>,
71    coordinate_system: crate::coordinate_system::CoordinateSystem,
72    rotation: i32, // Page rotation in degrees (0, 90, 180, 270)
73    /// Next MCID (Marked Content ID) for tagged PDF
74    next_mcid: u32,
75    /// Currently open marked content tags (for nesting validation)
76    marked_content_stack: Vec<String>,
77    /// Preserved resources from original PDF (for overlay operations)
78    /// Contains fonts, XObjects, ColorSpaces, etc. from parsed pages
79    preserved_resources: Option<crate::pdf_objects::Dictionary>,
80}
81
82impl Page {
83    /// Creates a new page with the specified width and height in points.
84    ///
85    /// Points are 1/72 of an inch.
86    pub fn new(width: f64, height: f64) -> Self {
87        Self {
88            width,
89            height,
90            margins: Margins::default(),
91            content: Vec::new(),
92            graphics_context: GraphicsContext::new(),
93            text_context: TextContext::new(),
94            images: HashMap::new(),
95            header: None,
96            footer: None,
97            annotations: Vec::new(),
98            coordinate_system: crate::coordinate_system::CoordinateSystem::PdfStandard,
99            rotation: 0, // Default to no rotation
100            next_mcid: 0,
101            marked_content_stack: Vec::new(),
102            preserved_resources: None,
103        }
104    }
105
106    /// Creates a writable Page from a parsed page dictionary.
107    ///
108    /// This method bridges the gap between the parser (read-only) and writer (writable)
109    /// by converting a parsed page into a Page structure that can be modified and saved.
110    ///
111    /// **IMPORTANT**: This method preserves the existing content stream and resources,
112    /// allowing you to overlay new content on top of the existing page content without
113    /// manual recreation.
114    ///
115    /// # Arguments
116    ///
117    /// * `parsed_page` - Reference to a parsed page from the PDF parser
118    ///
119    /// # Returns
120    ///
121    /// A writable `Page` with existing content preserved, ready for modification
122    ///
123    /// # Example
124    ///
125    /// ```rust,no_run
126    /// use oxidize_pdf::parser::{PdfReader, PdfDocument};
127    /// use oxidize_pdf::Page;
128    ///
129    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
130    /// // Load existing PDF
131    /// let reader = PdfReader::open("input.pdf")?;
132    /// let document = PdfDocument::new(reader);
133    /// let parsed_page = document.get_page(0)?;
134    ///
135    /// // Convert to writable page
136    /// let mut page = Page::from_parsed(&parsed_page)?;
137    ///
138    /// // Now you can add content on top of existing content
139    /// page.text()
140    ///     .set_font(oxidize_pdf::text::Font::Helvetica, 12.0)
141    ///     .at(100.0, 100.0)
142    ///     .write("Overlaid text")?;
143    /// # Ok(())
144    /// # }
145    /// ```
146    pub fn from_parsed(parsed_page: &crate::parser::page_tree::ParsedPage) -> Result<Self> {
147        // Extract dimensions from MediaBox
148        let media_box = parsed_page.media_box;
149        let width = media_box[2] - media_box[0];
150        let height = media_box[3] - media_box[1];
151
152        // Extract rotation
153        let rotation = parsed_page.rotation;
154
155        // Create base page
156        let mut page = Self::new(width, height);
157        page.rotation = rotation;
158
159        // TODO: Extract and preserve Resources (fonts, images, XObjects)
160        // This requires deeper integration with the parser's resource manager
161
162        // Extract and preserve existing content streams
163        // Note: This requires a PdfDocument reference to resolve content streams
164        // For now, we mark the content field to indicate it should be preserved
165        // The actual content stream extraction will be done when we have access to the reader
166
167        Ok(page)
168    }
169
170    /// Creates a writable Page from a parsed page with content stream preservation.
171    ///
172    /// This is an extended version of `from_parsed()` that requires access to the
173    /// PdfDocument to extract and preserve the original content streams.
174    ///
175    /// # Arguments
176    ///
177    /// * `parsed_page` - Reference to a parsed page
178    /// * `document` - Reference to the PDF document (for content stream resolution)
179    ///
180    /// # Returns
181    ///
182    /// A writable `Page` with original content streams preserved
183    ///
184    /// # Example
185    ///
186    /// ```rust,no_run
187    /// use oxidize_pdf::parser::{PdfReader, PdfDocument};
188    /// use oxidize_pdf::Page;
189    ///
190    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
191    /// let reader = PdfReader::open("input.pdf")?;
192    /// let document = PdfDocument::new(reader);
193    /// let parsed_page = document.get_page(0)?;
194    ///
195    /// // Convert with content preservation
196    /// let mut page = Page::from_parsed_with_content(&parsed_page, &document)?;
197    ///
198    /// // Original content is preserved, overlay will be added on top
199    /// page.text()
200    ///     .set_font(oxidize_pdf::text::Font::Helvetica, 12.0)
201    ///     .at(100.0, 100.0)
202    ///     .write("Overlay text")?;
203    /// # Ok(())
204    /// # }
205    /// ```
206    pub fn from_parsed_with_content<R: std::io::Read + std::io::Seek>(
207        parsed_page: &crate::parser::page_tree::ParsedPage,
208        document: &crate::parser::document::PdfDocument<R>,
209    ) -> Result<Self> {
210        // Extract dimensions from MediaBox
211        let media_box = parsed_page.media_box;
212        let width = media_box[2] - media_box[0];
213        let height = media_box[3] - media_box[1];
214
215        // Extract rotation
216        let rotation = parsed_page.rotation;
217
218        // Create base page
219        let mut page = Self::new(width, height);
220        page.rotation = rotation;
221
222        // Extract and preserve existing content streams
223        let content_streams = parsed_page.content_streams_with_document(document)?;
224
225        // Concatenate all content streams
226        let mut preserved_content = Vec::new();
227        for stream in content_streams {
228            preserved_content.extend_from_slice(&stream);
229            // Add a newline between streams for safety
230            preserved_content.push(b'\n');
231        }
232
233        // Store the original content
234        // We'll need to wrap it with q/Q to isolate it when overlaying
235        page.content = preserved_content;
236
237        // Extract and preserve Resources (fonts, images, XObjects, etc.)
238        if let Some(resources) = parsed_page.get_resources() {
239            let mut unified_resources = Self::convert_parser_dict_to_unified(resources);
240
241            // Phase 3.2: Resolve embedded font streams
242            // For each font in resources, resolve FontDescriptor and font stream references
243            // and embed the stream data directly so writer doesn't need to resolve references
244            if let Some(crate::pdf_objects::Object::Dictionary(fonts)) =
245                unified_resources.get("Font")
246            {
247                let fonts_clone = fonts.clone();
248                let mut resolved_fonts = crate::pdf_objects::Dictionary::new();
249
250                for (font_name, font_obj) in fonts_clone.iter() {
251                    // Step 1: Resolve reference if needed to get actual font dictionary
252                    let font_dict = match font_obj {
253                        crate::pdf_objects::Object::Reference(id) => {
254                            // Resolve reference to get actual font dictionary from document
255                            match document.get_object(id.number(), id.generation()) {
256                                Ok(resolved_obj) => {
257                                    // Convert parser object to unified format
258                                    match Self::convert_parser_object_to_unified(&resolved_obj) {
259                                        crate::pdf_objects::Object::Dictionary(dict) => dict,
260                                        _ => {
261                                            // Not a dictionary, keep original reference
262                                            resolved_fonts.set(font_name.clone(), font_obj.clone());
263                                            continue;
264                                        }
265                                    }
266                                }
267                                Err(_) => {
268                                    // Resolution failed, keep original reference
269                                    resolved_fonts.set(font_name.clone(), font_obj.clone());
270                                    continue;
271                                }
272                            }
273                        }
274                        crate::pdf_objects::Object::Dictionary(dict) => dict.clone(),
275                        _ => {
276                            // Neither reference nor dictionary, keep as-is
277                            resolved_fonts.set(font_name.clone(), font_obj.clone());
278                            continue;
279                        }
280                    };
281
282                    // Step 2: Now font_dict is guaranteed to be a Dictionary, resolve embedded streams
283                    match Self::resolve_font_streams(&font_dict, document) {
284                        Ok(resolved_dict) => {
285                            resolved_fonts.set(
286                                font_name.clone(),
287                                crate::pdf_objects::Object::Dictionary(resolved_dict),
288                            );
289                        }
290                        Err(_) => {
291                            // If stream resolution fails, keep the resolved dictionary without streams
292                            resolved_fonts.set(
293                                font_name.clone(),
294                                crate::pdf_objects::Object::Dictionary(font_dict),
295                            );
296                        }
297                    }
298                }
299
300                // Replace Font dictionary with resolved version
301                unified_resources.set(
302                    "Font",
303                    crate::pdf_objects::Object::Dictionary(resolved_fonts),
304                );
305            }
306
307            page.preserved_resources = Some(unified_resources);
308        }
309
310        Ok(page)
311    }
312
313    /// Creates a new A4 page (595 x 842 points).
314    pub fn a4() -> Self {
315        Self::new(595.0, 842.0)
316    }
317
318    /// Creates a new A4 landscape page (842 x 595 points).
319    pub fn a4_landscape() -> Self {
320        Self::new(842.0, 595.0)
321    }
322
323    /// Creates a new US Letter page (612 x 792 points).
324    pub fn letter() -> Self {
325        Self::new(612.0, 792.0)
326    }
327
328    /// Creates a new US Letter landscape page (792 x 612 points).
329    pub fn letter_landscape() -> Self {
330        Self::new(792.0, 612.0)
331    }
332
333    /// Creates a new US Legal page (612 x 1008 points).
334    pub fn legal() -> Self {
335        Self::new(612.0, 1008.0)
336    }
337
338    /// Creates a new US Legal landscape page (1008 x 612 points).
339    pub fn legal_landscape() -> Self {
340        Self::new(1008.0, 612.0)
341    }
342
343    /// Returns a mutable reference to the graphics context for drawing shapes.
344    pub fn graphics(&mut self) -> &mut GraphicsContext {
345        &mut self.graphics_context
346    }
347
348    /// Returns a mutable reference to the text context for adding text.
349    pub fn text(&mut self) -> &mut TextContext {
350        &mut self.text_context
351    }
352
353    pub fn set_margins(&mut self, left: f64, right: f64, top: f64, bottom: f64) {
354        self.margins = Margins {
355            left,
356            right,
357            top,
358            bottom,
359        };
360    }
361
362    pub fn margins(&self) -> &Margins {
363        &self.margins
364    }
365
366    pub fn content_width(&self) -> f64 {
367        self.width - self.margins.left - self.margins.right
368    }
369
370    pub fn content_height(&self) -> f64 {
371        self.height - self.margins.top - self.margins.bottom
372    }
373
374    pub fn content_area(&self) -> (f64, f64, f64, f64) {
375        (
376            self.margins.left,
377            self.margins.bottom,
378            self.width - self.margins.right,
379            self.height - self.margins.top,
380        )
381    }
382
383    pub fn width(&self) -> f64 {
384        self.width
385    }
386
387    pub fn height(&self) -> f64 {
388        self.height
389    }
390
391    /// Get the current coordinate system for this page
392    pub fn coordinate_system(&self) -> crate::coordinate_system::CoordinateSystem {
393        self.coordinate_system
394    }
395
396    /// Set the coordinate system for this page
397    pub fn set_coordinate_system(
398        &mut self,
399        coordinate_system: crate::coordinate_system::CoordinateSystem,
400    ) -> &mut Self {
401        self.coordinate_system = coordinate_system;
402        self
403    }
404
405    /// Sets the page rotation in degrees.
406    /// Valid values are 0, 90, 180, and 270.
407    /// Other values will be normalized to the nearest valid rotation.
408    pub fn set_rotation(&mut self, rotation: i32) {
409        // Normalize rotation to valid values (0, 90, 180, 270)
410        let normalized = rotation.rem_euclid(360); // Ensure positive
411        self.rotation = match normalized {
412            0..=44 | 316..=360 => 0,
413            45..=134 => 90,
414            135..=224 => 180,
415            225..=315 => 270,
416            _ => 0, // Should not happen, but default to 0
417        };
418    }
419
420    /// Converts a parser Dictionary to unified pdf_objects Dictionary
421    fn convert_parser_dict_to_unified(
422        parser_dict: &crate::parser::objects::PdfDictionary,
423    ) -> crate::pdf_objects::Dictionary {
424        use crate::pdf_objects::{Dictionary, Name};
425
426        let mut unified_dict = Dictionary::new();
427
428        for (key, value) in &parser_dict.0 {
429            let unified_key = Name::new(key.as_str());
430            let unified_value = Self::convert_parser_object_to_unified(value);
431            unified_dict.set(unified_key, unified_value);
432        }
433
434        unified_dict
435    }
436
437    /// Converts a parser PdfObject to unified Object
438    fn convert_parser_object_to_unified(
439        parser_obj: &crate::parser::objects::PdfObject,
440    ) -> crate::pdf_objects::Object {
441        use crate::parser::objects::PdfObject;
442        use crate::pdf_objects::{Array, BinaryString, Name, Object, ObjectId, Stream};
443
444        match parser_obj {
445            PdfObject::Null => Object::Null,
446            PdfObject::Boolean(b) => Object::Boolean(*b),
447            PdfObject::Integer(i) => Object::Integer(*i),
448            PdfObject::Real(f) => Object::Real(*f),
449            PdfObject::String(s) => Object::String(BinaryString::new(s.as_bytes().to_vec())),
450            PdfObject::Name(n) => Object::Name(Name::new(n.as_str())),
451            PdfObject::Array(arr) => {
452                let mut unified_arr = Array::new();
453                for item in &arr.0 {
454                    unified_arr.push(Self::convert_parser_object_to_unified(item));
455                }
456                Object::Array(unified_arr)
457            }
458            PdfObject::Dictionary(dict) => {
459                Object::Dictionary(Self::convert_parser_dict_to_unified(dict))
460            }
461            PdfObject::Stream(stream) => {
462                let dict = Self::convert_parser_dict_to_unified(&stream.dict);
463                let data = stream.data.clone();
464                Object::Stream(Stream::new(dict, data))
465            }
466            PdfObject::Reference(num, gen) => Object::Reference(ObjectId::new(*num, *gen)),
467        }
468    }
469
470    /// Resolves embedded font streams from a font dictionary (Phase 3.2 + Phase 3.4)
471    ///
472    /// Takes a font dictionary and resolves any FontDescriptor + FontFile references,
473    /// embedding the stream data directly so the writer doesn't need to resolve references.
474    ///
475    /// For Type0 (composite) fonts, this also resolves the complete hierarchy:
476    /// Type0 → DescendantFonts → CIDFont → FontDescriptor → FontFile2/FontFile3
477    ///
478    /// # Returns
479    /// Font dictionary with embedded streams (if font has embedded data),
480    /// or original dictionary (if standard font or resolution fails)
481    fn resolve_font_streams<R: std::io::Read + std::io::Seek>(
482        font_dict: &crate::pdf_objects::Dictionary,
483        document: &crate::parser::document::PdfDocument<R>,
484    ) -> Result<crate::pdf_objects::Dictionary> {
485        use crate::pdf_objects::Object;
486
487        let mut resolved_dict = font_dict.clone();
488
489        // Phase 3.4: Check if this is a Type0 (composite) font
490        if detect_type0_font(font_dict) {
491            // Create a resolver closure that converts parser objects to unified format
492            let resolver =
493                |id: crate::pdf_objects::ObjectId| -> Option<crate::pdf_objects::Object> {
494                    match document.get_object(id.number(), id.generation()) {
495                        Ok(parser_obj) => Some(Self::convert_parser_object_to_unified(&parser_obj)),
496                        Err(_) => None,
497                    }
498                };
499
500            // Resolve the complete Type0 hierarchy
501            if let Some(info) = resolve_type0_hierarchy(font_dict, resolver) {
502                // Embed the resolved CIDFont as DescendantFonts
503                if let Some(cidfont) = info.cidfont_dict {
504                    let mut resolved_cidfont = cidfont;
505
506                    // Embed FontDescriptor with resolved font stream
507                    if let Some(descriptor) = info.font_descriptor {
508                        let mut resolved_descriptor = descriptor;
509
510                        // Embed the font stream directly in FontDescriptor
511                        if let Some(stream) = info.font_stream {
512                            // Determine which key to use based on font_file_type
513                            let key = match info.font_file_type {
514                                Some(crate::fonts::type0_parsing::FontFileType::TrueType) => {
515                                    "FontFile2"
516                                }
517                                Some(crate::fonts::type0_parsing::FontFileType::CFF) => "FontFile3",
518                                Some(crate::fonts::type0_parsing::FontFileType::Type1) => {
519                                    "FontFile"
520                                }
521                                None => "FontFile2", // Default for CIDFontType2
522                            };
523                            resolved_descriptor.set(key, Object::Stream(stream));
524                        }
525
526                        resolved_cidfont
527                            .set("FontDescriptor", Object::Dictionary(resolved_descriptor));
528                    }
529
530                    // Replace DescendantFonts array with resolved CIDFont
531                    let mut descendants = crate::pdf_objects::Array::new();
532                    descendants.push(Object::Dictionary(resolved_cidfont));
533                    resolved_dict.set("DescendantFonts", Object::Array(descendants));
534                }
535
536                // Embed ToUnicode stream if present
537                if let Some(tounicode) = info.tounicode_stream {
538                    resolved_dict.set("ToUnicode", Object::Stream(tounicode));
539                }
540            }
541
542            return Ok(resolved_dict);
543        }
544
545        // Original Phase 3.2 logic for simple fonts (Type1, TrueType, etc.)
546        // Check if font has a FontDescriptor
547        if let Some(Object::Reference(descriptor_id)) = font_dict.get("FontDescriptor") {
548            // Resolve FontDescriptor from document
549            let descriptor_obj =
550                document.get_object(descriptor_id.number(), descriptor_id.generation())?;
551
552            // Convert to unified format
553            let descriptor_unified = Self::convert_parser_object_to_unified(&descriptor_obj);
554
555            if let Object::Dictionary(mut descriptor_dict) = descriptor_unified {
556                // Check for embedded font streams (FontFile, FontFile2, FontFile3)
557                let font_file_keys = ["FontFile", "FontFile2", "FontFile3"];
558                let mut stream_resolved = false;
559
560                for key in &font_file_keys {
561                    if let Some(Object::Reference(stream_id)) = descriptor_dict.get(*key) {
562                        // Resolve font stream from document
563                        match document.get_object(stream_id.number(), stream_id.generation()) {
564                            Ok(stream_obj) => {
565                                // Convert to unified format (includes stream data)
566                                let stream_unified =
567                                    Self::convert_parser_object_to_unified(&stream_obj);
568
569                                // Replace reference with actual stream object
570                                descriptor_dict.set(*key, stream_unified);
571                                stream_resolved = true;
572                            }
573                            Err(_) => {
574                                // Resolution failed, keep reference as-is
575                                continue;
576                            }
577                        }
578                    }
579                }
580
581                // If we resolved any streams, update FontDescriptor in font dictionary
582                if stream_resolved {
583                    resolved_dict.set("FontDescriptor", Object::Dictionary(descriptor_dict));
584                }
585            }
586        }
587
588        Ok(resolved_dict)
589    }
590
591    /// Gets the preserved resources from the original PDF (if any)
592    pub fn get_preserved_resources(&self) -> Option<&crate::pdf_objects::Dictionary> {
593        self.preserved_resources.as_ref()
594    }
595
596    /// Gets the current page rotation in degrees.
597    pub fn get_rotation(&self) -> i32 {
598        self.rotation
599    }
600
601    /// Gets the effective width considering rotation.
602    /// For 90° and 270° rotations, returns the height.
603    pub fn effective_width(&self) -> f64 {
604        match self.rotation {
605            90 | 270 => self.height,
606            _ => self.width,
607        }
608    }
609
610    /// Gets the effective height considering rotation.
611    /// For 90° and 270° rotations, returns the width.
612    pub fn effective_height(&self) -> f64 {
613        match self.rotation {
614            90 | 270 => self.width,
615            _ => self.height,
616        }
617    }
618
619    pub fn text_flow(&self) -> TextFlowContext {
620        TextFlowContext::new(self.width, self.height, self.margins.clone())
621    }
622
623    pub fn add_text_flow(&mut self, text_flow: &TextFlowContext) {
624        let operations = text_flow.generate_operations();
625        self.content.extend_from_slice(&operations);
626    }
627
628    pub fn add_image(&mut self, name: impl Into<String>, image: Image) {
629        self.images.insert(name.into(), image);
630    }
631
632    pub fn draw_image(
633        &mut self,
634        name: &str,
635        x: f64,
636        y: f64,
637        width: f64,
638        height: f64,
639    ) -> Result<()> {
640        if self.images.contains_key(name) {
641            // Draw the image using the graphics context
642            self.graphics_context.draw_image(name, x, y, width, height);
643            Ok(())
644        } else {
645            Err(crate::PdfError::InvalidReference(format!(
646                "Image '{name}' not found"
647            )))
648        }
649    }
650
651    pub(crate) fn images(&self) -> &HashMap<String, Image> {
652        &self.images
653    }
654
655    /// Add a table to the page.
656    ///
657    /// This method renders a table at the specified position using the current
658    /// graphics context. The table will be drawn with borders, text, and any
659    /// configured styling options.
660    ///
661    /// # Arguments
662    ///
663    /// * `table` - The table to render on the page
664    ///
665    /// # Example
666    ///
667    /// ```rust
668    /// use oxidize_pdf::{Page, text::{Table, TableOptions}};
669    ///
670    /// let mut page = Page::a4();
671    ///
672    /// // Create a table with 3 columns
673    /// let mut table = Table::new(vec![100.0, 150.0, 100.0]);
674    /// table.set_position(50.0, 700.0);
675    ///
676    /// // Add header row
677    /// table.add_header_row(vec![
678    ///     "Name".to_string(),
679    ///     "Description".to_string(),
680    ///     "Price".to_string()
681    /// ])?;
682    ///
683    /// // Add data rows
684    /// table.add_row(vec![
685    ///     "Item 1".to_string(),
686    ///     "First item description".to_string(),
687    ///     "$10.00".to_string()
688    /// ])?;
689    ///
690    /// // Render the table on the page
691    /// page.add_table(&table)?;
692    /// # Ok::<(), oxidize_pdf::PdfError>(())
693    /// ```
694    pub fn add_table(&mut self, table: &Table) -> Result<()> {
695        self.graphics_context.render_table(table)
696    }
697
698    /// Get ExtGState resources from the graphics context
699    pub fn get_extgstate_resources(
700        &self,
701    ) -> Option<&std::collections::HashMap<String, crate::graphics::ExtGState>> {
702        if self.graphics_context.has_extgstates() {
703            Some(self.graphics_context.extgstate_manager().states())
704        } else {
705            None
706        }
707    }
708
709    /// Adds an annotation to this page
710    pub fn add_annotation(&mut self, annotation: Annotation) {
711        self.annotations.push(annotation);
712    }
713
714    /// Returns a reference to the annotations
715    pub fn annotations(&self) -> &[Annotation] {
716        &self.annotations
717    }
718
719    /// Returns a mutable reference to the annotations  
720    pub fn annotations_mut(&mut self) -> &mut Vec<Annotation> {
721        &mut self.annotations
722    }
723
724    /// Add a form field widget to the page.
725    ///
726    /// This method adds a widget annotation and returns the reference ID that
727    /// should be used to link the widget to its corresponding form field.
728    ///
729    /// # Arguments
730    ///
731    /// * `widget` - The widget to add to the page
732    ///
733    /// # Returns
734    ///
735    /// An ObjectReference that should be used to link this widget to a form field
736    ///
737    /// # Example
738    ///
739    /// ```rust,no_run
740    /// use oxidize_pdf::{Page, forms::Widget, geometry::{Rectangle, Point}};
741    ///
742    /// let mut page = Page::a4();
743    /// let widget = Widget::new(
744    ///     Rectangle::new(Point::new(100.0, 700.0), Point::new(300.0, 720.0))
745    /// );
746    /// let widget_ref = page.add_form_widget(widget);
747    /// ```
748    pub fn add_form_widget(&mut self, widget: Widget) -> ObjectReference {
749        // Create a placeholder object reference for this widget
750        // The actual ObjectId will be assigned by the document writer
751        // We use a placeholder ID that doesn't conflict with real ObjectIds
752        let widget_ref = ObjectReference::new(
753            0, // Placeholder ID - writer will assign the real ID
754            0,
755        );
756
757        // Convert widget to annotation
758        let mut annot = Annotation::new(crate::annotations::AnnotationType::Widget, widget.rect);
759
760        // Add widget-specific properties
761        for (key, value) in widget.to_annotation_dict().iter() {
762            annot.properties.set(key, value.clone());
763        }
764
765        // Add to page's annotations
766        self.annotations.push(annot);
767
768        widget_ref
769    }
770
771    /// Sets the header for this page.
772    ///
773    /// # Example
774    ///
775    /// ```rust
776    /// use oxidize_pdf::{Page, text::HeaderFooter};
777    ///
778    /// let mut page = Page::a4();
779    /// page.set_header(HeaderFooter::new_header("Company Report 2024"));
780    /// ```
781    pub fn set_header(&mut self, header: HeaderFooter) {
782        self.header = Some(header);
783    }
784
785    /// Sets the footer for this page.
786    ///
787    /// # Example
788    ///
789    /// ```rust
790    /// use oxidize_pdf::{Page, text::HeaderFooter};
791    ///
792    /// let mut page = Page::a4();
793    /// page.set_footer(HeaderFooter::new_footer("Page {{page_number}} of {{total_pages}}"));
794    /// ```
795    pub fn set_footer(&mut self, footer: HeaderFooter) {
796        self.footer = Some(footer);
797    }
798
799    /// Gets a reference to the header if set.
800    pub fn header(&self) -> Option<&HeaderFooter> {
801        self.header.as_ref()
802    }
803
804    /// Gets a reference to the footer if set.
805    pub fn footer(&self) -> Option<&HeaderFooter> {
806        self.footer.as_ref()
807    }
808
809    /// Sets the page content directly.
810    ///
811    /// This is used internally when processing headers and footers.
812    pub(crate) fn set_content(&mut self, content: Vec<u8>) {
813        self.content = content;
814    }
815
816    pub(crate) fn generate_content(&mut self) -> Result<Vec<u8>> {
817        // Generate content with no page info (used for simple pages without headers/footers)
818        self.generate_content_with_page_info(None, None, None)
819    }
820
821    /// Generates page content with header/footer support.
822    ///
823    /// This method is used internally by the writer to render pages with
824    /// proper page numbering in headers and footers.
825    pub(crate) fn generate_content_with_page_info(
826        &mut self,
827        page_number: Option<usize>,
828        total_pages: Option<usize>,
829        custom_values: Option<&HashMap<String, String>>,
830    ) -> Result<Vec<u8>> {
831        let mut final_content = Vec::new();
832
833        // Render header if present
834        if let Some(header) = &self.header {
835            if let (Some(page_num), Some(total)) = (page_number, total_pages) {
836                let header_content =
837                    self.render_header_footer(header, page_num, total, custom_values)?;
838                final_content.extend_from_slice(&header_content);
839            }
840        }
841
842        // Add graphics operations
843        final_content.extend_from_slice(&self.graphics_context.generate_operations()?);
844
845        // Add text operations
846        final_content.extend_from_slice(&self.text_context.generate_operations()?);
847
848        // Add any content that was added via add_text_flow
849        // Phase 2.3: Rewrite font references in preserved content if fonts were renamed
850        let content_to_add = if let Some(ref preserved_res) = self.preserved_resources {
851            // Check if we have preserved fonts that need renaming
852            if let Some(fonts_dict) = preserved_res.get("Font") {
853                if let crate::pdf_objects::Object::Dictionary(ref fonts) = fonts_dict {
854                    // Build font mapping (F1 → OrigF1, Arial → OrigArial, etc.)
855                    let mut font_mapping = std::collections::HashMap::new();
856                    for (original_name, _) in fonts.iter() {
857                        let new_name = format!("Orig{}", original_name.as_str());
858                        font_mapping.insert(original_name.as_str().to_string(), new_name);
859                    }
860
861                    // Rewrite font references in preserved content
862                    if !font_mapping.is_empty() && !self.content.is_empty() {
863                        use crate::writer::rewrite_font_references;
864                        rewrite_font_references(&self.content, &font_mapping)
865                    } else {
866                        self.content.clone()
867                    }
868                } else {
869                    self.content.clone()
870                }
871            } else {
872                self.content.clone()
873            }
874        } else {
875            self.content.clone()
876        };
877
878        final_content.extend_from_slice(&content_to_add);
879
880        // Render footer if present
881        if let Some(footer) = &self.footer {
882            if let (Some(page_num), Some(total)) = (page_number, total_pages) {
883                let footer_content =
884                    self.render_header_footer(footer, page_num, total, custom_values)?;
885                final_content.extend_from_slice(&footer_content);
886            }
887        }
888
889        Ok(final_content)
890    }
891
892    /// Renders a header or footer with the given page information.
893    fn render_header_footer(
894        &self,
895        header_footer: &HeaderFooter,
896        page_number: usize,
897        total_pages: usize,
898        custom_values: Option<&HashMap<String, String>>,
899    ) -> Result<Vec<u8>> {
900        use crate::text::measure_text;
901
902        // Render the content with placeholders replaced
903        let content = header_footer.render(page_number, total_pages, custom_values);
904
905        // Calculate text width for alignment
906        let text_width = measure_text(
907            &content,
908            header_footer.options().font.clone(),
909            header_footer.options().font_size,
910        );
911
912        // Calculate positions
913        let x = header_footer.calculate_x_position(self.width, text_width);
914        let y = header_footer.calculate_y_position(self.height);
915
916        // Create a temporary text context for the header/footer
917        let mut text_ctx = TextContext::new();
918        text_ctx
919            .set_font(
920                header_footer.options().font.clone(),
921                header_footer.options().font_size,
922            )
923            .at(x, y)
924            .write(&content)?;
925
926        text_ctx.generate_operations()
927    }
928
929    /// Convert page to dictionary for PDF structure
930    pub(crate) fn to_dict(&self) -> Dictionary {
931        let mut dict = Dictionary::new();
932
933        // MediaBox
934        let media_box = Array::from(vec![
935            Object::Real(0.0),
936            Object::Real(0.0),
937            Object::Real(self.width),
938            Object::Real(self.height),
939        ]);
940        dict.set("MediaBox", Object::Array(media_box.into()));
941
942        // Add rotation if not zero
943        if self.rotation != 0 {
944            dict.set("Rotate", Object::Integer(self.rotation as i64));
945        }
946
947        // Resources (empty for now, would include fonts, images, etc.)
948        let resources = Dictionary::new();
949        dict.set("Resources", Object::Dictionary(resources));
950
951        // Annotations - will be added by the writer with proper object references
952        // The Page struct holds the annotation data, but the writer is responsible
953        // for creating object references and writing the annotation objects
954        //
955        // NOTE: We don't add Annots array here anymore because the writer
956        // will handle this properly with sequential ObjectIds. The temporary
957        // ObjectIds (1000+) were causing invalid references in the final PDF.
958        // The writer now handles all ObjectId allocation and writing.
959
960        // Contents would be added by the writer
961
962        dict
963    }
964
965    /// Gets all characters used in this page.
966    ///
967    /// Combines characters from both graphics_context and text_context
968    /// to ensure proper font subsetting for custom fonts (fixes issue #97).
969    pub(crate) fn get_used_characters(&self) -> Option<HashSet<char>> {
970        let graphics_chars = self.graphics_context.get_used_characters();
971        let text_chars = self.text_context.get_used_characters();
972
973        match (graphics_chars, text_chars) {
974            (None, None) => None,
975            (Some(chars), None) | (None, Some(chars)) => Some(chars),
976            (Some(mut g_chars), Some(t_chars)) => {
977                g_chars.extend(t_chars);
978                Some(g_chars)
979            }
980        }
981    }
982
983    // ==================== Tagged PDF / Marked Content Support ====================
984
985    /// Begins a marked content sequence for Tagged PDF
986    ///
987    /// This adds a BDC (Begin Marked Content with Properties) operator to the content stream
988    /// with an MCID (Marked Content ID) property. The MCID connects the content to a
989    /// structure element in the structure tree.
990    ///
991    /// # Returns
992    ///
993    /// Returns the assigned MCID, which should be added to the corresponding StructureElement
994    /// via `StructureElement::add_mcid(page_index, mcid)`.
995    ///
996    /// # Example
997    ///
998    /// ```rust,no_run
999    /// use oxidize_pdf::{Page, structure::{StructTree, StructureElement, StandardStructureType}};
1000    ///
1001    /// let mut page = Page::a4();
1002    /// let mut tree = StructTree::new();
1003    ///
1004    /// // Create structure
1005    /// let doc = StructureElement::new(StandardStructureType::Document);
1006    /// let doc_idx = tree.set_root(doc);
1007    /// let mut para = StructureElement::new(StandardStructureType::P);
1008    ///
1009    /// // Begin marked content for paragraph
1010    /// let mcid = page.begin_marked_content("P")?;
1011    ///
1012    /// // Add content
1013    /// page.text().write("Hello, Tagged PDF!")?;
1014    ///
1015    /// // End marked content
1016    /// page.end_marked_content()?;
1017    ///
1018    /// // Connect MCID to structure element
1019    /// para.add_mcid(0, mcid);  // page_index=0, mcid from above
1020    /// tree.add_child(doc_idx, para).map_err(|e| oxidize_pdf::PdfError::InvalidOperation(e))?;
1021    /// # Ok::<(), oxidize_pdf::PdfError>(())
1022    /// ```
1023    pub fn begin_marked_content(&mut self, tag: &str) -> Result<u32> {
1024        let mcid = self.next_mcid;
1025        self.next_mcid += 1;
1026
1027        // Add BDC operator with MCID property to text context
1028        // Format: /Tag <</MCID mcid>> BDC
1029        let bdc_op = format!("/{} <</MCID {}>> BDC\n", tag, mcid);
1030        self.text_context.append_raw_operation(&bdc_op);
1031
1032        self.marked_content_stack.push(tag.to_string());
1033
1034        Ok(mcid)
1035    }
1036
1037    /// Ends the current marked content sequence
1038    ///
1039    /// This adds an EMC (End Marked Content) operator to close the most recently
1040    /// opened marked content sequence.
1041    ///
1042    /// # Errors
1043    ///
1044    /// Returns an error if there is no open marked content sequence.
1045    pub fn end_marked_content(&mut self) -> Result<()> {
1046        if self.marked_content_stack.is_empty() {
1047            return Err(crate::PdfError::InvalidOperation(
1048                "No marked content sequence to end (EMC without BDC)".to_string(),
1049            ));
1050        }
1051
1052        self.marked_content_stack.pop();
1053
1054        // Add EMC operator to text context
1055        self.text_context.append_raw_operation("EMC\n");
1056
1057        Ok(())
1058    }
1059
1060    /// Returns the next MCID that will be assigned
1061    ///
1062    /// This is useful for pre-allocating structure elements before adding content.
1063    pub fn next_mcid(&self) -> u32 {
1064        self.next_mcid
1065    }
1066
1067    /// Returns the current depth of nested marked content
1068    pub fn marked_content_depth(&self) -> usize {
1069        self.marked_content_stack.len()
1070    }
1071}
1072
1073#[cfg(test)]
1074mod tests {
1075    use super::*;
1076    use crate::graphics::Color;
1077    use crate::text::Font;
1078
1079    #[test]
1080    fn test_page_new() {
1081        let page = Page::new(100.0, 200.0);
1082        assert_eq!(page.width(), 100.0);
1083        assert_eq!(page.height(), 200.0);
1084        assert_eq!(page.margins().left, 72.0);
1085        assert_eq!(page.margins().right, 72.0);
1086        assert_eq!(page.margins().top, 72.0);
1087        assert_eq!(page.margins().bottom, 72.0);
1088    }
1089
1090    #[test]
1091    fn test_page_a4() {
1092        let page = Page::a4();
1093        assert_eq!(page.width(), 595.0);
1094        assert_eq!(page.height(), 842.0);
1095    }
1096
1097    #[test]
1098    fn test_page_letter() {
1099        let page = Page::letter();
1100        assert_eq!(page.width(), 612.0);
1101        assert_eq!(page.height(), 792.0);
1102    }
1103
1104    #[test]
1105    fn test_set_margins() {
1106        let mut page = Page::a4();
1107        page.set_margins(10.0, 20.0, 30.0, 40.0);
1108
1109        assert_eq!(page.margins().left, 10.0);
1110        assert_eq!(page.margins().right, 20.0);
1111        assert_eq!(page.margins().top, 30.0);
1112        assert_eq!(page.margins().bottom, 40.0);
1113    }
1114
1115    #[test]
1116    fn test_content_dimensions() {
1117        let mut page = Page::new(300.0, 400.0);
1118        page.set_margins(50.0, 50.0, 50.0, 50.0);
1119
1120        assert_eq!(page.content_width(), 200.0);
1121        assert_eq!(page.content_height(), 300.0);
1122    }
1123
1124    #[test]
1125    fn test_content_area() {
1126        let mut page = Page::new(300.0, 400.0);
1127        page.set_margins(10.0, 20.0, 30.0, 40.0);
1128
1129        let (left, bottom, right, top) = page.content_area();
1130        assert_eq!(left, 10.0);
1131        assert_eq!(bottom, 40.0);
1132        assert_eq!(right, 280.0);
1133        assert_eq!(top, 370.0);
1134    }
1135
1136    #[test]
1137    fn test_graphics_context() {
1138        let mut page = Page::a4();
1139        let graphics = page.graphics();
1140        graphics.set_fill_color(Color::red());
1141        graphics.rect(100.0, 100.0, 200.0, 150.0);
1142        graphics.fill();
1143
1144        // Graphics context should be accessible and modifiable
1145        assert!(page.generate_content().is_ok());
1146    }
1147
1148    #[test]
1149    fn test_text_context() {
1150        let mut page = Page::a4();
1151        let text = page.text();
1152        text.set_font(Font::Helvetica, 12.0);
1153        text.at(100.0, 700.0);
1154        text.write("Hello World").unwrap();
1155
1156        // Text context should be accessible and modifiable
1157        assert!(page.generate_content().is_ok());
1158    }
1159
1160    #[test]
1161    fn test_text_flow() {
1162        let page = Page::a4();
1163        let text_flow = page.text_flow();
1164
1165        // Text flow should be created with page dimensions and margins
1166        // Just verify it can be created
1167        drop(text_flow);
1168    }
1169
1170    #[test]
1171    fn test_add_text_flow() {
1172        let mut page = Page::a4();
1173        let mut text_flow = page.text_flow();
1174        text_flow.at(100.0, 700.0);
1175        text_flow.set_font(Font::TimesRoman, 14.0);
1176        text_flow.write_wrapped("Test text flow").unwrap();
1177
1178        page.add_text_flow(&text_flow);
1179
1180        let content = page.generate_content().unwrap();
1181        assert!(!content.is_empty());
1182    }
1183
1184    #[test]
1185    fn test_add_image() {
1186        let mut page = Page::a4();
1187        // Create a minimal valid JPEG with SOF0 header
1188        let jpeg_data = vec![
1189            0xFF, 0xD8, // SOI marker
1190            0xFF, 0xC0, // SOF0 marker
1191            0x00, 0x11, // Length (17 bytes)
1192            0x08, // Precision (8 bits)
1193            0x00, 0x64, // Height (100)
1194            0x00, 0xC8, // Width (200)
1195            0x03, // Components (3 = RGB)
1196            0xFF, 0xD9, // EOI marker
1197        ];
1198        let image = Image::from_jpeg_data(jpeg_data).unwrap();
1199
1200        page.add_image("test_image", image);
1201        assert!(page.images().contains_key("test_image"));
1202        assert_eq!(page.images().len(), 1);
1203    }
1204
1205    #[test]
1206    fn test_draw_image() {
1207        let mut page = Page::a4();
1208        // Create a minimal valid JPEG with SOF0 header
1209        let jpeg_data = vec![
1210            0xFF, 0xD8, // SOI marker
1211            0xFF, 0xC0, // SOF0 marker
1212            0x00, 0x11, // Length (17 bytes)
1213            0x08, // Precision (8 bits)
1214            0x00, 0x64, // Height (100)
1215            0x00, 0xC8, // Width (200)
1216            0x03, // Components (3 = RGB)
1217            0xFF, 0xD9, // EOI marker
1218        ];
1219        let image = Image::from_jpeg_data(jpeg_data).unwrap();
1220
1221        page.add_image("test_image", image);
1222        let result = page.draw_image("test_image", 50.0, 50.0, 200.0, 200.0);
1223        assert!(result.is_ok());
1224    }
1225
1226    #[test]
1227    fn test_draw_nonexistent_image() {
1228        let mut page = Page::a4();
1229        let result = page.draw_image("nonexistent", 50.0, 50.0, 200.0, 200.0);
1230        assert!(result.is_err());
1231    }
1232
1233    #[test]
1234    fn test_generate_content() {
1235        let mut page = Page::a4();
1236
1237        // Add some graphics
1238        page.graphics()
1239            .set_fill_color(Color::blue())
1240            .circle(200.0, 400.0, 50.0)
1241            .fill();
1242
1243        // Add some text
1244        page.text()
1245            .set_font(Font::Courier, 10.0)
1246            .at(50.0, 650.0)
1247            .write("Test content")
1248            .unwrap();
1249
1250        let content = page.generate_content().unwrap();
1251        assert!(!content.is_empty());
1252    }
1253
1254    #[test]
1255    fn test_margins_default() {
1256        let margins = Margins::default();
1257        assert_eq!(margins.left, 72.0);
1258        assert_eq!(margins.right, 72.0);
1259        assert_eq!(margins.top, 72.0);
1260        assert_eq!(margins.bottom, 72.0);
1261    }
1262
1263    #[test]
1264    fn test_page_clone() {
1265        let mut page1 = Page::a4();
1266        page1.set_margins(10.0, 20.0, 30.0, 40.0);
1267        // Create a minimal valid JPEG with SOF0 header
1268        let jpeg_data = vec![
1269            0xFF, 0xD8, // SOI marker
1270            0xFF, 0xC0, // SOF0 marker
1271            0x00, 0x11, // Length (17 bytes)
1272            0x08, // Precision (8 bits)
1273            0x00, 0x32, // Height (50)
1274            0x00, 0x32, // Width (50)
1275            0x03, // Components (3 = RGB)
1276            0xFF, 0xD9, // EOI marker
1277        ];
1278        let image = Image::from_jpeg_data(jpeg_data).unwrap();
1279        page1.add_image("img1", image);
1280
1281        let page2 = page1.clone();
1282        assert_eq!(page2.width(), page1.width());
1283        assert_eq!(page2.height(), page1.height());
1284        assert_eq!(page2.margins().left, page1.margins().left);
1285        assert_eq!(page2.images().len(), page1.images().len());
1286    }
1287
1288    #[test]
1289    fn test_header_footer_basic() {
1290        use crate::text::HeaderFooter;
1291
1292        let mut page = Page::a4();
1293
1294        let header = HeaderFooter::new_header("Test Header");
1295        let footer = HeaderFooter::new_footer("Test Footer");
1296
1297        page.set_header(header);
1298        page.set_footer(footer);
1299
1300        assert!(page.header().is_some());
1301        assert!(page.footer().is_some());
1302        assert_eq!(page.header().unwrap().content(), "Test Header");
1303        assert_eq!(page.footer().unwrap().content(), "Test Footer");
1304    }
1305
1306    #[test]
1307    fn test_header_footer_with_page_numbers() {
1308        use crate::text::HeaderFooter;
1309
1310        let mut page = Page::a4();
1311
1312        let footer = HeaderFooter::new_footer("Page {{page_number}} of {{total_pages}}");
1313        page.set_footer(footer);
1314
1315        // Generate content with page info
1316        let content = page
1317            .generate_content_with_page_info(Some(3), Some(10), None)
1318            .unwrap();
1319        assert!(!content.is_empty());
1320
1321        // The content should contain the rendered footer
1322        let content_str = String::from_utf8_lossy(&content);
1323        assert!(content_str.contains("Page 3 of 10"));
1324    }
1325
1326    #[test]
1327    fn test_page_content_with_headers_footers() {
1328        use crate::text::{HeaderFooter, TextAlign};
1329
1330        let mut page = Page::a4();
1331
1332        // Add header
1333        let header = HeaderFooter::new_header("Document Title")
1334            .with_font(Font::HelveticaBold, 14.0)
1335            .with_alignment(TextAlign::Center);
1336        page.set_header(header);
1337
1338        // Add footer
1339        let footer = HeaderFooter::new_footer("Page {{page_number}}")
1340            .with_font(Font::Helvetica, 10.0)
1341            .with_alignment(TextAlign::Right);
1342        page.set_footer(footer);
1343
1344        // Add main content
1345        page.text()
1346            .set_font(Font::TimesRoman, 12.0)
1347            .at(100.0, 700.0)
1348            .write("Main content here")
1349            .unwrap();
1350
1351        // Generate with page info
1352        let content = page
1353            .generate_content_with_page_info(Some(1), Some(5), None)
1354            .unwrap();
1355        assert!(!content.is_empty());
1356
1357        // Verify that content was generated (it includes header, main content, and footer)
1358        // Note: We generate raw PDF content streams here, not the full PDF
1359        // The content may be in PDF format with operators like BT/ET, Tj, etc.
1360        assert!(content.len() > 100); // Should have substantial content
1361    }
1362
1363    #[test]
1364    fn test_no_headers_footers() {
1365        let mut page = Page::a4();
1366
1367        // No headers/footers set
1368        assert!(page.header().is_none());
1369        assert!(page.footer().is_none());
1370
1371        // Content generation should work without headers/footers
1372        let content = page
1373            .generate_content_with_page_info(Some(1), Some(1), None)
1374            .unwrap();
1375        assert!(content.is_empty() || !content.is_empty()); // May be empty or contain default content
1376    }
1377
1378    #[test]
1379    fn test_header_footer_custom_values() {
1380        use crate::text::HeaderFooter;
1381        use std::collections::HashMap;
1382
1383        let mut page = Page::a4();
1384
1385        let header = HeaderFooter::new_header("{{company}} - {{title}}");
1386        page.set_header(header);
1387
1388        let mut custom_values = HashMap::new();
1389        custom_values.insert("company".to_string(), "ACME Corp".to_string());
1390        custom_values.insert("title".to_string(), "Annual Report".to_string());
1391
1392        let content = page
1393            .generate_content_with_page_info(Some(1), Some(1), Some(&custom_values))
1394            .unwrap();
1395        let content_str = String::from_utf8_lossy(&content);
1396        assert!(content_str.contains("ACME Corp - Annual Report"));
1397    }
1398
1399    // Integration tests for Page ↔ Document ↔ Writer interactions
1400    mod integration_tests {
1401        use super::*;
1402        use crate::document::Document;
1403        use crate::writer::PdfWriter;
1404        use std::fs;
1405        use tempfile::TempDir;
1406
1407        #[test]
1408        fn test_page_document_integration() {
1409            let mut doc = Document::new();
1410            doc.set_title("Page Integration Test");
1411
1412            // Create pages with different sizes
1413            let page1 = Page::a4();
1414            let page2 = Page::letter();
1415            let mut page3 = Page::new(400.0, 600.0);
1416
1417            // Add content to custom page
1418            page3.set_margins(20.0, 20.0, 20.0, 20.0);
1419            page3
1420                .text()
1421                .set_font(Font::Helvetica, 14.0)
1422                .at(50.0, 550.0)
1423                .write("Custom page content")
1424                .unwrap();
1425
1426            doc.add_page(page1);
1427            doc.add_page(page2);
1428            doc.add_page(page3);
1429
1430            assert_eq!(doc.pages.len(), 3);
1431
1432            // Verify page properties are preserved
1433            assert_eq!(doc.pages[0].width(), 595.0); // A4
1434            assert_eq!(doc.pages[1].width(), 612.0); // Letter
1435            assert_eq!(doc.pages[2].width(), 400.0); // Custom
1436
1437            // Verify content generation works
1438            let mut page_copy = doc.pages[2].clone();
1439            let content = page_copy.generate_content().unwrap();
1440            assert!(!content.is_empty());
1441        }
1442
1443        #[test]
1444        fn test_page_writer_integration() {
1445            let temp_dir = TempDir::new().unwrap();
1446            let file_path = temp_dir.path().join("page_writer_test.pdf");
1447
1448            let mut doc = Document::new();
1449            doc.set_title("Page Writer Integration");
1450
1451            // Create a page with complex content
1452            let mut page = Page::a4();
1453            page.set_margins(50.0, 50.0, 50.0, 50.0);
1454
1455            // Add text content
1456            page.text()
1457                .set_font(Font::Helvetica, 16.0)
1458                .at(100.0, 750.0)
1459                .write("Integration Test Header")
1460                .unwrap();
1461
1462            page.text()
1463                .set_font(Font::TimesRoman, 12.0)
1464                .at(100.0, 700.0)
1465                .write("This is body text for the integration test.")
1466                .unwrap();
1467
1468            // Add graphics content
1469            page.graphics()
1470                .set_fill_color(Color::rgb(0.2, 0.6, 0.9))
1471                .rect(100.0, 600.0, 200.0, 50.0)
1472                .fill();
1473
1474            page.graphics()
1475                .set_stroke_color(Color::rgb(0.8, 0.2, 0.2))
1476                .set_line_width(3.0)
1477                .circle(300.0, 500.0, 40.0)
1478                .stroke();
1479
1480            doc.add_page(page);
1481
1482            // Write to file
1483            let mut writer = PdfWriter::new(&file_path).unwrap();
1484            writer.write_document(&mut doc).unwrap();
1485
1486            // Verify file was created and has content
1487            assert!(file_path.exists());
1488            let metadata = fs::metadata(&file_path).unwrap();
1489            assert!(metadata.len() > 1000); // Should be substantial
1490
1491            // Verify PDF structure (text may be compressed, so check for basic structure)
1492            let content = fs::read(&file_path).unwrap();
1493            let content_str = String::from_utf8_lossy(&content);
1494            assert!(content_str.contains("obj")); // Should contain PDF objects
1495            assert!(content_str.contains("stream")); // Should contain content streams
1496        }
1497
1498        #[test]
1499        fn test_page_margins_integration() {
1500            let temp_dir = TempDir::new().unwrap();
1501            let file_path = temp_dir.path().join("margins_test.pdf");
1502
1503            let mut doc = Document::new();
1504            doc.set_title("Margins Integration Test");
1505
1506            // Test different margin configurations
1507            let mut page1 = Page::a4();
1508            page1.set_margins(10.0, 20.0, 30.0, 40.0);
1509
1510            let mut page2 = Page::letter();
1511            page2.set_margins(72.0, 72.0, 72.0, 72.0); // 1 inch margins
1512
1513            let mut page3 = Page::new(500.0, 700.0);
1514            page3.set_margins(0.0, 0.0, 0.0, 0.0); // No margins
1515
1516            // Add content that uses margin information
1517            for (i, page) in [&mut page1, &mut page2, &mut page3].iter_mut().enumerate() {
1518                let (left, bottom, right, top) = page.content_area();
1519
1520                // Place text at content area boundaries
1521                page.text()
1522                    .set_font(Font::Helvetica, 10.0)
1523                    .at(left, top - 20.0)
1524                    .write(&format!(
1525                        "Page {} - Content area: ({:.1}, {:.1}, {:.1}, {:.1})",
1526                        i + 1,
1527                        left,
1528                        bottom,
1529                        right,
1530                        top
1531                    ))
1532                    .unwrap();
1533
1534                // Draw border around content area
1535                page.graphics()
1536                    .set_stroke_color(Color::rgb(0.5, 0.5, 0.5))
1537                    .set_line_width(1.0)
1538                    .rect(left, bottom, right - left, top - bottom)
1539                    .stroke();
1540            }
1541
1542            doc.add_page(page1);
1543            doc.add_page(page2);
1544            doc.add_page(page3);
1545
1546            // Write and verify
1547            let mut writer = PdfWriter::new(&file_path).unwrap();
1548            writer.write_document(&mut doc).unwrap();
1549
1550            assert!(file_path.exists());
1551            let metadata = fs::metadata(&file_path).unwrap();
1552            assert!(metadata.len() > 500); // Should contain substantial content
1553        }
1554
1555        #[test]
1556        fn test_page_image_integration() {
1557            let temp_dir = TempDir::new().unwrap();
1558            let file_path = temp_dir.path().join("image_test.pdf");
1559
1560            let mut doc = Document::new();
1561            doc.set_title("Image Integration Test");
1562
1563            let mut page = Page::a4();
1564
1565            // Create test images
1566            let jpeg_data1 = vec![
1567                0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x64, 0x00, 0xC8, 0x03, 0xFF, 0xD9,
1568            ];
1569            let image1 = Image::from_jpeg_data(jpeg_data1).unwrap();
1570
1571            let jpeg_data2 = vec![
1572                0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x32, 0x00, 0x32, 0x01, 0xFF, 0xD9,
1573            ];
1574            let image2 = Image::from_jpeg_data(jpeg_data2).unwrap();
1575
1576            // Add images to page
1577            page.add_image("image1", image1);
1578            page.add_image("image2", image2);
1579
1580            // Draw images at different positions
1581            page.draw_image("image1", 100.0, 600.0, 200.0, 100.0)
1582                .unwrap();
1583            page.draw_image("image2", 350.0, 600.0, 50.0, 50.0).unwrap();
1584
1585            // Add text labels
1586            page.text()
1587                .set_font(Font::Helvetica, 12.0)
1588                .at(100.0, 580.0)
1589                .write("Image 1 (200x100)")
1590                .unwrap();
1591
1592            page.text()
1593                .set_font(Font::Helvetica, 12.0)
1594                .at(350.0, 580.0)
1595                .write("Image 2 (50x50)")
1596                .unwrap();
1597
1598            // Verify images were added before moving page
1599            assert_eq!(page.images().len(), 2, "Two images should be added to page");
1600
1601            doc.add_page(page);
1602
1603            // Write and verify
1604            let mut writer = PdfWriter::new(&file_path).unwrap();
1605            writer.write_document(&mut doc).unwrap();
1606
1607            assert!(file_path.exists());
1608            let metadata = fs::metadata(&file_path).unwrap();
1609            assert!(metadata.len() > 500); // Should contain images and text
1610
1611            // Verify XObject references in PDF
1612            let content = fs::read(&file_path).unwrap();
1613            let content_str = String::from_utf8_lossy(&content);
1614
1615            // Debug: print what we're looking for
1616            tracing::debug!("PDF size: {} bytes", content.len());
1617            tracing::debug!("Contains 'XObject': {}", content_str.contains("XObject"));
1618            tracing::debug!("Contains '/XObject': {}", content_str.contains("/XObject"));
1619
1620            // Check for image-related content
1621            if content_str.contains("/Type /Image") || content_str.contains("DCTDecode") {
1622                tracing::debug!("Found image-related content but no XObject dictionary");
1623            }
1624
1625            // Verify XObject is properly written
1626            assert!(content_str.contains("XObject"));
1627        }
1628
1629        #[test]
1630        fn test_page_text_flow_integration() {
1631            let temp_dir = TempDir::new().unwrap();
1632            let file_path = temp_dir.path().join("text_flow_test.pdf");
1633
1634            let mut doc = Document::new();
1635            doc.set_title("Text Flow Integration Test");
1636
1637            let mut page = Page::a4();
1638            page.set_margins(50.0, 50.0, 50.0, 50.0);
1639
1640            // Create text flow with long content
1641            let mut text_flow = page.text_flow();
1642            text_flow.set_font(Font::TimesRoman, 12.0);
1643            text_flow.at(100.0, 700.0);
1644
1645            let long_text =
1646                "This is a long paragraph that should demonstrate text flow capabilities. "
1647                    .repeat(10);
1648            text_flow.write_wrapped(&long_text).unwrap();
1649
1650            // Add the text flow to the page
1651            page.add_text_flow(&text_flow);
1652
1653            // Also add regular text
1654            page.text()
1655                .set_font(Font::Helvetica, 14.0)
1656                .at(100.0, 750.0)
1657                .write("Regular Text Above Text Flow")
1658                .unwrap();
1659
1660            doc.add_page(page);
1661
1662            // Write and verify
1663            let mut writer = PdfWriter::new(&file_path).unwrap();
1664            writer.write_document(&mut doc).unwrap();
1665
1666            assert!(file_path.exists());
1667            let metadata = fs::metadata(&file_path).unwrap();
1668            assert!(metadata.len() > 1000); // Should contain text content
1669
1670            // Verify text structure appears in PDF
1671            let content = fs::read(&file_path).unwrap();
1672            let content_str = String::from_utf8_lossy(&content);
1673            assert!(content_str.contains("obj")); // Should contain PDF objects
1674            assert!(content_str.contains("stream")); // Should contain content streams
1675        }
1676
1677        #[test]
1678        fn test_page_complex_content_integration() {
1679            let temp_dir = TempDir::new().unwrap();
1680            let file_path = temp_dir.path().join("complex_content_test.pdf");
1681
1682            let mut doc = Document::new();
1683            doc.set_title("Complex Content Integration Test");
1684
1685            let mut page = Page::a4();
1686            page.set_margins(40.0, 40.0, 40.0, 40.0);
1687
1688            // Create complex layered content
1689
1690            // Background graphics
1691            page.graphics()
1692                .set_fill_color(Color::rgb(0.95, 0.95, 0.95))
1693                .rect(50.0, 50.0, 495.0, 742.0)
1694                .fill();
1695
1696            // Header section
1697            page.graphics()
1698                .set_fill_color(Color::rgb(0.2, 0.4, 0.8))
1699                .rect(50.0, 750.0, 495.0, 42.0)
1700                .fill();
1701
1702            page.text()
1703                .set_font(Font::HelveticaBold, 18.0)
1704                .at(60.0, 765.0)
1705                .write("Complex Content Integration Test")
1706                .unwrap();
1707
1708            // Content sections with mixed elements
1709            let mut y_pos = 700.0;
1710            for i in 1..=3 {
1711                // Section header
1712                page.graphics()
1713                    .set_fill_color(Color::rgb(0.8, 0.8, 0.9))
1714                    .rect(60.0, y_pos, 475.0, 20.0)
1715                    .fill();
1716
1717                page.text()
1718                    .set_font(Font::HelveticaBold, 12.0)
1719                    .at(70.0, y_pos + 5.0)
1720                    .write(&format!("Section {i}"))
1721                    .unwrap();
1722
1723                y_pos -= 30.0;
1724
1725                // Section content
1726                page.text()
1727                    .set_font(Font::TimesRoman, 10.0)
1728                    .at(70.0, y_pos)
1729                    .write(&format!(
1730                        "This is the content for section {i}. It demonstrates mixed content."
1731                    ))
1732                    .unwrap();
1733
1734                // Section graphics
1735                page.graphics()
1736                    .set_stroke_color(Color::rgb(0.6, 0.2, 0.2))
1737                    .set_line_width(2.0)
1738                    .move_to(70.0, y_pos - 10.0)
1739                    .line_to(530.0, y_pos - 10.0)
1740                    .stroke();
1741
1742                y_pos -= 50.0;
1743            }
1744
1745            // Footer
1746            page.graphics()
1747                .set_fill_color(Color::rgb(0.3, 0.3, 0.3))
1748                .rect(50.0, 50.0, 495.0, 30.0)
1749                .fill();
1750
1751            page.text()
1752                .set_font(Font::Helvetica, 10.0)
1753                .at(60.0, 60.0)
1754                .write("Generated by oxidize-pdf integration test")
1755                .unwrap();
1756
1757            doc.add_page(page);
1758
1759            // Write and verify
1760            let mut writer = PdfWriter::new(&file_path).unwrap();
1761            writer.write_document(&mut doc).unwrap();
1762
1763            assert!(file_path.exists());
1764            let metadata = fs::metadata(&file_path).unwrap();
1765            assert!(metadata.len() > 500); // Should contain substantial content
1766
1767            // Verify content structure (text may be compressed, so check for basic structure)
1768            let content = fs::read(&file_path).unwrap();
1769            let content_str = String::from_utf8_lossy(&content);
1770            assert!(content_str.contains("obj")); // Should contain PDF objects
1771            assert!(content_str.contains("stream")); // Should contain content streams
1772            assert!(content_str.contains("endobj")); // Should contain object endings
1773        }
1774
1775        #[test]
1776        fn test_page_content_generation_performance() {
1777            let mut page = Page::a4();
1778
1779            // Add many elements to test performance
1780            for i in 0..100 {
1781                let y = 800.0 - (i as f64 * 7.0);
1782                if y > 50.0 {
1783                    page.text()
1784                        .set_font(Font::Helvetica, 8.0)
1785                        .at(50.0, y)
1786                        .write(&format!("Performance test line {i}"))
1787                        .unwrap();
1788                }
1789            }
1790
1791            // Add graphics elements
1792            for i in 0..50 {
1793                let x = 50.0 + (i as f64 * 10.0);
1794                if x < 550.0 {
1795                    page.graphics()
1796                        .set_fill_color(Color::rgb(0.5, 0.5, 0.8))
1797                        .rect(x, 400.0, 8.0, 8.0)
1798                        .fill();
1799                }
1800            }
1801
1802            // Content generation should complete in reasonable time
1803            let start = std::time::Instant::now();
1804            let content = page.generate_content().unwrap();
1805            let duration = start.elapsed();
1806
1807            assert!(!content.is_empty());
1808            assert!(duration.as_millis() < 1000); // Should complete within 1 second
1809        }
1810
1811        #[test]
1812        fn test_page_error_handling() {
1813            let mut page = Page::a4();
1814
1815            // Test drawing non-existent image
1816            let result = page.draw_image("nonexistent", 100.0, 100.0, 50.0, 50.0);
1817            assert!(result.is_err());
1818
1819            // Test with invalid parameters - should still work
1820            let result = page.draw_image("still_nonexistent", -100.0, -100.0, 0.0, 0.0);
1821            assert!(result.is_err());
1822
1823            // Add an image and test valid drawing
1824            let jpeg_data = vec![
1825                0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x32, 0x00, 0x32, 0x01, 0xFF, 0xD9,
1826            ];
1827            let image = Image::from_jpeg_data(jpeg_data).unwrap();
1828            page.add_image("valid_image", image);
1829
1830            let result = page.draw_image("valid_image", 100.0, 100.0, 50.0, 50.0);
1831            assert!(result.is_ok());
1832        }
1833
1834        #[test]
1835        fn test_page_memory_management() {
1836            let mut pages = Vec::new();
1837
1838            // Create many pages to test memory usage
1839            for i in 0..100 {
1840                let mut page = Page::a4();
1841                page.set_margins(i as f64, i as f64, i as f64, i as f64);
1842
1843                page.text()
1844                    .set_font(Font::Helvetica, 12.0)
1845                    .at(100.0, 700.0)
1846                    .write(&format!("Page {i}"))
1847                    .unwrap();
1848
1849                pages.push(page);
1850            }
1851
1852            // All pages should be valid
1853            assert_eq!(pages.len(), 100);
1854
1855            // Content generation should work for all pages
1856            for page in pages.iter_mut() {
1857                let content = page.generate_content().unwrap();
1858                assert!(!content.is_empty());
1859            }
1860        }
1861
1862        #[test]
1863        fn test_page_standard_sizes() {
1864            let a4 = Page::a4();
1865            let letter = Page::letter();
1866            let custom = Page::new(200.0, 300.0);
1867
1868            // Test standard dimensions
1869            assert_eq!(a4.width(), 595.0);
1870            assert_eq!(a4.height(), 842.0);
1871            assert_eq!(letter.width(), 612.0);
1872            assert_eq!(letter.height(), 792.0);
1873            assert_eq!(custom.width(), 200.0);
1874            assert_eq!(custom.height(), 300.0);
1875
1876            // Test content areas with default margins
1877            let a4_content_width = a4.content_width();
1878            let letter_content_width = letter.content_width();
1879            let custom_content_width = custom.content_width();
1880
1881            assert_eq!(a4_content_width, 595.0 - 144.0); // 595 - 2*72
1882            assert_eq!(letter_content_width, 612.0 - 144.0); // 612 - 2*72
1883            assert_eq!(custom_content_width, 200.0 - 144.0); // 200 - 2*72
1884        }
1885
1886        #[test]
1887        fn test_header_footer_document_integration() {
1888            use crate::text::{HeaderFooter, TextAlign};
1889
1890            let temp_dir = TempDir::new().unwrap();
1891            let file_path = temp_dir.path().join("header_footer_test.pdf");
1892
1893            let mut doc = Document::new();
1894            doc.set_title("Header Footer Integration Test");
1895
1896            // Create multiple pages with headers and footers
1897            for i in 1..=3 {
1898                let mut page = Page::a4();
1899
1900                // Set header
1901                let header = HeaderFooter::new_header(format!("Chapter {i}"))
1902                    .with_font(Font::HelveticaBold, 16.0)
1903                    .with_alignment(TextAlign::Center);
1904                page.set_header(header);
1905
1906                // Set footer with page numbers
1907                let footer = HeaderFooter::new_footer("Page {{page_number}} of {{total_pages}}")
1908                    .with_font(Font::Helvetica, 10.0)
1909                    .with_alignment(TextAlign::Center);
1910                page.set_footer(footer);
1911
1912                // Add content
1913                page.text()
1914                    .set_font(Font::TimesRoman, 12.0)
1915                    .at(100.0, 700.0)
1916                    .write(&format!("This is the content of chapter {i}"))
1917                    .unwrap();
1918
1919                doc.add_page(page);
1920            }
1921
1922            // Write to file
1923            let mut writer = PdfWriter::new(&file_path).unwrap();
1924            writer.write_document(&mut doc).unwrap();
1925
1926            // Verify file was created
1927            assert!(file_path.exists());
1928            let metadata = fs::metadata(&file_path).unwrap();
1929            assert!(metadata.len() > 1000);
1930
1931            // Read and verify content
1932            let content = fs::read(&file_path).unwrap();
1933            let content_str = String::from_utf8_lossy(&content);
1934
1935            // PDF was created successfully and has substantial content
1936            assert!(content.len() > 2000);
1937            // Verify basic PDF structure
1938            assert!(content_str.contains("%PDF"));
1939            assert!(content_str.contains("endobj"));
1940
1941            // Note: Content may be compressed, so we can't directly check for text strings
1942            // The important thing is that the PDF was generated without errors
1943        }
1944
1945        #[test]
1946        fn test_header_footer_alignment_integration() {
1947            use crate::text::{HeaderFooter, TextAlign};
1948
1949            let temp_dir = TempDir::new().unwrap();
1950            let file_path = temp_dir.path().join("alignment_test.pdf");
1951
1952            let mut doc = Document::new();
1953
1954            let mut page = Page::a4();
1955
1956            // Left-aligned header
1957            let header = HeaderFooter::new_header("Left Header")
1958                .with_font(Font::Helvetica, 12.0)
1959                .with_alignment(TextAlign::Left)
1960                .with_margin(50.0);
1961            page.set_header(header);
1962
1963            // Right-aligned footer
1964            let footer = HeaderFooter::new_footer("Right Footer - Page {{page_number}}")
1965                .with_font(Font::Helvetica, 10.0)
1966                .with_alignment(TextAlign::Right)
1967                .with_margin(50.0);
1968            page.set_footer(footer);
1969
1970            doc.add_page(page);
1971
1972            // Write to file
1973            let mut writer = PdfWriter::new(&file_path).unwrap();
1974            writer.write_document(&mut doc).unwrap();
1975
1976            assert!(file_path.exists());
1977        }
1978
1979        #[test]
1980        fn test_header_footer_date_time_integration() {
1981            use crate::text::HeaderFooter;
1982
1983            let temp_dir = TempDir::new().unwrap();
1984            let file_path = temp_dir.path().join("date_time_test.pdf");
1985
1986            let mut doc = Document::new();
1987
1988            let mut page = Page::a4();
1989
1990            // Header with date/time
1991            let header = HeaderFooter::new_header("Report generated on {{date}} at {{time}}")
1992                .with_font(Font::Helvetica, 11.0);
1993            page.set_header(header);
1994
1995            // Footer with year
1996            let footer =
1997                HeaderFooter::new_footer("© {{year}} Company Name").with_font(Font::Helvetica, 9.0);
1998            page.set_footer(footer);
1999
2000            doc.add_page(page);
2001
2002            // Write to file
2003            let mut writer = PdfWriter::new(&file_path).unwrap();
2004            writer.write_document(&mut doc).unwrap();
2005
2006            assert!(file_path.exists());
2007
2008            // Verify the file was created successfully
2009            let content = fs::read(&file_path).unwrap();
2010            assert!(content.len() > 500);
2011
2012            // Verify basic PDF structure
2013            let content_str = String::from_utf8_lossy(&content);
2014            assert!(content_str.contains("%PDF"));
2015            assert!(content_str.contains("endobj"));
2016
2017            // Note: We can't check for specific text content as it may be compressed
2018            // The test validates that headers/footers with date placeholders don't cause errors
2019        }
2020    }
2021}
2022
2023#[cfg(test)]
2024mod unit_tests {
2025    use super::*;
2026    use crate::graphics::Color;
2027    use crate::text::Font;
2028
2029    // ============= Constructor Tests =============
2030
2031    #[test]
2032    fn test_new_page_dimensions() {
2033        let page = Page::new(100.0, 200.0);
2034        assert_eq!(page.width(), 100.0);
2035        assert_eq!(page.height(), 200.0);
2036    }
2037
2038    #[test]
2039    fn test_a4_page_dimensions() {
2040        let page = Page::a4();
2041        assert_eq!(page.width(), 595.0);
2042        assert_eq!(page.height(), 842.0);
2043    }
2044
2045    #[test]
2046    fn test_letter_page_dimensions() {
2047        let page = Page::letter();
2048        assert_eq!(page.width(), 612.0);
2049        assert_eq!(page.height(), 792.0);
2050    }
2051
2052    #[test]
2053    fn test_legal_page_dimensions() {
2054        let page = Page::legal();
2055        assert_eq!(page.width(), 612.0);
2056        assert_eq!(page.height(), 1008.0);
2057    }
2058
2059    // ============= Margins Tests =============
2060
2061    #[test]
2062    fn test_default_margins() {
2063        let page = Page::a4();
2064        let margins = page.margins();
2065        assert_eq!(margins.left, 72.0);
2066        assert_eq!(margins.right, 72.0);
2067        assert_eq!(margins.top, 72.0);
2068        assert_eq!(margins.bottom, 72.0);
2069    }
2070
2071    #[test]
2072    fn test_set_margins() {
2073        let mut page = Page::a4();
2074        page.set_margins(10.0, 20.0, 30.0, 40.0);
2075
2076        let margins = page.margins();
2077        assert_eq!(margins.left, 10.0);
2078        assert_eq!(margins.right, 20.0);
2079        assert_eq!(margins.top, 30.0);
2080        assert_eq!(margins.bottom, 40.0);
2081    }
2082
2083    #[test]
2084    fn test_content_width() {
2085        let mut page = Page::new(600.0, 800.0);
2086        page.set_margins(50.0, 50.0, 0.0, 0.0);
2087        assert_eq!(page.content_width(), 500.0);
2088    }
2089
2090    #[test]
2091    fn test_content_height() {
2092        let mut page = Page::new(600.0, 800.0);
2093        page.set_margins(0.0, 0.0, 100.0, 100.0);
2094        assert_eq!(page.content_height(), 600.0);
2095    }
2096
2097    #[test]
2098    fn test_content_area() {
2099        let mut page = Page::new(600.0, 800.0);
2100        page.set_margins(50.0, 60.0, 70.0, 80.0);
2101
2102        let (x, y, right, top) = page.content_area();
2103        assert_eq!(x, 50.0); // left margin
2104        assert_eq!(y, 80.0); // bottom margin
2105        assert_eq!(right, 540.0); // page width (600) - right margin (60)
2106        assert_eq!(top, 730.0); // page height (800) - top margin (70)
2107    }
2108
2109    // ============= Graphics Context Tests =============
2110
2111    #[test]
2112    fn test_graphics_context_access() {
2113        let mut page = Page::a4();
2114        let gc = page.graphics();
2115
2116        // Test that we can perform basic operations
2117        gc.move_to(0.0, 0.0);
2118        gc.line_to(100.0, 100.0);
2119
2120        // Operations should be recorded
2121        let ops = gc.get_operations();
2122        assert!(!ops.is_empty());
2123    }
2124
2125    #[test]
2126    fn test_graphics_operations_chain() {
2127        let mut page = Page::a4();
2128
2129        page.graphics()
2130            .set_fill_color(Color::red())
2131            .rectangle(10.0, 10.0, 100.0, 50.0)
2132            .fill();
2133
2134        let ops = page.graphics().get_operations();
2135        assert!(ops.contains("re")); // rectangle operator
2136        assert!(ops.contains("f")); // fill operator
2137    }
2138
2139    // ============= Text Context Tests =============
2140
2141    #[test]
2142    fn test_text_context_access() {
2143        let mut page = Page::a4();
2144        let tc = page.text();
2145
2146        tc.set_font(Font::Helvetica, 12.0);
2147        tc.at(100.0, 100.0);
2148
2149        // Should be able to write text without error
2150        let result = tc.write("Test text");
2151        assert!(result.is_ok());
2152    }
2153
2154    // Issue #97: Test that get_used_characters combines both contexts
2155    #[test]
2156    fn test_get_used_characters_from_text_context() {
2157        let mut page = Page::a4();
2158
2159        // Write text via text_context
2160        page.text().write("ABC").unwrap();
2161
2162        // Should capture characters from text_context
2163        let chars = page.get_used_characters();
2164        assert!(chars.is_some());
2165        let chars = chars.unwrap();
2166        assert!(chars.contains(&'A'));
2167        assert!(chars.contains(&'B'));
2168        assert!(chars.contains(&'C'));
2169    }
2170
2171    #[test]
2172    fn test_get_used_characters_combines_both_contexts() {
2173        let mut page = Page::a4();
2174
2175        // Write text via text_context
2176        page.text().write("AB").unwrap();
2177
2178        // Write text via graphics_context
2179        let _ = page.graphics().draw_text("CD", 100.0, 100.0);
2180
2181        // Should capture characters from both contexts
2182        let chars = page.get_used_characters();
2183        assert!(chars.is_some());
2184        let chars = chars.unwrap();
2185        assert!(chars.contains(&'A'));
2186        assert!(chars.contains(&'B'));
2187        assert!(chars.contains(&'C'));
2188        assert!(chars.contains(&'D'));
2189    }
2190
2191    #[test]
2192    fn test_get_used_characters_cjk_via_text_context() {
2193        let mut page = Page::a4();
2194
2195        // Write CJK text via text_context (the bug scenario from issue #97)
2196        page.text()
2197            .set_font(Font::Custom("NotoSansCJK".to_string()), 12.0);
2198        page.text().write("中文").unwrap();
2199
2200        let chars = page.get_used_characters();
2201        assert!(chars.is_some());
2202        let chars = chars.unwrap();
2203        assert!(chars.contains(&'中'));
2204        assert!(chars.contains(&'æ–‡'));
2205    }
2206
2207    #[test]
2208    fn test_text_flow_creation() {
2209        let page = Page::a4();
2210        let text_flow = page.text_flow();
2211
2212        // Test that text flow is created without panic
2213        // TextFlowContext doesn't expose its internal state
2214        // but we can verify it's created correctly
2215        let _ = text_flow; // Just ensure it can be created
2216    }
2217
2218    // ============= Image Tests =============
2219
2220    #[test]
2221    fn test_add_image() {
2222        let mut page = Page::a4();
2223
2224        // Create a minimal JPEG image
2225        let image_data = vec![
2226            0xFF, 0xD8, // SOI marker
2227            0xFF, 0xC0, // SOF0 marker
2228            0x00, 0x11, // Length
2229            0x08, // Precision
2230            0x00, 0x10, // Height
2231            0x00, 0x10, // Width
2232            0x03, // Components
2233            0x01, 0x11, 0x00, // Component 1
2234            0x02, 0x11, 0x00, // Component 2
2235            0x03, 0x11, 0x00, // Component 3
2236            0xFF, 0xD9, // EOI marker
2237        ];
2238
2239        let image = Image::from_jpeg_data(image_data).unwrap();
2240        page.add_image("test_image", image);
2241
2242        // Image should be stored
2243        assert!(page.images.contains_key("test_image"));
2244    }
2245
2246    #[test]
2247    fn test_draw_image_simple() {
2248        let mut page = Page::a4();
2249
2250        // Create and add image
2251        let image_data = vec![
2252            0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x10, 0x00, 0x10, 0x03, 0x01, 0x11,
2253            0x00, 0x02, 0x11, 0x00, 0x03, 0x11, 0x00, 0xFF, 0xD9,
2254        ];
2255
2256        let image = Image::from_jpeg_data(image_data).unwrap();
2257        page.add_image("img1", image);
2258
2259        // Draw the image
2260        let result = page.draw_image("img1", 100.0, 100.0, 200.0, 200.0);
2261        assert!(result.is_ok());
2262    }
2263
2264    // ============= Annotations Tests =============
2265
2266    #[test]
2267    fn test_add_annotation() {
2268        use crate::annotations::{Annotation, AnnotationType};
2269        use crate::geometry::{Point, Rectangle};
2270
2271        let mut page = Page::a4();
2272        let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 150.0));
2273        let annotation = Annotation::new(AnnotationType::Text, rect);
2274
2275        page.add_annotation(annotation);
2276        assert_eq!(page.annotations().len(), 1);
2277    }
2278
2279    #[test]
2280    fn test_annotations_mut() {
2281        use crate::annotations::{Annotation, AnnotationType};
2282        use crate::geometry::{Point, Rectangle};
2283
2284        let mut page = Page::a4();
2285        let _rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 150.0));
2286
2287        // Add multiple annotations
2288        for i in 0..3 {
2289            let annotation = Annotation::new(
2290                AnnotationType::Text,
2291                Rectangle::new(
2292                    Point::new(100.0 + i as f64 * 10.0, 100.0),
2293                    Point::new(200.0 + i as f64 * 10.0, 150.0),
2294                ),
2295            );
2296            page.add_annotation(annotation);
2297        }
2298
2299        // Modify annotations
2300        let annotations = page.annotations_mut();
2301        annotations.clear();
2302        assert_eq!(page.annotations().len(), 0);
2303    }
2304
2305    // ============= Form Widget Tests =============
2306
2307    #[test]
2308    fn test_add_form_widget() {
2309        use crate::forms::Widget;
2310        use crate::geometry::{Point, Rectangle};
2311
2312        let mut page = Page::a4();
2313        let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 120.0));
2314        let widget = Widget::new(rect);
2315
2316        let obj_ref = page.add_form_widget(widget);
2317        assert_eq!(obj_ref.number(), 0);
2318        assert_eq!(obj_ref.generation(), 0);
2319
2320        // Annotations should include the widget
2321        assert_eq!(page.annotations().len(), 1);
2322    }
2323
2324    // ============= Header/Footer Tests =============
2325
2326    #[test]
2327    fn test_set_header() {
2328        use crate::text::HeaderFooter;
2329
2330        let mut page = Page::a4();
2331        let header = HeaderFooter::new_header("Test Header");
2332
2333        page.set_header(header);
2334        assert!(page.header().is_some());
2335
2336        if let Some(h) = page.header() {
2337            assert_eq!(h.content(), "Test Header");
2338        }
2339    }
2340
2341    #[test]
2342    fn test_set_footer() {
2343        use crate::text::HeaderFooter;
2344
2345        let mut page = Page::a4();
2346        let footer = HeaderFooter::new_footer("Page {{page}} of {{total}}");
2347
2348        page.set_footer(footer);
2349        assert!(page.footer().is_some());
2350
2351        if let Some(f) = page.footer() {
2352            assert_eq!(f.content(), "Page {{page}} of {{total}}");
2353        }
2354    }
2355
2356    #[test]
2357    fn test_header_footer_rendering() {
2358        use crate::text::HeaderFooter;
2359
2360        let mut page = Page::a4();
2361
2362        // Set both header and footer
2363        page.set_header(HeaderFooter::new_header("Header"));
2364        page.set_footer(HeaderFooter::new_footer("Footer"));
2365
2366        // Generate content with header/footer
2367        let result = page.generate_content_with_page_info(Some(1), Some(1), None);
2368        assert!(result.is_ok());
2369
2370        let content = result.unwrap();
2371        assert!(!content.is_empty());
2372    }
2373
2374    // ============= Table Tests =============
2375
2376    #[test]
2377    fn test_add_table() {
2378        use crate::text::Table;
2379
2380        let mut page = Page::a4();
2381        let mut table = Table::with_equal_columns(2, 200.0);
2382
2383        // Add some rows
2384        table
2385            .add_row(vec!["Cell 1".to_string(), "Cell 2".to_string()])
2386            .unwrap();
2387        table
2388            .add_row(vec!["Cell 3".to_string(), "Cell 4".to_string()])
2389            .unwrap();
2390
2391        let result = page.add_table(&table);
2392        assert!(result.is_ok());
2393    }
2394
2395    // ============= Content Generation Tests =============
2396
2397    #[test]
2398    fn test_generate_operations_empty() {
2399        let page = Page::a4();
2400        // Page doesn't have generate_operations, use graphics_context
2401        let ops = page.graphics_context.generate_operations();
2402
2403        // Even empty page should have valid PDF operations
2404        assert!(ops.is_ok());
2405    }
2406
2407    #[test]
2408    fn test_generate_operations_with_graphics() {
2409        let mut page = Page::a4();
2410
2411        page.graphics().rectangle(50.0, 50.0, 100.0, 100.0).fill();
2412
2413        // Page doesn't have generate_operations, use graphics_context
2414        let ops = page.graphics_context.generate_operations();
2415        assert!(ops.is_ok());
2416
2417        let content = ops.unwrap();
2418        let content_str = String::from_utf8_lossy(&content);
2419        assert!(content_str.contains("re")); // rectangle
2420        assert!(content_str.contains("f")); // fill
2421    }
2422
2423    #[test]
2424    fn test_generate_operations_with_text() {
2425        let mut page = Page::a4();
2426
2427        page.text()
2428            .set_font(Font::Helvetica, 12.0)
2429            .at(100.0, 700.0)
2430            .write("Hello")
2431            .unwrap();
2432
2433        // Text operations are in text_context, not graphics_context
2434        let ops = page.text_context.generate_operations();
2435        assert!(ops.is_ok());
2436
2437        let content = ops.unwrap();
2438        let content_str = String::from_utf8_lossy(&content);
2439        assert!(content_str.contains("BT")); // Begin text
2440        assert!(content_str.contains("ET")); // End text
2441    }
2442
2443    // ============= Edge Cases and Error Handling =============
2444
2445    #[test]
2446    fn test_negative_margins() {
2447        let mut page = Page::a4();
2448        page.set_margins(-10.0, -20.0, -30.0, -40.0);
2449
2450        // Negative margins should still work (might be intentional)
2451        let margins = page.margins();
2452        assert_eq!(margins.left, -10.0);
2453        assert_eq!(margins.right, -20.0);
2454    }
2455
2456    #[test]
2457    fn test_zero_dimensions() {
2458        let page = Page::new(0.0, 0.0);
2459        assert_eq!(page.width(), 0.0);
2460        assert_eq!(page.height(), 0.0);
2461
2462        // Content area with default margins would be negative
2463        let (_, _, width, height) = page.content_area();
2464        assert!(width < 0.0);
2465        assert!(height < 0.0);
2466    }
2467
2468    #[test]
2469    fn test_huge_dimensions() {
2470        let page = Page::new(1_000_000.0, 1_000_000.0);
2471        assert_eq!(page.width(), 1_000_000.0);
2472        assert_eq!(page.height(), 1_000_000.0);
2473    }
2474
2475    #[test]
2476    fn test_draw_nonexistent_image() {
2477        let mut page = Page::a4();
2478
2479        // Try to draw an image that wasn't added
2480        let result = page.draw_image("nonexistent", 100.0, 100.0, 200.0, 200.0);
2481
2482        // Should fail gracefully
2483        assert!(result.is_err());
2484    }
2485
2486    #[test]
2487    fn test_clone_page() {
2488        let mut page = Page::a4();
2489        page.set_margins(10.0, 20.0, 30.0, 40.0);
2490
2491        page.graphics().rectangle(50.0, 50.0, 100.0, 100.0).fill();
2492
2493        let cloned = page.clone();
2494        assert_eq!(cloned.width(), page.width());
2495        assert_eq!(cloned.height(), page.height());
2496        assert_eq!(cloned.margins().left, page.margins().left);
2497    }
2498
2499    #[test]
2500    fn test_page_from_parsed_basic() {
2501        use crate::parser::objects::PdfDictionary;
2502        use crate::parser::page_tree::ParsedPage;
2503
2504        // Create a test parsed page
2505        let parsed_page = ParsedPage {
2506            obj_ref: (1, 0),
2507            dict: PdfDictionary::new(),
2508            inherited_resources: None,
2509            media_box: [0.0, 0.0, 612.0, 792.0], // US Letter
2510            crop_box: None,
2511            rotation: 0,
2512            annotations: None,
2513        };
2514
2515        // Convert to writable page
2516        let page = Page::from_parsed(&parsed_page).unwrap();
2517
2518        // Verify dimensions
2519        assert_eq!(page.width(), 612.0);
2520        assert_eq!(page.height(), 792.0);
2521        assert_eq!(page.get_rotation(), 0);
2522    }
2523
2524    #[test]
2525    fn test_page_from_parsed_with_rotation() {
2526        use crate::parser::objects::PdfDictionary;
2527        use crate::parser::page_tree::ParsedPage;
2528
2529        // Create a test parsed page with 90-degree rotation
2530        let parsed_page = ParsedPage {
2531            obj_ref: (1, 0),
2532            dict: PdfDictionary::new(),
2533            inherited_resources: None,
2534            media_box: [0.0, 0.0, 595.0, 842.0], // A4
2535            crop_box: None,
2536            rotation: 90,
2537            annotations: None,
2538        };
2539
2540        // Convert to writable page
2541        let page = Page::from_parsed(&parsed_page).unwrap();
2542
2543        // Verify rotation was preserved
2544        assert_eq!(page.get_rotation(), 90);
2545        assert_eq!(page.width(), 595.0);
2546        assert_eq!(page.height(), 842.0);
2547
2548        // Verify effective dimensions (rotated)
2549        assert_eq!(page.effective_width(), 842.0);
2550        assert_eq!(page.effective_height(), 595.0);
2551    }
2552
2553    #[test]
2554    fn test_page_from_parsed_with_cropbox() {
2555        use crate::parser::objects::PdfDictionary;
2556        use crate::parser::page_tree::ParsedPage;
2557
2558        // Create a test parsed page with CropBox
2559        let parsed_page = ParsedPage {
2560            obj_ref: (1, 0),
2561            dict: PdfDictionary::new(),
2562            inherited_resources: None,
2563            media_box: [0.0, 0.0, 612.0, 792.0],
2564            crop_box: Some([10.0, 10.0, 602.0, 782.0]),
2565            rotation: 0,
2566            annotations: None,
2567        };
2568
2569        // Convert to writable page
2570        let page = Page::from_parsed(&parsed_page).unwrap();
2571
2572        // CropBox doesn't affect page dimensions (only visible area)
2573        assert_eq!(page.width(), 612.0);
2574        assert_eq!(page.height(), 792.0);
2575    }
2576
2577    #[test]
2578    fn test_page_from_parsed_small_mediabox() {
2579        use crate::parser::objects::PdfDictionary;
2580        use crate::parser::page_tree::ParsedPage;
2581
2582        // Create a test parsed page with custom small dimensions
2583        let parsed_page = ParsedPage {
2584            obj_ref: (1, 0),
2585            dict: PdfDictionary::new(),
2586            inherited_resources: None,
2587            media_box: [0.0, 0.0, 200.0, 300.0],
2588            crop_box: None,
2589            rotation: 0,
2590            annotations: None,
2591        };
2592
2593        // Convert to writable page
2594        let page = Page::from_parsed(&parsed_page).unwrap();
2595
2596        assert_eq!(page.width(), 200.0);
2597        assert_eq!(page.height(), 300.0);
2598    }
2599
2600    #[test]
2601    fn test_page_from_parsed_non_zero_origin() {
2602        use crate::parser::objects::PdfDictionary;
2603        use crate::parser::page_tree::ParsedPage;
2604
2605        // Create a test parsed page with non-zero origin
2606        let parsed_page = ParsedPage {
2607            obj_ref: (1, 0),
2608            dict: PdfDictionary::new(),
2609            inherited_resources: None,
2610            media_box: [10.0, 20.0, 610.0, 820.0], // Offset origin
2611            crop_box: None,
2612            rotation: 0,
2613            annotations: None,
2614        };
2615
2616        // Convert to writable page
2617        let page = Page::from_parsed(&parsed_page).unwrap();
2618
2619        // Width and height should be calculated correctly
2620        assert_eq!(page.width(), 600.0); // 610 - 10
2621        assert_eq!(page.height(), 800.0); // 820 - 20
2622    }
2623
2624    #[test]
2625    fn test_page_rotation() {
2626        let mut page = Page::a4();
2627
2628        // Test default rotation
2629        assert_eq!(page.get_rotation(), 0);
2630
2631        // Test setting valid rotations
2632        page.set_rotation(90);
2633        assert_eq!(page.get_rotation(), 90);
2634
2635        page.set_rotation(180);
2636        assert_eq!(page.get_rotation(), 180);
2637
2638        page.set_rotation(270);
2639        assert_eq!(page.get_rotation(), 270);
2640
2641        page.set_rotation(360);
2642        assert_eq!(page.get_rotation(), 0);
2643
2644        // Test rotation normalization
2645        page.set_rotation(45);
2646        assert_eq!(page.get_rotation(), 90);
2647
2648        page.set_rotation(135);
2649        assert_eq!(page.get_rotation(), 180);
2650
2651        page.set_rotation(-90);
2652        assert_eq!(page.get_rotation(), 270);
2653    }
2654
2655    #[test]
2656    fn test_effective_dimensions() {
2657        let mut page = Page::new(600.0, 800.0);
2658
2659        // No rotation - same dimensions
2660        assert_eq!(page.effective_width(), 600.0);
2661        assert_eq!(page.effective_height(), 800.0);
2662
2663        // 90 degree rotation - swapped dimensions
2664        page.set_rotation(90);
2665        assert_eq!(page.effective_width(), 800.0);
2666        assert_eq!(page.effective_height(), 600.0);
2667
2668        // 180 degree rotation - same dimensions
2669        page.set_rotation(180);
2670        assert_eq!(page.effective_width(), 600.0);
2671        assert_eq!(page.effective_height(), 800.0);
2672
2673        // 270 degree rotation - swapped dimensions
2674        page.set_rotation(270);
2675        assert_eq!(page.effective_width(), 800.0);
2676        assert_eq!(page.effective_height(), 600.0);
2677    }
2678
2679    #[test]
2680    fn test_rotation_in_pdf_dict() {
2681        let mut page = Page::a4();
2682
2683        // No rotation should not include Rotate field
2684        let dict = page.to_dict();
2685        assert!(dict.get("Rotate").is_none());
2686
2687        // With rotation should include Rotate field
2688        page.set_rotation(90);
2689        let dict = page.to_dict();
2690        assert_eq!(dict.get("Rotate"), Some(&Object::Integer(90)));
2691
2692        page.set_rotation(270);
2693        let dict = page.to_dict();
2694        assert_eq!(dict.get("Rotate"), Some(&Object::Integer(270)));
2695    }
2696}
2697
2698/// Layout manager for intelligent positioning of elements on a page
2699///
2700/// This manager handles automatic positioning of tables, images, and other elements
2701/// using different coordinate systems while preventing overlaps and managing page flow.
2702#[derive(Debug)]
2703pub struct LayoutManager {
2704    /// Coordinate system being used
2705    pub coordinate_system: crate::coordinate_system::CoordinateSystem,
2706    /// Current Y position for next element
2707    pub current_y: f64,
2708    /// Page dimensions
2709    pub page_width: f64,
2710    pub page_height: f64,
2711    /// Page margins
2712    pub margins: Margins,
2713    /// Spacing between elements
2714    pub element_spacing: f64,
2715}
2716
2717impl LayoutManager {
2718    /// Create a new layout manager for the given page
2719    pub fn new(page: &Page, coordinate_system: crate::coordinate_system::CoordinateSystem) -> Self {
2720        let current_y = match coordinate_system {
2721            crate::coordinate_system::CoordinateSystem::PdfStandard => {
2722                // PDF coordinates: start from top (high Y value)
2723                page.height() - page.margins().top
2724            }
2725            crate::coordinate_system::CoordinateSystem::ScreenSpace => {
2726                // Screen coordinates: start from top (low Y value)
2727                page.margins().top
2728            }
2729            crate::coordinate_system::CoordinateSystem::Custom(_) => {
2730                // For custom systems, start conservatively in the middle
2731                page.height() / 2.0
2732            }
2733        };
2734
2735        Self {
2736            coordinate_system,
2737            current_y,
2738            page_width: page.width(),
2739            page_height: page.height(),
2740            margins: page.margins().clone(),
2741            element_spacing: 10.0,
2742        }
2743    }
2744
2745    /// Set custom spacing between elements
2746    pub fn with_element_spacing(mut self, spacing: f64) -> Self {
2747        self.element_spacing = spacing;
2748        self
2749    }
2750
2751    /// Check if an element of given height will fit on the current page
2752    pub fn will_fit(&self, element_height: f64) -> bool {
2753        let required_space = element_height + self.element_spacing;
2754
2755        match self.coordinate_system {
2756            crate::coordinate_system::CoordinateSystem::PdfStandard => {
2757                // In PDF coords, we subtract height and check against bottom margin
2758                self.current_y - required_space >= self.margins.bottom
2759            }
2760            crate::coordinate_system::CoordinateSystem::ScreenSpace => {
2761                // In screen coords, we add height and check against page height
2762                self.current_y + required_space <= self.page_height - self.margins.bottom
2763            }
2764            crate::coordinate_system::CoordinateSystem::Custom(_) => {
2765                // Conservative check for custom coordinate systems
2766                required_space <= (self.page_height - self.margins.top - self.margins.bottom) / 2.0
2767            }
2768        }
2769    }
2770
2771    /// Get the current available space remaining on the page
2772    pub fn remaining_space(&self) -> f64 {
2773        match self.coordinate_system {
2774            crate::coordinate_system::CoordinateSystem::PdfStandard => {
2775                (self.current_y - self.margins.bottom).max(0.0)
2776            }
2777            crate::coordinate_system::CoordinateSystem::ScreenSpace => {
2778                (self.page_height - self.margins.bottom - self.current_y).max(0.0)
2779            }
2780            crate::coordinate_system::CoordinateSystem::Custom(_) => {
2781                self.page_height / 2.0 // Conservative estimate
2782            }
2783        }
2784    }
2785
2786    /// Reserve space for an element and return its Y position
2787    ///
2788    /// Returns `None` if the element doesn't fit on the current page.
2789    /// If it fits, returns the Y coordinate where the element should be placed
2790    /// and updates the internal current_y for the next element.
2791    pub fn add_element(&mut self, element_height: f64) -> Option<f64> {
2792        if !self.will_fit(element_height) {
2793            return None;
2794        }
2795
2796        let position_y = match self.coordinate_system {
2797            crate::coordinate_system::CoordinateSystem::PdfStandard => {
2798                // Position element at current_y (top of element)
2799                // Then move current_y down by element height + spacing
2800                let y_position = self.current_y - element_height;
2801                self.current_y = y_position - self.element_spacing;
2802                self.current_y + element_height // Return the bottom Y of the element area
2803            }
2804            crate::coordinate_system::CoordinateSystem::ScreenSpace => {
2805                // Position element at current_y (top of element)
2806                // Then move current_y down by element height + spacing
2807                let y_position = self.current_y;
2808                self.current_y += element_height + self.element_spacing;
2809                y_position
2810            }
2811            crate::coordinate_system::CoordinateSystem::Custom(_) => {
2812                // Simple implementation for custom coordinate systems
2813                let y_position = self.current_y;
2814                self.current_y -= element_height + self.element_spacing;
2815                y_position
2816            }
2817        };
2818
2819        Some(position_y)
2820    }
2821
2822    /// Reset the layout manager for a new page
2823    pub fn new_page(&mut self) {
2824        self.current_y = match self.coordinate_system {
2825            crate::coordinate_system::CoordinateSystem::PdfStandard => {
2826                self.page_height - self.margins.top
2827            }
2828            crate::coordinate_system::CoordinateSystem::ScreenSpace => self.margins.top,
2829            crate::coordinate_system::CoordinateSystem::Custom(_) => self.page_height / 2.0,
2830        };
2831    }
2832
2833    /// Get the X position for centering an element of given width
2834    pub fn center_x(&self, element_width: f64) -> f64 {
2835        let available_width = self.page_width - self.margins.left - self.margins.right;
2836        self.margins.left + (available_width - element_width) / 2.0
2837    }
2838
2839    /// Get the left margin X position
2840    pub fn left_x(&self) -> f64 {
2841        self.margins.left
2842    }
2843
2844    /// Get the right margin X position minus element width
2845    pub fn right_x(&self, element_width: f64) -> f64 {
2846        self.page_width - self.margins.right - element_width
2847    }
2848}
2849
2850#[cfg(test)]
2851mod layout_manager_tests {
2852    use super::*;
2853    use crate::coordinate_system::CoordinateSystem;
2854
2855    #[test]
2856    fn test_layout_manager_pdf_standard() {
2857        let page = Page::a4(); // 595 x 842
2858        let mut layout = LayoutManager::new(&page, CoordinateSystem::PdfStandard);
2859
2860        // Check initial position (should be near top in PDF coords)
2861        // A4 height is 842, with default margin of 72, so current_y should be 842 - 72 = 770
2862        assert!(layout.current_y > 750.0); // Near top of A4, adjusted for actual margins
2863
2864        // Add an element
2865        let element_height = 100.0;
2866        let position = layout.add_element(element_height);
2867
2868        assert!(position.is_some());
2869        let y_pos = position.unwrap();
2870        assert!(y_pos > 700.0); // Should be positioned high up
2871
2872        // Current Y should have moved down
2873        assert!(layout.current_y < y_pos);
2874    }
2875
2876    #[test]
2877    fn test_layout_manager_screen_space() {
2878        let page = Page::a4();
2879        let mut layout = LayoutManager::new(&page, CoordinateSystem::ScreenSpace);
2880
2881        // Check initial position (should be near top in screen coords)
2882        assert!(layout.current_y < 100.0); // Near top margin
2883
2884        // Add an element
2885        let element_height = 100.0;
2886        let position = layout.add_element(element_height);
2887
2888        assert!(position.is_some());
2889        let y_pos = position.unwrap();
2890        assert!(y_pos < 100.0); // Should be positioned near top
2891
2892        // Current Y should have moved down (increased)
2893        assert!(layout.current_y > y_pos);
2894    }
2895
2896    #[test]
2897    fn test_layout_manager_overflow() {
2898        let page = Page::a4();
2899        let mut layout = LayoutManager::new(&page, CoordinateSystem::PdfStandard);
2900
2901        // Try to add an element that's too large
2902        let huge_element = 900.0; // Larger than page height
2903        let position = layout.add_element(huge_element);
2904
2905        assert!(position.is_none()); // Should not fit
2906
2907        // Fill the page with smaller elements
2908        let mut count = 0;
2909        while layout.add_element(50.0).is_some() {
2910            count += 1;
2911            if count > 100 {
2912                break;
2913            } // Safety valve
2914        }
2915
2916        // Should have added multiple elements
2917        assert!(count > 5);
2918
2919        // Next element should not fit
2920        assert!(layout.add_element(50.0).is_none());
2921    }
2922
2923    #[test]
2924    fn test_layout_manager_centering() {
2925        let page = Page::a4();
2926        let layout = LayoutManager::new(&page, CoordinateSystem::PdfStandard);
2927
2928        let element_width = 200.0;
2929        let center_x = layout.center_x(element_width);
2930
2931        // Should be centered considering margins
2932        let expected_center = page.margins().left
2933            + (page.width() - page.margins().left - page.margins().right - element_width) / 2.0;
2934        assert_eq!(center_x, expected_center);
2935    }
2936}