pdfium 0.10.3

Modern Rust interface to PDFium, the PDF library from Google
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
// PDFium-rs -- Modern Rust interface to PDFium, the PDF library from Google
//
// Copyright (c) 2025-2026 Martin van der Werff <github (at) newinnovations.nl>
//
// This file is part of PDFium-rs.
//
// PDFium-rs is free software: you can redistribute it and/or modify it under the terms of
// the GNU General Public License as published by the Free Software Foundation, either version 3
// of the License, or (at your option) any later version.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

use crate::{
    PdfiumBitmap, PdfiumBitmapFormat, PdfiumColor, PdfiumError, PdfiumMatrix, PdfiumPage,
    PdfiumRect, PdfiumResult, PdfiumRotation, lib, pdfium_constants,
    pdfium_types::{FS_MATRIX, FS_RECTF},
};

use bitflags::bitflags;

bitflags! {
    /// Flags controlling the PDFium rendering behavior.
    ///
    /// These flags can be combined using bitwise OR operations to customize
    /// the rendering process for different use cases such as printing,
    /// debugging, or optimizing for specific display types.
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
    pub struct PdfiumRenderFlags: i32 {
        /// ANNOT: Render annotations (comments, highlights, form fields, etc.)
        /// Enable this to include interactive elements in the rendered output.
        const ANNOT = pdfium_constants::FPDF_ANNOT;

        /// LCD_TEXT: Use LCD text rendering for better clarity on LCD screens.
        /// This uses subpixel rendering to improve text sharpness on LCD displays.
        const LCD_TEXT = pdfium_constants::FPDF_LCD_TEXT;

        /// NO_NATIVETEXT: Disable native text rendering optimizations.
        /// Forces PDFium to render text as paths instead of using system fonts.
        const NO_NATIVETEXT = pdfium_constants::FPDF_NO_NATIVETEXT;

        /// GRAYSCALE: Render in grayscale mode.
        /// Converts all colors to grayscale values, useful for black & white printing.
        const GRAYSCALE = pdfium_constants::FPDF_GRAYSCALE;

        /// REVERSE_BYTE_ORDER: Reverse byte order for pixel data.
        /// Changes from BGRA to RGBA or vice versa, depending on platform requirements.
        const REVERSE_BYTE_ORDER = pdfium_constants::FPDF_REVERSE_BYTE_ORDER;

        /// CONVERT_FILL_TO_STROKE: Convert filled paths to stroked paths.
        /// Useful for creating outlines or wireframe representations.
        const CONVERT_FILL_TO_STROKE = pdfium_constants::FPDF_CONVERT_FILL_TO_STROKE;

        /// DEBUG_INFO: Include debug information in the rendering process.
        /// May affect performance but provides additional diagnostic information.
        const DEBUG_INFO = pdfium_constants::FPDF_DEBUG_INFO;

        /// NO_CATCH: Don't catch exceptions during rendering.
        /// Allows exceptions to propagate for debugging purposes - use with caution.
        const NO_CATCH = pdfium_constants::FPDF_NO_CATCH;

        /// RENDER_LIMITEDIMAGECACHE: Limit image cache size during rendering.
        /// Reduces memory usage at the cost of potential performance impact.
        const RENDER_LIMITEDIMAGECACHE = pdfium_constants::FPDF_RENDER_LIMITEDIMAGECACHE;

        /// RENDER_FORCEHALFTONE: Force halftone rendering for images.
        /// Applies halftone patterns, typically used for print output optimization.
        const RENDER_FORCEHALFTONE = pdfium_constants::FPDF_RENDER_FORCEHALFTONE;

        /// PRINTING: Optimize rendering for printing output.
        /// Applies print-specific optimizations and color handling.
        const PRINTING = pdfium_constants::FPDF_PRINTING;

        /// RENDER_NO_SMOOTHTEXT: Disable anti-aliasing for text rendering.
        /// Results in pixelated but potentially faster text rendering.
        const RENDER_NO_SMOOTHTEXT = pdfium_constants::FPDF_RENDER_NO_SMOOTHTEXT;

        /// RENDER_NO_SMOOTHIMAGE: Disable anti-aliasing for image rendering.
        /// Images will appear pixelated but render faster with less memory usage.
        const RENDER_NO_SMOOTHIMAGE = pdfium_constants::FPDF_RENDER_NO_SMOOTHIMAGE;

        /// RENDER_NO_SMOOTHPATH: Disable anti-aliasing for vector path rendering.
        /// Paths and shapes will have jagged edges but render more quickly.
        const RENDER_NO_SMOOTHPATH = pdfium_constants::FPDF_RENDER_NO_SMOOTHPATH;
    }
}

impl PdfiumRenderFlags {
    /// Returns flags optimized for fast rendering with minimal quality.
    /// Disables anti-aliasing and other quality features for maximum speed.
    pub fn fast_rendering() -> Self {
        Self::RENDER_NO_SMOOTHTEXT | Self::RENDER_NO_SMOOTHIMAGE | Self::RENDER_NO_SMOOTHPATH
    }

    /// Returns flags for debugging purposes.
    /// Includes debug information and disables exception catching.
    pub fn debug() -> Self {
        Self::DEBUG_INFO | Self::NO_CATCH
    }
}

/// Configuration for PDF page rendering operations.
///
/// Controls how PDF pages are rendered to bitmaps, including dimensions,
/// appearance, image format and performance characteristics.
///
/// ## Parameter Rules
///
/// The configuration handles dimensions and scaling in two distinct modes:
///
/// ### Auto-scaling mode (width OR height specified)
/// - Provide only `width` or only `height`
/// - The missing dimension is calculated automatically using the page's aspect ratio
///   and taking into account and if specied the rotation
/// - Scaling is determined automatically to fully fit the page into the bitmap
/// - **Error**: Do not provide a `scale` or `matrix` in this mode
///
/// ### Manual scaling mode (both width AND height specified)
/// - Provide both `width` and `height`
/// - You must also provide either `scale` OR `matrix`
/// - **Error**: Providing neither scaling instructions will cause an error
///
/// ## PDFium Integration
///
/// All parameters are passed directly to PDFium, except for the automatic
/// dimension and scaling calculations described above.
///
/// # Examples
///
/// ```rust
/// use pdfium::*;
///
/// // Auto-scaling at specified width (height calculated automatically), grayscale,
/// // and reset the rendering flags (default is ANNOT and LCD_TEXT)
/// let config = PdfiumRenderConfig::new()
///     .with_width(800)
///     .with_format(PdfiumBitmapFormat::Gray)
///     .with_flags(PdfiumRenderFlags::empty());
///
/// // Manual scaling and panning
/// let config = PdfiumRenderConfig::new()
///     .with_size(1920, 1080)
///     .with_scale(2.5)
///     .with_pan(900.0, -200.0);
/// ```
#[derive(Debug, Clone)]
pub struct PdfiumRenderConfig {
    /// Target width in pixels. If None, calculated from height maintaining aspect ratio.
    width: Option<i32>,
    /// Target height in pixels. If None, calculated from width maintaining aspect ratio.
    height: Option<i32>,
    /// The pixel format for the rendered bitmap (BGRA, RGB, etc.).
    format: PdfiumBitmapFormat,
    /// Background color for the bitmap. If None, background is transparent.
    background: Option<PdfiumColor>,
    /// Bitflags controlling various rendering behaviors and optimizations.
    flags: PdfiumRenderFlags,
    /// Scaling factor.
    scale: Option<f32>,
    /// Translation offset (pan_x, pan_y) in bitmap coordinates.
    pan: Option<(f32, f32)>,
    /// Rotation in degrees
    rotation: PdfiumRotation,
    /// Custom transformation matrix. Cannot be combined with scale, pan or rotation.
    matrix: Option<PdfiumMatrix>,
    /// Clipping rectangle to restrict rendering to a specific area of the bitmap.
    clipping: Option<PdfiumRect>,
}

impl Default for PdfiumRenderConfig {
    fn default() -> Self {
        Self {
            width: None,
            height: None,
            format: PdfiumBitmapFormat::Bgra,
            background: Some(PdfiumColor::WHITE),
            flags: PdfiumRenderFlags::ANNOT | PdfiumRenderFlags::LCD_TEXT,
            scale: None,
            pan: None,
            rotation: PdfiumRotation::None,
            matrix: None,
            clipping: None,
        }
    }
}

impl PdfiumRenderConfig {
    /// Creates a new render configuration with default values.
    ///
    /// Default configuration uses BGRA format with a white background, the
    /// [`PdfiumRenderFlags::ANNOT`] and [`PdfiumRenderFlags::LCD_TEXT`] rendering
    /// flags and no clipping.
    ///
    /// You must specify at least width or height before rendering.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets width and height of the bitmap in pixels.
    ///
    /// Short for `.with_width(width).with_height(height)`
    ///
    /// When both dimensions are specified, you must also provide either a scale
    /// factor or a transformation matrix to define how the page maps to the bitmap.
    ///
    /// # Arguments
    /// * `width` - Target bitmap width in pixels (must be > 0)
    /// * `height` - Target bitmap height in pixels (must be > 0)
    pub fn with_size(mut self, width: i32, height: i32) -> Self {
        self.width = Some(width);
        self.height = Some(height);
        self
    }

    /// Sets the bitmap width.
    ///
    /// If `height` is not provided, it will be calculated automatically according to the
    /// aspect ratio of the page. In that case also the required scale factor will be
    /// calculated.
    ///
    /// # Arguments
    /// * `width` - Target bitmap width in pixels (must be > 0)
    pub fn with_width(mut self, width: i32) -> Self {
        self.width = Some(width);
        self
    }

    /// Sets the bitmap height.
    ///
    /// If `width` is not provided, it will be calculated automatically according to the
    /// aspect ratio of the page. In that case also the required scale factor will be
    /// calculated.
    ///
    /// # Arguments
    /// * `height` - Target bitmap height in pixels (must be > 0)
    pub fn with_height(mut self, height: i32) -> Self {
        self.height = Some(height);
        self
    }

    /// Sets the pixel format for the rendered bitmap.
    ///
    /// Different formats have different memory requirements and compatibility:
    /// - [`PdfiumBitmapFormat::Bgra`]: 32-bit with alpha, most common for display
    /// - [`PdfiumBitmapFormat::Bgr`]: 24-bit without alpha, smaller memory footprint
    /// - [`PdfiumBitmapFormat::Gray`]: 8-bit grayscale, smallest memory usage
    pub fn with_format(mut self, format: PdfiumBitmapFormat) -> Self {
        self.format = format;
        self
    }

    /// Sets the background color for areas not covered by page content.
    ///
    /// # Arguments
    /// * `color` - The background color to use
    pub fn with_background(mut self, color: PdfiumColor) -> Self {
        self.background = Some(color);
        self
    }

    /// Removes the background color, resulting in a transparent background.
    ///
    /// Only meaningful when using a pixel format that supports transparency (e.g., BGRA).
    pub fn with_transparent_background(mut self) -> Self {
        self.background = None;
        self
    }

    /// Sets the rendering flags to control various behaviors.
    ///
    /// You can combine multiple flags using the bitwise OR operator (|).
    /// Default flags are [`PdfiumRenderFlags::ANNOT`] and [`PdfiumRenderFlags::LCD_TEXT`]
    ///
    /// # Arguments
    /// * `flags` - Combination of PdfiumRenderFlags
    pub fn with_flags(mut self, flags: PdfiumRenderFlags) -> Self {
        self.flags = flags;
        self
    }

    /// Adds additional flags to the existing configuration.
    ///
    /// This is useful when you want to add flags without replacing existing ones.
    ///
    /// # Arguments
    /// * `flags` - Combination of PdfiumRenderFlags to add
    pub fn add_flags(mut self, flags: PdfiumRenderFlags) -> Self {
        self.flags |= flags;
        self
    }

    /// Sets a clipping rectangle to render only a portion of the bitmap.
    ///
    /// The rectangle is specified in bitmap pixels. Default is to render the
    /// entire bitmap.
    ///
    /// # Arguments
    /// * `rect` - The clipping rectangle in bitmap pixels
    pub fn with_clipping(mut self, rect: PdfiumRect) -> Self {
        self.clipping = Some(rect);
        self
    }

    /// Sets the scaling factor for the rendered bitmap.
    ///
    /// Cannot be used with custom transformation matrices.
    ///
    /// # Arguments
    /// * `scale` - Scaling factor (must be > 0.0)
    pub fn with_scale(mut self, scale: f32) -> Self {
        self.scale = Some(scale);
        self
    }

    /// Sets the pan (translation) values for the rendered bitmap.
    ///
    /// Pan values are in bitmap pixel coordinates and are applied after scaling.
    /// Positive values move the content right/down, negative values move left/up.
    ///
    /// Cannot be used with custom transformation matrices.
    ///
    /// # Arguments
    /// * `pan_x` - Horizontal translation in pixels
    /// * `pan_y` - Vertical translation in pixels
    pub fn with_pan(mut self, pan_x: f32, pan_y: f32) -> Self {
        self.pan = Some((pan_x, pan_y));
        self
    }

    /// Sets the rotation value for the rendered bitmap.
    ///
    /// Cannot be used with custom transformation matrices.
    ///
    /// # Arguments
    /// * `rotation` - Rotation in degrees
    pub fn with_rotation(mut self, rotation: PdfiumRotation) -> Self {
        self.rotation = rotation;
        self
    }

    /// Sets a custom transformation matrix for advanced rendering control.
    ///
    /// When specified, scale and pan parameters are not allowed.
    ///
    /// # Arguments
    /// * `matrix` - The transformation matrix to apply
    pub fn with_matrix(mut self, matrix: PdfiumMatrix) -> Self {
        self.matrix = Some(matrix);
        self
    }

    /// Validates the configuration for internal consistency.
    ///
    /// This method checks for conflicting or impossible parameter combinations
    /// and returns descriptive error messages for invalid configurations.
    pub fn validate(&self) -> PdfiumResult<()> {
        // Check for basic dimension requirements
        if self.width.is_none() && self.height.is_none() {
            return Err(PdfiumError::InvalidConfiguration(
                "At least width or height must be specified".to_string(),
            ));
        }

        // Check for positive dimensions for width
        if let Some(w) = self.width {
            if w <= 0 {
                return Err(PdfiumError::InvalidConfiguration(
                    "Width must be greater than 0".to_string(),
                ));
            }
        }

        // Check for positive dimensions for height
        if let Some(h) = self.height {
            if h <= 0 {
                return Err(PdfiumError::InvalidConfiguration(
                    "Height must be greater than 0".to_string(),
                ));
            }
        }

        // Check scale parameter validity
        if let Some(scale) = self.scale {
            if scale <= 0.0 || !scale.is_finite() {
                return Err(PdfiumError::InvalidConfiguration(
                    "Scale must be a positive finite number".to_string(),
                ));
            }
        }

        // Check for conflicting transformation parameters
        if self.matrix.is_some() && self.scale.is_some() {
            return Err(PdfiumError::InvalidConfiguration(
                "Cannot specify both matrix and scale parameters".to_string(),
            ));
        }

        if self.matrix.is_some() && self.pan.is_some() {
            return Err(PdfiumError::InvalidConfiguration(
                "Cannot specify both matrix and pan parameters".to_string(),
            ));
        }

        if self.matrix.is_some() && self.rotation != PdfiumRotation::None {
            return Err(PdfiumError::InvalidConfiguration(
                "Cannot specify both matrix and rotation parameters".to_string(),
            ));
        }

        // Check for dimension/transformation compatibility
        if self.width.is_some()
            && self.height.is_some()
            && self.matrix.is_none()
            && self.scale.is_none()
        {
            return Err(PdfiumError::InvalidConfiguration(
                "When both width and height are specified, scale or matrix must be provided"
                    .to_string(),
            ));
        }

        Ok(())
    }
}

impl PdfiumPage {
    /// Renders this [`PdfiumPage`] into a new [`PdfiumBitmap`] using the specified configuration.
    ///
    /// This method handles the complex logic of translating the configuration parameters
    /// into the appropriate PDFium rendering calls, including dimension calculations,
    /// transformation matrix setup, and bitmap creation.
    ///
    /// # Arguments
    /// * `config` - The rendering configuration to use
    ///
    /// # Returns
    /// * `Ok(PdfiumBitmap)` - The rendered bitmap on success
    /// * `Err(PdfiumError)` - An error describing what went wrong
    ///
    /// # Errors
    /// This method can return errors for:
    /// - Invalid configuration parameters
    /// - PDFium rendering failures
    /// - Memory allocation failures
    /// - Page boundary calculation errors
    pub fn render(&self, config: &PdfiumRenderConfig) -> PdfiumResult<PdfiumBitmap> {
        // Validate configuration first
        config.validate()?;

        // Calculate final dimensions and transformation matrix
        let (width, height, matrix) = self.calculate_render_parameters(config)?;

        let matrix = match config.rotation {
            PdfiumRotation::None => matrix,
            PdfiumRotation::Cw90 => {
                PdfiumMatrix::new_pan(width as f32, 0.0)
                    * matrix
                    * PdfiumMatrix::rotation(PdfiumRotation::Cw270)
            }
            PdfiumRotation::Cw180 => {
                PdfiumMatrix::new_pan(width as f32, height as f32)
                    * matrix
                    * PdfiumMatrix::rotation(PdfiumRotation::Cw180)
            }
            PdfiumRotation::Cw270 => {
                PdfiumMatrix::new_pan(0.0, height as f32)
                    * matrix
                    * PdfiumMatrix::rotation(PdfiumRotation::Cw90)
            }
        };

        // Create the target bitmap
        let bitmap = PdfiumBitmap::empty(width, height, config.format)?;

        // Fill background if specified
        if let Some(color) = config.background {
            bitmap.fill(&color)?;
        };

        // Set up clipping rectangle (default to full bitmap if not specified)
        let clipping =
            config
                .clipping
                .unwrap_or(PdfiumRect::new(0.0, 0.0, width as f32, height as f32));

        // Convert to PDFium types ...
        let clipping: FS_RECTF = (&clipping).into();
        let matrix: FS_MATRIX = (&matrix).into();

        // ... and render
        lib().FPDF_RenderPageBitmapWithMatrix(
            &bitmap,
            self,
            &matrix,
            &clipping,
            config.flags.bits(),
        );

        Ok(bitmap)
    }

    /// Calculates the final rendering parameters (width, height, matrix) from the configuration.
    ///
    /// This internal method handles the complex logic of determining final dimensions
    /// and transformation based on the various configuration options.
    fn calculate_render_parameters(
        &self,
        config: &PdfiumRenderConfig,
    ) -> PdfiumResult<(i32, i32, PdfiumMatrix)> {
        match (config.width, config.height) {
            (None, None) => {
                // This should be caught by validate(), but just in case
                Err(PdfiumError::InvalidConfiguration(
                    "At least width or height needs to be specified".to_string(),
                ))
            }
            (None, Some(h)) => {
                // Height specified, calculate width from aspect ratio
                if config.matrix.is_some() || config.scale.is_some() {
                    return Err(PdfiumError::InvalidConfiguration(
                        "Cannot specify matrix or scale when only height is provided".to_string(),
                    ));
                }
                let bounds = self.boundaries().default()?;
                let bounds = if (self.rotation() + config.rotation).needs_transpose() {
                    bounds.transpose()
                } else {
                    bounds
                };
                let scale = h as f32 / bounds.height();
                let w = (bounds.width() * scale) as i32;
                let m = PdfiumMatrix::new_scale_opt_pan(scale, config.pan);
                Ok((w, h, m))
            }
            (Some(w), None) => {
                // Width specified, calculate height from aspect ratio
                if config.matrix.is_some() || config.scale.is_some() {
                    return Err(PdfiumError::InvalidConfiguration(
                        "Cannot specify matrix or scale when only width is provided".to_string(),
                    ));
                }
                let bounds = self.boundaries().default()?;
                let bounds = if (self.rotation() + config.rotation).needs_transpose() {
                    bounds.transpose()
                } else {
                    bounds
                };
                let scale = w as f32 / bounds.width();
                let h = (bounds.height() * scale) as i32;
                let m = PdfiumMatrix::new_scale_opt_pan(scale, config.pan);
                Ok((w, h, m))
            }
            (Some(w), Some(h)) => {
                // Both dimensions specified, need explicit transformation
                let m = match (config.matrix, config.scale) {
                    (None, None) => return Err(PdfiumError::InvalidConfiguration(
                        "When both width and height are specified, scale or matrix must be provided"
                            .to_string(),
                    )),
                    (None, Some(s)) => PdfiumMatrix::new_scale_opt_pan(s, config.pan),
                    (Some(m), None) => {
                        if config.pan.is_some() {
                            return Err(PdfiumError::InvalidConfiguration(
                                "Cannot specify both matrix and pan parameters".to_string(),
                            ));
                        };
                        m
                    }
                    (Some(_), Some(_)) => {
                        return Err(PdfiumError::InvalidConfiguration(
                            "Cannot specify both matrix and scale parameters".to_string(),
                        ))
                    }
                };
                Ok((w, h, m))
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::*;

    #[test]
    fn test_render_at_height() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(0).unwrap();
        let config = PdfiumRenderConfig::new().with_height(1080);
        let bitmap = page.render(&config).unwrap();
        assert_eq!(bitmap.width(), 763);
        assert_eq!(bitmap.height(), 1080);
    }

    #[test]
    fn test_render_at_width() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(1).unwrap();
        let config = PdfiumRenderConfig::new().with_width(1920);
        let bitmap = page.render(&config).unwrap();
        assert_eq!(bitmap.width(), 1920);
        assert_eq!(bitmap.height(), 2716);
    }

    #[test]
    fn test_render_color_scale_pan() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(0).unwrap();
        let config = PdfiumRenderConfig::new()
            .with_background(PdfiumColor::BLUE)
            .with_width(800)
            .with_height(600)
            .with_scale(1.5)
            .with_pan(400.0, 300.0);
        let bitmap = page.render(&config).unwrap();
        assert_eq!(bitmap.width(), 800);
        assert_eq!(bitmap.height(), 600);
        bitmap
            .save("groningen-color-scale-pan.jpg", image::ImageFormat::Jpeg)
            .unwrap();
    }

    #[test]
    fn test_invalid_config_no_dimensions() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(0).unwrap();
        let config = PdfiumRenderConfig::new();
        let result = page.render(&config);
        assert!(result.is_err());
        if let Err(PdfiumError::InvalidConfiguration(msg)) = result {
            assert!(msg.contains("At least width or height"));
        }
    }

    #[test]
    fn test_invalid_config_negative_dimensions() {
        let config = PdfiumRenderConfig::new().with_width(-100);
        assert!(config.validate().is_err());

        let config = PdfiumRenderConfig::new().with_height(-50);
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_invalid_config_zero_dimensions() {
        let config = PdfiumRenderConfig::new().with_width(0);
        assert!(config.validate().is_err());

        let config = PdfiumRenderConfig::new().with_height(0);
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_invalid_config_matrix_and_scale() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(0).unwrap();
        let matrix = PdfiumMatrix::identity();
        let config = PdfiumRenderConfig::new()
            .with_size(800, 600)
            .with_scale(2.0)
            .with_matrix(matrix);
        let result = page.render(&config);
        assert!(result.is_err());
    }

    #[test]
    fn test_invalid_config_matrix_and_pan() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(0).unwrap();
        let matrix = PdfiumMatrix::identity();
        let config = PdfiumRenderConfig::new()
            .with_size(800, 600)
            .with_matrix(matrix)
            .with_pan(10.0, 20.0);
        let result = page.render(&config);
        assert!(result.is_err());
    }

    #[test]
    fn test_invalid_config_matrix_and_rotation() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(0).unwrap();
        let matrix = PdfiumMatrix::identity();
        let config = PdfiumRenderConfig::new()
            .with_size(800, 600)
            .with_matrix(matrix)
            .with_rotation(PdfiumRotation::Cw90);
        let result = page.render(&config);
        assert!(result.is_err());
    }

    #[test]
    fn test_invalid_config_both_dimensions_no_transform() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(0).unwrap();
        let config = PdfiumRenderConfig::new().with_size(800, 600);
        let result = page.render(&config);
        assert!(result.is_err());
    }

    #[test]
    fn test_invalid_config_matrix_with_single_dimension() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(0).unwrap();
        let matrix = PdfiumMatrix::identity();
        let config = PdfiumRenderConfig::new()
            .with_width(800)
            .with_matrix(matrix);
        let result = page.render(&config);
        assert!(result.is_err());
    }

    #[test]
    fn test_invalid_config_scale_with_single_dimension() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(0).unwrap();
        let config = PdfiumRenderConfig::new().with_height(600).with_scale(1.5);
        let result = page.render(&config);
        assert!(result.is_err());
    }

    #[test]
    fn test_invalid_scale_values() {
        let config = PdfiumRenderConfig::new().with_scale(0.0);
        assert!(config.validate().is_err());

        let config = PdfiumRenderConfig::new().with_scale(-1.0);
        assert!(config.validate().is_err());

        let config = PdfiumRenderConfig::new().with_scale(f32::INFINITY);
        assert!(config.validate().is_err());

        let config = PdfiumRenderConfig::new().with_scale(f32::NAN);
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_valid_config_with_scale() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(0).unwrap();
        let config = PdfiumRenderConfig::new()
            .with_size(800, 600)
            .with_scale(1.5);
        let result = page.render(&config);
        assert!(result.is_ok());
    }

    #[test]
    fn test_valid_config_with_matrix() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(0).unwrap();
        let matrix = PdfiumMatrix::identity();
        let config = PdfiumRenderConfig::new()
            .with_size(800, 600)
            .with_matrix(matrix);
        let result = page.render(&config);
        assert!(result.is_ok());
    }

    #[test]
    fn test_transparent_background() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(0).unwrap();
        let config = PdfiumRenderConfig::new()
            .with_width(800)
            .with_transparent_background()
            .with_format(PdfiumBitmapFormat::Bgra);
        let result = page.render(&config);
        assert!(result.is_ok());
    }

    #[test]
    fn test_custom_background_color() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(0).unwrap();
        let config = PdfiumRenderConfig::new()
            .with_width(800)
            .with_background(PdfiumColor::new(255, 0, 0, 255)); // Red background
        let result = page.render(&config);
        assert!(result.is_ok());
    }

    #[test]
    fn test_clipping_rectangle() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(0).unwrap();
        let clipping_rect = PdfiumRect::new(0.0, 0.0, 400.0, 300.0);
        let config = PdfiumRenderConfig::new()
            .with_width(800)
            .with_clipping(clipping_rect);
        let result = page.render(&config);
        assert!(result.is_ok());
    }

    #[test]
    fn test_add_flags() {
        let config = PdfiumRenderConfig::new()
            .with_width(800)
            .with_flags(PdfiumRenderFlags::ANNOT)
            .add_flags(PdfiumRenderFlags::LCD_TEXT | PdfiumRenderFlags::GRAYSCALE);

        assert!(config.flags.contains(PdfiumRenderFlags::ANNOT));
        assert!(config.flags.contains(PdfiumRenderFlags::LCD_TEXT));
        assert!(config.flags.contains(PdfiumRenderFlags::GRAYSCALE));
    }

    #[test]
    fn test_config_validation_passes_valid_config() {
        let config = PdfiumRenderConfig::new()
            .with_width(800)
            .with_flags(PdfiumRenderFlags::ANNOT)
            .with_background(PdfiumColor::WHITE);

        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_different_bitmap_formats() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(0).unwrap();

        // Test grayscale format
        let config = PdfiumRenderConfig::new()
            .with_width(1000)
            .with_format(PdfiumBitmapFormat::Gray);
        let result = page.render(&config);
        assert!(result.is_ok());
        result
            .unwrap()
            .save("groningen-gray.jpg", image::ImageFormat::Jpeg)
            .unwrap();

        // Test bgr format
        let config = PdfiumRenderConfig::new()
            .with_width(1000)
            .with_format(PdfiumBitmapFormat::Bgr);
        let result = page.render(&config);
        assert!(result.is_ok());
        result
            .unwrap()
            .save("groningen-bgr.jpg", image::ImageFormat::Jpeg)
            .unwrap();

        // Test invalid format
        let config = PdfiumRenderConfig::new()
            .with_width(1000)
            .with_format(PdfiumBitmapFormat::Unknown);
        let result = page.render(&config);
        assert!(result.is_err());
    }

    #[test]
    fn test_error_message_content() {
        let config = PdfiumRenderConfig::new();
        if let Err(PdfiumError::InvalidConfiguration(msg)) = config.validate() {
            assert!(msg.contains("At least width or height must be specified"));
        } else {
            panic!("Expected InvalidConfiguration error");
        }

        let config = PdfiumRenderConfig::new().with_width(-5);
        if let Err(PdfiumError::InvalidConfiguration(msg)) = config.validate() {
            assert!(msg.contains("Width must be greater than 0"));
        } else {
            panic!("Expected InvalidConfiguration error");
        }
    }

    #[test]
    fn test_builder_pattern_chaining() {
        let config = PdfiumRenderConfig::new()
            .with_width(1920)
            .with_format(PdfiumBitmapFormat::Bgra)
            .with_background(PdfiumColor::new(240, 240, 240, 255))
            .add_flags(PdfiumRenderFlags::PRINTING);

        assert_eq!(config.width, Some(1920));
        assert_eq!(config.format, PdfiumBitmapFormat::Bgra);
        assert!(config.flags.contains(PdfiumRenderFlags::ANNOT));
        assert!(config.flags.contains(PdfiumRenderFlags::LCD_TEXT));
        assert!(config.flags.contains(PdfiumRenderFlags::PRINTING));
    }

    #[test]
    fn test_edge_case_very_small_dimensions() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(0).unwrap();
        let config = PdfiumRenderConfig::new().with_width(1); // Very small
        let result = page.render(&config);
        assert!(result.is_ok());
        let bitmap = result.unwrap();
        assert_eq!(bitmap.width(), 1);
        assert!(bitmap.height() > 0); // Height should be calculated
    }

    #[test]
    fn test_edge_case_very_large_scale() {
        let config = PdfiumRenderConfig::new()
            .with_size(100, 100)
            .with_scale(1000.0); // Very large scale

        // Should validate successfully
        assert!(config.validate().is_ok());
    }
}