1use crate::bindgen::{
5 FPDF_ANNOT, FPDF_CONVERT_FILL_TO_STROKE, FPDF_DWORD, FPDF_GRAYSCALE, FPDF_LCD_TEXT,
6 FPDF_NO_NATIVETEXT, FPDF_PRINTING, FPDF_RENDER_FORCEHALFTONE, FPDF_RENDER_LIMITEDIMAGECACHE,
7 FPDF_RENDER_NO_SMOOTHIMAGE, FPDF_RENDER_NO_SMOOTHPATH, FPDF_RENDER_NO_SMOOTHTEXT,
8 FPDF_REVERSE_BYTE_ORDER, FS_MATRIX, FS_RECTF,
9};
10use crate::create_transform_setters;
11use crate::error::PdfiumError;
12use crate::pdf::bitmap::{PdfBitmap, PdfBitmapFormat, Pixels};
13use crate::pdf::color::PdfColor;
14use crate::pdf::document::page::field::PdfFormFieldType;
15use crate::pdf::document::page::PdfPageOrientation::{Landscape, Portrait};
16use crate::pdf::document::page::{PdfPage, PdfPageOrientation, PdfPageRenderRotation};
17use crate::pdf::matrix::{PdfMatrix, PdfMatrixValue};
18use crate::pdf::points::PdfPoints;
19use std::os::raw::c_int;
20
21pub struct PdfRenderConfig {
35 use_auto_scaling: bool,
36 start_x: Pixels,
37 start_y: Pixels,
38 fixed_width: Option<Pixels>,
39 fixed_height: Option<Pixels>,
40 target_width: Option<Pixels>,
41 target_height: Option<Pixels>,
42 scale_width_factor: Option<f32>,
43 scale_height_factor: Option<f32>,
44 maximum_width: Option<Pixels>,
45 maximum_height: Option<Pixels>,
46 portrait_rotation: PdfPageRenderRotation,
47 portrait_rotation_do_rotate_constraints: bool,
48 landscape_rotation: PdfPageRenderRotation,
49 landscape_rotation_do_rotate_constraints: bool,
50 format: PdfBitmapFormat,
51 do_clear_bitmap_before_rendering: bool,
52 clear_color: PdfColor,
53 do_render_form_data: bool,
54 form_field_highlight: Option<Vec<(PdfFormFieldType, PdfColor)>>,
55 transformation_matrix: PdfMatrix,
56 clip_rect: Option<(Pixels, Pixels, Pixels, Pixels)>,
57
58 do_set_flag_render_annotations: bool, do_set_flag_use_lcd_text_rendering: bool, do_set_flag_no_native_text: bool, do_set_flag_grayscale: bool, do_set_flag_render_limited_image_cache: bool, do_set_flag_render_force_half_tone: bool, do_set_flag_render_for_printing: bool, do_set_flag_render_no_smooth_text: bool, do_set_flag_render_no_smooth_image: bool, do_set_flag_render_no_smooth_path: bool, do_set_flag_reverse_byte_order: bool, do_set_flag_convert_fill_to_stroke: bool, }
73
74impl PdfRenderConfig {
75 pub fn new() -> Self {
77 PdfRenderConfig {
78 use_auto_scaling: true,
79 start_x: 0,
80 start_y: 0,
81 fixed_width: None,
82 fixed_height: None,
83 target_width: None,
84 target_height: None,
85 scale_width_factor: None,
86 scale_height_factor: None,
87 maximum_width: None,
88 maximum_height: None,
89 portrait_rotation: PdfPageRenderRotation::None,
90 portrait_rotation_do_rotate_constraints: false,
91 landscape_rotation: PdfPageRenderRotation::None,
92 landscape_rotation_do_rotate_constraints: false,
93 format: PdfBitmapFormat::default(),
94 do_clear_bitmap_before_rendering: true,
95 clear_color: PdfColor::WHITE,
96 do_render_form_data: true,
97 form_field_highlight: None,
98 transformation_matrix: PdfMatrix::IDENTITY,
99 clip_rect: None,
100 do_set_flag_render_annotations: true,
101 do_set_flag_use_lcd_text_rendering: false,
102 do_set_flag_no_native_text: false,
103 do_set_flag_grayscale: false,
104 do_set_flag_render_limited_image_cache: false,
105 do_set_flag_render_force_half_tone: false,
106 do_set_flag_render_for_printing: false,
107 do_set_flag_render_no_smooth_text: false,
108 do_set_flag_render_no_smooth_image: false,
109 do_set_flag_render_no_smooth_path: false,
110 do_set_flag_convert_fill_to_stroke: false,
111
112 do_set_flag_reverse_byte_order: true,
116 }
117 }
118
119 #[inline]
132 pub fn thumbnail(self, size: Pixels) -> Self {
133 self.set_target_size(size, size)
134 .set_maximum_width(size)
135 .set_maximum_height(size)
136 .rotate(PdfPageRenderRotation::None, false)
137 .use_print_quality(false)
138 .set_image_smoothing(false)
139 .render_annotations(false)
140 .render_form_data(false)
141 }
142
143 #[inline]
149 pub fn set_fixed_size_to_bitmap(self, bitmap: &PdfBitmap) -> Self {
150 self.set_fixed_size(bitmap.width(), bitmap.height())
151 }
152
153 #[inline]
158 pub fn set_fixed_size(self, width: Pixels, height: Pixels) -> Self {
159 self.set_fixed_width(width).set_fixed_height(height)
160 }
161
162 #[inline]
165 pub fn set_fixed_width(mut self, width: Pixels) -> Self {
166 self.use_auto_scaling = false;
167 self.fixed_width = Some(width);
168
169 self
170 }
171
172 #[inline]
175 pub fn set_fixed_height(mut self, height: Pixels) -> Self {
176 self.use_auto_scaling = false;
177 self.fixed_height = Some(height);
178
179 self
180 }
181
182 #[inline]
187 pub fn set_target_size(self, width: Pixels, height: Pixels) -> Self {
188 self.set_target_width(width).set_target_height(height)
189 }
190
191 #[inline]
197 pub fn set_target_width(mut self, width: Pixels) -> Self {
198 self.use_auto_scaling = true;
199 self.target_width = Some(width);
200
201 self
202 }
203
204 #[inline]
210 pub fn set_target_height(mut self, height: Pixels) -> Self {
211 self.use_auto_scaling = true;
212 self.target_height = Some(height);
213
214 self
215 }
216
217 #[inline]
224 pub fn scale_page_to_bitmap(self, bitmap: &PdfBitmap) -> Self {
225 self.scale_page_to_display_size(bitmap.width(), bitmap.height())
226 }
227
228 #[inline]
236 pub fn scale_page_to_display_size(mut self, width: Pixels, height: Pixels) -> Self {
237 self.scale_width_factor = None;
238 self.scale_height_factor = None;
239
240 self.set_target_width(width)
241 .set_maximum_width(width)
242 .set_maximum_height(height)
243 .rotate_if_landscape(PdfPageRenderRotation::Degrees90, true)
244 }
245
246 #[inline]
251 pub fn scale_page_by_factor(self, scale: f32) -> Self {
252 let result = self.scale_page_width_by_factor(scale);
253
254 result.scale_page_height_by_factor(scale)
255 }
256
257 #[inline]
263 pub fn scale_page_width_by_factor(mut self, scale: f32) -> Self {
264 self.scale_width_factor = Some(scale);
265
266 self
267 }
268
269 #[inline]
275 pub fn scale_page_height_by_factor(mut self, scale: f32) -> Self {
276 self.scale_height_factor = Some(scale);
277
278 self
279 }
280
281 #[inline]
283 pub fn set_maximum_width(mut self, width: Pixels) -> Self {
284 self.maximum_width = Some(width);
285
286 self
287 }
288
289 #[inline]
291 pub fn set_maximum_height(mut self, height: Pixels) -> Self {
292 self.maximum_height = Some(height);
293
294 self
295 }
296
297 #[inline]
304 pub fn rotate(self, rotation: PdfPageRenderRotation, do_rotate_constraints: bool) -> Self {
305 self.rotate_if_portrait(rotation, do_rotate_constraints)
306 .rotate_if_landscape(rotation, do_rotate_constraints)
307 }
308
309 #[inline]
317 pub fn rotate_if_portrait(
318 mut self,
319 rotation: PdfPageRenderRotation,
320 do_rotate_constraints: bool,
321 ) -> Self {
322 self.portrait_rotation = rotation;
323
324 if rotation == PdfPageRenderRotation::Degrees90
325 || rotation == PdfPageRenderRotation::Degrees270
326 {
327 self.portrait_rotation_do_rotate_constraints = do_rotate_constraints;
328 }
329
330 self
331 }
332
333 #[inline]
341 pub fn rotate_if_landscape(
342 mut self,
343 rotation: PdfPageRenderRotation,
344 do_rotate_constraints: bool,
345 ) -> Self {
346 self.landscape_rotation = rotation;
347
348 if rotation == PdfPageRenderRotation::Degrees90
349 || rotation == PdfPageRenderRotation::Degrees270
350 {
351 self.landscape_rotation_do_rotate_constraints = do_rotate_constraints;
352 }
353
354 self
355 }
356
357 #[inline]
360 pub fn set_format(mut self, format: PdfBitmapFormat) -> Self {
361 self.format = format;
362
363 self
364 }
365
366 #[inline]
370 pub fn clear_before_rendering(mut self, do_clear: bool) -> Self {
371 self.do_clear_bitmap_before_rendering = do_clear;
372
373 self
374 }
375
376 #[inline]
380 pub fn set_clear_color(mut self, color: PdfColor) -> Self {
381 self.clear_color = color;
382
383 self
384 }
385
386 #[inline]
393 pub fn render_form_data(mut self, do_render: bool) -> Self {
394 self.do_render_form_data = do_render;
395
396 self
397 }
398
399 #[inline]
402 pub fn render_annotations(mut self, do_render: bool) -> Self {
403 self.do_set_flag_render_annotations = do_render;
404
405 self
406 }
407
408 #[inline]
413 pub fn use_lcd_text_rendering(mut self, do_set_flag: bool) -> Self {
414 self.do_set_flag_use_lcd_text_rendering = do_set_flag;
415
416 self
417 }
418
419 #[inline]
424 pub fn disable_native_text_rendering(mut self, do_set_flag: bool) -> Self {
425 self.do_set_flag_no_native_text = do_set_flag;
426
427 self
428 }
429
430 #[inline]
433 pub fn use_grayscale_rendering(mut self, do_set_flag: bool) -> Self {
434 self.do_set_flag_grayscale = do_set_flag;
435
436 self
437 }
438
439 #[inline]
443 pub fn limit_render_image_cache_size(mut self, do_set_flag: bool) -> Self {
444 self.do_set_flag_render_limited_image_cache = do_set_flag;
445
446 self
447 }
448
449 #[inline]
453 pub fn force_half_tone(mut self, do_set_flag: bool) -> Self {
454 self.do_set_flag_render_force_half_tone = do_set_flag;
455
456 self
457 }
458
459 #[inline]
466 pub fn use_print_quality(mut self, do_set_flag: bool) -> Self {
467 self.do_set_flag_render_for_printing = do_set_flag;
468
469 self
470 }
471
472 #[inline]
478 pub fn set_text_smoothing(mut self, do_set_flag: bool) -> Self {
479 self.do_set_flag_render_no_smooth_text = !do_set_flag;
480
481 self
482 }
483
484 #[inline]
487 pub fn set_image_smoothing(mut self, do_set_flag: bool) -> Self {
488 self.do_set_flag_render_no_smooth_image = !do_set_flag;
489
490 self
491 }
492
493 #[inline]
496 pub fn set_path_smoothing(mut self, do_set_flag: bool) -> Self {
497 self.do_set_flag_render_no_smooth_path = !do_set_flag;
498
499 self
500 }
501
502 #[inline]
510 pub fn set_reverse_byte_order(mut self, do_set_flag: bool) -> Self {
511 self.do_set_flag_reverse_byte_order = do_set_flag;
512
513 self
514 }
515
516 #[inline]
519 pub fn render_fills_as_strokes(mut self, do_set_flag: bool) -> Self {
520 self.do_set_flag_convert_fill_to_stroke = do_set_flag;
521
522 self
523 }
524
525 #[inline]
528 pub fn highlight_all_form_fields(self, color: PdfColor) -> Self {
529 self.highlight_form_fields_of_type(PdfFormFieldType::Unknown, color)
530 }
531
532 #[inline]
535 pub fn highlight_button_form_fields(self, color: PdfColor) -> Self {
536 self.highlight_form_fields_of_type(PdfFormFieldType::PushButton, color)
537 }
538
539 #[inline]
542 pub fn highlight_checkbox_form_fields(self, color: PdfColor) -> Self {
543 self.highlight_form_fields_of_type(PdfFormFieldType::Checkbox, color)
544 }
545
546 #[inline]
549 pub fn highlight_radio_button_form_fields(self, color: PdfColor) -> Self {
550 self.highlight_form_fields_of_type(PdfFormFieldType::RadioButton, color)
551 }
552
553 #[inline]
556 pub fn highlight_combobox_form_fields(self, color: PdfColor) -> Self {
557 self.highlight_form_fields_of_type(PdfFormFieldType::ComboBox, color)
558 }
559
560 #[inline]
563 pub fn highlight_listbox_form_fields(self, color: PdfColor) -> Self {
564 self.highlight_form_fields_of_type(PdfFormFieldType::ListBox, color)
565 }
566
567 #[inline]
570 pub fn highlight_text_form_fields(self, color: PdfColor) -> Self {
571 self.highlight_form_fields_of_type(PdfFormFieldType::Text, color)
572 }
573
574 #[inline]
577 pub fn highlight_signature_form_fields(self, color: PdfColor) -> Self {
578 self.highlight_form_fields_of_type(PdfFormFieldType::Signature, color)
579 }
580
581 #[inline]
584 pub fn highlight_form_fields_of_type(
585 mut self,
586 form_field_type: PdfFormFieldType,
587 color: PdfColor,
588 ) -> Self {
589 if let Some(form_field_highlight) = self.form_field_highlight.as_mut() {
590 form_field_highlight.push((form_field_type, color));
591 } else {
592 self.form_field_highlight = Some(vec![(form_field_type, color)]);
593 }
594
595 self
596 }
597
598 create_transform_setters!(
599 Self,
600 Result<Self, PdfiumError>,
601 "the [PdfPage] during rendering",
602 "the [PdfPage] during rendering.",
603 "the [PdfPage] during rendering,",
604 "Pdfium's rendering pipeline supports _either_ rendering with form data _or_ rendering with
605 a custom transformation matrix, but not both at the same time. Applying a transformation via
606 these setters automatically disables rendering of form data. If you must render form data while
607 simultaneously applying transformations, consider using the [PdfPage::flatten()] function to
608 flatten the form elements and form data into the containing page. Note that matrix-based
609 transformations will be applied _in addition to_ any intrinsic page rotation previously set
610 using the [PdfRenderConfig::rotate()], [PdfRenderConfig::rotate_if_portrait()], or
611 [PdfRenderConfig::rotate_if_landscape()] functions."
612 );
613
614 fn transform_impl(
616 mut self,
617 a: PdfMatrixValue,
618 b: PdfMatrixValue,
619 c: PdfMatrixValue,
620 d: PdfMatrixValue,
621 e: PdfMatrixValue,
622 f: PdfMatrixValue,
623 ) -> Result<Self, PdfiumError> {
624 let result = self
625 .transformation_matrix
626 .multiply(PdfMatrix::new(a, b, c, d, e, f));
627
628 if result.determinant() == 0.0 {
629 Err(PdfiumError::InvalidTransformationMatrix)
630 } else {
631 self.transformation_matrix = result;
632 self.do_render_form_data = false;
633
634 Ok(self)
635 }
636 }
637
638 fn reset_matrix_impl(mut self, matrix: PdfMatrix) -> Result<Self, PdfiumError> {
640 self.transformation_matrix = matrix;
641
642 Ok(self)
643 }
644
645 #[inline]
655 pub fn clip(mut self, left: Pixels, top: Pixels, right: Pixels, bottom: Pixels) -> Self {
656 self.clip_rect = Some((left, top, right, bottom));
657 self.do_render_form_data = false;
658
659 self
660 }
661
662 #[inline]
681 pub fn set_origin(mut self, left: Pixels, top: Pixels) -> Self {
682 self.start_x = left;
683 self.start_y = top;
684
685 self
686 }
687
688 #[inline]
691 pub(crate) fn apply_to_page(&self, page: &PdfPage) -> PdfPageRenderSettings {
692 let source_width = page.width();
693
694 let source_height = page.height();
695
696 let source_orientation =
697 PdfPageOrientation::from_width_and_height(source_width, source_height);
698
699 let (target_rotation, do_rotate_constraints) = if source_orientation == Portrait
702 && self.portrait_rotation != PdfPageRenderRotation::None
703 {
704 (
705 self.portrait_rotation,
706 self.portrait_rotation_do_rotate_constraints,
707 )
708 } else if source_orientation == Landscape
709 && self.landscape_rotation != PdfPageRenderRotation::None
710 {
711 (
712 self.landscape_rotation,
713 self.landscape_rotation_do_rotate_constraints,
714 )
715 } else {
716 (PdfPageRenderRotation::None, false)
717 };
718
719 let (output_width, output_height, width_scale, height_scale) = if self.use_auto_scaling {
720 let width_scale = if let Some(scale) = self.scale_width_factor {
723 Some(scale)
724 } else {
725 self.target_width
726 .map(|target| (target as f32) / source_width.value)
727 };
728
729 let height_scale = if let Some(scale) = self.scale_height_factor {
730 Some(scale)
731 } else {
732 self.target_height
733 .map(|target| (target as f32) / source_height.value)
734 };
735
736 let (do_maintain_aspect_ratio, mut width_scale, mut height_scale) =
739 match (width_scale, height_scale) {
740 (Some(width_scale), Some(height_scale)) => {
741 (width_scale == height_scale, width_scale, height_scale)
742 }
743 (Some(width_scale), None) => (true, width_scale, width_scale),
744 (None, Some(height_scale)) => (true, height_scale, height_scale),
745 (None, None) => {
746 (false, 1.0, 1.0)
749 }
750 };
751
752 let (source_width, source_height, width_constraint, height_constraint) =
755 if do_rotate_constraints {
756 (
757 source_height,
758 source_width,
759 self.maximum_height,
760 self.maximum_width,
761 )
762 } else {
763 (
764 source_width,
765 source_height,
766 self.maximum_width,
767 self.maximum_height,
768 )
769 };
770
771 if let Some(maximum) = width_constraint {
772 let maximum = maximum as f32;
773
774 if source_width.value * width_scale > maximum {
775 width_scale = maximum / source_width.value;
778
779 if do_maintain_aspect_ratio {
780 height_scale = width_scale;
781 }
782 }
783 }
784
785 if let Some(maximum) = height_constraint {
786 let maximum = maximum as f32;
787
788 if source_height.value * height_scale > maximum {
789 height_scale = maximum / source_height.value;
792
793 if do_maintain_aspect_ratio {
794 width_scale = height_scale;
795 }
796 }
797 }
798
799 (
800 (source_width.value * width_scale).round() as c_int,
801 (source_height.value * height_scale).round() as c_int,
802 width_scale,
803 height_scale,
804 )
805 } else {
806 (
809 self.fixed_width.unwrap_or(0) as c_int,
810 self.fixed_height.unwrap_or(0) as c_int,
811 self.scale_width_factor.unwrap_or(1.0),
812 self.scale_height_factor.unwrap_or(1.0),
813 )
814 };
815
816 let mut render_flags = 0;
819
820 if self.do_set_flag_render_annotations {
821 render_flags |= FPDF_ANNOT;
822 }
823
824 if self.do_set_flag_use_lcd_text_rendering {
825 render_flags |= FPDF_LCD_TEXT;
826 }
827
828 if self.do_set_flag_no_native_text {
829 render_flags |= FPDF_NO_NATIVETEXT;
830 }
831
832 if self.do_set_flag_grayscale {
833 render_flags |= FPDF_GRAYSCALE;
834 }
835
836 if self.do_set_flag_render_limited_image_cache {
837 render_flags |= FPDF_RENDER_LIMITEDIMAGECACHE;
838 }
839
840 if self.do_set_flag_render_force_half_tone {
841 render_flags |= FPDF_RENDER_FORCEHALFTONE;
842 }
843
844 if self.do_set_flag_render_for_printing {
845 render_flags |= FPDF_PRINTING;
846 }
847
848 if self.do_set_flag_render_no_smooth_text {
849 render_flags |= FPDF_RENDER_NO_SMOOTHTEXT;
850 }
851
852 if self.do_set_flag_render_no_smooth_image {
853 render_flags |= FPDF_RENDER_NO_SMOOTHIMAGE;
854 }
855
856 if self.do_set_flag_render_no_smooth_path {
857 render_flags |= FPDF_RENDER_NO_SMOOTHPATH;
858 }
859
860 if self.do_set_flag_reverse_byte_order {
861 render_flags |= FPDF_REVERSE_BYTE_ORDER;
862 }
863
864 if self.do_set_flag_convert_fill_to_stroke {
865 render_flags |= FPDF_CONVERT_FILL_TO_STROKE;
866 }
867
868 let transformation_matrix = if !self.do_render_form_data {
875 let result = if target_rotation != PdfPageRenderRotation::None {
876 let (delta_x, delta_y) = match target_rotation {
879 PdfPageRenderRotation::None => unreachable!(),
880 PdfPageRenderRotation::Degrees90 => (PdfPoints::ZERO, -source_width),
881 PdfPageRenderRotation::Degrees180 => (-source_width, -source_height),
882 PdfPageRenderRotation::Degrees270 => (-source_height, PdfPoints::ZERO),
883 };
884
885 self.transformation_matrix
886 .translate(delta_x, delta_y)
887 .and_then(|result| {
888 result.rotate_clockwise_degrees(target_rotation.as_degrees())
889 })
890 } else {
891 Ok(self.transformation_matrix)
892 };
893
894 result.and_then(|result| result.scale(width_scale, height_scale))
895 } else {
896 Ok(PdfMatrix::identity())
897 };
898
899 PdfPageRenderSettings {
900 start_x: self.start_x as c_int,
901 start_y: self.start_y as c_int,
902 width: output_width,
903 height: output_height,
904 format: self.format.as_pdfium() as c_int,
905 rotate: target_rotation.as_pdfium(),
906 do_clear_bitmap_before_rendering: self.do_clear_bitmap_before_rendering,
907 clear_color: self.clear_color.as_pdfium_color(),
908 do_render_form_data: self.do_render_form_data,
909 form_field_highlight: if !self.do_render_form_data
910 || self.form_field_highlight.is_none()
911 {
912 None
913 } else {
914 Some(
915 self.form_field_highlight
916 .as_ref()
917 .unwrap()
918 .iter()
919 .map(|(form_field_type, color)| {
920 (
921 form_field_type.as_pdfium() as c_int,
922 color.as_pdfium_color_with_alpha(),
923 )
924 })
925 .collect::<Vec<_>>(),
926 )
927 },
928 matrix: transformation_matrix
929 .unwrap_or(PdfMatrix::IDENTITY)
930 .as_pdfium(),
931 clipping: if let Some((left, top, right, bottom)) = self.clip_rect {
932 FS_RECTF {
933 left: left as f32,
934 top: top as f32,
935 right: right as f32,
936 bottom: bottom as f32,
937 }
938 } else {
939 FS_RECTF {
940 left: 0.0,
941 top: 0.0,
942 right: output_width as f32,
943 bottom: output_height as f32,
944 }
945 },
946 render_flags: render_flags as c_int,
947 is_reversed_byte_order_flag_set: self.do_set_flag_reverse_byte_order,
948 }
949 }
950}
951
952impl Default for PdfRenderConfig {
953 #[inline]
954 fn default() -> Self {
955 PdfRenderConfig::new()
956 }
957}
958
959#[derive(Debug, Clone)]
962pub(crate) struct PdfPageRenderSettings {
963 pub(crate) start_x: c_int,
964 pub(crate) start_y: c_int,
965 pub(crate) width: c_int,
966 pub(crate) height: c_int,
967 pub(crate) format: c_int,
968 pub(crate) rotate: c_int,
969 pub(crate) do_clear_bitmap_before_rendering: bool,
970 pub(crate) clear_color: FPDF_DWORD,
971 pub(crate) do_render_form_data: bool,
972 pub(crate) form_field_highlight: Option<Vec<(c_int, (FPDF_DWORD, u8))>>,
973 pub(crate) matrix: FS_MATRIX,
974 pub(crate) clipping: FS_RECTF,
975 pub(crate) render_flags: c_int,
976 pub(crate) is_reversed_byte_order_flag_set: bool,
977}
978
979#[cfg(test)]
980mod tests {
981 use crate::prelude::*;
982 use crate::utils::test::test_bind_to_pdfium; #[test]
985 fn test_set_origin_default_settings_zero() -> Result<(), PdfiumError> {
986 let render_settings = get_render_settings_from_config(PdfRenderConfig::new())?;
987
988 assert_eq!(render_settings.start_x, 0);
989 assert_eq!(render_settings.start_y, 0);
990
991 Ok(())
992 }
993
994 #[test]
995 fn test_set_origin_negative_offsets_pass_through() -> Result<(), PdfiumError> {
996 let render_settings =
997 get_render_settings_from_config(PdfRenderConfig::new().set_origin(-50, -400))?;
998
999 assert_eq!(render_settings.start_x, -50);
1000 assert_eq!(render_settings.start_y, -400);
1001
1002 Ok(())
1003 }
1004
1005 #[test]
1006 fn test_set_origin_positive_offsets_pass_through() -> Result<(), PdfiumError> {
1007 let render_settings =
1008 get_render_settings_from_config(PdfRenderConfig::new().set_origin(75, 125))?;
1009
1010 assert_eq!(render_settings.start_x, 75);
1011 assert_eq!(render_settings.start_y, 125);
1012
1013 Ok(())
1014 }
1015
1016 #[test]
1017 fn test_set_origin_strips_stitch_to_full_page() -> Result<(), PdfiumError> {
1018 let pdfium = test_bind_to_pdfium();
1022
1023 let document = pdfium.load_pdf_from_file("./test/image-test.pdf", None)?;
1024 let page = document.pages().first()?;
1025
1026 let target_width: Pixels = 1000;
1030 let target_height: Pixels = 800;
1031 let strips: i32 = 4; let strip_height: Pixels = target_height / strips as Pixels;
1033
1034 assert_eq!(strip_height * strips, target_height);
1035
1036 let full_bitmap = page.render_with_config(
1039 &PdfRenderConfig::new().set_fixed_size(target_width, target_height),
1040 )?;
1041 let full_bytes = full_bitmap.as_image()?.to_rgba8().into_raw();
1042 let row_bytes = (target_width as usize) * 4;
1043
1044 assert_eq!(full_bytes.len(), row_bytes * (target_height as usize));
1045
1046 let mut stitched: Vec<u8> = Vec::with_capacity(full_bytes.len());
1049
1050 for i in 0..strips {
1051 let mut strip_bitmap =
1052 PdfBitmap::empty(target_width, strip_height, full_bitmap.format()?)?;
1053
1054 page.render_into_bitmap_with_config(
1055 &mut strip_bitmap,
1056 &&PdfRenderConfig::new()
1057 .set_fixed_size(target_width, target_height)
1058 .set_origin(0, -(i * strip_height)),
1059 )?;
1060
1061 stitched.extend_from_slice(strip_bitmap.as_image()?.to_rgba8().as_raw());
1064 }
1065
1066 println!(
1069 "{}, {}, {}, {}",
1070 strip_height,
1071 strip_height * strips,
1072 target_height,
1073 row_bytes
1074 );
1075 assert_eq!(stitched.len(), full_bytes.len());
1076
1077 let mut sums = [0u64; 4]; let pixel_count = full_bytes.len() / 4;
1079
1080 for px in 0..pixel_count {
1081 for ch in 0..4 {
1082 let a = stitched[px * 4 + ch] as i32;
1083 let b = full_bytes[px * 4 + ch] as i32;
1084 sums[ch] += (a - b).unsigned_abs() as u64;
1085 }
1086 }
1087
1088 let n = pixel_count as f64;
1089 let max_drift = (0..4).map(|ch| sums[ch] as f64 / n).fold(0.0_f64, f64::max);
1090
1091 assert!(
1092 max_drift < 5.0,
1093 "stitched-vs-full per-channel mean drift {:.3}/255 exceeds tolerance",
1094 max_drift
1095 );
1096
1097 Ok(())
1098 }
1099
1100 #[test]
1101 fn test_fixed_size_render_config() -> Result<(), PdfiumError> {
1102 let render_settings =
1103 get_render_settings_from_config(PdfRenderConfig::new().set_fixed_size(2000, 2000))?;
1104
1105 assert_eq!(render_settings.width, 2000);
1106 assert_eq!(render_settings.height, 2000);
1107
1108 let render_settings = get_render_settings_from_config(
1111 PdfRenderConfig::new()
1112 .set_fixed_size(2000, 2000)
1113 .scale_page_by_factor(5.0),
1114 )?;
1115
1116 assert_eq!(render_settings.width, 2000);
1117 assert_eq!(render_settings.height, 2000);
1118
1119 Ok(())
1120 }
1121
1122 #[test]
1123 fn test_target_size_render_config() -> Result<(), PdfiumError> {
1124 let render_settings = get_render_settings_from_config(
1125 PdfRenderConfig::new().scale_page_to_display_size(2000, 2000),
1126 )?;
1127
1128 assert_eq!(render_settings.width, 1414);
1129 assert_eq!(render_settings.height, 2000);
1130
1131 let render_settings = get_render_settings_from_config(
1134 PdfRenderConfig::new()
1135 .set_target_size(2000, 2000)
1136 .scale_page_by_factor(5.0),
1137 )?;
1138
1139 assert_eq!(render_settings.width, 2976);
1140 assert_eq!(render_settings.height, 4209);
1141
1142 Ok(())
1143 }
1144
1145 fn get_render_settings_from_config(
1146 config: PdfRenderConfig,
1147 ) -> Result<PdfPageRenderSettings, PdfiumError> {
1148 let pdfium = test_bind_to_pdfium();
1149
1150 let mut document = pdfium.create_new_pdf()?;
1151 let page = document
1152 .pages_mut()
1153 .create_page_at_start(PdfPagePaperSize::Portrait(PdfPagePaperStandardSize::A4))?;
1154
1155 Ok(config.apply_to_page(&page))
1156 }
1157
1158 fn mean_abs_diff(a: &[u8], b: &[u8]) -> f64 {
1162 if a.len() != b.len() {
1163 return f64::INFINITY;
1164 }
1165
1166 let sum: u64 = a
1167 .iter()
1168 .zip(b)
1169 .map(|(x, y)| (*x as i64 - *y as i64).unsigned_abs())
1170 .sum();
1171
1172 sum as f64 / a.len() as f64
1173 }
1174
1175 fn create_asymmetric_a4(pdfium: &Pdfium) -> Result<PdfDocument<'_>, PdfiumError> {
1179 let mut document = pdfium.create_new_pdf()?;
1180
1181 {
1182 let mut page = document
1183 .pages_mut()
1184 .create_page_at_start(PdfPagePaperSize::Portrait(PdfPagePaperStandardSize::A4))?;
1185
1186 page.objects_mut().create_path_object_rect(
1187 PdfRect::new(
1188 PdfPoints::new(640.0),
1189 PdfPoints::new(60.0),
1190 PdfPoints::new(800.0),
1191 PdfPoints::new(300.0),
1192 ),
1193 None,
1194 None,
1195 Some(PdfColor::RED),
1196 )?;
1197
1198 page.objects_mut().create_path_object_rect(
1199 PdfRect::new(
1200 PdfPoints::new(40.0),
1201 PdfPoints::new(400.0),
1202 PdfPoints::new(120.0),
1203 PdfPoints::new(560.0),
1204 ),
1205 None,
1206 None,
1207 Some(PdfColor::new(0, 0, 255, 255)),
1208 )?;
1209 }
1210
1211 Ok(document)
1212 }
1213
1214 #[test]
1221 fn test_matrix_path_matches_form_path_for_each_intrinsic_rotation() -> Result<(), PdfiumError> {
1222 let pdfium = test_bind_to_pdfium();
1223 let mut document = create_asymmetric_a4(&pdfium)?;
1224 let mut page = document.pages_mut().first()?;
1225
1226 for rotation in [
1227 PdfPageRenderRotation::None,
1228 PdfPageRenderRotation::Degrees90,
1229 PdfPageRenderRotation::Degrees180,
1230 PdfPageRenderRotation::Degrees270,
1231 ] {
1232 page.set_rotation(rotation);
1233
1234 let width = page.width().value.round() as Pixels;
1235 let height = page.height().value.round() as Pixels;
1236
1237 let form = page
1239 .render_with_config(&PdfRenderConfig::new().set_target_size(width, height))?
1240 .as_image()?
1241 .to_rgba8()
1242 .into_raw();
1243
1244 let matrix = page
1246 .render_with_config(
1247 &PdfRenderConfig::new()
1248 .set_target_size(width, height)
1249 .render_form_data(false),
1250 )?
1251 .as_image()?
1252 .to_rgba8()
1253 .into_raw();
1254
1255 let drift = mean_abs_diff(&matrix, &form);
1256 assert!(
1257 drift < 1.0,
1258 "{rotation:?}: matrix path diverged from the form-data path ({drift:.3}/255), \
1259 so the matrix path is not applying /Rotate the way this test assumes",
1260 );
1261
1262 let control = page
1265 .render_with_config(
1266 &PdfRenderConfig::new()
1267 .set_fixed_size(width, height)
1268 .render_form_data(false)
1269 .scale_page_by_factor(0.5),
1270 )?
1271 .as_image()?
1272 .to_rgba8()
1273 .into_raw();
1274
1275 assert!(
1276 mean_abs_diff(&control, &form) > 5.0,
1277 "{rotation:?}: negative control did not diverge, the comparison is not discriminating",
1278 );
1279 }
1280
1281 Ok(())
1282 }
1283
1284 #[test]
1291 fn test_matrix_path_strip_stitch_matches_full_render() -> Result<(), PdfiumError> {
1292 let pdfium = test_bind_to_pdfium();
1293 let mut document = create_asymmetric_a4(&pdfium)?;
1294 let mut page = document.pages_mut().first()?;
1295 page.set_rotation(PdfPageRenderRotation::Degrees90);
1296
1297 let width = page.width().value.round() as Pixels;
1298 let height = page.height().value.round() as Pixels;
1299
1300 let full = page
1301 .render_with_config(&PdfRenderConfig::new().set_target_size(width, height))?
1302 .as_image()?
1303 .to_rgba8()
1304 .into_raw();
1305
1306 let strips: Pixels = 5;
1307 let mut stitched: Vec<u8> = Vec::with_capacity(full.len());
1308 let mut y_offset: Pixels = 0;
1309
1310 for i in 0..strips {
1311 let strip_height = if i == strips - 1 {
1313 height - y_offset
1314 } else {
1315 height / strips
1316 };
1317
1318 let strip = page
1319 .render_with_config(
1320 &PdfRenderConfig::new()
1321 .set_fixed_size(width, strip_height)
1322 .clip(0, 0, width, strip_height)
1323 .render_form_data(false)
1324 .reset_matrix(PdfMatrix::new(
1325 1.0,
1326 0.0,
1327 0.0,
1328 1.0,
1329 0.0,
1330 -(y_offset as f32),
1331 ))?,
1332 )?
1333 .as_image()?
1334 .to_rgba8()
1335 .into_raw();
1336
1337 stitched.extend_from_slice(&strip);
1338 y_offset += strip_height;
1339 }
1340
1341 assert_eq!(
1342 stitched.len(),
1343 full.len(),
1344 "stitched strips must cover the full page",
1345 );
1346
1347 let drift = mean_abs_diff(&stitched, &full);
1348 assert!(
1349 drift < 1.0,
1350 "matrix-path strips did not stitch to the full render ({drift:.3}/255)",
1351 );
1352
1353 Ok(())
1354 }
1355}