pdfium 0.10.3

Modern Rust interface to PDFium, the PDF library from Google
Documentation
// 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 image::{DynamicImage, ImageFormat, RgbaImage};

use crate::{
    PdfiumColor,
    error::{PdfiumError, PdfiumResult},
    lib, pdfium_constants,
    pdfium_types::{BitmapHandle, FPDF_BITMAP, Handle},
    try_lib,
};

/// Rust interface to FPDF_BITMAP
#[derive(Debug, Clone)]
pub struct PdfiumBitmap {
    handle: BitmapHandle,
}

impl PdfiumBitmap {
    pub(crate) fn new_from_handle(handle: FPDF_BITMAP) -> PdfiumResult<Self> {
        if handle.is_null() {
            Err(PdfiumError::NullHandle)
        } else {
            Ok(Self {
                handle: Handle::new(handle, Some(close_bitmap)),
            })
        }
    }

    /// Creates a new [`PdfiumBitmap`] with the given `width`, `height` and [`PdfiumBitmapFormat`].
    pub fn empty(width: i32, height: i32, format: PdfiumBitmapFormat) -> PdfiumResult<Self> {
        let lib = try_lib()?;
        lib.FPDFBitmap_CreateEx(
            width,
            height,
            format.into(),
            None, // If this parameter is NULL, then PDFium will create its own buffer.
            0,    // Number of bytes for each scan line, for external buffer only
        )
    }

    /// Fills this entire [`PdfiumBitmap`] with the given [`PdfiumColor`].
    pub fn fill(&self, color: &PdfiumColor) -> PdfiumResult<()> {
        let lib = lib();
        lib.FPDFBitmap_FillRect(
            self,
            0,
            0,
            lib.FPDFBitmap_GetWidth(self),
            lib.FPDFBitmap_GetHeight(self),
            color.into(),
        )
    }

    /// Returns the width of the image in the bitmap buffer backing this [`PdfiumBitmap`].
    #[inline]
    pub fn width(&self) -> i32 {
        lib().FPDFBitmap_GetWidth(self) as i32
    }

    /// Returns the height of the image in the bitmap buffer backing this [`PdfiumBitmap`].
    #[inline]
    pub fn height(&self) -> i32 {
        lib().FPDFBitmap_GetHeight(self) as i32
    }

    /// Returns the pixel format of the image in the bitmap buffer backing this [`PdfiumBitmap`].
    #[inline]
    pub fn format(&self) -> PdfiumBitmapFormat {
        lib().FPDFBitmap_GetFormat(self).into()
    }

    /// Returns an immutable reference to the bitmap buffer backing this [`PdfiumBitmap`].
    ///
    /// This function does not attempt any color channel normalization.
    pub fn as_raw_bytes<'a>(&self) -> &'a [u8] {
        let lib = lib();
        let buffer = lib.FPDFBitmap_GetBuffer(self.handle.handle());
        let len = lib.FPDFBitmap_GetStride(self) * lib.FPDFBitmap_GetHeight(self);
        unsafe { std::slice::from_raw_parts(buffer as *const u8, len as usize) }
    }

    /// Returns an owned copy of the bitmap buffer backing this [`PdfiumBitmap`] as RGBA.
    ///
    /// Normalizing all color channels into RGBA irrespective of the original pixel format.
    pub fn as_rgba_bytes(&self) -> PdfiumResult<Vec<u8>> {
        match self.format() {
            PdfiumBitmapFormat::Bgra => Ok(self
                .as_raw_bytes()
                .chunks_exact(4)
                .flat_map(|pixel| [pixel[2], pixel[1], pixel[0], pixel[3]]) // B,G,R,A -> R,G,B,A
                .collect()),
            PdfiumBitmapFormat::Bgr => Ok(self
                .as_raw_bytes()
                .chunks_exact(3)
                .flat_map(|pixel| [pixel[2], pixel[1], pixel[0], 255]) // B,G,R,A -> R,G,B,A
                .collect()),
            PdfiumBitmapFormat::Gray => Ok(self
                .as_raw_bytes()
                .chunks_exact(1)
                .flat_map(|pixel| [pixel[0], pixel[0], pixel[0], 255]) // B,G,R,A -> R,G,B,A
                .collect()),
            PdfiumBitmapFormat::Unknown
            | PdfiumBitmapFormat::Bgrx
            | PdfiumBitmapFormat::BgraPremul => Err(PdfiumError::UnsupportedImageFormat),
        }
    }

    /// Returns a copy of this a bitmap as a [`DynamicImage::ImageRgba8`]
    pub fn as_rgba8_image(&self) -> PdfiumResult<DynamicImage> {
        let rgba_bytes = self.as_rgba_bytes()?;
        match RgbaImage::from_raw(self.width() as u32, self.height() as u32, rgba_bytes) {
            Some(image) => Ok(DynamicImage::ImageRgba8(image)),
            None => Err(PdfiumError::ImageError),
        }
    }

    /// Returns a copy of this a bitmap as a [`DynamicImage::ImageRgb8`]
    pub fn as_rgb8_image(&self) -> PdfiumResult<DynamicImage> {
        Ok(self
            .as_rgba8_image()?
            .into_rgb8() // Convert RGBA to RGB by dropping the alpha channel
            .into())
    }

    /// Saves this bitmap to the given path.
    ///
    /// Include alpha channel only if the [`ImageFormat`] supports it.
    pub fn save(&self, path: &str, format: ImageFormat) -> PdfiumResult<()> {
        let image = if format == ImageFormat::Png {
            self.as_rgba8_image()?
        } else {
            self.as_rgb8_image()?
        };
        image
            .save_with_format(path, format)
            .or(Err(PdfiumError::ImageError))
    }
}

impl From<&PdfiumBitmap> for FPDF_BITMAP {
    #[inline]
    fn from(value: &PdfiumBitmap) -> Self {
        value.handle.handle()
    }
}

/// Closes this [PdfiumBitmap], releasing held memory.
fn close_bitmap(bitmap: FPDF_BITMAP) {
    lib().FPDFBitmap_Destroy(bitmap);
}

/// The pixel format of the backing buffer of a [PdfiumBitmap].
#[derive(Copy, Clone, Debug, PartialEq, Default)]
#[repr(i32)]
pub enum PdfiumBitmapFormat {
    /// Unknown bitmap format
    Unknown = pdfium_constants::FPDFBitmap_Unknown,
    /// 8-bit grayscale
    Gray = pdfium_constants::FPDFBitmap_Gray,
    /// 24-bit BGR (blue-green-red)
    Bgr = pdfium_constants::FPDFBitmap_BGR,
    /// 32-bit BGR with unused alpha
    Bgrx = pdfium_constants::FPDFBitmap_BGRx,
    #[default]
    /// 32-bit BGRA with alpha
    Bgra = pdfium_constants::FPDFBitmap_BGRA,
    /// 32-bit BGRA with premultiplied alpha
    BgraPremul = pdfium_constants::FPDFBitmap_BGRA_Premul,
}

impl From<i32> for PdfiumBitmapFormat {
    fn from(value: i32) -> Self {
        match value {
            pdfium_constants::FPDFBitmap_Gray => PdfiumBitmapFormat::Gray,
            pdfium_constants::FPDFBitmap_BGR => PdfiumBitmapFormat::Bgr,
            pdfium_constants::FPDFBitmap_BGRx => PdfiumBitmapFormat::Bgrx,
            pdfium_constants::FPDFBitmap_BGRA => PdfiumBitmapFormat::Bgra,
            pdfium_constants::FPDFBitmap_BGRA_Premul => PdfiumBitmapFormat::BgraPremul,
            _ => PdfiumBitmapFormat::Unknown,
        }
    }
}

impl From<PdfiumBitmapFormat> for i32 {
    fn from(value: PdfiumBitmapFormat) -> Self {
        match value {
            PdfiumBitmapFormat::Unknown => pdfium_constants::FPDFBitmap_Unknown,
            PdfiumBitmapFormat::Gray => pdfium_constants::FPDFBitmap_Gray,
            PdfiumBitmapFormat::Bgr => pdfium_constants::FPDFBitmap_BGR,
            PdfiumBitmapFormat::Bgrx => pdfium_constants::FPDFBitmap_BGRx,
            PdfiumBitmapFormat::Bgra => pdfium_constants::FPDFBitmap_BGRA,
            PdfiumBitmapFormat::BgraPremul => pdfium_constants::FPDFBitmap_BGRA_Premul,
        }
    }
}

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

    #[test]
    fn test_render_to_image() {
        let document = PdfiumDocument::new_from_path("resources/groningen.pdf", None).unwrap();
        let page = document.page(1).unwrap();
        drop(document); // Demonstrate that the page can be used after the document is dropped.
        let bounds = page.boundaries().media().unwrap();
        let height = 1080;
        let scale = height as f32 / bounds.height();
        let width = (bounds.width() * scale) as i32;
        let matrix = PdfiumMatrix::new_scale(scale);
        let config = PdfiumRenderConfig::new()
            .with_size(width, height)
            .with_format(PdfiumBitmapFormat::Bgra)
            .with_background(PdfiumColor::WHITE)
            .with_matrix(matrix);
        let bitmap = page.render(&config).unwrap();
        assert_eq!(bitmap.width(), width);
        assert_eq!(bitmap.height(), height);
        bitmap
            .save("groningen-page-2.png", image::ImageFormat::Png)
            .unwrap();
    }
}