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