1use crate::annotations::Annotation;
2use crate::error::Result;
3use crate::fonts::type0_parsing::{detect_type0_font, resolve_type0_hierarchy};
4use crate::forms::Widget;
5use crate::graphics::{GraphicsContext, Image};
6use crate::objects::{Array, Dictionary, Object, ObjectReference};
7use crate::text::metrics::FontMetricsStore;
8use crate::text::{HeaderFooter, Table, TextContext, TextFlowContext};
9use std::collections::{HashMap, HashSet};
10
11#[derive(Clone, Debug)]
13pub struct Margins {
14 pub left: f64,
16 pub right: f64,
18 pub top: f64,
20 pub bottom: f64,
22}
23
24impl Default for Margins {
25 fn default() -> Self {
26 Self {
27 left: 72.0, right: 72.0, top: 72.0, bottom: 72.0, }
32 }
33}
34
35fn validate_pdf_resource_name(name: &str) -> Result<()> {
77 use crate::error::PdfError;
78
79 if name.is_empty() {
80 return Err(PdfError::InvalidStructure(
81 "PDF resource name must not be empty (ISO 32000-1 §7.3.5)".to_string(),
82 ));
83 }
84
85 for (idx, byte) in name.as_bytes().iter().enumerate() {
86 let is_whitespace = matches!(*byte, 0x00 | 0x09 | 0x0A | 0x0C | 0x0D | 0x20);
88 let is_delimiter = matches!(
90 *byte,
91 b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
92 );
93 let is_hash = *byte == b'#';
96
97 if is_whitespace || is_delimiter || is_hash {
98 return Err(PdfError::InvalidStructure(format!(
99 "invalid PDF resource name {name:?}: byte 0x{byte:02X} at position {idx} \
100 is not allowed per ISO 32000-1 §7.3.5 (whitespace, delimiter, or `#`)"
101 )));
102 }
103 }
104 Ok(())
105}
106
107#[derive(Clone)]
108pub struct Page {
109 width: f64,
110 height: f64,
111 margins: Margins,
112 content: Vec<u8>,
113 graphics_context: GraphicsContext,
114 text_context: TextContext,
115 images: HashMap<String, Image>,
116 form_xobjects: HashMap<String, crate::graphics::FormXObject>,
117 color_spaces: HashMap<String, crate::graphics::PageColorSpace>,
123 patterns: HashMap<String, crate::graphics::TilingPattern>,
126 shadings: HashMap<String, crate::graphics::ShadingDefinition>,
129 advanced_shadings: HashMap<String, crate::graphics::AdvancedShading>,
133 header: Option<HeaderFooter>,
134 footer: Option<HeaderFooter>,
135 annotations: Vec<Annotation>,
136 coordinate_system: crate::coordinate_system::CoordinateSystem,
137 rotation: i32, next_mcid: u32,
140 marked_content_stack: Vec<String>,
142 preserved_resources: Option<crate::pdf_objects::Dictionary>,
145 page_ops: Vec<crate::graphics::ops::Op>,
153 pub(crate) font_metrics_store: Option<FontMetricsStore>,
158 pub(crate) preserved_font_rewrite_map: HashMap<String, String>,
165}
166
167impl Page {
168 pub fn new(width: f64, height: f64) -> Self {
172 Self {
173 width,
174 height,
175 margins: Margins::default(),
176 content: Vec::new(),
177 graphics_context: GraphicsContext::new(),
178 text_context: TextContext::new(),
179 images: HashMap::new(),
180 form_xobjects: HashMap::new(),
181 color_spaces: HashMap::new(),
182 patterns: HashMap::new(),
183 shadings: HashMap::new(),
184 advanced_shadings: HashMap::new(),
185 header: None,
186 footer: None,
187 annotations: Vec::new(),
188 coordinate_system: crate::coordinate_system::CoordinateSystem::PdfStandard,
189 rotation: 0, next_mcid: 0,
191 marked_content_stack: Vec::new(),
192 preserved_resources: None,
193 page_ops: Vec::new(),
194 font_metrics_store: None,
195 preserved_font_rewrite_map: HashMap::new(),
196 }
197 }
198
199 pub fn from_parsed(parsed_page: &crate::parser::page_tree::ParsedPage) -> Result<Self> {
240 let media_box = parsed_page.media_box;
242 let width = media_box[2] - media_box[0];
243 let height = media_box[3] - media_box[1];
244
245 let rotation = parsed_page.rotation;
247
248 let mut page = Self::new(width, height);
250 page.rotation = rotation;
251
252 Ok(page)
261 }
262
263 pub fn from_parsed_with_content<R: std::io::Read + std::io::Seek>(
300 parsed_page: &crate::parser::page_tree::ParsedPage,
301 document: &crate::parser::document::PdfDocument<R>,
302 ) -> Result<Self> {
303 let media_box = parsed_page.media_box;
305 let width = media_box[2] - media_box[0];
306 let height = media_box[3] - media_box[1];
307
308 let rotation = parsed_page.rotation;
310
311 let mut page = Self::new(width, height);
313 page.rotation = rotation;
314
315 let content_streams = parsed_page.content_streams_with_document(document)?;
317
318 let mut preserved_content = Vec::new();
320 for stream in content_streams {
321 preserved_content.extend_from_slice(&stream);
322 preserved_content.push(b'\n');
324 }
325
326 page.content = preserved_content;
329
330 if let Some(resources) = parsed_page.get_resources() {
332 let mut unified_resources = Self::convert_parser_dict_to_unified(resources);
333
334 let resolved_font: crate::pdf_objects::Object;
344 let font_resource = match unified_resources.get("Font") {
345 Some(crate::pdf_objects::Object::Reference(id)) => {
346 match document.get_object(id.number(), id.generation()) {
347 Ok(resolved_obj) => {
348 resolved_font = Self::convert_parser_object_to_unified(&resolved_obj);
349 Some(&resolved_font)
350 }
351 _ => None,
352 }
353 }
354 other => other,
355 };
356
357 if let Some(crate::pdf_objects::Object::Dictionary(fonts)) = font_resource {
358 let fonts_clone = fonts.clone();
359 let mut resolved_fonts = crate::pdf_objects::Dictionary::new();
360
361 for (font_name, font_obj) in fonts_clone.iter() {
362 let font_dict = match font_obj {
364 crate::pdf_objects::Object::Reference(id) => {
365 match document.get_object(id.number(), id.generation()) {
367 Ok(resolved_obj) => {
368 match Self::convert_parser_object_to_unified(&resolved_obj) {
370 crate::pdf_objects::Object::Dictionary(dict) => dict,
371 _ => {
372 resolved_fonts.set(font_name.clone(), font_obj.clone());
374 continue;
375 }
376 }
377 }
378 Err(_) => {
379 resolved_fonts.set(font_name.clone(), font_obj.clone());
381 continue;
382 }
383 }
384 }
385 crate::pdf_objects::Object::Dictionary(dict) => dict.clone(),
386 _ => {
387 resolved_fonts.set(font_name.clone(), font_obj.clone());
389 continue;
390 }
391 };
392
393 match Self::resolve_font_streams(&font_dict, document) {
395 Ok(resolved_dict) => {
396 resolved_fonts.set(
397 font_name.clone(),
398 crate::pdf_objects::Object::Dictionary(resolved_dict),
399 );
400 }
401 Err(_) => {
402 resolved_fonts.set(
404 font_name.clone(),
405 crate::pdf_objects::Object::Dictionary(font_dict),
406 );
407 }
408 }
409 }
410
411 unified_resources.set(
413 "Font",
414 crate::pdf_objects::Object::Dictionary(resolved_fonts),
415 );
416 }
417
418 if let Some(crate::pdf_objects::Object::Dictionary(xobjects)) =
421 unified_resources.get("XObject")
422 {
423 let xobjects_clone = xobjects.clone();
424 let mut resolved_xobjects = crate::pdf_objects::Dictionary::new();
425
426 for (xobj_name, xobj_obj) in xobjects_clone.iter() {
427 let resolved = match xobj_obj {
428 crate::pdf_objects::Object::Reference(id) => {
429 match document.get_object(id.number(), id.generation()) {
431 Ok(resolved_obj) => {
432 Self::convert_parser_object_to_unified(&resolved_obj)
433 }
434 Err(_) => {
435 xobj_obj.clone()
437 }
438 }
439 }
440 _ => xobj_obj.clone(),
441 };
442 resolved_xobjects.set(xobj_name.clone(), resolved);
443 }
444
445 unified_resources.set(
446 "XObject",
447 crate::pdf_objects::Object::Dictionary(resolved_xobjects),
448 );
449 }
450
451 if let Some(crate::pdf_objects::Object::Dictionary(extgstates)) =
453 unified_resources.get("ExtGState")
454 {
455 let extgstates_clone = extgstates.clone();
456 let mut resolved_extgstates = crate::pdf_objects::Dictionary::new();
457
458 for (gs_name, gs_obj) in extgstates_clone.iter() {
459 let resolved = match gs_obj {
460 crate::pdf_objects::Object::Reference(id) => {
461 match document.get_object(id.number(), id.generation()) {
462 Ok(resolved_obj) => {
463 Self::convert_parser_object_to_unified(&resolved_obj)
464 }
465 Err(_) => gs_obj.clone(),
466 }
467 }
468 _ => gs_obj.clone(),
469 };
470 resolved_extgstates.set(gs_name.clone(), resolved);
471 }
472
473 unified_resources.set(
474 "ExtGState",
475 crate::pdf_objects::Object::Dictionary(resolved_extgstates),
476 );
477 }
478
479 if let Some(crate::pdf_objects::Object::Dictionary(colorspaces)) =
481 unified_resources.get("ColorSpace")
482 {
483 let colorspaces_clone = colorspaces.clone();
484 let mut resolved_colorspaces = crate::pdf_objects::Dictionary::new();
485
486 for (cs_name, cs_obj) in colorspaces_clone.iter() {
487 let resolved = match cs_obj {
488 crate::pdf_objects::Object::Reference(id) => {
489 match document.get_object(id.number(), id.generation()) {
490 Ok(resolved_obj) => {
491 Self::convert_parser_object_to_unified(&resolved_obj)
492 }
493 Err(_) => cs_obj.clone(),
494 }
495 }
496 _ => cs_obj.clone(),
497 };
498 resolved_colorspaces.set(cs_name.clone(), resolved);
499 }
500
501 unified_resources.set(
502 "ColorSpace",
503 crate::pdf_objects::Object::Dictionary(resolved_colorspaces),
504 );
505 }
506
507 if let Some(crate::pdf_objects::Object::Dictionary(patterns)) =
509 unified_resources.get("Pattern")
510 {
511 let patterns_clone = patterns.clone();
512 let mut resolved_patterns = crate::pdf_objects::Dictionary::new();
513
514 for (pat_name, pat_obj) in patterns_clone.iter() {
515 let resolved = match pat_obj {
516 crate::pdf_objects::Object::Reference(id) => {
517 match document.get_object(id.number(), id.generation()) {
518 Ok(resolved_obj) => {
519 Self::convert_parser_object_to_unified(&resolved_obj)
520 }
521 Err(_) => pat_obj.clone(),
522 }
523 }
524 _ => pat_obj.clone(),
525 };
526 resolved_patterns.set(pat_name.clone(), resolved);
527 }
528
529 unified_resources.set(
530 "Pattern",
531 crate::pdf_objects::Object::Dictionary(resolved_patterns),
532 );
533 }
534
535 if let Some(crate::pdf_objects::Object::Dictionary(shadings)) =
537 unified_resources.get("Shading")
538 {
539 let shadings_clone = shadings.clone();
540 let mut resolved_shadings = crate::pdf_objects::Dictionary::new();
541
542 for (sh_name, sh_obj) in shadings_clone.iter() {
543 let resolved = match sh_obj {
544 crate::pdf_objects::Object::Reference(id) => {
545 match document.get_object(id.number(), id.generation()) {
546 Ok(resolved_obj) => {
547 Self::convert_parser_object_to_unified(&resolved_obj)
548 }
549 Err(_) => sh_obj.clone(),
550 }
551 }
552 _ => sh_obj.clone(),
553 };
554 resolved_shadings.set(sh_name.clone(), resolved);
555 }
556
557 unified_resources.set(
558 "Shading",
559 crate::pdf_objects::Object::Dictionary(resolved_shadings),
560 );
561 }
562
563 page.preserved_resources = Some(unified_resources);
564 }
565
566 Ok(page)
567 }
568
569 pub fn a4() -> Self {
571 Self::new(595.0, 842.0)
572 }
573
574 pub fn a4_landscape() -> Self {
576 Self::new(842.0, 595.0)
577 }
578
579 pub fn letter() -> Self {
581 Self::new(612.0, 792.0)
582 }
583
584 pub fn letter_landscape() -> Self {
586 Self::new(792.0, 612.0)
587 }
588
589 pub fn font_metrics_store(&self) -> Option<&FontMetricsStore> {
596 self.font_metrics_store.as_ref()
597 }
598
599 #[cfg(any(test, feature = "internal-testing"))]
607 #[doc(hidden)]
608 pub fn text_context_metrics_store_for_test(
609 &self,
610 ) -> Option<&crate::text::metrics::FontMetricsStore> {
611 self.text_context.font_metrics_store.as_ref()
612 }
613
614 #[cfg(any(test, feature = "internal-testing"))]
622 #[doc(hidden)]
623 pub fn text_context_ops_count_for_test(&self) -> usize {
624 self.text_context.ops_slice().len()
625 }
626
627 pub(crate) fn set_text_context_metrics_store(
632 &mut self,
633 store: Option<crate::text::metrics::FontMetricsStore>,
634 ) {
635 self.text_context.set_metrics_store(store);
636 }
637
638 pub(crate) fn a4_with_metrics(store: FontMetricsStore) -> Self {
643 let mut p = Self::a4();
644 p.font_metrics_store = Some(store.clone());
645 p.text_context = TextContext::with_metrics_store(Some(store));
646 p
647 }
648
649 pub(crate) fn letter_with_metrics(store: FontMetricsStore) -> Self {
651 let mut p = Self::letter();
652 p.font_metrics_store = Some(store.clone());
653 p.text_context = TextContext::with_metrics_store(Some(store));
654 p
655 }
656
657 pub(crate) fn new_with_metrics(width: f64, height: f64, store: FontMetricsStore) -> Self {
659 let mut p = Self::new(width, height);
660 p.font_metrics_store = Some(store.clone());
661 p.text_context = TextContext::with_metrics_store(Some(store));
662 p
663 }
664
665 pub fn legal() -> Self {
667 Self::new(612.0, 1008.0)
668 }
669
670 pub fn legal_landscape() -> Self {
672 Self::new(1008.0, 612.0)
673 }
674
675 pub fn graphics(&mut self) -> &mut GraphicsContext {
683 if !self.text_context.ops_slice().is_empty() {
684 let drained = self.text_context.drain_ops();
685 self.page_ops.extend(drained);
686 }
687 &mut self.graphics_context
688 }
689
690 pub fn graphics_operations(&self) -> String {
704 let mut buf = Vec::new();
705 crate::graphics::ops::serialize_ops(&mut buf, &self.page_ops);
706 let tail = self.graphics_context.operations();
707 let mut out =
708 String::from_utf8(buf).expect("serialize_ops emits ASCII content-stream tokens");
709 out.push_str(&tail);
710 out
711 }
712
713 pub fn text(&mut self) -> &mut TextContext {
740 if !self.graphics_context.ops_slice().is_empty() {
741 let drained = self.graphics_context.drain_ops();
742 self.page_ops.extend(drained);
743 }
744 if self.text_context.fill_color().is_none() {
745 let inherited = self.graphics_context.fill_color();
746 self.text_context.set_fill_color(inherited);
747 }
748 &mut self.text_context
749 }
750
751 pub fn set_margins(&mut self, left: f64, right: f64, top: f64, bottom: f64) {
752 self.margins = Margins {
753 left,
754 right,
755 top,
756 bottom,
757 };
758 }
759
760 pub fn margins(&self) -> &Margins {
761 &self.margins
762 }
763
764 pub fn content_width(&self) -> f64 {
765 self.width - self.margins.left - self.margins.right
766 }
767
768 pub fn content_height(&self) -> f64 {
769 self.height - self.margins.top - self.margins.bottom
770 }
771
772 pub fn content_area(&self) -> (f64, f64, f64, f64) {
773 (
774 self.margins.left,
775 self.margins.bottom,
776 self.width - self.margins.right,
777 self.height - self.margins.top,
778 )
779 }
780
781 pub fn width(&self) -> f64 {
782 self.width
783 }
784
785 pub fn height(&self) -> f64 {
786 self.height
787 }
788
789 pub fn coordinate_system(&self) -> crate::coordinate_system::CoordinateSystem {
791 self.coordinate_system
792 }
793
794 pub fn set_coordinate_system(
796 &mut self,
797 coordinate_system: crate::coordinate_system::CoordinateSystem,
798 ) -> &mut Self {
799 self.coordinate_system = coordinate_system;
800 self
801 }
802
803 pub fn set_rotation(&mut self, rotation: i32) {
807 let normalized = rotation.rem_euclid(360); self.rotation = match normalized {
810 0..=44 | 316..=360 => 0,
811 45..=134 => 90,
812 135..=224 => 180,
813 225..=315 => 270,
814 _ => 0, };
816 }
817
818 fn convert_parser_dict_to_unified(
820 parser_dict: &crate::parser::objects::PdfDictionary,
821 ) -> crate::pdf_objects::Dictionary {
822 use crate::pdf_objects::{Dictionary, Name};
823
824 let mut unified_dict = Dictionary::new();
825
826 for (key, value) in &parser_dict.0 {
827 let unified_key = Name::new(key.as_str());
828 let unified_value = Self::convert_parser_object_to_unified(value);
829 unified_dict.set(unified_key, unified_value);
830 }
831
832 unified_dict
833 }
834
835 fn convert_parser_object_to_unified(
837 parser_obj: &crate::parser::objects::PdfObject,
838 ) -> crate::pdf_objects::Object {
839 use crate::parser::objects::PdfObject;
840 use crate::pdf_objects::{Array, BinaryString, Name, Object, ObjectId, Stream};
841
842 match parser_obj {
843 PdfObject::Null => Object::Null,
844 PdfObject::Boolean(b) => Object::Boolean(*b),
845 PdfObject::Integer(i) => Object::Integer(*i),
846 PdfObject::Real(f) => Object::Real(*f),
847 PdfObject::String(s) => Object::String(BinaryString::new(s.as_bytes().to_vec())),
848 PdfObject::Name(n) => Object::Name(Name::new(n.as_str())),
849 PdfObject::Array(arr) => {
850 let mut unified_arr = Array::new();
851 for item in &arr.0 {
852 unified_arr.push(Self::convert_parser_object_to_unified(item));
853 }
854 Object::Array(unified_arr)
855 }
856 PdfObject::Dictionary(dict) => {
857 Object::Dictionary(Self::convert_parser_dict_to_unified(dict))
858 }
859 PdfObject::Stream(stream) => {
860 let dict = Self::convert_parser_dict_to_unified(&stream.dict);
861 let data = stream.data.clone();
862 Object::Stream(Stream::new(dict, data))
863 }
864 PdfObject::Reference(num, gen) => Object::Reference(ObjectId::new(*num, *gen)),
865 }
866 }
867
868 fn resolve_font_streams<R: std::io::Read + std::io::Seek>(
880 font_dict: &crate::pdf_objects::Dictionary,
881 document: &crate::parser::document::PdfDocument<R>,
882 ) -> Result<crate::pdf_objects::Dictionary> {
883 use crate::pdf_objects::Object;
884
885 let mut resolved_dict = font_dict.clone();
886
887 if detect_type0_font(font_dict) {
889 let resolver =
891 |id: crate::pdf_objects::ObjectId| -> Option<crate::pdf_objects::Object> {
892 match document.get_object(id.number(), id.generation()) {
893 Ok(parser_obj) => Some(Self::convert_parser_object_to_unified(&parser_obj)),
894 Err(_) => None,
895 }
896 };
897
898 if let Some(info) = resolve_type0_hierarchy(font_dict, resolver) {
900 if let Some(cidfont) = info.cidfont_dict {
902 let mut resolved_cidfont = cidfont;
903
904 if let Some(descriptor) = info.font_descriptor {
906 let mut resolved_descriptor = descriptor;
907
908 if let Some(stream) = info.font_stream {
910 let key = match info.font_file_type {
912 Some(crate::fonts::type0_parsing::FontFileType::TrueType) => {
913 "FontFile2"
914 }
915 Some(crate::fonts::type0_parsing::FontFileType::CFF) => "FontFile3",
916 Some(crate::fonts::type0_parsing::FontFileType::Type1) => {
917 "FontFile"
918 }
919 None => "FontFile2", };
921 resolved_descriptor.set(key, Object::Stream(stream));
922 }
923
924 resolved_cidfont
925 .set("FontDescriptor", Object::Dictionary(resolved_descriptor));
926 }
927
928 let mut descendants = crate::pdf_objects::Array::new();
930 descendants.push(Object::Dictionary(resolved_cidfont));
931 resolved_dict.set("DescendantFonts", Object::Array(descendants));
932 }
933
934 if let Some(tounicode) = info.tounicode_stream {
936 resolved_dict.set("ToUnicode", Object::Stream(tounicode));
937 }
938 }
939
940 return Ok(resolved_dict);
941 }
942
943 if let Some(Object::Reference(descriptor_id)) = font_dict.get("FontDescriptor") {
946 let descriptor_obj =
948 document.get_object(descriptor_id.number(), descriptor_id.generation())?;
949
950 let descriptor_unified = Self::convert_parser_object_to_unified(&descriptor_obj);
952
953 if let Object::Dictionary(mut descriptor_dict) = descriptor_unified {
954 let font_file_keys = ["FontFile", "FontFile2", "FontFile3"];
956 let mut stream_resolved = false;
957
958 for key in &font_file_keys {
959 if let Some(Object::Reference(stream_id)) = descriptor_dict.get(*key) {
960 match document.get_object(stream_id.number(), stream_id.generation()) {
962 Ok(stream_obj) => {
963 let stream_unified =
965 Self::convert_parser_object_to_unified(&stream_obj);
966
967 descriptor_dict.set(*key, stream_unified);
969 stream_resolved = true;
970 }
971 Err(_) => {
972 continue;
974 }
975 }
976 }
977 }
978
979 if stream_resolved {
981 resolved_dict.set("FontDescriptor", Object::Dictionary(descriptor_dict));
982 }
983 }
984 }
985
986 Ok(resolved_dict)
987 }
988
989 pub fn get_preserved_resources(&self) -> Option<&crate::pdf_objects::Dictionary> {
991 self.preserved_resources.as_ref()
992 }
993
994 pub fn get_rotation(&self) -> i32 {
996 self.rotation
997 }
998
999 pub fn effective_width(&self) -> f64 {
1002 match self.rotation {
1003 90 | 270 => self.height,
1004 _ => self.width,
1005 }
1006 }
1007
1008 pub fn effective_height(&self) -> f64 {
1011 match self.rotation {
1012 90 | 270 => self.width,
1013 _ => self.height,
1014 }
1015 }
1016
1017 pub fn text_flow(&self) -> TextFlowContext {
1018 let mut ctx = TextFlowContext::with_metrics_store(
1030 self.width,
1031 self.height,
1032 self.margins.clone(),
1033 self.font_metrics_store.clone(),
1034 );
1035 ctx.set_font(
1036 self.text_context.current_font().clone(),
1037 self.text_context.font_size(),
1038 );
1039 let effective_fill = self
1044 .text_context
1045 .fill_color()
1046 .unwrap_or_else(|| self.graphics_context.fill_color());
1047 ctx.set_fill_color(effective_fill);
1048 if let Some(spacing) = self.text_context.character_spacing() {
1049 ctx.set_character_spacing(spacing);
1050 }
1051 if let Some(spacing) = self.text_context.word_spacing() {
1052 ctx.set_word_spacing(spacing);
1053 }
1054 if let Some(scale) = self.text_context.horizontal_scaling() {
1055 ctx.set_horizontal_scaling(scale);
1056 }
1057 if let Some(leading) = self.text_context.leading() {
1058 ctx.set_leading(leading);
1059 }
1060 if let Some(rise) = self.text_context.text_rise() {
1061 ctx.set_text_rise(rise);
1062 }
1063 if let Some(mode) = self.text_context.rendering_mode() {
1064 ctx.set_rendering_mode(mode as u8);
1065 }
1066 if let Some(color) = self.text_context.stroke_color() {
1067 ctx.set_stroke_color(color);
1068 }
1069 ctx
1070 }
1071
1072 pub fn add_text_flow(&mut self, text_flow: &TextFlowContext) {
1073 self.flush_pending_contexts();
1080 let operations = text_flow.generate_operations();
1081 if !operations.is_empty() {
1082 self.page_ops
1083 .push(crate::graphics::ops::Op::Raw(operations));
1084 }
1085 self.graphics_context
1094 .merge_font_usage(text_flow.get_used_characters_by_font());
1095 }
1096
1097 fn flush_pending_contexts(&mut self) {
1103 if !self.graphics_context.ops_slice().is_empty() {
1104 let drained = self.graphics_context.drain_ops();
1105 self.page_ops.extend(drained);
1106 }
1107 if !self.text_context.ops_slice().is_empty() {
1108 let drained = self.text_context.drain_ops();
1109 self.page_ops.extend(drained);
1110 }
1111 }
1112
1113 pub fn add_image(&mut self, name: impl Into<String>, image: Image) {
1114 self.images.insert(name.into(), image);
1115 }
1116
1117 pub fn draw_image(
1118 &mut self,
1119 name: &str,
1120 x: f64,
1121 y: f64,
1122 width: f64,
1123 height: f64,
1124 ) -> Result<()> {
1125 if self.images.contains_key(name) {
1126 self.graphics_context.draw_image(name, x, y, width, height);
1128 Ok(())
1129 } else {
1130 Err(crate::PdfError::InvalidReference(format!(
1131 "Image '{name}' not found"
1132 )))
1133 }
1134 }
1135
1136 pub(crate) fn images(&self) -> &HashMap<String, Image> {
1137 &self.images
1138 }
1139
1140 pub fn add_form_xobject(
1171 &mut self,
1172 name: impl Into<String>,
1173 form: crate::graphics::FormXObject,
1174 ) -> Result<()> {
1175 let name = name.into();
1176 validate_pdf_resource_name(&name)?;
1177 self.form_xobjects.insert(name, form);
1178 Ok(())
1179 }
1180
1181 pub fn form_xobjects(&self) -> &HashMap<String, crate::graphics::FormXObject> {
1187 &self.form_xobjects
1188 }
1189
1190 pub fn add_color_space(
1215 &mut self,
1216 name: impl Into<String>,
1217 cs: crate::graphics::PageColorSpace,
1218 ) -> Result<()> {
1219 let name = name.into();
1220 validate_pdf_resource_name(&name)?;
1221 self.color_spaces.insert(name, cs);
1222 Ok(())
1223 }
1224
1225 pub fn add_icc_color_space(
1241 &mut self,
1242 name: impl Into<String>,
1243 profile: &crate::graphics::IccProfile,
1244 ) -> Result<()> {
1245 self.add_color_space(name, crate::graphics::PageColorSpace::from(profile))
1246 }
1247
1248 pub fn color_spaces(&self) -> &HashMap<String, crate::graphics::PageColorSpace> {
1253 &self.color_spaces
1254 }
1255
1256 pub fn add_pattern(
1269 &mut self,
1270 name: impl Into<String>,
1271 pattern: crate::graphics::TilingPattern,
1272 ) -> Result<()> {
1273 let name = name.into();
1274 validate_pdf_resource_name(&name)?;
1275 self.patterns.insert(name, pattern);
1276 Ok(())
1277 }
1278
1279 pub fn patterns(&self) -> &HashMap<String, crate::graphics::TilingPattern> {
1281 &self.patterns
1282 }
1283
1284 pub fn add_shading(
1295 &mut self,
1296 name: impl Into<String>,
1297 shading: crate::graphics::ShadingDefinition,
1298 ) -> Result<()> {
1299 let name = name.into();
1300 validate_pdf_resource_name(&name)?;
1301 self.shadings.insert(name, shading);
1302 Ok(())
1303 }
1304
1305 pub fn shadings(&self) -> &HashMap<String, crate::graphics::ShadingDefinition> {
1307 &self.shadings
1308 }
1309
1310 pub fn add_mesh_shading(
1319 &mut self,
1320 name: impl Into<String>,
1321 shading: crate::graphics::FreeFormGouraudShading,
1322 ) -> Result<()> {
1323 let name = name.into();
1324 validate_pdf_resource_name(&name)?;
1325 shading.validate()?;
1326 self.advanced_shadings
1327 .insert(name, crate::graphics::AdvancedShading::Mesh(shading));
1328 Ok(())
1329 }
1330
1331 pub fn add_conic_shading(
1339 &mut self,
1340 name: impl Into<String>,
1341 shading: crate::graphics::ConicShading,
1342 ) -> Result<()> {
1343 let name = name.into();
1344 validate_pdf_resource_name(&name)?;
1345 shading.validate()?;
1346 self.advanced_shadings
1347 .insert(name, crate::graphics::AdvancedShading::Conic(shading));
1348 Ok(())
1349 }
1350
1351 pub(crate) fn advanced_shadings(&self) -> &HashMap<String, crate::graphics::AdvancedShading> {
1354 &self.advanced_shadings
1355 }
1356
1357 pub(crate) fn append_raw_content(
1373 &mut self,
1374 data: &[u8],
1375 font_usage: &HashMap<String, HashSet<char>>,
1376 ) {
1377 self.flush_pending_contexts();
1382 if !data.is_empty() {
1383 self.page_ops
1384 .push(crate::graphics::ops::Op::Raw(data.to_vec()));
1385 }
1386 self.graphics_context.merge_font_usage(font_usage);
1387 }
1388
1389 pub fn add_table(&mut self, table: &Table) -> Result<()> {
1429 self.graphics_context.render_table(table)
1430 }
1431
1432 pub fn get_extgstate_resources(
1434 &self,
1435 ) -> Option<&std::collections::HashMap<String, crate::graphics::ExtGState>> {
1436 if self.graphics_context.has_extgstates() {
1437 Some(self.graphics_context.extgstate_manager().states())
1438 } else {
1439 None
1440 }
1441 }
1442
1443 pub fn add_annotation(&mut self, annotation: Annotation) {
1445 self.annotations.push(annotation);
1446 }
1447
1448 pub fn annotations(&self) -> &[Annotation] {
1450 &self.annotations
1451 }
1452
1453 pub fn annotations_mut(&mut self) -> &mut Vec<Annotation> {
1455 &mut self.annotations
1456 }
1457
1458 pub fn add_form_widget(&mut self, widget: Widget) -> ObjectReference {
1483 let widget_ref = ObjectReference::new(
1487 0, 0,
1489 );
1490
1491 let mut annot = Annotation::new(crate::annotations::AnnotationType::Widget, widget.rect);
1493
1494 for (key, value) in widget.to_annotation_dict().iter() {
1496 annot.properties.set(key, value.clone());
1497 }
1498
1499 self.annotations.push(annot);
1501
1502 widget_ref
1503 }
1504
1505 pub fn add_form_widget_with_ref(
1529 &mut self,
1530 widget: Widget,
1531 field_ref: ObjectReference,
1532 ) -> crate::error::Result<()> {
1533 let mut annot = Annotation::new(crate::annotations::AnnotationType::Widget, widget.rect);
1535
1536 for (key, value) in widget.to_annotation_dict().iter() {
1537 annot.properties.set(key, value.clone());
1538 }
1539
1540 annot.set_field_parent(field_ref);
1546
1547 self.annotations.push(annot);
1548 Ok(())
1549 }
1550
1551 pub fn set_header(&mut self, header: HeaderFooter) {
1562 self.register_header_footer_font_usage(&header);
1563 self.header = Some(header);
1564 }
1565
1566 pub fn set_footer(&mut self, footer: HeaderFooter) {
1577 self.register_header_footer_font_usage(&footer);
1578 self.footer = Some(footer);
1579 }
1580
1581 fn register_header_footer_font_usage(&mut self, hf: &HeaderFooter) {
1605 let font_name = hf.options().font.pdf_name();
1606
1607 let sampled = hf.render(1, 999, None);
1614
1615 let mut chars: HashSet<char> = sampled.chars().collect();
1616 chars.extend('0'..='9');
1621
1622 self.graphics_context
1623 .merge_font_usage(&std::iter::once((font_name, chars)).collect());
1624 }
1625
1626 pub fn header(&self) -> Option<&HeaderFooter> {
1628 self.header.as_ref()
1629 }
1630
1631 pub fn footer(&self) -> Option<&HeaderFooter> {
1633 self.footer.as_ref()
1634 }
1635
1636 pub(crate) fn set_content(&mut self, content: Vec<u8>) {
1640 self.content = content;
1641 }
1642
1643 pub(crate) fn generate_content(&mut self) -> Result<Vec<u8>> {
1644 self.generate_content_with_page_info(None, None, None)
1646 }
1647
1648 pub(crate) fn generate_content_with_page_info(
1653 &mut self,
1654 page_number: Option<usize>,
1655 total_pages: Option<usize>,
1656 custom_values: Option<&HashMap<String, String>>,
1657 ) -> Result<Vec<u8>> {
1658 let mut final_content = Vec::new();
1659
1660 if let Some(header) = &self.header {
1662 if let (Some(page_num), Some(total)) = (page_number, total_pages) {
1663 let header_content =
1664 self.render_header_footer(header, page_num, total, custom_values)?;
1665 final_content.extend_from_slice(&header_content);
1666 }
1667 }
1668
1669 crate::graphics::ops::serialize_ops(&mut final_content, &self.page_ops);
1682 let gfx_tail = self.graphics_context.generate_operations()?;
1683 final_content.extend_from_slice(&gfx_tail);
1684 let text_tail = self.text_context.generate_operations()?;
1685 final_content.extend_from_slice(&text_tail);
1686
1687 let content_to_add = if self.preserved_font_rewrite_map.is_empty()
1693 || self.content.is_empty()
1694 {
1695 self.content.clone()
1696 } else {
1697 crate::writer::rewrite_font_references(&self.content, &self.preserved_font_rewrite_map)
1698 };
1699
1700 final_content.extend_from_slice(&content_to_add);
1701
1702 if let Some(footer) = &self.footer {
1704 if let (Some(page_num), Some(total)) = (page_number, total_pages) {
1705 let footer_content =
1706 self.render_header_footer(footer, page_num, total, custom_values)?;
1707 final_content.extend_from_slice(&footer_content);
1708 }
1709 }
1710
1711 Ok(final_content)
1712 }
1713
1714 fn render_header_footer(
1716 &self,
1717 header_footer: &HeaderFooter,
1718 page_number: usize,
1719 total_pages: usize,
1720 custom_values: Option<&HashMap<String, String>>,
1721 ) -> Result<Vec<u8>> {
1722 use crate::text::measure_text;
1723
1724 let content = header_footer.render(page_number, total_pages, custom_values);
1726
1727 let text_width = measure_text(
1729 &content,
1730 &header_footer.options().font,
1731 header_footer.options().font_size,
1732 );
1733
1734 let x = header_footer.calculate_x_position(self.width, text_width);
1736 let y = header_footer.calculate_y_position(self.height);
1737
1738 let mut text_ctx = TextContext::new();
1740 text_ctx
1741 .set_font(
1742 header_footer.options().font.clone(),
1743 header_footer.options().font_size,
1744 )
1745 .at(x, y)
1746 .write(&content)?;
1747
1748 text_ctx.generate_operations()
1749 }
1750
1751 pub(crate) fn to_dict(&self) -> Dictionary {
1753 let mut dict = Dictionary::new();
1754
1755 let media_box = Array::from(vec![
1757 Object::Real(0.0),
1758 Object::Real(0.0),
1759 Object::Real(self.width),
1760 Object::Real(self.height),
1761 ]);
1762 dict.set("MediaBox", Object::Array(media_box.into()));
1763
1764 if self.rotation != 0 {
1766 dict.set("Rotate", Object::Integer(self.rotation as i64));
1767 }
1768
1769 let resources = Dictionary::new();
1771 dict.set("Resources", Object::Dictionary(resources));
1772
1773 dict
1785 }
1786
1787 pub(crate) fn get_used_characters_by_font(&self) -> HashMap<String, HashSet<char>> {
1797 let mut merged: HashMap<String, HashSet<char>> = HashMap::new();
1798 for (name, chars) in self.graphics_context.get_used_characters_by_font() {
1799 merged.entry(name.clone()).or_default().extend(chars);
1800 }
1801 for (name, chars) in self.text_context.get_used_characters_by_font() {
1802 merged.entry(name.clone()).or_default().extend(chars);
1803 }
1804 merged
1805 }
1806
1807 #[cfg(test)]
1813 pub(crate) fn get_used_characters(&self) -> Option<HashSet<char>> {
1814 let merged: HashSet<char> = self
1815 .get_used_characters_by_font()
1816 .into_values()
1817 .flatten()
1818 .collect();
1819 if merged.is_empty() {
1820 None
1821 } else {
1822 Some(merged)
1823 }
1824 }
1825
1826 pub fn begin_marked_content(&mut self, tag: &str) -> Result<u32> {
1867 let mcid = self.next_mcid;
1868 self.next_mcid += 1;
1869
1870 let bdc_op = format!("/{} <</MCID {}>> BDC\n", tag, mcid);
1873 self.text_context.append_raw_operation(&bdc_op);
1874
1875 self.marked_content_stack.push(tag.to_string());
1876
1877 Ok(mcid)
1878 }
1879
1880 pub fn end_marked_content(&mut self) -> Result<()> {
1889 if self.marked_content_stack.is_empty() {
1890 return Err(crate::PdfError::InvalidOperation(
1891 "No marked content sequence to end (EMC without BDC)".to_string(),
1892 ));
1893 }
1894
1895 self.marked_content_stack.pop();
1896
1897 self.text_context.append_raw_operation("EMC\n");
1899
1900 Ok(())
1901 }
1902
1903 pub fn next_mcid(&self) -> u32 {
1907 self.next_mcid
1908 }
1909
1910 pub fn marked_content_depth(&self) -> usize {
1912 self.marked_content_stack.len()
1913 }
1914}
1915
1916#[cfg(test)]
1917mod tests {
1918 use super::*;
1919 use crate::graphics::Color;
1920 use crate::text::Font;
1921
1922 #[test]
1923 fn test_page_new() {
1924 let page = Page::new(100.0, 200.0);
1925 assert_eq!(page.width(), 100.0);
1926 assert_eq!(page.height(), 200.0);
1927 assert_eq!(page.margins().left, 72.0);
1928 assert_eq!(page.margins().right, 72.0);
1929 assert_eq!(page.margins().top, 72.0);
1930 assert_eq!(page.margins().bottom, 72.0);
1931 }
1932
1933 #[test]
1934 fn test_page_a4() {
1935 let page = Page::a4();
1936 assert_eq!(page.width(), 595.0);
1937 assert_eq!(page.height(), 842.0);
1938 }
1939
1940 #[test]
1941 fn test_page_letter() {
1942 let page = Page::letter();
1943 assert_eq!(page.width(), 612.0);
1944 assert_eq!(page.height(), 792.0);
1945 }
1946
1947 #[test]
1948 fn test_set_margins() {
1949 let mut page = Page::a4();
1950 page.set_margins(10.0, 20.0, 30.0, 40.0);
1951
1952 assert_eq!(page.margins().left, 10.0);
1953 assert_eq!(page.margins().right, 20.0);
1954 assert_eq!(page.margins().top, 30.0);
1955 assert_eq!(page.margins().bottom, 40.0);
1956 }
1957
1958 #[test]
1959 fn test_content_dimensions() {
1960 let mut page = Page::new(300.0, 400.0);
1961 page.set_margins(50.0, 50.0, 50.0, 50.0);
1962
1963 assert_eq!(page.content_width(), 200.0);
1964 assert_eq!(page.content_height(), 300.0);
1965 }
1966
1967 #[test]
1968 fn test_content_area() {
1969 let mut page = Page::new(300.0, 400.0);
1970 page.set_margins(10.0, 20.0, 30.0, 40.0);
1971
1972 let (left, bottom, right, top) = page.content_area();
1973 assert_eq!(left, 10.0);
1974 assert_eq!(bottom, 40.0);
1975 assert_eq!(right, 280.0);
1976 assert_eq!(top, 370.0);
1977 }
1978
1979 #[test]
1980 fn test_graphics_context() {
1981 let mut page = Page::a4();
1982 let graphics = page.graphics();
1983 graphics.set_fill_color(Color::red());
1984 graphics.rect(100.0, 100.0, 200.0, 150.0);
1985 graphics.fill();
1986
1987 assert!(page.generate_content().is_ok());
1989 }
1990
1991 #[test]
1992 fn test_text_context() {
1993 let mut page = Page::a4();
1994 let text = page.text();
1995 text.set_font(Font::Helvetica, 12.0);
1996 text.at(100.0, 700.0);
1997 text.write("Hello World").unwrap();
1998
1999 assert!(page.generate_content().is_ok());
2001 }
2002
2003 #[test]
2004 fn test_text_flow() {
2005 let page = Page::a4();
2006 let text_flow = page.text_flow();
2007
2008 drop(text_flow);
2011 }
2012
2013 #[test]
2014 fn test_add_text_flow() {
2015 let mut page = Page::a4();
2016 let mut text_flow = page.text_flow();
2017 text_flow.at(100.0, 700.0);
2018 text_flow.set_font(Font::TimesRoman, 14.0);
2019 text_flow.write_wrapped("Test text flow").unwrap();
2020
2021 page.add_text_flow(&text_flow);
2022
2023 let content = page.generate_content().unwrap();
2024 assert!(!content.is_empty());
2025 }
2026
2027 #[test]
2028 fn test_add_image() {
2029 let mut page = Page::a4();
2030 let jpeg_data = vec![
2032 0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x64, 0x00, 0xC8, 0x03, 0xFF, 0xD9, ];
2041 let image = Image::from_jpeg_data(jpeg_data).unwrap();
2042
2043 page.add_image("test_image", image);
2044 assert!(page.images().contains_key("test_image"));
2045 assert_eq!(page.images().len(), 1);
2046 }
2047
2048 #[test]
2049 fn test_draw_image() {
2050 let mut page = Page::a4();
2051 let jpeg_data = vec![
2053 0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x64, 0x00, 0xC8, 0x03, 0xFF, 0xD9, ];
2062 let image = Image::from_jpeg_data(jpeg_data).unwrap();
2063
2064 page.add_image("test_image", image);
2065 let result = page.draw_image("test_image", 50.0, 50.0, 200.0, 200.0);
2066 assert!(result.is_ok());
2067 }
2068
2069 #[test]
2070 fn test_draw_nonexistent_image() {
2071 let mut page = Page::a4();
2072 let result = page.draw_image("nonexistent", 50.0, 50.0, 200.0, 200.0);
2073 assert!(result.is_err());
2074 }
2075
2076 #[test]
2077 fn test_generate_content() {
2078 let mut page = Page::a4();
2079
2080 page.graphics()
2082 .set_fill_color(Color::blue())
2083 .circle(200.0, 400.0, 50.0)
2084 .fill();
2085
2086 page.text()
2088 .set_font(Font::Courier, 10.0)
2089 .at(50.0, 650.0)
2090 .write("Test content")
2091 .unwrap();
2092
2093 let content = page.generate_content().unwrap();
2094 assert!(!content.is_empty());
2095 }
2096
2097 #[test]
2098 fn test_margins_default() {
2099 let margins = Margins::default();
2100 assert_eq!(margins.left, 72.0);
2101 assert_eq!(margins.right, 72.0);
2102 assert_eq!(margins.top, 72.0);
2103 assert_eq!(margins.bottom, 72.0);
2104 }
2105
2106 #[test]
2107 fn test_page_clone() {
2108 let mut page1 = Page::a4();
2109 page1.set_margins(10.0, 20.0, 30.0, 40.0);
2110 let jpeg_data = vec![
2112 0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x32, 0x00, 0x32, 0x03, 0xFF, 0xD9, ];
2121 let image = Image::from_jpeg_data(jpeg_data).unwrap();
2122 page1.add_image("img1", image);
2123
2124 let page2 = page1.clone();
2125 assert_eq!(page2.width(), page1.width());
2126 assert_eq!(page2.height(), page1.height());
2127 assert_eq!(page2.margins().left, page1.margins().left);
2128 assert_eq!(page2.images().len(), page1.images().len());
2129 }
2130
2131 #[test]
2132 fn test_header_footer_basic() {
2133 use crate::text::HeaderFooter;
2134
2135 let mut page = Page::a4();
2136
2137 let header = HeaderFooter::new_header("Test Header");
2138 let footer = HeaderFooter::new_footer("Test Footer");
2139
2140 page.set_header(header);
2141 page.set_footer(footer);
2142
2143 assert!(page.header().is_some());
2144 assert!(page.footer().is_some());
2145 assert_eq!(page.header().unwrap().content(), "Test Header");
2146 assert_eq!(page.footer().unwrap().content(), "Test Footer");
2147 }
2148
2149 #[test]
2150 fn test_header_footer_with_page_numbers() {
2151 use crate::text::HeaderFooter;
2152
2153 let mut page = Page::a4();
2154
2155 let footer = HeaderFooter::new_footer("Page {{page_number}} of {{total_pages}}");
2156 page.set_footer(footer);
2157
2158 let content = page
2160 .generate_content_with_page_info(Some(3), Some(10), None)
2161 .unwrap();
2162 assert!(!content.is_empty());
2163
2164 let content_str = String::from_utf8_lossy(&content);
2166 assert!(content_str.contains("Page 3 of 10"));
2167 }
2168
2169 #[test]
2170 fn test_page_content_with_headers_footers() {
2171 use crate::text::{HeaderFooter, TextAlign};
2172
2173 let mut page = Page::a4();
2174
2175 let header = HeaderFooter::new_header("Document Title")
2177 .with_font(Font::HelveticaBold, 14.0)
2178 .with_alignment(TextAlign::Center);
2179 page.set_header(header);
2180
2181 let footer = HeaderFooter::new_footer("Page {{page_number}}")
2183 .with_font(Font::Helvetica, 10.0)
2184 .with_alignment(TextAlign::Right);
2185 page.set_footer(footer);
2186
2187 page.text()
2189 .set_font(Font::TimesRoman, 12.0)
2190 .at(100.0, 700.0)
2191 .write("Main content here")
2192 .unwrap();
2193
2194 let content = page
2196 .generate_content_with_page_info(Some(1), Some(5), None)
2197 .unwrap();
2198 assert!(!content.is_empty());
2199
2200 assert!(content.len() > 100); }
2205
2206 #[test]
2207 fn test_no_headers_footers() {
2208 let mut page = Page::a4();
2209
2210 assert!(page.header().is_none());
2212 assert!(page.footer().is_none());
2213
2214 let content = page
2216 .generate_content_with_page_info(Some(1), Some(1), None)
2217 .unwrap();
2218 assert!(content.is_empty() || !content.is_empty()); }
2220
2221 #[test]
2222 fn test_header_footer_custom_values() {
2223 use crate::text::HeaderFooter;
2224 use std::collections::HashMap;
2225
2226 let mut page = Page::a4();
2227
2228 let header = HeaderFooter::new_header("{{company}} - {{title}}");
2229 page.set_header(header);
2230
2231 let mut custom_values = HashMap::new();
2232 custom_values.insert("company".to_string(), "ACME Corp".to_string());
2233 custom_values.insert("title".to_string(), "Annual Report".to_string());
2234
2235 let content = page
2236 .generate_content_with_page_info(Some(1), Some(1), Some(&custom_values))
2237 .unwrap();
2238 let content_str = String::from_utf8_lossy(&content);
2239 assert!(content_str.contains("ACME Corp - Annual Report"));
2240 }
2241
2242 mod integration_tests {
2244 use super::*;
2245 use crate::document::Document;
2246 use crate::writer::PdfWriter;
2247 use std::fs;
2248 use tempfile::TempDir;
2249
2250 #[test]
2251 fn test_page_document_integration() {
2252 let mut doc = Document::new();
2253 doc.set_title("Page Integration Test");
2254
2255 let page1 = Page::a4();
2257 let page2 = Page::letter();
2258 let mut page3 = Page::new(400.0, 600.0);
2259
2260 page3.set_margins(20.0, 20.0, 20.0, 20.0);
2262 page3
2263 .text()
2264 .set_font(Font::Helvetica, 14.0)
2265 .at(50.0, 550.0)
2266 .write("Custom page content")
2267 .unwrap();
2268
2269 doc.add_page(page1);
2270 doc.add_page(page2);
2271 doc.add_page(page3);
2272
2273 assert_eq!(doc.pages.len(), 3);
2274
2275 assert_eq!(doc.pages[0].width(), 595.0); assert_eq!(doc.pages[1].width(), 612.0); assert_eq!(doc.pages[2].width(), 400.0); let mut page_copy = doc.pages[2].clone();
2282 let content = page_copy.generate_content().unwrap();
2283 assert!(!content.is_empty());
2284 }
2285
2286 #[test]
2287 fn test_page_writer_integration() {
2288 let temp_dir = TempDir::new().unwrap();
2289 let file_path = temp_dir.path().join("page_writer_test.pdf");
2290
2291 let mut doc = Document::new();
2292 doc.set_title("Page Writer Integration");
2293
2294 let mut page = Page::a4();
2296 page.set_margins(50.0, 50.0, 50.0, 50.0);
2297
2298 page.text()
2300 .set_font(Font::Helvetica, 16.0)
2301 .at(100.0, 750.0)
2302 .write("Integration Test Header")
2303 .unwrap();
2304
2305 page.text()
2306 .set_font(Font::TimesRoman, 12.0)
2307 .at(100.0, 700.0)
2308 .write("This is body text for the integration test.")
2309 .unwrap();
2310
2311 page.graphics()
2313 .set_fill_color(Color::rgb(0.2, 0.6, 0.9))
2314 .rect(100.0, 600.0, 200.0, 50.0)
2315 .fill();
2316
2317 page.graphics()
2318 .set_stroke_color(Color::rgb(0.8, 0.2, 0.2))
2319 .set_line_width(3.0)
2320 .circle(300.0, 500.0, 40.0)
2321 .stroke();
2322
2323 doc.add_page(page);
2324
2325 let mut writer = PdfWriter::new(&file_path).unwrap();
2327 writer.write_document(&mut doc).unwrap();
2328
2329 assert!(file_path.exists());
2331 let metadata = fs::metadata(&file_path).unwrap();
2332 assert!(metadata.len() > 1000); let content = fs::read(&file_path).unwrap();
2336 let content_str = String::from_utf8_lossy(&content);
2337 assert!(content_str.contains("obj")); assert!(content_str.contains("stream")); }
2340
2341 #[test]
2342 fn test_page_margins_integration() {
2343 let temp_dir = TempDir::new().unwrap();
2344 let file_path = temp_dir.path().join("margins_test.pdf");
2345
2346 let mut doc = Document::new();
2347 doc.set_title("Margins Integration Test");
2348
2349 let mut page1 = Page::a4();
2351 page1.set_margins(10.0, 20.0, 30.0, 40.0);
2352
2353 let mut page2 = Page::letter();
2354 page2.set_margins(72.0, 72.0, 72.0, 72.0); let mut page3 = Page::new(500.0, 700.0);
2357 page3.set_margins(0.0, 0.0, 0.0, 0.0); for (i, page) in [&mut page1, &mut page2, &mut page3].iter_mut().enumerate() {
2361 let (left, bottom, right, top) = page.content_area();
2362
2363 page.text()
2365 .set_font(Font::Helvetica, 10.0)
2366 .at(left, top - 20.0)
2367 .write(&format!(
2368 "Page {} - Content area: ({:.1}, {:.1}, {:.1}, {:.1})",
2369 i + 1,
2370 left,
2371 bottom,
2372 right,
2373 top
2374 ))
2375 .unwrap();
2376
2377 page.graphics()
2379 .set_stroke_color(Color::rgb(0.5, 0.5, 0.5))
2380 .set_line_width(1.0)
2381 .rect(left, bottom, right - left, top - bottom)
2382 .stroke();
2383 }
2384
2385 doc.add_page(page1);
2386 doc.add_page(page2);
2387 doc.add_page(page3);
2388
2389 let mut writer = PdfWriter::new(&file_path).unwrap();
2391 writer.write_document(&mut doc).unwrap();
2392
2393 assert!(file_path.exists());
2394 let metadata = fs::metadata(&file_path).unwrap();
2395 assert!(metadata.len() > 500); }
2397
2398 #[test]
2399 fn test_page_image_integration() {
2400 let temp_dir = TempDir::new().unwrap();
2401 let file_path = temp_dir.path().join("image_test.pdf");
2402
2403 let mut doc = Document::new();
2404 doc.set_title("Image Integration Test");
2405
2406 let mut page = Page::a4();
2407
2408 let jpeg_data1 = vec![
2410 0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x64, 0x00, 0xC8, 0x03, 0xFF, 0xD9,
2411 ];
2412 let image1 = Image::from_jpeg_data(jpeg_data1).unwrap();
2413
2414 let jpeg_data2 = vec![
2415 0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x32, 0x00, 0x32, 0x01, 0xFF, 0xD9,
2416 ];
2417 let image2 = Image::from_jpeg_data(jpeg_data2).unwrap();
2418
2419 page.add_image("image1", image1);
2421 page.add_image("image2", image2);
2422
2423 page.draw_image("image1", 100.0, 600.0, 200.0, 100.0)
2425 .unwrap();
2426 page.draw_image("image2", 350.0, 600.0, 50.0, 50.0).unwrap();
2427
2428 page.text()
2430 .set_font(Font::Helvetica, 12.0)
2431 .at(100.0, 580.0)
2432 .write("Image 1 (200x100)")
2433 .unwrap();
2434
2435 page.text()
2436 .set_font(Font::Helvetica, 12.0)
2437 .at(350.0, 580.0)
2438 .write("Image 2 (50x50)")
2439 .unwrap();
2440
2441 assert_eq!(page.images().len(), 2, "Two images should be added to page");
2443
2444 doc.add_page(page);
2445
2446 let mut writer = PdfWriter::new(&file_path).unwrap();
2448 writer.write_document(&mut doc).unwrap();
2449
2450 assert!(file_path.exists());
2451 let metadata = fs::metadata(&file_path).unwrap();
2452 assert!(metadata.len() > 500); let content = fs::read(&file_path).unwrap();
2456 let content_str = String::from_utf8_lossy(&content);
2457
2458 tracing::debug!("PDF size: {} bytes", content.len());
2460 tracing::debug!("Contains 'XObject': {}", content_str.contains("XObject"));
2461 tracing::debug!("Contains '/XObject': {}", content_str.contains("/XObject"));
2462
2463 if content_str.contains("/Type /Image") || content_str.contains("DCTDecode") {
2465 tracing::debug!("Found image-related content but no XObject dictionary");
2466 }
2467
2468 assert!(content_str.contains("XObject"));
2470 }
2471
2472 #[test]
2473 fn test_page_text_flow_integration() {
2474 let temp_dir = TempDir::new().unwrap();
2475 let file_path = temp_dir.path().join("text_flow_test.pdf");
2476
2477 let mut doc = Document::new();
2478 doc.set_title("Text Flow Integration Test");
2479
2480 let mut page = Page::a4();
2481 page.set_margins(50.0, 50.0, 50.0, 50.0);
2482
2483 let mut text_flow = page.text_flow();
2485 text_flow.set_font(Font::TimesRoman, 12.0);
2486 text_flow.at(100.0, 700.0);
2487
2488 let long_text =
2489 "This is a long paragraph that should demonstrate text flow capabilities. "
2490 .repeat(10);
2491 text_flow.write_wrapped(&long_text).unwrap();
2492
2493 page.add_text_flow(&text_flow);
2495
2496 page.text()
2498 .set_font(Font::Helvetica, 14.0)
2499 .at(100.0, 750.0)
2500 .write("Regular Text Above Text Flow")
2501 .unwrap();
2502
2503 doc.add_page(page);
2504
2505 let mut writer = PdfWriter::new(&file_path).unwrap();
2507 writer.write_document(&mut doc).unwrap();
2508
2509 assert!(file_path.exists());
2510 let metadata = fs::metadata(&file_path).unwrap();
2511 assert!(metadata.len() > 1000); let content = fs::read(&file_path).unwrap();
2515 let content_str = String::from_utf8_lossy(&content);
2516 assert!(content_str.contains("obj")); assert!(content_str.contains("stream")); }
2519
2520 #[test]
2521 fn test_page_complex_content_integration() {
2522 let temp_dir = TempDir::new().unwrap();
2523 let file_path = temp_dir.path().join("complex_content_test.pdf");
2524
2525 let mut doc = Document::new();
2526 doc.set_title("Complex Content Integration Test");
2527
2528 let mut page = Page::a4();
2529 page.set_margins(40.0, 40.0, 40.0, 40.0);
2530
2531 page.graphics()
2535 .set_fill_color(Color::rgb(0.95, 0.95, 0.95))
2536 .rect(50.0, 50.0, 495.0, 742.0)
2537 .fill();
2538
2539 page.graphics()
2541 .set_fill_color(Color::rgb(0.2, 0.4, 0.8))
2542 .rect(50.0, 750.0, 495.0, 42.0)
2543 .fill();
2544
2545 page.text()
2546 .set_font(Font::HelveticaBold, 18.0)
2547 .at(60.0, 765.0)
2548 .write("Complex Content Integration Test")
2549 .unwrap();
2550
2551 let mut y_pos = 700.0;
2553 for i in 1..=3 {
2554 page.graphics()
2556 .set_fill_color(Color::rgb(0.8, 0.8, 0.9))
2557 .rect(60.0, y_pos, 475.0, 20.0)
2558 .fill();
2559
2560 page.text()
2561 .set_font(Font::HelveticaBold, 12.0)
2562 .at(70.0, y_pos + 5.0)
2563 .write(&format!("Section {i}"))
2564 .unwrap();
2565
2566 y_pos -= 30.0;
2567
2568 page.text()
2570 .set_font(Font::TimesRoman, 10.0)
2571 .at(70.0, y_pos)
2572 .write(&format!(
2573 "This is the content for section {i}. It demonstrates mixed content."
2574 ))
2575 .unwrap();
2576
2577 page.graphics()
2579 .set_stroke_color(Color::rgb(0.6, 0.2, 0.2))
2580 .set_line_width(2.0)
2581 .move_to(70.0, y_pos - 10.0)
2582 .line_to(530.0, y_pos - 10.0)
2583 .stroke();
2584
2585 y_pos -= 50.0;
2586 }
2587
2588 page.graphics()
2590 .set_fill_color(Color::rgb(0.3, 0.3, 0.3))
2591 .rect(50.0, 50.0, 495.0, 30.0)
2592 .fill();
2593
2594 page.text()
2595 .set_font(Font::Helvetica, 10.0)
2596 .at(60.0, 60.0)
2597 .write("Generated by oxidize-pdf integration test")
2598 .unwrap();
2599
2600 doc.add_page(page);
2601
2602 let mut writer = PdfWriter::new(&file_path).unwrap();
2604 writer.write_document(&mut doc).unwrap();
2605
2606 assert!(file_path.exists());
2607 let metadata = fs::metadata(&file_path).unwrap();
2608 assert!(metadata.len() > 500); let content = fs::read(&file_path).unwrap();
2612 let content_str = String::from_utf8_lossy(&content);
2613 assert!(content_str.contains("obj")); assert!(content_str.contains("stream")); assert!(content_str.contains("endobj")); }
2617
2618 #[test]
2619 fn test_page_content_generation_performance() {
2620 let mut page = Page::a4();
2621
2622 for i in 0..100 {
2624 let y = 800.0 - (i as f64 * 7.0);
2625 if y > 50.0 {
2626 page.text()
2627 .set_font(Font::Helvetica, 8.0)
2628 .at(50.0, y)
2629 .write(&format!("Performance test line {i}"))
2630 .unwrap();
2631 }
2632 }
2633
2634 for i in 0..50 {
2636 let x = 50.0 + (i as f64 * 10.0);
2637 if x < 550.0 {
2638 page.graphics()
2639 .set_fill_color(Color::rgb(0.5, 0.5, 0.8))
2640 .rect(x, 400.0, 8.0, 8.0)
2641 .fill();
2642 }
2643 }
2644
2645 let start = std::time::Instant::now();
2647 let content = page.generate_content().unwrap();
2648 let duration = start.elapsed();
2649
2650 assert!(!content.is_empty());
2651 assert!(duration.as_millis() < 1000); }
2653
2654 #[test]
2655 fn test_page_error_handling() {
2656 let mut page = Page::a4();
2657
2658 let result = page.draw_image("nonexistent", 100.0, 100.0, 50.0, 50.0);
2660 assert!(result.is_err());
2661
2662 let result = page.draw_image("still_nonexistent", -100.0, -100.0, 0.0, 0.0);
2664 assert!(result.is_err());
2665
2666 let jpeg_data = vec![
2668 0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x32, 0x00, 0x32, 0x01, 0xFF, 0xD9,
2669 ];
2670 let image = Image::from_jpeg_data(jpeg_data).unwrap();
2671 page.add_image("valid_image", image);
2672
2673 let result = page.draw_image("valid_image", 100.0, 100.0, 50.0, 50.0);
2674 assert!(result.is_ok());
2675 }
2676
2677 #[test]
2678 fn test_page_memory_management() {
2679 let mut pages = Vec::new();
2680
2681 for i in 0..100 {
2683 let mut page = Page::a4();
2684 page.set_margins(i as f64, i as f64, i as f64, i as f64);
2685
2686 page.text()
2687 .set_font(Font::Helvetica, 12.0)
2688 .at(100.0, 700.0)
2689 .write(&format!("Page {i}"))
2690 .unwrap();
2691
2692 pages.push(page);
2693 }
2694
2695 assert_eq!(pages.len(), 100);
2697
2698 for page in pages.iter_mut() {
2700 let content = page.generate_content().unwrap();
2701 assert!(!content.is_empty());
2702 }
2703 }
2704
2705 #[test]
2706 fn test_page_standard_sizes() {
2707 let a4 = Page::a4();
2708 let letter = Page::letter();
2709 let custom = Page::new(200.0, 300.0);
2710
2711 assert_eq!(a4.width(), 595.0);
2713 assert_eq!(a4.height(), 842.0);
2714 assert_eq!(letter.width(), 612.0);
2715 assert_eq!(letter.height(), 792.0);
2716 assert_eq!(custom.width(), 200.0);
2717 assert_eq!(custom.height(), 300.0);
2718
2719 let a4_content_width = a4.content_width();
2721 let letter_content_width = letter.content_width();
2722 let custom_content_width = custom.content_width();
2723
2724 assert_eq!(a4_content_width, 595.0 - 144.0); assert_eq!(letter_content_width, 612.0 - 144.0); assert_eq!(custom_content_width, 200.0 - 144.0); }
2728
2729 #[test]
2730 fn test_header_footer_document_integration() {
2731 use crate::text::{HeaderFooter, TextAlign};
2732
2733 let temp_dir = TempDir::new().unwrap();
2734 let file_path = temp_dir.path().join("header_footer_test.pdf");
2735
2736 let mut doc = Document::new();
2737 doc.set_title("Header Footer Integration Test");
2738
2739 for i in 1..=3 {
2741 let mut page = Page::a4();
2742
2743 let header = HeaderFooter::new_header(format!("Chapter {i}"))
2745 .with_font(Font::HelveticaBold, 16.0)
2746 .with_alignment(TextAlign::Center);
2747 page.set_header(header);
2748
2749 let footer = HeaderFooter::new_footer("Page {{page_number}} of {{total_pages}}")
2751 .with_font(Font::Helvetica, 10.0)
2752 .with_alignment(TextAlign::Center);
2753 page.set_footer(footer);
2754
2755 page.text()
2757 .set_font(Font::TimesRoman, 12.0)
2758 .at(100.0, 700.0)
2759 .write(&format!("This is the content of chapter {i}"))
2760 .unwrap();
2761
2762 doc.add_page(page);
2763 }
2764
2765 let mut writer = PdfWriter::new(&file_path).unwrap();
2767 writer.write_document(&mut doc).unwrap();
2768
2769 assert!(file_path.exists());
2771 let metadata = fs::metadata(&file_path).unwrap();
2772 assert!(metadata.len() > 1000);
2773
2774 let content = fs::read(&file_path).unwrap();
2776 let content_str = String::from_utf8_lossy(&content);
2777
2778 assert!(content.len() > 2000);
2780 assert!(content_str.contains("%PDF"));
2782 assert!(content_str.contains("endobj"));
2783
2784 }
2787
2788 #[test]
2789 fn test_header_footer_alignment_integration() {
2790 use crate::text::{HeaderFooter, TextAlign};
2791
2792 let temp_dir = TempDir::new().unwrap();
2793 let file_path = temp_dir.path().join("alignment_test.pdf");
2794
2795 let mut doc = Document::new();
2796
2797 let mut page = Page::a4();
2798
2799 let header = HeaderFooter::new_header("Left Header")
2801 .with_font(Font::Helvetica, 12.0)
2802 .with_alignment(TextAlign::Left)
2803 .with_margin(50.0);
2804 page.set_header(header);
2805
2806 let footer = HeaderFooter::new_footer("Right Footer - Page {{page_number}}")
2808 .with_font(Font::Helvetica, 10.0)
2809 .with_alignment(TextAlign::Right)
2810 .with_margin(50.0);
2811 page.set_footer(footer);
2812
2813 doc.add_page(page);
2814
2815 let mut writer = PdfWriter::new(&file_path).unwrap();
2817 writer.write_document(&mut doc).unwrap();
2818
2819 assert!(file_path.exists());
2820 }
2821
2822 #[test]
2823 fn test_header_footer_date_time_integration() {
2824 use crate::text::HeaderFooter;
2825
2826 let temp_dir = TempDir::new().unwrap();
2827 let file_path = temp_dir.path().join("date_time_test.pdf");
2828
2829 let mut doc = Document::new();
2830
2831 let mut page = Page::a4();
2832
2833 let header = HeaderFooter::new_header("Report generated on {{date}} at {{time}}")
2835 .with_font(Font::Helvetica, 11.0);
2836 page.set_header(header);
2837
2838 let footer =
2840 HeaderFooter::new_footer("© {{year}} Company Name").with_font(Font::Helvetica, 9.0);
2841 page.set_footer(footer);
2842
2843 doc.add_page(page);
2844
2845 let mut writer = PdfWriter::new(&file_path).unwrap();
2847 writer.write_document(&mut doc).unwrap();
2848
2849 assert!(file_path.exists());
2850
2851 let content = fs::read(&file_path).unwrap();
2853 assert!(content.len() > 500);
2854
2855 let content_str = String::from_utf8_lossy(&content);
2857 assert!(content_str.contains("%PDF"));
2858 assert!(content_str.contains("endobj"));
2859
2860 }
2863 }
2864
2865 #[test]
2868 fn test_page_a4_default_has_no_metrics_store() {
2869 let page = Page::a4();
2870 assert!(
2871 page.font_metrics_store.is_none(),
2872 "Page::a4() must not bind a store; binding happens via Document"
2873 );
2874 }
2875
2876 #[test]
2877 fn test_page_a4_with_metrics_carries_store() {
2878 use crate::text::metrics::FontMetricsStore;
2879 let store = FontMetricsStore::new();
2880 let page = Page::a4_with_metrics(store);
2881 assert!(page.font_metrics_store.is_some());
2882 }
2883
2884 #[test]
2885 fn test_page_text_flow_propagates_store() {
2886 use crate::text::metrics::FontMetricsStore;
2887 let store = FontMetricsStore::new();
2888 let page = Page::a4_with_metrics(store);
2889 let flow = page.text_flow();
2890 assert!(
2891 flow.font_metrics_store.is_some(),
2892 "page.text_flow() must propagate the store handle"
2893 );
2894 }
2895}
2896
2897#[cfg(test)]
2898mod unit_tests {
2899 use super::*;
2900 use crate::graphics::Color;
2901 use crate::text::Font;
2902
2903 #[test]
2906 fn test_new_page_dimensions() {
2907 let page = Page::new(100.0, 200.0);
2908 assert_eq!(page.width(), 100.0);
2909 assert_eq!(page.height(), 200.0);
2910 }
2911
2912 #[test]
2913 fn test_a4_page_dimensions() {
2914 let page = Page::a4();
2915 assert_eq!(page.width(), 595.0);
2916 assert_eq!(page.height(), 842.0);
2917 }
2918
2919 #[test]
2920 fn test_letter_page_dimensions() {
2921 let page = Page::letter();
2922 assert_eq!(page.width(), 612.0);
2923 assert_eq!(page.height(), 792.0);
2924 }
2925
2926 #[test]
2927 fn test_legal_page_dimensions() {
2928 let page = Page::legal();
2929 assert_eq!(page.width(), 612.0);
2930 assert_eq!(page.height(), 1008.0);
2931 }
2932
2933 #[test]
2936 fn test_default_margins() {
2937 let page = Page::a4();
2938 let margins = page.margins();
2939 assert_eq!(margins.left, 72.0);
2940 assert_eq!(margins.right, 72.0);
2941 assert_eq!(margins.top, 72.0);
2942 assert_eq!(margins.bottom, 72.0);
2943 }
2944
2945 #[test]
2946 fn test_set_margins() {
2947 let mut page = Page::a4();
2948 page.set_margins(10.0, 20.0, 30.0, 40.0);
2949
2950 let margins = page.margins();
2951 assert_eq!(margins.left, 10.0);
2952 assert_eq!(margins.right, 20.0);
2953 assert_eq!(margins.top, 30.0);
2954 assert_eq!(margins.bottom, 40.0);
2955 }
2956
2957 #[test]
2958 fn test_content_width() {
2959 let mut page = Page::new(600.0, 800.0);
2960 page.set_margins(50.0, 50.0, 0.0, 0.0);
2961 assert_eq!(page.content_width(), 500.0);
2962 }
2963
2964 #[test]
2965 fn test_content_height() {
2966 let mut page = Page::new(600.0, 800.0);
2967 page.set_margins(0.0, 0.0, 100.0, 100.0);
2968 assert_eq!(page.content_height(), 600.0);
2969 }
2970
2971 #[test]
2972 fn test_content_area() {
2973 let mut page = Page::new(600.0, 800.0);
2974 page.set_margins(50.0, 60.0, 70.0, 80.0);
2975
2976 let (x, y, right, top) = page.content_area();
2977 assert_eq!(x, 50.0); assert_eq!(y, 80.0); assert_eq!(right, 540.0); assert_eq!(top, 730.0); }
2982
2983 #[test]
2986 fn test_graphics_context_access() {
2987 let mut page = Page::a4();
2988 let gc = page.graphics();
2989
2990 gc.move_to(0.0, 0.0);
2992 gc.line_to(100.0, 100.0);
2993
2994 let ops = gc.get_operations();
2996 assert!(!ops.is_empty());
2997 }
2998
2999 #[test]
3000 fn test_graphics_operations_chain() {
3001 let mut page = Page::a4();
3002
3003 page.graphics()
3004 .set_fill_color(Color::red())
3005 .rectangle(10.0, 10.0, 100.0, 50.0)
3006 .fill();
3007
3008 let ops = page.graphics().get_operations();
3009 assert!(ops.contains("re")); assert!(ops.contains("f")); }
3012
3013 #[test]
3016 fn test_text_context_access() {
3017 let mut page = Page::a4();
3018 let tc = page.text();
3019
3020 tc.set_font(Font::Helvetica, 12.0);
3021 tc.at(100.0, 100.0);
3022
3023 let result = tc.write("Test text");
3025 assert!(result.is_ok());
3026 }
3027
3028 #[test]
3030 fn test_get_used_characters_from_text_context() {
3031 let mut page = Page::a4();
3032
3033 page.text().write("ABC").unwrap();
3035
3036 let chars = page.get_used_characters();
3038 assert!(chars.is_some());
3039 let chars = chars.unwrap();
3040 assert!(chars.contains(&'A'));
3041 assert!(chars.contains(&'B'));
3042 assert!(chars.contains(&'C'));
3043 }
3044
3045 #[test]
3046 fn test_get_used_characters_combines_both_contexts() {
3047 let mut page = Page::a4();
3048
3049 page.text().write("AB").unwrap();
3051
3052 let _ = page.graphics().draw_text("CD", 100.0, 100.0);
3054
3055 let chars = page.get_used_characters();
3057 assert!(chars.is_some());
3058 let chars = chars.unwrap();
3059 assert!(chars.contains(&'A'));
3060 assert!(chars.contains(&'B'));
3061 assert!(chars.contains(&'C'));
3062 assert!(chars.contains(&'D'));
3063 }
3064
3065 #[test]
3066 fn test_get_used_characters_cjk_via_text_context() {
3067 let mut page = Page::a4();
3068
3069 page.text()
3071 .set_font(Font::Custom("NotoSansCJK".to_string()), 12.0);
3072 page.text().write("中文").unwrap();
3073
3074 let chars = page.get_used_characters();
3075 assert!(chars.is_some());
3076 let chars = chars.unwrap();
3077 assert!(chars.contains(&'中'));
3078 assert!(chars.contains(&'文'));
3079 }
3080
3081 #[test]
3082 fn test_text_flow_creation() {
3083 let page = Page::a4();
3084 let text_flow = page.text_flow();
3085
3086 let _ = text_flow; }
3091
3092 #[test]
3095 fn test_add_image() {
3096 let mut page = Page::a4();
3097
3098 let image_data = vec![
3100 0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x10, 0x00, 0x10, 0x03, 0x01, 0x11, 0x00, 0x02, 0x11, 0x00, 0x03, 0x11, 0x00, 0xFF, 0xD9, ];
3112
3113 let image = Image::from_jpeg_data(image_data).unwrap();
3114 page.add_image("test_image", image);
3115
3116 assert!(page.images.contains_key("test_image"));
3118 }
3119
3120 #[test]
3121 fn test_draw_image_simple() {
3122 let mut page = Page::a4();
3123
3124 let image_data = vec![
3126 0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x10, 0x00, 0x10, 0x03, 0x01, 0x11,
3127 0x00, 0x02, 0x11, 0x00, 0x03, 0x11, 0x00, 0xFF, 0xD9,
3128 ];
3129
3130 let image = Image::from_jpeg_data(image_data).unwrap();
3131 page.add_image("img1", image);
3132
3133 let result = page.draw_image("img1", 100.0, 100.0, 200.0, 200.0);
3135 assert!(result.is_ok());
3136 }
3137
3138 #[test]
3141 fn test_add_annotation() {
3142 use crate::annotations::{Annotation, AnnotationType};
3143 use crate::geometry::{Point, Rectangle};
3144
3145 let mut page = Page::a4();
3146 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 150.0));
3147 let annotation = Annotation::new(AnnotationType::Text, rect);
3148
3149 page.add_annotation(annotation);
3150 assert_eq!(page.annotations().len(), 1);
3151 }
3152
3153 #[test]
3154 fn test_annotations_mut() {
3155 use crate::annotations::{Annotation, AnnotationType};
3156 use crate::geometry::{Point, Rectangle};
3157
3158 let mut page = Page::a4();
3159 let _rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 150.0));
3160
3161 for i in 0..3 {
3163 let annotation = Annotation::new(
3164 AnnotationType::Text,
3165 Rectangle::new(
3166 Point::new(100.0 + i as f64 * 10.0, 100.0),
3167 Point::new(200.0 + i as f64 * 10.0, 150.0),
3168 ),
3169 );
3170 page.add_annotation(annotation);
3171 }
3172
3173 let annotations = page.annotations_mut();
3175 annotations.clear();
3176 assert_eq!(page.annotations().len(), 0);
3177 }
3178
3179 #[test]
3182 fn test_add_form_widget() {
3183 use crate::forms::Widget;
3184 use crate::geometry::{Point, Rectangle};
3185
3186 let mut page = Page::a4();
3187 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 120.0));
3188 let widget = Widget::new(rect);
3189
3190 let obj_ref = page.add_form_widget(widget);
3191 assert_eq!(obj_ref.number(), 0);
3192 assert_eq!(obj_ref.generation(), 0);
3193
3194 assert_eq!(page.annotations().len(), 1);
3196 }
3197
3198 #[test]
3201 fn test_set_header() {
3202 use crate::text::HeaderFooter;
3203
3204 let mut page = Page::a4();
3205 let header = HeaderFooter::new_header("Test Header");
3206
3207 page.set_header(header);
3208 assert!(page.header().is_some());
3209
3210 if let Some(h) = page.header() {
3211 assert_eq!(h.content(), "Test Header");
3212 }
3213 }
3214
3215 #[test]
3216 fn test_set_footer() {
3217 use crate::text::HeaderFooter;
3218
3219 let mut page = Page::a4();
3220 let footer = HeaderFooter::new_footer("Page {{page}} of {{total}}");
3221
3222 page.set_footer(footer);
3223 assert!(page.footer().is_some());
3224
3225 if let Some(f) = page.footer() {
3226 assert_eq!(f.content(), "Page {{page}} of {{total}}");
3227 }
3228 }
3229
3230 #[test]
3231 fn test_header_footer_rendering() {
3232 use crate::text::HeaderFooter;
3233
3234 let mut page = Page::a4();
3235
3236 page.set_header(HeaderFooter::new_header("Header"));
3238 page.set_footer(HeaderFooter::new_footer("Footer"));
3239
3240 let result = page.generate_content_with_page_info(Some(1), Some(1), None);
3242 assert!(result.is_ok());
3243
3244 let content = result.unwrap();
3245 assert!(!content.is_empty());
3246 }
3247
3248 #[test]
3251 fn test_add_table() {
3252 use crate::text::Table;
3253
3254 let mut page = Page::a4();
3255 let mut table = Table::with_equal_columns(2, 200.0);
3256
3257 table
3259 .add_row(vec!["Cell 1".to_string(), "Cell 2".to_string()])
3260 .unwrap();
3261 table
3262 .add_row(vec!["Cell 3".to_string(), "Cell 4".to_string()])
3263 .unwrap();
3264
3265 let result = page.add_table(&table);
3266 assert!(result.is_ok());
3267 }
3268
3269 #[test]
3272 fn test_generate_operations_empty() {
3273 let page = Page::a4();
3274 let ops = page.graphics_context.generate_operations();
3276
3277 assert!(ops.is_ok());
3279 }
3280
3281 #[test]
3282 fn test_generate_operations_with_graphics() {
3283 let mut page = Page::a4();
3284
3285 page.graphics().rectangle(50.0, 50.0, 100.0, 100.0).fill();
3286
3287 let ops = page.graphics_context.generate_operations();
3289 assert!(ops.is_ok());
3290
3291 let content = ops.unwrap();
3292 let content_str = String::from_utf8_lossy(&content);
3293 assert!(content_str.contains("re")); assert!(content_str.contains("f")); }
3296
3297 #[test]
3298 fn test_generate_operations_with_text() {
3299 let mut page = Page::a4();
3300
3301 page.text()
3302 .set_font(Font::Helvetica, 12.0)
3303 .at(100.0, 700.0)
3304 .write("Hello")
3305 .unwrap();
3306
3307 let ops = page.text_context.generate_operations();
3309 assert!(ops.is_ok());
3310
3311 let content = ops.unwrap();
3312 let content_str = String::from_utf8_lossy(&content);
3313 assert!(content_str.contains("BT")); assert!(content_str.contains("ET")); }
3316
3317 #[test]
3320 fn test_negative_margins() {
3321 let mut page = Page::a4();
3322 page.set_margins(-10.0, -20.0, -30.0, -40.0);
3323
3324 let margins = page.margins();
3326 assert_eq!(margins.left, -10.0);
3327 assert_eq!(margins.right, -20.0);
3328 }
3329
3330 #[test]
3331 fn test_zero_dimensions() {
3332 let page = Page::new(0.0, 0.0);
3333 assert_eq!(page.width(), 0.0);
3334 assert_eq!(page.height(), 0.0);
3335
3336 let (_, _, width, height) = page.content_area();
3338 assert!(width < 0.0);
3339 assert!(height < 0.0);
3340 }
3341
3342 #[test]
3343 fn test_huge_dimensions() {
3344 let page = Page::new(1_000_000.0, 1_000_000.0);
3345 assert_eq!(page.width(), 1_000_000.0);
3346 assert_eq!(page.height(), 1_000_000.0);
3347 }
3348
3349 #[test]
3350 fn test_draw_nonexistent_image() {
3351 let mut page = Page::a4();
3352
3353 let result = page.draw_image("nonexistent", 100.0, 100.0, 200.0, 200.0);
3355
3356 assert!(result.is_err());
3358 }
3359
3360 #[test]
3361 fn test_clone_page() {
3362 let mut page = Page::a4();
3363 page.set_margins(10.0, 20.0, 30.0, 40.0);
3364
3365 page.graphics().rectangle(50.0, 50.0, 100.0, 100.0).fill();
3366
3367 let cloned = page.clone();
3368 assert_eq!(cloned.width(), page.width());
3369 assert_eq!(cloned.height(), page.height());
3370 assert_eq!(cloned.margins().left, page.margins().left);
3371 }
3372
3373 #[test]
3374 fn test_page_from_parsed_basic() {
3375 use crate::parser::objects::PdfDictionary;
3376 use crate::parser::page_tree::ParsedPage;
3377
3378 let parsed_page = ParsedPage {
3380 obj_ref: (1, 0),
3381 dict: PdfDictionary::new(),
3382 inherited_resources: None,
3383 media_box: [0.0, 0.0, 612.0, 792.0], crop_box: None,
3385 rotation: 0,
3386 annotations: None,
3387 };
3388
3389 let page = Page::from_parsed(&parsed_page).unwrap();
3391
3392 assert_eq!(page.width(), 612.0);
3394 assert_eq!(page.height(), 792.0);
3395 assert_eq!(page.get_rotation(), 0);
3396 }
3397
3398 #[test]
3399 fn test_page_from_parsed_with_rotation() {
3400 use crate::parser::objects::PdfDictionary;
3401 use crate::parser::page_tree::ParsedPage;
3402
3403 let parsed_page = ParsedPage {
3405 obj_ref: (1, 0),
3406 dict: PdfDictionary::new(),
3407 inherited_resources: None,
3408 media_box: [0.0, 0.0, 595.0, 842.0], crop_box: None,
3410 rotation: 90,
3411 annotations: None,
3412 };
3413
3414 let page = Page::from_parsed(&parsed_page).unwrap();
3416
3417 assert_eq!(page.get_rotation(), 90);
3419 assert_eq!(page.width(), 595.0);
3420 assert_eq!(page.height(), 842.0);
3421
3422 assert_eq!(page.effective_width(), 842.0);
3424 assert_eq!(page.effective_height(), 595.0);
3425 }
3426
3427 #[test]
3428 fn test_page_from_parsed_with_cropbox() {
3429 use crate::parser::objects::PdfDictionary;
3430 use crate::parser::page_tree::ParsedPage;
3431
3432 let parsed_page = ParsedPage {
3434 obj_ref: (1, 0),
3435 dict: PdfDictionary::new(),
3436 inherited_resources: None,
3437 media_box: [0.0, 0.0, 612.0, 792.0],
3438 crop_box: Some([10.0, 10.0, 602.0, 782.0]),
3439 rotation: 0,
3440 annotations: None,
3441 };
3442
3443 let page = Page::from_parsed(&parsed_page).unwrap();
3445
3446 assert_eq!(page.width(), 612.0);
3448 assert_eq!(page.height(), 792.0);
3449 }
3450
3451 #[test]
3452 fn test_page_from_parsed_small_mediabox() {
3453 use crate::parser::objects::PdfDictionary;
3454 use crate::parser::page_tree::ParsedPage;
3455
3456 let parsed_page = ParsedPage {
3458 obj_ref: (1, 0),
3459 dict: PdfDictionary::new(),
3460 inherited_resources: None,
3461 media_box: [0.0, 0.0, 200.0, 300.0],
3462 crop_box: None,
3463 rotation: 0,
3464 annotations: None,
3465 };
3466
3467 let page = Page::from_parsed(&parsed_page).unwrap();
3469
3470 assert_eq!(page.width(), 200.0);
3471 assert_eq!(page.height(), 300.0);
3472 }
3473
3474 #[test]
3475 fn test_page_from_parsed_non_zero_origin() {
3476 use crate::parser::objects::PdfDictionary;
3477 use crate::parser::page_tree::ParsedPage;
3478
3479 let parsed_page = ParsedPage {
3481 obj_ref: (1, 0),
3482 dict: PdfDictionary::new(),
3483 inherited_resources: None,
3484 media_box: [10.0, 20.0, 610.0, 820.0], crop_box: None,
3486 rotation: 0,
3487 annotations: None,
3488 };
3489
3490 let page = Page::from_parsed(&parsed_page).unwrap();
3492
3493 assert_eq!(page.width(), 600.0); assert_eq!(page.height(), 800.0); }
3497
3498 #[test]
3499 fn test_page_rotation() {
3500 let mut page = Page::a4();
3501
3502 assert_eq!(page.get_rotation(), 0);
3504
3505 page.set_rotation(90);
3507 assert_eq!(page.get_rotation(), 90);
3508
3509 page.set_rotation(180);
3510 assert_eq!(page.get_rotation(), 180);
3511
3512 page.set_rotation(270);
3513 assert_eq!(page.get_rotation(), 270);
3514
3515 page.set_rotation(360);
3516 assert_eq!(page.get_rotation(), 0);
3517
3518 page.set_rotation(45);
3520 assert_eq!(page.get_rotation(), 90);
3521
3522 page.set_rotation(135);
3523 assert_eq!(page.get_rotation(), 180);
3524
3525 page.set_rotation(-90);
3526 assert_eq!(page.get_rotation(), 270);
3527 }
3528
3529 #[test]
3530 fn test_effective_dimensions() {
3531 let mut page = Page::new(600.0, 800.0);
3532
3533 assert_eq!(page.effective_width(), 600.0);
3535 assert_eq!(page.effective_height(), 800.0);
3536
3537 page.set_rotation(90);
3539 assert_eq!(page.effective_width(), 800.0);
3540 assert_eq!(page.effective_height(), 600.0);
3541
3542 page.set_rotation(180);
3544 assert_eq!(page.effective_width(), 600.0);
3545 assert_eq!(page.effective_height(), 800.0);
3546
3547 page.set_rotation(270);
3549 assert_eq!(page.effective_width(), 800.0);
3550 assert_eq!(page.effective_height(), 600.0);
3551 }
3552
3553 #[test]
3554 fn test_rotation_in_pdf_dict() {
3555 let mut page = Page::a4();
3556
3557 let dict = page.to_dict();
3559 assert!(dict.get("Rotate").is_none());
3560
3561 page.set_rotation(90);
3563 let dict = page.to_dict();
3564 assert_eq!(dict.get("Rotate"), Some(&Object::Integer(90)));
3565
3566 page.set_rotation(270);
3567 let dict = page.to_dict();
3568 assert_eq!(dict.get("Rotate"), Some(&Object::Integer(270)));
3569 }
3570}
3571
3572#[derive(Debug)]
3577pub struct LayoutManager {
3578 pub coordinate_system: crate::coordinate_system::CoordinateSystem,
3580 pub current_y: f64,
3582 pub page_width: f64,
3584 pub page_height: f64,
3585 pub margins: Margins,
3587 pub element_spacing: f64,
3589}
3590
3591impl LayoutManager {
3592 pub fn new(page: &Page, coordinate_system: crate::coordinate_system::CoordinateSystem) -> Self {
3594 let current_y = match coordinate_system {
3595 crate::coordinate_system::CoordinateSystem::PdfStandard => {
3596 page.height() - page.margins().top
3598 }
3599 crate::coordinate_system::CoordinateSystem::ScreenSpace => {
3600 page.margins().top
3602 }
3603 crate::coordinate_system::CoordinateSystem::Custom(_) => {
3604 page.height() / 2.0
3606 }
3607 };
3608
3609 Self {
3610 coordinate_system,
3611 current_y,
3612 page_width: page.width(),
3613 page_height: page.height(),
3614 margins: page.margins().clone(),
3615 element_spacing: 10.0,
3616 }
3617 }
3618
3619 pub fn with_element_spacing(mut self, spacing: f64) -> Self {
3621 self.element_spacing = spacing;
3622 self
3623 }
3624
3625 pub fn will_fit(&self, element_height: f64) -> bool {
3627 let required_space = element_height + self.element_spacing;
3628
3629 match self.coordinate_system {
3630 crate::coordinate_system::CoordinateSystem::PdfStandard => {
3631 self.current_y - required_space >= self.margins.bottom
3633 }
3634 crate::coordinate_system::CoordinateSystem::ScreenSpace => {
3635 self.current_y + required_space <= self.page_height - self.margins.bottom
3637 }
3638 crate::coordinate_system::CoordinateSystem::Custom(_) => {
3639 required_space <= (self.page_height - self.margins.top - self.margins.bottom) / 2.0
3641 }
3642 }
3643 }
3644
3645 pub fn remaining_space(&self) -> f64 {
3647 match self.coordinate_system {
3648 crate::coordinate_system::CoordinateSystem::PdfStandard => {
3649 (self.current_y - self.margins.bottom).max(0.0)
3650 }
3651 crate::coordinate_system::CoordinateSystem::ScreenSpace => {
3652 (self.page_height - self.margins.bottom - self.current_y).max(0.0)
3653 }
3654 crate::coordinate_system::CoordinateSystem::Custom(_) => {
3655 self.page_height / 2.0 }
3657 }
3658 }
3659
3660 pub fn add_element(&mut self, element_height: f64) -> Option<f64> {
3666 if !self.will_fit(element_height) {
3667 return None;
3668 }
3669
3670 let position_y = match self.coordinate_system {
3671 crate::coordinate_system::CoordinateSystem::PdfStandard => {
3672 let y_position = self.current_y - element_height;
3675 self.current_y = y_position - self.element_spacing;
3676 self.current_y + element_height }
3678 crate::coordinate_system::CoordinateSystem::ScreenSpace => {
3679 let y_position = self.current_y;
3682 self.current_y += element_height + self.element_spacing;
3683 y_position
3684 }
3685 crate::coordinate_system::CoordinateSystem::Custom(_) => {
3686 let y_position = self.current_y;
3688 self.current_y -= element_height + self.element_spacing;
3689 y_position
3690 }
3691 };
3692
3693 Some(position_y)
3694 }
3695
3696 pub fn new_page(&mut self) {
3698 self.current_y = match self.coordinate_system {
3699 crate::coordinate_system::CoordinateSystem::PdfStandard => {
3700 self.page_height - self.margins.top
3701 }
3702 crate::coordinate_system::CoordinateSystem::ScreenSpace => self.margins.top,
3703 crate::coordinate_system::CoordinateSystem::Custom(_) => self.page_height / 2.0,
3704 };
3705 }
3706
3707 pub fn center_x(&self, element_width: f64) -> f64 {
3709 let available_width = self.page_width - self.margins.left - self.margins.right;
3710 self.margins.left + (available_width - element_width) / 2.0
3711 }
3712
3713 pub fn left_x(&self) -> f64 {
3715 self.margins.left
3716 }
3717
3718 pub fn right_x(&self, element_width: f64) -> f64 {
3720 self.page_width - self.margins.right - element_width
3721 }
3722}
3723
3724#[cfg(test)]
3725mod layout_manager_tests {
3726 use super::*;
3727 use crate::coordinate_system::CoordinateSystem;
3728
3729 #[test]
3730 fn test_layout_manager_pdf_standard() {
3731 let page = Page::a4(); let mut layout = LayoutManager::new(&page, CoordinateSystem::PdfStandard);
3733
3734 assert!(layout.current_y > 750.0); let element_height = 100.0;
3740 let position = layout.add_element(element_height);
3741
3742 assert!(position.is_some());
3743 let y_pos = position.unwrap();
3744 assert!(y_pos > 700.0); assert!(layout.current_y < y_pos);
3748 }
3749
3750 #[test]
3751 fn test_layout_manager_screen_space() {
3752 let page = Page::a4();
3753 let mut layout = LayoutManager::new(&page, CoordinateSystem::ScreenSpace);
3754
3755 assert!(layout.current_y < 100.0); let element_height = 100.0;
3760 let position = layout.add_element(element_height);
3761
3762 assert!(position.is_some());
3763 let y_pos = position.unwrap();
3764 assert!(y_pos < 100.0); assert!(layout.current_y > y_pos);
3768 }
3769
3770 #[test]
3771 fn test_layout_manager_overflow() {
3772 let page = Page::a4();
3773 let mut layout = LayoutManager::new(&page, CoordinateSystem::PdfStandard);
3774
3775 let huge_element = 900.0; let position = layout.add_element(huge_element);
3778
3779 assert!(position.is_none()); let mut count = 0;
3783 while layout.add_element(50.0).is_some() {
3784 count += 1;
3785 if count > 100 {
3786 break;
3787 } }
3789
3790 assert!(count > 5);
3792
3793 assert!(layout.add_element(50.0).is_none());
3795 }
3796
3797 #[test]
3798 fn test_layout_manager_centering() {
3799 let page = Page::a4();
3800 let layout = LayoutManager::new(&page, CoordinateSystem::PdfStandard);
3801
3802 let element_width = 200.0;
3803 let center_x = layout.center_x(element_width);
3804
3805 let expected_center = page.margins().left
3807 + (page.width() - page.margins().left - page.margins().right - element_width) / 2.0;
3808 assert_eq!(center_x, expected_center);
3809 }
3810}