use crate::{
PdfiumBitmap, PdfiumBitmapFormat, PdfiumColor, PdfiumError, PdfiumMatrix, PdfiumPage,
PdfiumRect, PdfiumResult, PdfiumRotation, lib, pdfium_constants,
pdfium_types::{FS_MATRIX, FS_RECTF},
};
use bitflags::bitflags;
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PdfiumRenderFlags: i32 {
const ANNOT = pdfium_constants::FPDF_ANNOT;
const LCD_TEXT = pdfium_constants::FPDF_LCD_TEXT;
const NO_NATIVETEXT = pdfium_constants::FPDF_NO_NATIVETEXT;
const GRAYSCALE = pdfium_constants::FPDF_GRAYSCALE;
const REVERSE_BYTE_ORDER = pdfium_constants::FPDF_REVERSE_BYTE_ORDER;
const CONVERT_FILL_TO_STROKE = pdfium_constants::FPDF_CONVERT_FILL_TO_STROKE;
const DEBUG_INFO = pdfium_constants::FPDF_DEBUG_INFO;
const NO_CATCH = pdfium_constants::FPDF_NO_CATCH;
const RENDER_LIMITEDIMAGECACHE = pdfium_constants::FPDF_RENDER_LIMITEDIMAGECACHE;
const RENDER_FORCEHALFTONE = pdfium_constants::FPDF_RENDER_FORCEHALFTONE;
const PRINTING = pdfium_constants::FPDF_PRINTING;
const RENDER_NO_SMOOTHTEXT = pdfium_constants::FPDF_RENDER_NO_SMOOTHTEXT;
const RENDER_NO_SMOOTHIMAGE = pdfium_constants::FPDF_RENDER_NO_SMOOTHIMAGE;
const RENDER_NO_SMOOTHPATH = pdfium_constants::FPDF_RENDER_NO_SMOOTHPATH;
}
}
impl PdfiumRenderFlags {
pub fn fast_rendering() -> Self {
Self::RENDER_NO_SMOOTHTEXT | Self::RENDER_NO_SMOOTHIMAGE | Self::RENDER_NO_SMOOTHPATH
}
pub fn debug() -> Self {
Self::DEBUG_INFO | Self::NO_CATCH
}
}
#[derive(Debug, Clone)]
pub struct PdfiumRenderConfig {
width: Option<i32>,
height: Option<i32>,
format: PdfiumBitmapFormat,
background: Option<PdfiumColor>,
flags: PdfiumRenderFlags,
scale: Option<f32>,
pan: Option<(f32, f32)>,
rotation: PdfiumRotation,
matrix: Option<PdfiumMatrix>,
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 {
pub fn new() -> Self {
Self::default()
}
pub fn with_size(mut self, width: i32, height: i32) -> Self {
self.width = Some(width);
self.height = Some(height);
self
}
pub fn with_width(mut self, width: i32) -> Self {
self.width = Some(width);
self
}
pub fn with_height(mut self, height: i32) -> Self {
self.height = Some(height);
self
}
pub fn with_format(mut self, format: PdfiumBitmapFormat) -> Self {
self.format = format;
self
}
pub fn with_background(mut self, color: PdfiumColor) -> Self {
self.background = Some(color);
self
}
pub fn with_transparent_background(mut self) -> Self {
self.background = None;
self
}
pub fn with_flags(mut self, flags: PdfiumRenderFlags) -> Self {
self.flags = flags;
self
}
pub fn add_flags(mut self, flags: PdfiumRenderFlags) -> Self {
self.flags |= flags;
self
}
pub fn with_clipping(mut self, rect: PdfiumRect) -> Self {
self.clipping = Some(rect);
self
}
pub fn with_scale(mut self, scale: f32) -> Self {
self.scale = Some(scale);
self
}
pub fn with_pan(mut self, pan_x: f32, pan_y: f32) -> Self {
self.pan = Some((pan_x, pan_y));
self
}
pub fn with_rotation(mut self, rotation: PdfiumRotation) -> Self {
self.rotation = rotation;
self
}
pub fn with_matrix(mut self, matrix: PdfiumMatrix) -> Self {
self.matrix = Some(matrix);
self
}
pub fn validate(&self) -> PdfiumResult<()> {
if self.width.is_none() && self.height.is_none() {
return Err(PdfiumError::InvalidConfiguration(
"At least width or height must be specified".to_string(),
));
}
if let Some(w) = self.width {
if w <= 0 {
return Err(PdfiumError::InvalidConfiguration(
"Width must be greater than 0".to_string(),
));
}
}
if let Some(h) = self.height {
if h <= 0 {
return Err(PdfiumError::InvalidConfiguration(
"Height must be greater than 0".to_string(),
));
}
}
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(),
));
}
}
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(),
));
}
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 {
pub fn render(&self, config: &PdfiumRenderConfig) -> PdfiumResult<PdfiumBitmap> {
config.validate()?;
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)
}
};
let bitmap = PdfiumBitmap::empty(width, height, config.format)?;
if let Some(color) = config.background {
bitmap.fill(&color)?;
};
let clipping =
config
.clipping
.unwrap_or(PdfiumRect::new(0.0, 0.0, width as f32, height as f32));
let clipping: FS_RECTF = (&clipping).into();
let matrix: FS_MATRIX = (&matrix).into();
lib().FPDF_RenderPageBitmapWithMatrix(
&bitmap,
self,
&matrix,
&clipping,
config.flags.bits(),
);
Ok(bitmap)
}
fn calculate_render_parameters(
&self,
config: &PdfiumRenderConfig,
) -> PdfiumResult<(i32, i32, PdfiumMatrix)> {
match (config.width, config.height) {
(None, None) => {
Err(PdfiumError::InvalidConfiguration(
"At least width or height needs to be specified".to_string(),
))
}
(None, Some(h)) => {
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) => {
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)) => {
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)); 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();
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();
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();
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); let result = page.render(&config);
assert!(result.is_ok());
let bitmap = result.unwrap();
assert_eq!(bitmap.width(), 1);
assert!(bitmap.height() > 0); }
#[test]
fn test_edge_case_very_large_scale() {
let config = PdfiumRenderConfig::new()
.with_size(100, 100)
.with_scale(1000.0);
assert!(config.validate().is_ok());
}
}