1use super::document::{PdfDocument, PdfPage};
4use super::font_config::FontConfig;
5use super::image::ImageXObject;
6use crate::image::ImageInfo;
7use fop_layout::{AreaId, AreaTree, AreaType};
8use fop_types::{Length, Result};
9use std::collections::HashMap;
10
11use super::document::LinkDestination;
12
13pub struct PdfRenderer {
15 #[allow(dead_code)]
17 page_width: Length,
18
19 #[allow(dead_code)]
21 page_height: Length,
22
23 font_config: FontConfig,
25}
26
27impl PdfRenderer {
28 pub fn new() -> Self {
34 Self {
35 page_width: Length::from_mm(210.0),
36 page_height: Length::from_mm(297.0),
37 font_config: FontConfig::new(),
38 }
39 }
40
41 pub fn with_system_fonts() -> Self {
44 Self {
45 page_width: Length::from_mm(210.0),
46 page_height: Length::from_mm(297.0),
47 font_config: FontConfig::with_system_fonts(),
48 }
49 }
50
51 pub fn with_font_config(mut self, font_config: FontConfig) -> Self {
55 self.font_config = font_config;
56 self
57 }
58
59 pub fn render_with_fo(
61 &self,
62 area_tree: &AreaTree,
63 fo_tree: &fop_core::FoArena,
64 ) -> Result<PdfDocument> {
65 let mut doc = self.render(area_tree)?;
66 if let Ok(Some(outline)) = super::outline::extract_outline_from_fo_tree(fo_tree) {
68 doc.set_outline(outline);
69 }
70 if let Some(ref lang) = fo_tree.document_lang {
72 doc.info.lang = Some(lang.clone());
73 }
74 Ok(doc)
75 }
76
77 pub fn render(&self, area_tree: &AreaTree) -> Result<PdfDocument> {
79 let mut doc = PdfDocument::new();
80 doc.info.title = Some("FOP Generated PDF".to_string());
81
82 let mut image_map = HashMap::new();
84 self.collect_images(area_tree, &mut doc, &mut image_map)?;
85
86 let mut opacity_map = HashMap::new();
88 self.collect_opacity_states(area_tree, &mut doc, &mut opacity_map);
89
90 let font_cache = self.build_font_cache(area_tree, &mut doc)?;
93
94 for (id, node) in area_tree.iter() {
96 if matches!(node.area.area_type, AreaType::Page) {
97 let page =
98 self.render_page(area_tree, id, &image_map, &opacity_map, &font_cache)?;
99 doc.add_page(page);
100 }
101 }
102
103 Ok(doc)
104 }
105
106 fn build_font_cache(
110 &self,
111 area_tree: &AreaTree,
112 doc: &mut PdfDocument,
113 ) -> Result<HashMap<String, usize>> {
114 let mut cache: HashMap<String, usize> = HashMap::new();
115
116 for (_, node) in area_tree.iter() {
117 if let Some(family) = node.area.traits.font_family.as_deref() {
118 let key = family.to_lowercase();
119 if cache.contains_key(&key) {
120 continue;
121 }
122
123 if let Some(path) = self.font_config.find_font(family) {
125 match std::fs::read(path) {
126 Ok(data) => match doc.embed_font(data) {
127 Ok(idx) => {
128 cache.insert(key, idx);
129 }
130 Err(_) => {
131 }
134 },
135 Err(_) => {
136 }
138 }
139 } else {
140 if let Some(idx) = doc.font_manager.find_by_name(family) {
143 cache.insert(key, idx);
144 }
145 }
146 }
147 }
148
149 Ok(cache)
150 }
151
152 fn collect_images(
154 &self,
155 area_tree: &AreaTree,
156 doc: &mut PdfDocument,
157 image_map: &mut HashMap<AreaId, usize>,
158 ) -> Result<()> {
159 for (id, node) in area_tree.iter() {
160 if matches!(node.area.area_type, AreaType::Viewport) {
162 if let Some(image_data) = node.area.image_data() {
164 let image_index = self.add_image_from_data(doc, image_data)?;
166 image_map.insert(id, image_index);
167 }
168 }
169 }
170 Ok(())
171 }
172
173 pub fn add_image_from_data(&self, doc: &mut PdfDocument, image_data: &[u8]) -> Result<usize> {
175 let image_info = ImageInfo::from_bytes(image_data)?;
176 let xobject = ImageXObject::from_image_info(&image_info)?;
177 Ok(doc.add_image_xobject(xobject))
178 }
179
180 pub fn collect_images_public(
182 &self,
183 area_tree: &AreaTree,
184 doc: &mut PdfDocument,
185 image_map: &mut HashMap<AreaId, usize>,
186 ) -> Result<()> {
187 self.collect_images(area_tree, doc, image_map)
188 }
189
190 pub fn collect_opacity_states_public(
192 &self,
193 area_tree: &AreaTree,
194 doc: &mut PdfDocument,
195 opacity_map: &mut HashMap<AreaId, usize>,
196 ) {
197 self.collect_opacity_states(area_tree, doc, opacity_map)
198 }
199
200 pub fn render_page_public(
202 &self,
203 area_tree: &AreaTree,
204 page_id: AreaId,
205 image_map: &HashMap<AreaId, usize>,
206 opacity_map: &HashMap<AreaId, usize>,
207 font_cache: &HashMap<String, usize>,
208 ) -> Result<PdfPage> {
209 self.render_page(area_tree, page_id, image_map, opacity_map, font_cache)
210 }
211
212 fn collect_opacity_states(
214 &self,
215 area_tree: &AreaTree,
216 doc: &mut PdfDocument,
217 opacity_map: &mut HashMap<AreaId, usize>,
218 ) {
219 for (id, node) in area_tree.iter() {
220 if let Some(opacity) = node.area.traits.opacity {
222 if (opacity - 1.0).abs() > f64::EPSILON {
223 let gs_index = doc.add_ext_g_state(opacity, opacity);
225 opacity_map.insert(id, gs_index);
226 }
227 }
228 }
229 }
230
231 fn render_page(
233 &self,
234 area_tree: &AreaTree,
235 page_id: AreaId,
236 image_map: &HashMap<AreaId, usize>,
237 opacity_map: &HashMap<AreaId, usize>,
238 font_cache: &HashMap<String, usize>,
239 ) -> Result<PdfPage> {
240 let page_node = area_tree
241 .get(page_id)
242 .ok_or_else(|| fop_types::FopError::Generic("Page not found".to_string()))?;
243
244 let mut pdf_page = PdfPage::new(page_node.area.width(), page_node.area.height());
245
246 let page_height = pdf_page.height;
248 render_children(
249 area_tree,
250 page_id,
251 &mut pdf_page,
252 Length::ZERO,
253 Length::ZERO,
254 page_height,
255 image_map,
256 opacity_map,
257 font_cache,
258 )?;
259
260 Ok(pdf_page)
261 }
262}
263
264impl Default for PdfRenderer {
265 fn default() -> Self {
266 Self::new()
267 }
268}
269
270#[allow(clippy::too_many_arguments)]
272#[allow(clippy::only_used_in_recursion)]
273fn render_children(
274 area_tree: &AreaTree,
275 parent_id: AreaId,
276 pdf_page: &mut PdfPage,
277 offset_x: Length,
278 offset_y: Length,
279 page_height: Length,
280 image_map: &HashMap<AreaId, usize>,
281 opacity_map: &HashMap<AreaId, usize>,
282 font_cache: &HashMap<String, usize>,
283) -> Result<()> {
284 let children = area_tree.children(parent_id);
285
286 for child_id in children {
287 if let Some(child_node) = area_tree.get(child_id) {
288 let abs_x = offset_x + child_node.area.geometry.x;
290 let abs_y = offset_y + child_node.area.geometry.y;
291
292 let needs_clipping = child_node
294 .area
295 .traits
296 .overflow
297 .map(|o| o.clips_content())
298 .unwrap_or(false);
299
300 if needs_clipping {
302 let pdf_y = page_height - abs_y - child_node.area.height();
303 pdf_page.save_clip_state(
304 abs_x,
305 pdf_y,
306 child_node.area.width(),
307 child_node.area.height(),
308 )?;
309 }
310
311 if let Some(bg_color) = child_node.area.traits.background_color {
313 let pdf_y = page_height - abs_y - child_node.area.height();
314 let border_radius = child_node.area.traits.border_radius;
315
316 if let Some(&gs_index) = opacity_map.get(&child_id) {
318 pdf_page.add_background_with_opacity(
320 abs_x,
321 pdf_y,
322 child_node.area.width(),
323 child_node.area.height(),
324 bg_color,
325 border_radius,
326 gs_index,
327 );
328 } else {
329 pdf_page.add_background_with_radius(
331 abs_x,
332 pdf_y,
333 child_node.area.width(),
334 child_node.area.height(),
335 bg_color,
336 border_radius,
337 );
338 }
339 }
340
341 if let (Some(border_widths), Some(border_colors), Some(border_styles)) = (
343 child_node.area.traits.border_width,
344 child_node.area.traits.border_color,
345 child_node.area.traits.border_style,
346 ) {
347 let pdf_y = page_height - abs_y - child_node.area.height();
348 let border_radius = child_node.area.traits.border_radius;
349
350 if let Some(&gs_index) = opacity_map.get(&child_id) {
352 pdf_page.add_borders_with_opacity(
354 abs_x,
355 pdf_y,
356 child_node.area.width(),
357 child_node.area.height(),
358 border_widths,
359 border_colors,
360 border_styles,
361 border_radius,
362 gs_index,
363 );
364 } else {
365 pdf_page.add_borders_with_radius(
367 abs_x,
368 pdf_y,
369 child_node.area.width(),
370 child_node.area.height(),
371 border_widths,
372 border_colors,
373 border_styles,
374 border_radius,
375 );
376 }
377 }
378
379 match child_node.area.area_type {
380 AreaType::Text => {
381 if let Some(leader_pattern) = &child_node.area.traits.is_leader {
383 render_leader(
385 pdf_page,
386 leader_pattern,
387 abs_x,
388 abs_y,
389 child_node.area.width(),
390 child_node.area.height(),
391 page_height,
392 &child_node.area.traits,
393 );
394 } else if let Some(text_content) = child_node.area.text_content() {
395 let font_size = child_node
397 .area
398 .traits
399 .font_size
400 .unwrap_or(Length::from_pt(12.0));
401
402 let pdf_y = page_height - abs_y - font_size;
404
405 let letter_spacing = child_node.area.traits.letter_spacing;
407 let word_spacing = child_node.area.traits.word_spacing;
408
409 if let Some(family) = child_node.area.traits.font_family.as_deref() {
412 if let Some(&font_idx) = font_cache.get(&family.to_lowercase()) {
413 pdf_page.add_text_with_font_and_spacing(
414 text_content,
415 abs_x,
416 pdf_y,
417 font_size,
418 font_idx,
419 letter_spacing,
420 word_spacing,
421 );
422 } else {
423 pdf_page.add_text_with_spacing(
425 text_content,
426 abs_x,
427 pdf_y,
428 font_size,
429 letter_spacing,
430 word_spacing,
431 );
432 }
433 } else {
434 pdf_page.add_text_with_spacing(
435 text_content,
436 abs_x,
437 pdf_y,
438 font_size,
439 letter_spacing,
440 word_spacing,
441 );
442 }
443
444 if let Some(link_dest) = &child_node.area.traits.link_destination {
446 let destination = if link_dest.starts_with("http://")
447 || link_dest.starts_with("https://")
448 || link_dest.starts_with("mailto:")
449 {
450 LinkDestination::External(link_dest.clone())
451 } else {
452 LinkDestination::Internal(link_dest.clone())
453 };
454
455 pdf_page.add_link_annotation(
457 abs_x,
458 pdf_y,
459 child_node.area.width(),
460 font_size,
461 destination,
462 );
463 }
464 }
465 }
466 AreaType::Inline => {
467 if let Some(leader_pattern) = &child_node.area.traits.is_leader {
469 render_leader(
470 pdf_page,
471 leader_pattern,
472 abs_x,
473 abs_y,
474 child_node.area.width(),
475 child_node.area.height(),
476 page_height,
477 &child_node.area.traits,
478 );
479 } else {
480 if let Some(link_dest) = &child_node.area.traits.link_destination {
482 let destination = if link_dest.starts_with("http://")
483 || link_dest.starts_with("https://")
484 || link_dest.starts_with("mailto:")
485 {
486 LinkDestination::External(link_dest.clone())
487 } else {
488 LinkDestination::Internal(link_dest.clone())
489 };
490
491 let pdf_y = page_height - abs_y - child_node.area.height();
493
494 pdf_page.add_link_annotation(
496 abs_x,
497 pdf_y,
498 child_node.area.width(),
499 child_node.area.height(),
500 destination,
501 );
502 }
503
504 render_children(
506 area_tree,
507 child_id,
508 pdf_page,
509 abs_x,
510 abs_y,
511 page_height,
512 image_map,
513 opacity_map,
514 font_cache,
515 )?;
516 }
517 }
518 AreaType::Viewport => {
519 if let Some(&image_index) = image_map.get(&child_id) {
521 let pdf_y = page_height - abs_y - child_node.area.height();
522 pdf_page.add_image(
523 image_index,
524 abs_x,
525 pdf_y,
526 child_node.area.width(),
527 child_node.area.height(),
528 );
529 }
530 render_children(
532 area_tree,
533 child_id,
534 pdf_page,
535 abs_x,
536 abs_y,
537 page_height,
538 image_map,
539 opacity_map,
540 font_cache,
541 )?;
542 }
543 _ => {
544 render_children(
546 area_tree,
547 child_id,
548 pdf_page,
549 abs_x,
550 abs_y,
551 page_height,
552 image_map,
553 opacity_map,
554 font_cache,
555 )?;
556 }
557 }
558
559 if needs_clipping {
561 pdf_page.restore_clip_state()?;
562 }
563 }
564 }
565
566 Ok(())
567}
568
569#[allow(clippy::too_many_arguments)]
571fn render_leader(
572 pdf_page: &mut PdfPage,
573 leader_pattern: &str,
574 x: Length,
575 y: Length,
576 width: Length,
577 height: Length,
578 page_height: Length,
579 traits: &fop_layout::area::TraitSet,
580) {
581 match leader_pattern {
582 "rule" => {
583 let thickness = traits.rule_thickness.unwrap_or(Length::from_pt(0.5));
585
586 let style = traits.rule_style.as_deref().unwrap_or("solid");
587
588 let half_diff = Length::from_millipoints((height - thickness).millipoints() / 2);
590 let rule_y = y + half_diff;
591
592 let pdf_y = page_height - rule_y - thickness;
594
595 let color = traits.color.unwrap_or(fop_types::Color::BLACK);
597
598 pdf_page.add_rule(x, pdf_y, width, thickness, color, style);
599 }
600 "dots" => {
601 }
604 "space" => {
605 }
607 _ => {
608 }
610 }
611}
612
613#[cfg(test)]
614mod tests {
615 use super::*;
616 use fop_layout::{Area, AreaTree};
617 use fop_types::{Point, Rect, Size};
618
619 #[test]
620 fn test_renderer_creation() {
621 let renderer = PdfRenderer::new();
622 assert_eq!(renderer.page_width, Length::from_mm(210.0));
623 assert_eq!(renderer.page_height, Length::from_mm(297.0));
624 }
625
626 #[test]
627 fn test_render_empty_tree() {
628 let renderer = PdfRenderer::new();
629 let tree = AreaTree::new();
630
631 let doc = renderer.render(&tree).expect("test: should succeed");
632 assert_eq!(doc.pages.len(), 0);
633 }
634
635 #[test]
636 fn test_render_single_page() {
637 let renderer = PdfRenderer::new();
638 let mut tree = AreaTree::new();
639
640 let page_rect = Rect::from_point_size(
642 Point::ZERO,
643 Size::new(Length::from_mm(210.0), Length::from_mm(297.0)),
644 );
645 let page = Area::new(AreaType::Page, page_rect);
646 tree.add_area(page);
647
648 let doc = renderer.render(&tree).expect("test: should succeed");
649 assert_eq!(doc.pages.len(), 1);
650 }
651
652 #[test]
653 fn test_add_image_to_document() {
654 let renderer = PdfRenderer::new();
655 let mut doc = PdfDocument::new();
656
657 let mut png_data = Vec::new();
659 let mut encoder = png::Encoder::new(&mut png_data, 1, 1);
660 encoder.set_color(png::ColorType::Rgb);
661 encoder.set_depth(png::BitDepth::Eight);
662
663 let mut writer = encoder.write_header().expect("test: should succeed");
664 let data = vec![255, 0, 0]; writer
666 .write_image_data(&data)
667 .expect("test: should succeed");
668 drop(writer);
669
670 let image_index = renderer
672 .add_image_from_data(&mut doc, &png_data)
673 .expect("test: should succeed");
674 assert_eq!(image_index, 0);
675 assert_eq!(doc.image_xobjects.len(), 1);
676
677 let xobject = &doc.image_xobjects[0];
679 assert_eq!(xobject.width, 1);
680 assert_eq!(xobject.height, 1);
681 assert_eq!(xobject.color_space, "DeviceRGB");
682 assert_eq!(xobject.filter, "FlateDecode");
683 }
684
685 #[test]
686 fn test_pdf_with_image_generates_valid_bytes() {
687 let renderer = PdfRenderer::new();
688 let mut doc = PdfDocument::new();
689
690 let mut png_data = Vec::new();
692 let mut encoder = png::Encoder::new(&mut png_data, 2, 2);
693 encoder.set_color(png::ColorType::Rgb);
694 encoder.set_depth(png::BitDepth::Eight);
695
696 let mut writer = encoder.write_header().expect("test: should succeed");
697 let data = vec![255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255]; writer
699 .write_image_data(&data)
700 .expect("test: should succeed");
701 drop(writer);
702
703 renderer
705 .add_image_from_data(&mut doc, &png_data)
706 .expect("test: should succeed");
707
708 let mut page = super::PdfPage::new(Length::from_mm(210.0), Length::from_mm(297.0));
709 page.add_image(
710 0,
711 Length::from_pt(100.0),
712 Length::from_pt(100.0),
713 Length::from_pt(50.0),
714 Length::from_pt(50.0),
715 );
716 doc.add_page(page);
717
718 let bytes = doc.to_bytes().expect("test: should succeed");
720
721 let pdf_str = String::from_utf8_lossy(&bytes);
723 assert!(pdf_str.starts_with("%PDF-"));
724 assert!(pdf_str.contains("/Type /XObject"));
725 assert!(pdf_str.contains("/Subtype /Image"));
726 assert!(pdf_str.contains("/Filter /FlateDecode"));
727 assert!(pdf_str.contains("/Im0 Do")); assert!(pdf_str.contains("%%EOF"));
729 }
730}
731
732#[cfg(test)]
733mod tests_writer_comprehensive {
734 use super::*;
735 use fop_layout::{Area, AreaTree, AreaType};
736 use fop_types::{Length, Point, Rect, Size};
737
738 fn make_page_area(w_mm: f64, h_mm: f64) -> Area {
739 let rect = Rect::from_point_size(
740 Point::ZERO,
741 Size::new(Length::from_mm(w_mm), Length::from_mm(h_mm)),
742 );
743 Area::new(AreaType::Page, rect)
744 }
745
746 #[test]
749 fn test_renderer_new_default_page_width() {
750 let r = PdfRenderer::new();
751 assert_eq!(r.page_width, Length::from_mm(210.0));
752 }
753
754 #[test]
755 fn test_renderer_new_default_page_height() {
756 let r = PdfRenderer::new();
757 assert_eq!(r.page_height, Length::from_mm(297.0));
758 }
759
760 #[test]
761 fn test_renderer_default_equals_new() {
762 let r1 = PdfRenderer::new();
763 let r2 = PdfRenderer::default();
764 assert_eq!(r1.page_width, r2.page_width);
765 assert_eq!(r1.page_height, r2.page_height);
766 }
767
768 #[test]
771 fn test_render_empty_tree_no_pages() {
772 let r = PdfRenderer::new();
773 let tree = AreaTree::new();
774 let doc = r.render(&tree).expect("test: should succeed");
775 assert_eq!(doc.pages.len(), 0);
776 }
777
778 #[test]
779 fn test_render_empty_tree_produces_valid_pdf() {
780 let r = PdfRenderer::new();
781 let tree = AreaTree::new();
782 let doc = r.render(&tree).expect("test: should succeed");
783 let bytes = doc.to_bytes().expect("test: should succeed");
784 assert!(bytes.starts_with(b"%PDF-"));
785 }
786
787 #[test]
790 fn test_render_one_page_produces_one_page_doc() {
791 let r = PdfRenderer::new();
792 let mut tree = AreaTree::new();
793 tree.add_area(make_page_area(210.0, 297.0));
794 let doc = r.render(&tree).expect("test: should succeed");
795 assert_eq!(doc.pages.len(), 1);
796 }
797
798 #[test]
799 fn test_render_two_pages_produces_two_page_doc() {
800 let r = PdfRenderer::new();
801 let mut tree = AreaTree::new();
802 tree.add_area(make_page_area(210.0, 297.0));
803 tree.add_area(make_page_area(210.0, 297.0));
804 let doc = r.render(&tree).expect("test: should succeed");
805 assert_eq!(doc.pages.len(), 2);
806 }
807
808 #[test]
809 fn test_render_five_pages_produces_five_page_doc() {
810 let r = PdfRenderer::new();
811 let mut tree = AreaTree::new();
812 for _ in 0..5 {
813 tree.add_area(make_page_area(210.0, 297.0));
814 }
815 let doc = r.render(&tree).expect("test: should succeed");
816 assert_eq!(doc.pages.len(), 5);
817 }
818
819 #[test]
820 fn test_render_page_count_in_catalog_bytes() {
821 let r = PdfRenderer::new();
822 let mut tree = AreaTree::new();
823 tree.add_area(make_page_area(210.0, 297.0));
824 tree.add_area(make_page_area(210.0, 297.0));
825 let doc = r.render(&tree).expect("test: should succeed");
826 let bytes = doc.to_bytes().expect("test: should succeed");
827 let s = String::from_utf8_lossy(&bytes);
828 assert!(s.contains("/Count 2"));
829 }
830
831 #[test]
834 fn test_render_sets_default_title() {
835 let r = PdfRenderer::new();
836 let tree = AreaTree::new();
837 let doc = r.render(&tree).expect("test: should succeed");
838 assert_eq!(doc.info.title.as_deref(), Some("FOP Generated PDF"));
839 }
840
841 #[test]
844 fn test_render_output_has_eof_marker() {
845 let r = PdfRenderer::new();
846 let tree = AreaTree::new();
847 let doc = r.render(&tree).expect("test: should succeed");
848 let bytes = doc.to_bytes().expect("test: should succeed");
849 let s = String::from_utf8_lossy(&bytes);
850 assert!(s.contains("%%EOF"));
851 }
852
853 #[test]
854 fn test_render_output_has_catalog() {
855 let r = PdfRenderer::new();
856 let tree = AreaTree::new();
857 let doc = r.render(&tree).expect("test: should succeed");
858 let bytes = doc.to_bytes().expect("test: should succeed");
859 let s = String::from_utf8_lossy(&bytes);
860 assert!(s.contains("/Type /Catalog"));
861 }
862
863 #[test]
864 fn test_render_output_has_pages_dict() {
865 let r = PdfRenderer::new();
866 let tree = AreaTree::new();
867 let doc = r.render(&tree).expect("test: should succeed");
868 let bytes = doc.to_bytes().expect("test: should succeed");
869 let s = String::from_utf8_lossy(&bytes);
870 assert!(s.contains("/Type /Pages"));
871 }
872
873 #[test]
874 fn test_render_output_has_font_resource() {
875 let r = PdfRenderer::new();
876 let tree = AreaTree::new();
877 let doc = r.render(&tree).expect("test: should succeed");
878 let bytes = doc.to_bytes().expect("test: should succeed");
879 let s = String::from_utf8_lossy(&bytes);
880 assert!(s.contains("/BaseFont /Helvetica"));
882 }
883
884 fn make_png_1x1_red() -> Vec<u8> {
887 let mut buf = Vec::new();
888 let mut enc = png::Encoder::new(&mut buf, 1, 1);
889 enc.set_color(png::ColorType::Rgb);
890 enc.set_depth(png::BitDepth::Eight);
891 let mut w = enc.write_header().expect("test: should succeed");
892 w.write_image_data(&[255, 0, 0])
893 .expect("test: should succeed");
894 drop(w);
895 buf
896 }
897
898 #[test]
899 fn test_add_image_returns_index_zero_for_first() {
900 let r = PdfRenderer::new();
901 let mut doc = super::PdfDocument::new();
902 let idx = r
903 .add_image_from_data(&mut doc, &make_png_1x1_red())
904 .expect("test: should succeed");
905 assert_eq!(idx, 0);
906 }
907
908 #[test]
909 fn test_add_image_increments_index_for_second() {
910 let r = PdfRenderer::new();
911 let mut doc = super::PdfDocument::new();
912 r.add_image_from_data(&mut doc, &make_png_1x1_red())
913 .expect("test: should succeed");
914 let idx2 = r
915 .add_image_from_data(&mut doc, &make_png_1x1_red())
916 .expect("test: should succeed");
917 assert_eq!(idx2, 1);
918 }
919
920 #[test]
921 fn test_add_image_grows_image_xobjects() {
922 let r = PdfRenderer::new();
923 let mut doc = super::PdfDocument::new();
924 r.add_image_from_data(&mut doc, &make_png_1x1_red())
925 .expect("test: should succeed");
926 r.add_image_from_data(&mut doc, &make_png_1x1_red())
927 .expect("test: should succeed");
928 assert_eq!(doc.image_xobjects.len(), 2);
929 }
930
931 #[test]
934 fn test_collect_images_public_empty_tree_no_images() {
935 let r = PdfRenderer::new();
936 let tree = AreaTree::new();
937 let mut doc = super::PdfDocument::new();
938 let mut map = HashMap::new();
939 r.collect_images_public(&tree, &mut doc, &mut map)
940 .expect("test: should succeed");
941 assert!(doc.image_xobjects.is_empty());
942 assert!(map.is_empty());
943 }
944
945 #[test]
948 fn test_collect_opacity_states_empty_tree_no_states() {
949 let r = PdfRenderer::new();
950 let tree = AreaTree::new();
951 let mut doc = super::PdfDocument::new();
952 let mut map = HashMap::new();
953 r.collect_opacity_states_public(&tree, &mut doc, &mut map);
954 assert!(doc.ext_g_states.is_empty());
955 assert!(map.is_empty());
956 }
957
958 #[test]
961 fn test_render_page_public_produces_correct_dimensions() {
962 let r = PdfRenderer::new();
963 let mut tree = AreaTree::new();
964 let page_id = tree.add_area(make_page_area(210.0, 297.0));
965 let doc = super::PdfDocument::new();
966 let _ = doc; let img_map = HashMap::new();
968 let op_map = HashMap::new();
969 let font_cache = HashMap::new();
970 let page = r
971 .render_page_public(&tree, page_id, &img_map, &op_map, &font_cache)
972 .expect("test: should succeed");
973 assert_eq!(page.width, Length::from_mm(210.0));
974 assert_eq!(page.height, Length::from_mm(297.0));
975 }
976
977 #[test]
980 fn test_with_system_fonts_can_render_empty_tree() {
981 let r = PdfRenderer::with_system_fonts();
982 let tree = AreaTree::new();
983 let doc = r.render(&tree).expect("test: should succeed");
984 assert_eq!(doc.pages.len(), 0);
985 }
986
987 #[test]
990 fn test_full_round_trip_single_page_pdf_is_valid() {
991 let r = PdfRenderer::new();
992 let mut tree = AreaTree::new();
993 tree.add_area(make_page_area(210.0, 297.0));
994 let doc = r.render(&tree).expect("test: should succeed");
995 let bytes = doc.to_bytes().expect("test: should succeed");
996 let s = String::from_utf8_lossy(&bytes);
997 assert!(s.starts_with("%PDF-"));
998 assert!(s.contains("%%EOF"));
999 assert!(s.contains("/Count 1"));
1000 }
1001
1002 #[test]
1003 fn test_a5_page_dimensions_in_media_box() {
1004 let r = PdfRenderer::new();
1005 let mut tree = AreaTree::new();
1006 tree.add_area(make_page_area(148.0, 210.0));
1008 let doc = r.render(&tree).expect("test: should succeed");
1009 let bytes = doc.to_bytes().expect("test: should succeed");
1010 let s = String::from_utf8_lossy(&bytes);
1011 assert!(s.contains("/MediaBox"));
1012 }
1013}