Skip to main content

pdfium_render/pdf/document/page/
render_config.rs

1//! Defines the [PdfRenderConfig] struct, a builder-based approach to configuring
2//! the rendering of [PdfBitmap] objects from one or more [PdfPage] objects.
3
4use 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
21/// Configures the scaling, rotation, and rendering settings that should be applied to
22/// a [PdfPage] to create a [PdfBitmap] for that page. [PdfRenderConfig] can accommodate pages of
23/// different sizes while correctly maintaining each page's aspect ratio, automatically
24/// rotate portrait or landscape pages, generate page thumbnails, apply maximum pixel size
25/// constraints to the scaled width and height of the final rendering, highlight form fields
26/// with different colors, apply custom transforms to the page during rendering, and set
27/// internal Pdfium rendering flags.
28///
29/// Pdfium's rendering pipeline supports _either_ rendering with form data _or_ rendering with
30/// a custom transformation matrix, but not both at the same time. Applying any transformation
31/// automatically disables rendering of form data. If you must render form data while simultaneously
32/// applying transformations, consider using the [PdfPage::flatten()] function to flatten the
33/// form elements and form data into the containing page.
34pub 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    // The fields below set Pdfium's page rendering flags. Coverage for the
59    // FPDF_DEBUG_INFO and FPDF_NO_CATCH flags is omitted since they are obsolete.
60    do_set_flag_render_annotations: bool,     // Sets FPDF_ANNOT
61    do_set_flag_use_lcd_text_rendering: bool, // Sets FPDF_LCD_TEXT
62    do_set_flag_no_native_text: bool,         // Sets FPDF_NO_NATIVETEXT
63    do_set_flag_grayscale: bool,              // Sets FPDF_GRAYSCALE
64    do_set_flag_render_limited_image_cache: bool, // Sets FPDF_RENDER_LIMITEDIMAGECACHE
65    do_set_flag_render_force_half_tone: bool, // Sets FPDF_RENDER_FORCEHALFTONE
66    do_set_flag_render_for_printing: bool,    // Sets FPDF_PRINTING
67    do_set_flag_render_no_smooth_text: bool,  // Sets FPDF_RENDER_NO_SMOOTHTEXT
68    do_set_flag_render_no_smooth_image: bool, // Sets FPDF_RENDER_NO_SMOOTHIMAGE
69    do_set_flag_render_no_smooth_path: bool,  // Sets FPDF_RENDER_NO_SMOOTHPATH
70    do_set_flag_reverse_byte_order: bool,     // Sets FPDF_REVERSE_BYTE_ORDER
71    do_set_flag_convert_fill_to_stroke: bool, // Sets FPDF_CONVERT_FILL_TO_STROKE
72}
73
74impl PdfRenderConfig {
75    /// Creates a new [PdfRenderConfig] object with all settings initialized to their default values.
76    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            // We ask Pdfium to reverse its bitmap byte order from BGR8 to RGB8 in order
113            // to make working with Image::DynamicImage easier after version 0.24. See:
114            // https://github.com/ajrcarey/pdfium-render/issues/9
115            do_set_flag_reverse_byte_order: true,
116        }
117    }
118
119    /// Applies settings suitable for generating a thumbnail.
120    ///
121    /// * The source [PdfPage] will be rendered with a maximum width and height of the given
122    ///   pixel size.
123    /// * The page will not be rotated, irrespective of its orientation.
124    /// * Image quality settings will be reduced to improve performance.
125    /// * Annotations and user-filled form field data will not be rendered.
126    ///
127    /// These settings are applied to this [PdfRenderConfig] object immediately and can be
128    /// selectively overridden by later function calls. For instance, a later call to
129    /// [PdfRenderConfig::rotate()] can specify a custom rotation setting that will apply
130    /// to the thumbnail.
131    #[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    /// Sets the desired pixel width and height of a rendered [PdfPage] to the
144    /// width and height of the given [PdfBitmap]. No attempt will be made to scale or adjust
145    /// the aspect ratio to match the source page. Overrides any previous call to
146    /// [PdfRenderConfig::set_target_size], [PdfRenderConfig::set_target_width], or
147    /// [PdfRenderConfig::set_target_height].
148    #[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    /// Sets the desired pixel width and height of a rendered [PdfPage] to the given
154    /// pixel values. No attempt will be made to scale or adjust the aspect ratio to
155    /// match the source page. Overrides any previous call to [PdfRenderConfig::set_target_size],
156    /// [PdfRenderConfig::set_target_width], or [PdfRenderConfig::set_target_height].
157    #[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    /// Sets the desired pixel width of a rendered [PdfPage] to the given value. Overrides
163    /// any previous call to [PdfRenderConfig::set_target_size] or [PdfRenderConfig::set_target_width].
164    #[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    /// Sets the desired pixel height of a rendered [PdfPage] to the given value. Overrides
173    /// any previous call to [PdfRenderConfig::set_target_size] or [PdfRenderConfig::set_target_height].
174    #[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    /// Converts the width and height of a [PdfPage] from points to pixels, scaling each
183    /// dimension to the given target pixel sizes. The aspect ratio of the source page
184    /// will not be maintained. Overrides any previous call to [PdfRenderConfig::set_fixed_width()]
185    /// or [PdfRenderConfig::set_fixed_height()].
186    #[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    /// Converts the width of a [PdfPage] from points to pixels, scaling the source page
192    /// width to the given target pixel width. The aspect ratio of the source page
193    /// will be maintained so long as there is no call to [PdfRenderConfig::set_target_size()]
194    /// or [PdfRenderConfig::set_target_height()] that overrides it. Overrides any previous
195    /// call to [PdfRenderConfig::set_fixed_width()] or [PdfRenderConfig::set_fixed_height()].
196    #[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    /// Converts the height of a [PdfPage] from points to pixels, scaling the source page
205    /// height to the given target pixel height. The aspect ratio of the source page
206    /// will be maintained so long as there is no call to [PdfRenderConfig::set_target_size()]
207    /// or [PdfRenderConfig::set_target_width()] that overrides it. Overrides any previous
208    /// call to [PdfRenderConfig::set_fixed_width()] or [PdfRenderConfig::set_fixed_height()].
209    #[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    /// Applies settings to this [PdfRenderConfig] suitable for filling the given [PdfBitmap].
218    ///
219    /// The source page's dimensions will be scaled so that both width and height attempt
220    /// to fill, but do not exceed, the pixel dimensions of the bitmap. The aspect ratio
221    /// of the source page will be maintained. Landscape pages will be automatically rotated
222    /// by 90 degrees and will be scaled down if necessary to fit the bitmap width.
223    #[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    /// Applies settings to this [PdfRenderConfig] suitable for filling the given
229    /// screen display size.
230    ///
231    /// The source page's dimensions will be scaled so that both width and height attempt
232    /// to fill, but do not exceed, the given pixel dimensions. The aspect ratio of the
233    /// source page will be maintained. Landscape pages will be automatically rotated
234    /// by 90 degrees and will be scaled down if necessary to fit the display width.
235    #[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    /// Converts the width and height of a [PdfPage] from points to pixels by applying
247    /// the given scale factor to both dimensions. The aspect ratio of the source page
248    /// will be maintained. Overrides any previous call to [PdfRenderConfig::scale_page_by_factor()],
249    /// [PdfRenderConfig::scale_page_width_by_factor()], or [PdfRenderConfig::scale_page_height_by_factor()].
250    #[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    /// Converts the width of the [PdfPage] from points to pixels by applying the given
258    /// scale factor. The aspect ratio of the source page will not be maintained if a
259    /// different scale factor is applied to the height. Overrides any previous call to
260    /// [PdfRenderConfig::scale_page_by_factor()], [PdfRenderConfig::scale_page_width_by_factor()],
261    /// or [PdfRenderConfig::scale_page_height_by_factor()].
262    #[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    /// Converts the height of the [PdfPage] from points to pixels by applying the given
270    /// scale factor. The aspect ratio of the source page will not be maintained if a
271    /// different scale factor is applied to the width. Overrides any previous call to
272    /// [PdfRenderConfig::scale_page_by_factor()], [PdfRenderConfig::scale_page_width_by_factor()],
273    /// or [PdfRenderConfig::scale_page_height_by_factor()].
274    #[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    /// Specifies that the final pixel width of the [PdfPage] will not exceed the given maximum.
282    #[inline]
283    pub fn set_maximum_width(mut self, width: Pixels) -> Self {
284        self.maximum_width = Some(width);
285
286        self
287    }
288
289    /// Specifies that the final pixel height of the [PdfPage] will not exceed the given maximum.
290    #[inline]
291    pub fn set_maximum_height(mut self, height: Pixels) -> Self {
292        self.maximum_height = Some(height);
293
294        self
295    }
296
297    /// Applies the given clockwise rotation setting to the [PdfPage] during rendering, irrespective
298    /// of its orientation. If the given flag is set to `true` then any maximum
299    /// constraint on the final pixel width set by a call to [PdfRenderConfig::set_maximum_width()]
300    /// will be rotated so it becomes a constraint on the final pixel height, and any
301    /// maximum constraint on the final pixel height set by a call to [PdfRenderConfig::set_maximum_height()]
302    /// will be rotated so it becomes a constraint on the final pixel width.
303    #[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    /// Applies the given clockwise rotation settings to the [PdfPage] during rendering, if the page
310    /// is in portrait orientation. If the given flag is set to `true` and the given
311    /// rotation setting is [PdfPageRenderRotation::Degrees90] or [PdfPageRenderRotation::Degrees270]
312    /// then any maximum constraint on the final pixel width set by a call to [PdfRenderConfig::set_maximum_width()]
313    /// will be rotated so it becomes a constraint on the final pixel height and any
314    /// maximum constraint on the final pixel height set by a call to [PdfRenderConfig::set_maximum_height()]
315    /// will be rotated so it becomes a constraint on the final pixel width.
316    #[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    /// Applies the given rotation settings to the [PdfPage] during rendering, if the page
334    /// is in landscape orientation. If the given flag is set to `true` and the given
335    /// rotation setting is [PdfPageRenderRotation::Degrees90] or [PdfPageRenderRotation::Degrees270]
336    /// then any maximum constraint on the final pixel width set by a call to [PdfRenderConfig::set_maximum_width()]
337    /// will be rotated so it becomes a constraint on the final pixel height and any
338    /// maximum constraint on the final pixel height set by a call to [PdfRenderConfig::set_maximum_height()]
339    /// will be rotated so it becomes a constraint on the final pixel width.
340    #[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    /// Sets the pixel format that will be used during rendering of the [PdfPage].
358    /// The default is [PdfBitmapFormat::BGRA].
359    #[inline]
360    pub fn set_format(mut self, format: PdfBitmapFormat) -> Self {
361        self.format = format;
362
363        self
364    }
365
366    /// Controls whether the destination bitmap should be cleared by setting every pixel to a
367    /// known color value before rendering the [PdfPage]. The default is `true`.
368    /// The color used during clearing can be customised by calling [PdfRenderConfig::set_clear_color()].
369    #[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    /// Sets the color applied to every pixel in the destination bitmap when clearing the bitmap
377    /// before rendering the [PdfPage]. The default is [PdfColor::WHITE]. This setting
378    /// has no effect if [PdfRenderConfig::clear_before_rendering()] is set to `false`.
379    #[inline]
380    pub fn set_clear_color(mut self, color: PdfColor) -> Self {
381        self.clear_color = color;
382
383        self
384    }
385
386    /// Controls whether form data widgets and user-supplied form data should be included
387    /// during rendering of the [PdfPage]. The default is `true`.
388    ///
389    /// Pdfium's rendering pipeline supports _either_ rendering with form data _or_ rendering with
390    /// a custom transformation matrix, but not both at the same time. Applying any transformation
391    /// automatically sets this value to `false`, disabling rendering of form data.
392    #[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    /// Controls whether user-supplied annotations should be included during rendering of
400    /// the [PdfPage]. The default is `true`.
401    #[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    /// Controls whether text rendering should be optimized for LCD display.
409    /// The default is `false`.
410    /// Has no effect if anti-aliasing of text has been disabled by a call to
411    /// `PdfRenderConfig::set_text_smoothing(false)`.
412    #[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    /// Controls whether platform text rendering should be disabled on platforms that support it.
420    /// The alternative is for Pdfium to render all text internally, which may give more
421    /// consistent rendering results across platforms but may also be slower.
422    /// The default is `false`.
423    #[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    /// Controls whether rendering output should be grayscale rather than full color.
431    /// The default is `false`.
432    #[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    /// Controls whether Pdfium should limit its image cache size during rendering.
440    /// A smaller cache size may result in lower memory usage at the cost of slower rendering.
441    /// The default is `false`.
442    #[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    /// Controls whether Pdfium should always use halftone for image stretching.
450    /// Halftone image stretching is often higher quality than linear image stretching
451    /// but is much slower. The default is `false`.
452    #[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    /// Controls whether Pdfium should render for printing. The default is `false`.
460    ///
461    /// Certain PDF files may stipulate different quality settings for on-screen display
462    /// compared to printing. For these files, changing this setting to `true` will result
463    /// in a higher quality rendered bitmap but slower performance. For PDF files that do
464    /// not stipulate different quality settings, changing this setting will have no effect.
465    #[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    /// Controls whether rendered text should be anti-aliased.
473    /// The default is `true`.
474    /// The enabling of LCD-optimized text rendering via a call to
475    /// `PdfiumBitmapConfig::use_lcd_text_rendering(true)` has no effect if this flag
476    /// is set to `false`.
477    #[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    /// Controls whether rendered images should be anti-aliased.
485    /// The default is `true`.
486    #[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    /// Controls whether rendered vector paths should be anti-aliased.
494    /// The default is `true`.
495    #[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    /// Controls whether the byte order of generated image data should be reversed
503    /// during rendering. The default is `true`, so that Pdfium returns pixel data as
504    /// four-channel RGBA rather than its default of four-channel BGRA.
505    ///
506    /// There should generally be no need to change this flag unless you want to do raw
507    /// image processing and specifically need the pixel data returned by the
508    /// [PdfBitmap::as_raw_bytes()] function to be in BGR8 format.
509    #[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    /// Controls whether rendered vector fill paths need to be stroked.
517    /// The default is `false`.
518    #[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    /// Highlights all rendered form fields with the given color.
526    /// Note that specifying a solid color with no opacity will overprint any user data in the field.
527    #[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    /// Highlights all rendered push button form fields with the given color.
533    /// Note that specifying a solid color with no opacity will overprint any user data in the field.
534    #[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    /// Highlights all rendered checkbox form fields with the given color.
540    /// Note that specifying a solid color with no opacity will overprint any user data in the field.
541    #[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    /// Highlights all rendered radio button form fields with the given color.
547    /// Note that specifying a solid color with no opacity will overprint any user data in the field.
548    #[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    /// Highlights all rendered combobox form fields with the given color.
554    /// Note that specifying a solid color with no opacity will overprint any user data in the field.
555    #[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    /// Highlights all rendered listbox form fields with the given color.
561    /// Note that specifying a solid color with no opacity will overprint any user data in the field.
562    #[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    /// Highlights all rendered text entry form fields with the given color.
568    /// Note that specifying a solid color with no opacity will overprint any user data in the field.
569    #[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    /// Highlights all rendered signature form fields with the given color.
575    /// Note that specifying a solid color with no opacity will overprint any user data in the field.
576    #[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    /// Highlights all rendered form fields matching the given type with the given color.
582    /// Note that specifying a solid color with no opacity will overprint any user data in the field.
583    #[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    // The internal implementation of the transform() function used by the create_transform_setters!() macro.
615    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    // The internal implementation of the reset_matrix() function used by the create_transform_setters!() macro.
639    fn reset_matrix_impl(mut self, matrix: PdfMatrix) -> Result<Self, PdfiumError> {
640        self.transformation_matrix = matrix;
641
642        Ok(self)
643    }
644
645    /// Clips rendering output to the given pixel coordinates. Pdfium will not render outside
646    /// the clipping area; any existing image data in the destination [PdfBitmap] will remain
647    /// intact.
648    ///
649    /// Pdfium's rendering pipeline supports _either_ rendering with form data _or_ clipping rendering
650    /// output, but not both at the same time. Applying a clipping rectangle automatically disables
651    /// rendering of form data. If you must render form data while simultaneously applying a
652    /// clipping rectangle, consider using the [PdfPage::flatten()] function to flatten the
653    /// form elements and form data into the containing page.
654    #[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    /// Sets the position of the page's top-left corner within the destination [PdfBitmap],
663    /// in bitmap pixel coordinates.
664    ///
665    /// The default is `(0, 0)`, which renders the page's top-left corner at the bitmap's
666    /// top-left corner. Negative offsets push the page off the bitmap's top-left edge;
667    /// positive offsets push it down and right. The rendered page is clipped to the
668    /// destination bitmap's own width and height, so a strip-sized destination bitmap
669    /// combined with a negative `y` offset can be used to render a horizontal strip from
670    /// a large page without having to render the page in its entirety.
671    ///
672    /// To render rows `[y_offset, y_offset + strip_height)` of a page into a strip-sized
673    /// destination bitmap, call `set_origin(0, -y_offset)` and pass a destination bitmap
674    /// with a height of `strip_height`.
675    ///
676    /// Pdfium's rendering pipeline _either_ rendering with form data _or_ rendering with
677    /// a custom transformation matrix, but not both at the same time. Since `set_origin()`
678    /// affects rendering with form data, it is disabled automatically on applying any
679    /// transformation.
680    #[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    /// Computes the pixel dimensions and rotation settings for the given [PdfPage]
689    /// based on the configuration of this [PdfRenderConfig].
690    #[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        // Do we need to apply any rotation?
700
701        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            // Compute output width and height based on target sizes and page dimensions.
721
722            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            // Maintain source aspect ratio if only one dimension's scale is set.
737
738            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                        // Set default scale to 1.0 if neither dimension is specified.
747
748                        (false, 1.0, 1.0)
749                    }
750                };
751
752            // Apply constraints on maximum width and height, if any.
753
754            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                    // Constrain the width, so it does not exceed the maximum.
776
777                    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                    // Constrain the height, so it does not exceed the maximum.
790
791                    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            // Take output width and height directly from user's fixed settings.
807
808            (
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        // Compose render flags.
817
818        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        // Pages can be rendered either _with_ transformation matrices and clipping
869        // but _without_ form data, or _with_ form data but _without_ transformation matrices
870        // and clipping. We need to be prepared for either option. If rendering of form data
871        // is disabled, then the scaled output width and height and any user-specified
872        // 90-degree rotation need to be applied to the transformation matrix now.
873
874        let transformation_matrix = if !self.do_render_form_data {
875            let result = if target_rotation != PdfPageRenderRotation::None {
876                // Translate the origin to the center of the page before rotating.
877
878                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/// Finalized rendering settings, ready to be passed to a Pdfium rendering function.
960/// Generated by calling [PdfRenderConfig::apply_to_page()].
961#[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; // Temporary until PdfParagraph is included in the prelude.
983
984    #[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        // Compare a full page render of a sample page against a render assembled
1019        // from multiple stripped renders. The results should be identical.
1020
1021        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        // `image-test.pdf` is single-page A4: 595 × 842 points at 72 DPI outputs to
1027        // 595 × 842 pixels when rendered with `set_target_size(595, 842)`.
1028
1029        let target_width: Pixels = 1000;
1030        let target_height: Pixels = 800;
1031        let strips: i32 = 4; // The number of render passes, with each pass outputting a single strip
1032        let strip_height: Pixels = target_height / strips as Pixels;
1033
1034        assert_eq!(strip_height * strips, target_height);
1035
1036        // First, create a full page render of the target page.
1037
1038        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        // Next, render the page in strips...
1047
1048        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            // ... joining each strip in memory to build a complete rendered image.
1062
1063            stitched.extend_from_slice(strip_bitmap.as_image()?.to_rgba8().as_raw());
1064        }
1065
1066        // The render output assembled from the strips should exactly match the full page render.
1067
1068        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]; // Track per-channel mean drift between stitched and full-page renders.
1078        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        // Applying scaling does not affect the rendered bitmap size.
1109
1110        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        // Applying scaling does affected the rendered bitmap size.
1132
1133        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    /// Per-pixel mean absolute difference across all RGBA channels. Returns
1159    /// infinity on a length mismatch so a dimension difference fails an
1160    /// equality check and passes a divergence check.
1161    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    /// Builds a single-page A4 document with two asymmetric filled rectangles,
1176    /// so that orientation is observable. A blank page would compare equal under
1177    /// any rotation, which is why content is required.
1178    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    /// `FPDF_RenderPageBitmapWithMatrix` composes the caller matrix on top of
1215    /// pdfium's display transform, which already applies the page's intrinsic
1216    /// `/Rotate`. So the matrix render path with an identity matrix must produce
1217    /// byte-identical output to the form-data path, which also applies `/Rotate`,
1218    /// for every `/Rotate` value. A caller that mistakenly believed the matrix
1219    /// path ignores `/Rotate` and pre-composed a rotation would fail this test.
1220    #[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            // Form-data path: applies `/Rotate` automatically.
1238            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            // Matrix path with an identity matrix.
1245            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            // Negative control: a half-scale matrix-path render must differ, so a
1263            // zero drift above can only mean genuine agreement, not two blanks.
1264            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    /// Renders a `/Rotate 90` page as horizontal strips through the matrix path,
1285    /// using the device-space strip matrix `[s, 0, 0, s, 0, -y_offset]`, and
1286    /// stitches them. The result must match the full form-data render. If a
1287    /// caller baked a rotation into the strip matrix, the strips would not stitch
1288    /// to the correctly-rotated full render. `reset_matrix()` does not disable
1289    /// form data on its own, so `render_form_data(false)` is required.
1290    #[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            // The last strip absorbs any remainder so the strips cover the page.
1312            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}