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