1use std::ffi::c_int;
4
5use crate::coords::PageTransform;
6use crate::error::{Error, Result};
7use crate::page::{PdfPage, Rotation};
8use crate::sys;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12#[non_exhaustive]
13pub enum PixelFormat {
14 #[default]
17 Bgra8,
18 Rgba8,
22 Bgr8,
24 Gray8,
26}
27
28impl PixelFormat {
29 pub fn bytes_per_pixel(self) -> usize {
31 match self {
32 PixelFormat::Bgra8 | PixelFormat::Rgba8 => 4,
33 PixelFormat::Bgr8 => 3,
34 PixelFormat::Gray8 => 1,
35 }
36 }
37
38 pub fn has_alpha(self) -> bool {
40 matches!(self, PixelFormat::Bgra8 | PixelFormat::Rgba8)
41 }
42
43 fn as_fpdf(self) -> c_int {
44 match self {
45 PixelFormat::Bgra8 | PixelFormat::Rgba8 => sys::FPDFBitmap_BGRA,
47 PixelFormat::Bgr8 => sys::FPDFBitmap_BGR,
48 PixelFormat::Gray8 => sys::FPDFBitmap_Gray,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct Color {
56 pub r: u8,
58 pub g: u8,
60 pub b: u8,
62 pub a: u8,
64}
65
66impl Color {
67 pub const WHITE: Color = Color::rgb(0xFF, 0xFF, 0xFF);
69 pub const BLACK: Color = Color::rgb(0x00, 0x00, 0x00);
71 pub const TRANSPARENT: Color = Color {
74 r: 0,
75 g: 0,
76 b: 0,
77 a: 0,
78 };
79
80 pub const fn rgb(r: u8, g: u8, b: u8) -> Color {
82 Color { r, g, b, a: 0xFF }
83 }
84
85 pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Color {
87 Color { r, g, b, a }
88 }
89
90 fn luma(self) -> u8 {
92 let y = 0.299 * f32::from(self.r) + 0.587 * f32::from(self.g) + 0.114 * f32::from(self.b);
93 y.round().clamp(0.0, 255.0) as u8
94 }
95
96 fn encode(self, format: PixelFormat) -> ([u8; 4], usize) {
98 match format {
99 PixelFormat::Bgra8 => ([self.b, self.g, self.r, self.a], 4),
100 PixelFormat::Rgba8 => ([self.r, self.g, self.b, self.a], 4),
101 PixelFormat::Bgr8 => ([self.b, self.g, self.r, 0], 3),
102 PixelFormat::Gray8 => ([self.luma(), 0, 0, 0], 1),
103 }
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq)]
108enum SizeSpec {
109 Scale(f32),
111 Width(u32),
113 Height(u32),
115 Fit(u32, u32),
117 Exact(u32, u32),
119}
120
121#[derive(Debug, Clone, PartialEq)]
133pub struct RenderConfig {
134 size: SizeSpec,
135 format: PixelFormat,
136 background: Color,
137 annotations: bool,
138 form_fields: bool,
139 extra_rotation: Rotation,
140 text_antialiasing: bool,
141 image_antialiasing: bool,
142 path_antialiasing: bool,
143 max_output_bytes: u64,
144}
145
146impl Default for RenderConfig {
147 fn default() -> Self {
148 RenderConfig {
149 size: SizeSpec::Scale(1.0),
150 format: PixelFormat::Bgra8,
151 background: Color::WHITE,
152 annotations: true,
153 form_fields: true,
154 extra_rotation: Rotation::None,
155 text_antialiasing: true,
156 image_antialiasing: true,
157 path_antialiasing: true,
158 max_output_bytes: RenderConfig::DEFAULT_MAX_OUTPUT_BYTES,
159 }
160 }
161}
162
163impl RenderConfig {
164 pub const DEFAULT_MAX_OUTPUT_BYTES: u64 = 1 << 30;
166
167 pub fn new() -> RenderConfig {
170 RenderConfig::default()
171 }
172
173 pub fn scale(mut self, factor: f32) -> Self {
177 self.size = SizeSpec::Scale(factor);
178 self
179 }
180
181 pub fn dpi(mut self, dpi: f32) -> Self {
183 self.size = SizeSpec::Scale(dpi / 72.0);
184 self
185 }
186
187 pub fn width(mut self, pixels: u32) -> Self {
189 self.size = SizeSpec::Width(pixels);
190 self
191 }
192
193 pub fn height(mut self, pixels: u32) -> Self {
195 self.size = SizeSpec::Height(pixels);
196 self
197 }
198
199 pub fn fit(mut self, width: u32, height: u32) -> Self {
202 self.size = SizeSpec::Fit(width, height);
203 self
204 }
205
206 pub fn exact(mut self, width: u32, height: u32) -> Self {
208 self.size = SizeSpec::Exact(width, height);
209 self
210 }
211
212 pub fn pixel_format(mut self, format: PixelFormat) -> Self {
214 self.format = format;
215 self
216 }
217
218 pub fn background(mut self, color: Color) -> Self {
221 self.background = color;
222 self
223 }
224
225 pub fn annotations(mut self, on: bool) -> Self {
227 self.annotations = on;
228 self
229 }
230
231 pub fn form_fields(mut self, on: bool) -> Self {
236 self.form_fields = on;
237 self
238 }
239
240 pub fn rotate(mut self, rotation: Rotation) -> Self {
243 self.extra_rotation = rotation;
244 self
245 }
246
247 pub fn text_antialiasing(mut self, on: bool) -> Self {
249 self.text_antialiasing = on;
250 self
251 }
252
253 pub fn image_antialiasing(mut self, on: bool) -> Self {
255 self.image_antialiasing = on;
256 self
257 }
258
259 pub fn path_antialiasing(mut self, on: bool) -> Self {
261 self.path_antialiasing = on;
262 self
263 }
264
265 pub fn max_output_bytes(mut self, limit: u64) -> Self {
270 self.max_output_bytes = limit;
271 self
272 }
273
274 pub(crate) fn resolve_dimensions(&self, page: crate::PageSize) -> Result<(u32, u32)> {
277 let (pw, ph) = if self.extra_rotation.swaps_axes() {
279 (page.height as f64, page.width as f64)
280 } else {
281 (page.width as f64, page.height as f64)
282 };
283 if !(pw.is_finite() && ph.is_finite()) || pw <= 0.0 || ph <= 0.0 {
284 return Err(Error::InvalidConfig(format!(
285 "page has degenerate dimensions {pw}x{ph}pt"
286 )));
287 }
288
289 let scaled = |scale: f64| -> Result<(u32, u32)> {
290 if !scale.is_finite() || scale <= 0.0 {
291 return Err(Error::InvalidConfig(format!(
292 "scale must be positive, got {scale}"
293 )));
294 }
295 Ok((
296 (pw * scale).round().max(1.0) as u32,
297 (ph * scale).round().max(1.0) as u32,
298 ))
299 };
300
301 let (w, h) = match self.size {
302 SizeSpec::Scale(s) => scaled(f64::from(s))?,
303 SizeSpec::Width(px) => {
304 nonzero(px, "width")?;
305 scaled(f64::from(px) / pw)?
306 }
307 SizeSpec::Height(px) => {
308 nonzero(px, "height")?;
309 scaled(f64::from(px) / ph)?
310 }
311 SizeSpec::Fit(bw, bh) => {
312 nonzero(bw, "fit width")?;
313 nonzero(bh, "fit height")?;
314 scaled((f64::from(bw) / pw).min(f64::from(bh) / ph))?
315 }
316 SizeSpec::Exact(w, h) => {
317 nonzero(w, "width")?;
318 nonzero(h, "height")?;
319 (w, h)
320 }
321 };
322
323 let bpp = u128::from(self.format.bytes_per_pixel() as u64);
328 let required = u128::from(w) * u128::from(h) * bpp;
329 let required_bytes = u64::try_from(required).unwrap_or(u64::MAX);
330 let too_large = w > i32::MAX as u32
331 || h > i32::MAX as u32
332 || u128::from(w) * bpp > i32::MAX as u128
333 || required > u128::from(self.max_output_bytes);
334 if too_large {
335 return Err(Error::RenderTooLarge {
336 required_bytes,
337 limit: self.max_output_bytes,
338 });
339 }
340 Ok((w, h))
341 }
342
343 fn flags(&self) -> c_int {
344 let mut flags = 0;
345 if self.annotations {
346 flags |= sys::FPDF_ANNOT;
347 }
348 if self.format == PixelFormat::Rgba8 {
349 flags |= sys::FPDF_REVERSE_BYTE_ORDER;
350 }
351 if !self.text_antialiasing {
352 flags |= sys::FPDF_RENDER_NO_SMOOTHTEXT;
353 }
354 if !self.image_antialiasing {
355 flags |= sys::FPDF_RENDER_NO_SMOOTHIMAGE;
356 }
357 if !self.path_antialiasing {
358 flags |= sys::FPDF_RENDER_NO_SMOOTHPATH;
359 }
360 flags
361 }
362}
363
364fn nonzero(v: u32, what: &str) -> Result<()> {
365 if v == 0 {
366 return Err(Error::InvalidConfig(format!("{what} must be nonzero")));
367 }
368 Ok(())
369}
370
371#[derive(Debug, Clone)]
377pub struct RenderedPage {
378 width: u32,
379 height: u32,
380 stride: usize,
381 format: PixelFormat,
382 data: Vec<u8>,
383 page_index: usize,
384 transform: PageTransform,
385}
386
387impl RenderedPage {
388 pub fn width(&self) -> u32 {
390 self.width
391 }
392
393 pub fn height(&self) -> u32 {
395 self.height
396 }
397
398 pub fn stride(&self) -> usize {
404 self.stride
405 }
406
407 pub fn format(&self) -> PixelFormat {
409 self.format
410 }
411
412 pub fn pixels(&self) -> &[u8] {
414 &self.data
415 }
416
417 pub fn into_pixels(self) -> Vec<u8> {
419 self.data
420 }
421
422 pub fn row(&self, y: u32) -> &[u8] {
428 assert!(
429 y < self.height,
430 "row {y} out of bounds (height {})",
431 self.height
432 );
433 let start = y as usize * self.stride;
434 &self.data[start..start + self.width as usize * self.format.bytes_per_pixel()]
435 }
436
437 pub fn pixel(&self, x: u32, y: u32) -> &[u8] {
443 assert!(
444 x < self.width,
445 "column {x} out of bounds (width {})",
446 self.width
447 );
448 let bpp = self.format.bytes_per_pixel();
449 let row = self.row(y);
450 &row[x as usize * bpp..(x as usize + 1) * bpp]
451 }
452
453 pub fn page_index(&self) -> usize {
455 self.page_index
456 }
457
458 pub fn transform(&self) -> &PageTransform {
460 &self.transform
461 }
462
463 pub fn to_rgba8(&self) -> Vec<u8> {
466 let w = self.width as usize;
467 let h = self.height as usize;
468 let mut out = Vec::with_capacity(w * h * 4);
469 for y in 0..h {
470 let row = &self.data[y * self.stride..];
471 match self.format {
472 PixelFormat::Rgba8 => out.extend_from_slice(&row[..w * 4]),
473 PixelFormat::Bgra8 => {
474 for px in row[..w * 4].chunks_exact(4) {
475 out.extend_from_slice(&[px[2], px[1], px[0], px[3]]);
476 }
477 }
478 PixelFormat::Bgr8 => {
479 for px in row[..w * 3].chunks_exact(3) {
480 out.extend_from_slice(&[px[2], px[1], px[0], 0xFF]);
481 }
482 }
483 PixelFormat::Gray8 => {
484 for &g in &row[..w] {
485 out.extend_from_slice(&[g, g, g, 0xFF]);
486 }
487 }
488 }
489 }
490 out
491 }
492}
493
494impl<'doc> PdfPage<'doc> {
495 pub fn render(&self, config: &RenderConfig) -> Result<RenderedPage> {
501 let (width, height) = config.resolve_dimensions(self.size())?;
502 let bpp = config.format.bytes_per_pixel();
503 let stride = width as usize * bpp;
504 let mut data = vec![0u8; stride * height as usize];
505 fill_background(&mut data, config.format, config.background);
506
507 let rotate = config.extra_rotation.as_raw();
508 let flags = config.flags();
509 let draw_forms = config.form_fields && self.document().form_env().is_some();
510
511 let transform = self.ffi(|b| -> Result<PageTransform> {
512 let bitmap = unsafe {
517 b.FPDFBitmap_CreateEx(
518 width as c_int,
519 height as c_int,
520 config.format.as_fpdf(),
521 data.as_mut_ptr().cast(),
522 stride as c_int,
523 )
524 };
525 if bitmap.is_null() {
526 return Err(Error::RenderFailed {
527 reason: "FPDFBitmap_CreateEx returned null",
528 });
529 }
530
531 unsafe {
534 b.FPDF_RenderPageBitmap(
535 bitmap,
536 self.handle(),
537 0,
538 0,
539 width as c_int,
540 height as c_int,
541 rotate,
542 flags,
543 );
544 }
545
546 if draw_forms {
547 if let Some(env) = self.document().form_env() {
548 unsafe {
552 b.FPDF_FFLDraw(
553 env.handle(),
554 bitmap,
555 self.handle(),
556 0,
557 0,
558 width as c_int,
559 height as c_int,
560 rotate,
561 flags,
562 );
563 }
564 }
565 }
566
567 unsafe { b.FPDFBitmap_Destroy(bitmap) };
570
571 derive_transform(b, self.handle(), width, height, rotate)
572 })?;
573
574 Ok(RenderedPage {
575 width,
576 height,
577 stride,
578 format: config.format,
579 data,
580 page_index: self.index(),
581 transform,
582 })
583 }
584
585 pub fn transform_for(&self, config: &RenderConfig) -> Result<PageTransform> {
589 let (width, height) = config.resolve_dimensions(self.size())?;
590 let rotate = config.extra_rotation.as_raw();
591 self.ffi(|b| derive_transform(b, self.handle(), width, height, rotate))
592 }
593
594 pub fn device_to_page(
603 &self,
604 config: &RenderConfig,
605 pixel: crate::PixelPoint,
606 ) -> Result<crate::PagePoint> {
607 let (width, height) = config.resolve_dimensions(self.size())?;
608 let rotate = config.extra_rotation.as_raw();
609 let dx = clamp_to_c_int(pixel.x)?;
610 let dy = clamp_to_c_int(pixel.y)?;
611 let (mut px, mut py) = (0.0f64, 0.0f64);
612 let ok = self.ffi(|b| unsafe {
616 b.FPDF_DeviceToPage(
617 self.handle(),
618 0,
619 0,
620 width as c_int,
621 height as c_int,
622 rotate,
623 dx,
624 dy,
625 &mut px,
626 &mut py,
627 )
628 });
629 if ok != 0 {
630 Ok(crate::PagePoint::new(px, py))
631 } else {
632 Err(Error::RenderFailed {
633 reason: "FPDF_DeviceToPage failed",
634 })
635 }
636 }
637
638 pub fn page_to_device(
645 &self,
646 config: &RenderConfig,
647 point: crate::PagePoint,
648 ) -> Result<crate::PixelPoint> {
649 let (width, height) = config.resolve_dimensions(self.size())?;
650 let rotate = config.extra_rotation.as_raw();
651 let (mut dx, mut dy) = (0 as c_int, 0 as c_int);
652 let ok = self.ffi(|b| unsafe {
655 b.FPDF_PageToDevice(
656 self.handle(),
657 0,
658 0,
659 width as c_int,
660 height as c_int,
661 rotate,
662 point.x,
663 point.y,
664 &mut dx,
665 &mut dy,
666 )
667 });
668 if ok != 0 {
669 Ok(crate::PixelPoint::new(f64::from(dx), f64::from(dy)))
670 } else {
671 Err(Error::RenderFailed {
672 reason: "FPDF_PageToDevice failed",
673 })
674 }
675 }
676}
677
678fn clamp_to_c_int(v: f64) -> Result<c_int> {
679 let r = v.round();
680 if r.is_finite() && (f64::from(i32::MIN)..=f64::from(i32::MAX)).contains(&r) {
681 Ok(r as c_int)
682 } else {
683 Err(Error::InvalidConfig(format!(
684 "device coordinate {v} is outside the addressable integer range"
685 )))
686 }
687}
688
689fn derive_transform(
694 b: &sys::Bindings,
695 page: sys::FPDF_PAGE,
696 width: u32,
697 height: u32,
698 rotate: c_int,
699) -> Result<PageTransform> {
700 let corner = |dx: c_int, dy: c_int| -> Result<(f64, f64)> {
701 let (mut px, mut py) = (0.0f64, 0.0f64);
702 let ok = unsafe {
705 b.FPDF_DeviceToPage(
706 page,
707 0,
708 0,
709 width as c_int,
710 height as c_int,
711 rotate,
712 dx,
713 dy,
714 &mut px,
715 &mut py,
716 )
717 };
718 if ok != 0 {
719 Ok((px, py))
720 } else {
721 Err(Error::RenderFailed {
722 reason: "FPDF_DeviceToPage failed",
723 })
724 }
725 };
726
727 let origin = corner(0, 0)?;
728 let x_axis = corner(width as c_int, 0)?;
729 let y_axis = corner(0, height as c_int)?;
730 PageTransform::from_corners(width, height, origin, x_axis, y_axis).ok_or(Error::RenderFailed {
731 reason: "degenerate page transform",
732 })
733}
734
735fn fill_background(data: &mut [u8], format: PixelFormat, color: Color) {
741 let (pattern, bpp) = color.encode(format);
742 let pattern = &pattern[..bpp];
743 if pattern.iter().all(|&b| b == pattern[0]) {
744 data.fill(pattern[0]);
745 } else {
746 for px in data.chunks_exact_mut(bpp) {
747 px.copy_from_slice(pattern);
748 }
749 }
750}
751
752#[cfg(test)]
753mod tests {
754 use super::*;
755 use crate::page::PageSize;
756
757 fn dims(cfg: &RenderConfig, w: f32, h: f32) -> Result<(u32, u32)> {
758 cfg.resolve_dimensions(PageSize {
759 width: w,
760 height: h,
761 })
762 }
763
764 #[test]
765 fn scale_and_dpi() {
766 assert_eq!(
767 dims(&RenderConfig::new().scale(2.0), 200.0, 100.0).unwrap(),
768 (400, 200)
769 );
770 assert_eq!(
771 dims(&RenderConfig::new().dpi(144.0), 200.0, 100.0).unwrap(),
772 (400, 200)
773 );
774 }
775
776 #[test]
777 fn fixed_axes_preserve_aspect() {
778 assert_eq!(
779 dims(&RenderConfig::new().width(400), 200.0, 100.0).unwrap(),
780 (400, 200)
781 );
782 assert_eq!(
783 dims(&RenderConfig::new().height(50), 200.0, 100.0).unwrap(),
784 (100, 50)
785 );
786 assert_eq!(
787 dims(&RenderConfig::new().fit(1000, 300), 200.0, 100.0).unwrap(),
788 (600, 300)
789 );
790 assert_eq!(
791 dims(&RenderConfig::new().exact(37, 91), 200.0, 100.0).unwrap(),
792 (37, 91)
793 );
794 }
795
796 #[test]
797 fn rotation_swaps_output_axes() {
798 let cfg = RenderConfig::new().scale(1.0).rotate(Rotation::Clockwise90);
799 assert_eq!(dims(&cfg, 200.0, 100.0).unwrap(), (100, 200));
800 let cfg = RenderConfig::new().width(300).rotate(Rotation::Clockwise90);
802 assert_eq!(dims(&cfg, 200.0, 100.0).unwrap(), (300, 600));
803 }
804
805 #[test]
806 fn size_cap_enforced() {
807 let cfg = RenderConfig::new().scale(100.0).max_output_bytes(1024);
808 match dims(&cfg, 200.0, 100.0) {
809 Err(Error::RenderTooLarge {
810 required_bytes,
811 limit,
812 }) => {
813 assert_eq!(limit, 1024);
814 assert!(required_bytes > 1024);
815 }
816 other => panic!("expected RenderTooLarge, got {other:?}"),
817 }
818 }
819
820 #[test]
821 fn invalid_inputs_rejected() {
822 assert!(matches!(
823 dims(&RenderConfig::new().scale(0.0), 200.0, 100.0),
824 Err(Error::InvalidConfig(_))
825 ));
826 assert!(matches!(
827 dims(&RenderConfig::new().scale(f32::NAN), 200.0, 100.0),
828 Err(Error::InvalidConfig(_))
829 ));
830 assert!(matches!(
831 dims(&RenderConfig::new().exact(0, 10), 200.0, 100.0),
832 Err(Error::InvalidConfig(_))
833 ));
834 }
835
836 #[test]
837 fn background_fill_patterns() {
838 let mut buf = vec![0u8; 12];
839 fill_background(&mut buf, PixelFormat::Bgra8, Color::rgba(1, 2, 3, 4));
840 assert_eq!(&buf[..4], &[3, 2, 1, 4]);
841 fill_background(&mut buf, PixelFormat::Rgba8, Color::rgba(1, 2, 3, 4));
842 assert_eq!(&buf[..4], &[1, 2, 3, 4]);
843 let mut buf3 = vec![0u8; 9];
844 fill_background(&mut buf3, PixelFormat::Bgr8, Color::rgb(10, 20, 30));
845 assert_eq!(&buf3[..3], &[30, 20, 10]);
846 }
847}