Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a012cb65a6 | ||
|
|
6567e1ab2d | ||
|
|
3af409d27f |
@@ -49,6 +49,9 @@ ttf-parser = "0.25"
|
||||
lopdf = { version = "0.42.0", features = ["rayon"] }
|
||||
rayon = "1.10"
|
||||
env_logger = "0.11"
|
||||
# Optional native page rendering for OCR pipelines. PDFium is loaded at
|
||||
# runtime, so enabling this feature does not link or download a native library.
|
||||
firecrawl-pdfium = { version = "0.1.0", optional = true }
|
||||
|
||||
# Browser builds use JavaScript randomness for encrypted PDFs and embed the
|
||||
# bundled CMaps because there is no filesystem at runtime.
|
||||
@@ -62,6 +65,7 @@ tempfile = "3.3"
|
||||
[features]
|
||||
default = []
|
||||
python = ["pyo3"]
|
||||
render-pdfium = ["dep:firecrawl-pdfium"]
|
||||
|
||||
[[bin]]
|
||||
name = "pdf2md"
|
||||
|
||||
+40
-1
@@ -1,6 +1,6 @@
|
||||
# pdf-inspector
|
||||
|
||||
Fast PDF classification and text extraction. Detects whether a PDF is text-based or scanned, extracts text with position awareness, and converts to clean Markdown — all without OCR. Pure Rust, no ML models, no external services; the only PDF dependency is [lopdf](https://crates.io/crates/lopdf). Also available for [Python](https://pypi.org/project/pdf-inspector/) and [Node.js](https://www.npmjs.com/package/@firecrawl/pdf-inspector).
|
||||
Fast PDF classification and text extraction. Detects whether a PDF is text-based or scanned, extracts text with position awareness, and converts to clean Markdown — all without OCR. The default build is pure Rust, has no ML models or external services, and uses [lopdf](https://crates.io/crates/lopdf) for PDF parsing. Also available for [Python](https://pypi.org/project/pdf-inspector/) and [Node.js](https://www.npmjs.com/package/@firecrawl/pdf-inspector/).
|
||||
|
||||
Built by [Firecrawl](https://firecrawl.dev) to handle text-based PDFs locally in under 200ms, skipping expensive OCR services for the ~54% of PDFs that don't need them.
|
||||
|
||||
@@ -117,6 +117,45 @@ let bytes = std::fs::read("document.pdf")?;
|
||||
let result = process_pdf_mem(&bytes)?;
|
||||
```
|
||||
|
||||
### Optional native page rendering
|
||||
|
||||
The `render-pdfium` feature adds a native-only page renderer backed by
|
||||
[`firecrawl-pdfium`](https://crates.io/crates/firecrawl-pdfium). It is the
|
||||
rendering boundary for OCR pipelines; enabling it does not include an OCR
|
||||
model or change the existing extraction functions.
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
pdf-inspector = { version = "1", features = ["render-pdfium"] }
|
||||
```
|
||||
|
||||
PDFium is loaded at runtime. Set `PDFIUM_LIB_PATH`, place its shared library
|
||||
next to the executable, or use another discovery route supported by
|
||||
`firecrawl-pdfium`.
|
||||
|
||||
```rust
|
||||
use pdf_inspector::vision::{PdfiumRenderer, RenderOptions};
|
||||
|
||||
let renderer = PdfiumRenderer::load()?;
|
||||
let bytes = std::fs::read("document.pdf")?;
|
||||
let pages = renderer.render_pages(
|
||||
&bytes,
|
||||
&[1, 3], // 1-indexed, matching pages_needing_ocr
|
||||
None, // optional PDF password
|
||||
&RenderOptions::new().dpi(150.0),
|
||||
)?;
|
||||
|
||||
for page in pages {
|
||||
// Owned RGB pixels can leave the PDFium critical section and be sent to
|
||||
// an OCR worker. OCR pixel boxes can be mapped back to PDF coordinates.
|
||||
let rect = page.pixel_rect_to_pdf_rect(20.0, 30.0, 100.0, 24.0);
|
||||
println!("page {}: {}x{}, rect={rect:?}", page.page(), page.width(), page.height());
|
||||
}
|
||||
```
|
||||
|
||||
Browser WASM remains on the default text-only path and does not expose native
|
||||
PDFium rendering.
|
||||
|
||||
Extract per-page Markdown (one string per page, plus document-wide layout
|
||||
metadata):
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ mod text_quality;
|
||||
pub mod text_utils;
|
||||
pub mod tounicode;
|
||||
pub mod types;
|
||||
pub mod vision;
|
||||
|
||||
pub use detector::{
|
||||
detect_pdf_type, detect_pdf_type_mem, detect_pdf_type_mem_with_config,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
//! Optional native vision primitives used by local extraction pipelines.
|
||||
//!
|
||||
//! The existing lopdf extractor remains the default path. Native page
|
||||
//! rendering is available only with the `render-pdfium` feature and is kept
|
||||
//! separate so browser WASM and text-only consumers do not take on PDFium.
|
||||
|
||||
#[cfg(all(feature = "render-pdfium", not(target_arch = "wasm32")))]
|
||||
mod pdfium;
|
||||
|
||||
#[cfg(all(feature = "render-pdfium", not(target_arch = "wasm32")))]
|
||||
pub use pdfium::{
|
||||
PagePoint, PdfiumRenderer, RenderError, RenderOptions, RenderPixelFormat, RenderedPage,
|
||||
};
|
||||
@@ -0,0 +1,410 @@
|
||||
//! PDFium-backed page rendering for OCR.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use firecrawl_pdfium::{
|
||||
PageRect as PdfiumPageRect, Pdfium, PixelFormat, PixelPoint, PixelRect, RenderConfig,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::PdfRect;
|
||||
|
||||
/// Default rendering resolution for OCR.
|
||||
pub const DEFAULT_RENDER_DPI: f32 = 150.0;
|
||||
|
||||
/// Default maximum size of one rendered page: 256 MiB.
|
||||
pub const DEFAULT_MAX_OUTPUT_BYTES: u64 = 256 * 1024 * 1024;
|
||||
|
||||
/// Pixel layout returned by [`RenderedPage`].
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum RenderPixelFormat {
|
||||
/// Three bytes per pixel in red, green, blue order. This is the default
|
||||
/// because OCR preprocessors conventionally consume RGB images.
|
||||
#[default]
|
||||
Rgb8,
|
||||
/// Four bytes per pixel in red, green, blue, alpha order.
|
||||
Rgba8,
|
||||
/// One luminance byte per pixel.
|
||||
Gray8,
|
||||
}
|
||||
|
||||
impl RenderPixelFormat {
|
||||
/// Number of bytes used by one pixel.
|
||||
pub fn bytes_per_pixel(self) -> usize {
|
||||
match self {
|
||||
Self::Rgb8 => 3,
|
||||
Self::Rgba8 => 4,
|
||||
Self::Gray8 => 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn pdfium_format(self) -> PixelFormat {
|
||||
match self {
|
||||
// PDFium produces BGR directly; `RenderedPage::from_pdfium`
|
||||
// swaps the red and blue channels in place.
|
||||
Self::Rgb8 => PixelFormat::Bgr8,
|
||||
Self::Rgba8 => PixelFormat::Rgba8,
|
||||
Self::Gray8 => PixelFormat::Gray8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for pages rendered as input to a local vision pipeline.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RenderOptions {
|
||||
/// Output resolution. Defaults to 150 DPI.
|
||||
pub dpi: f32,
|
||||
/// Pixel layout. Defaults to three-channel RGB.
|
||||
pub pixel_format: RenderPixelFormat,
|
||||
/// Include PDF annotations in the rendered bitmap.
|
||||
pub annotations: bool,
|
||||
/// Include visible static AcroForm field appearances.
|
||||
pub form_fields: bool,
|
||||
/// Maximum allocation for each rendered page.
|
||||
pub max_output_bytes_per_page: u64,
|
||||
}
|
||||
|
||||
impl Default for RenderOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dpi: DEFAULT_RENDER_DPI,
|
||||
pixel_format: RenderPixelFormat::Rgb8,
|
||||
annotations: true,
|
||||
form_fields: true,
|
||||
max_output_bytes_per_page: DEFAULT_MAX_OUTPUT_BYTES,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOptions {
|
||||
/// Creates local-rendering options with OCR-oriented defaults.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Sets the output resolution in dots per inch.
|
||||
pub fn dpi(mut self, dpi: f32) -> Self {
|
||||
self.dpi = dpi;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the output pixel layout.
|
||||
pub fn pixel_format(mut self, pixel_format: RenderPixelFormat) -> Self {
|
||||
self.pixel_format = pixel_format;
|
||||
self
|
||||
}
|
||||
|
||||
/// Toggles annotation rendering.
|
||||
pub fn annotations(mut self, annotations: bool) -> Self {
|
||||
self.annotations = annotations;
|
||||
self
|
||||
}
|
||||
|
||||
/// Toggles visible static form-field rendering.
|
||||
pub fn form_fields(mut self, form_fields: bool) -> Self {
|
||||
self.form_fields = form_fields;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the maximum allocation for each rendered page.
|
||||
pub fn max_output_bytes_per_page(mut self, bytes: u64) -> Self {
|
||||
self.max_output_bytes_per_page = bytes;
|
||||
self
|
||||
}
|
||||
|
||||
fn pdfium_config(&self) -> RenderConfig {
|
||||
RenderConfig::new()
|
||||
.dpi(self.dpi)
|
||||
.pixel_format(self.pixel_format.pdfium_format())
|
||||
.annotations(self.annotations)
|
||||
.form_fields(self.form_fields)
|
||||
.max_output_bytes(self.max_output_bytes_per_page)
|
||||
}
|
||||
}
|
||||
|
||||
/// A point in PDF page space, measured in points from the bottom-left.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct PagePoint {
|
||||
/// Horizontal position in PDF points.
|
||||
pub x: f32,
|
||||
/// Vertical position in PDF points, increasing upward.
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
/// One rendered page with owned pixels and its pixel-to-PDF transform.
|
||||
///
|
||||
/// The value contains no live PDFium page or document handles. It can be
|
||||
/// moved to an OCR worker and retained after [`PdfiumRenderer::render_pages`]
|
||||
/// returns.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderedPage {
|
||||
page: u32,
|
||||
page_width: f32,
|
||||
page_height: f32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
stride: usize,
|
||||
format: RenderPixelFormat,
|
||||
pixels: Vec<u8>,
|
||||
transform: firecrawl_pdfium::PageTransform,
|
||||
}
|
||||
|
||||
impl RenderedPage {
|
||||
fn from_pdfium(
|
||||
page: u32,
|
||||
page_width: f32,
|
||||
page_height: f32,
|
||||
format: RenderPixelFormat,
|
||||
rendered: firecrawl_pdfium::RenderedPage,
|
||||
) -> Self {
|
||||
let width = rendered.width();
|
||||
let height = rendered.height();
|
||||
let stride = rendered.stride();
|
||||
let transform = *rendered.transform();
|
||||
let mut pixels = rendered.into_pixels();
|
||||
|
||||
if format == RenderPixelFormat::Rgb8 {
|
||||
bgr_to_rgb_in_place(&mut pixels, width, height, stride);
|
||||
}
|
||||
|
||||
Self {
|
||||
page,
|
||||
page_width,
|
||||
page_height,
|
||||
width,
|
||||
height,
|
||||
stride,
|
||||
format,
|
||||
pixels,
|
||||
transform,
|
||||
}
|
||||
}
|
||||
|
||||
/// 1-indexed page number.
|
||||
pub fn page(&self) -> u32 {
|
||||
self.page
|
||||
}
|
||||
|
||||
/// Page width in PDF points after applying the page's `/Rotate` entry.
|
||||
pub fn page_width(&self) -> f32 {
|
||||
self.page_width
|
||||
}
|
||||
|
||||
/// Page height in PDF points after applying the page's `/Rotate` entry.
|
||||
pub fn page_height(&self) -> f32 {
|
||||
self.page_height
|
||||
}
|
||||
|
||||
/// Bitmap width in pixels.
|
||||
pub fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
/// Bitmap height in pixels.
|
||||
pub fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
|
||||
/// Number of bytes between adjacent bitmap rows.
|
||||
pub fn stride(&self) -> usize {
|
||||
self.stride
|
||||
}
|
||||
|
||||
/// Pixel layout of [`pixels`](Self::pixels).
|
||||
pub fn format(&self) -> RenderPixelFormat {
|
||||
self.format
|
||||
}
|
||||
|
||||
/// Owned bitmap bytes, with rows ordered top-to-bottom.
|
||||
pub fn pixels(&self) -> &[u8] {
|
||||
&self.pixels
|
||||
}
|
||||
|
||||
/// Consumes the page and returns its pixel buffer.
|
||||
pub fn into_pixels(self) -> Vec<u8> {
|
||||
self.pixels
|
||||
}
|
||||
|
||||
/// Converts a bitmap point (top-left origin, y-down) to PDF page space
|
||||
/// (bottom-left origin, y-up).
|
||||
pub fn pixel_to_page(&self, x: f64, y: f64) -> PagePoint {
|
||||
let point = self.transform.pixel_to_page(PixelPoint::new(x, y));
|
||||
PagePoint {
|
||||
x: point.x as f32,
|
||||
y: point.y as f32,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a bitmap rectangle to the repository's existing PDF-space
|
||||
/// rectangle type. The returned page number remains 1-indexed.
|
||||
pub fn pixel_rect_to_pdf_rect(&self, x: f64, y: f64, width: f64, height: f64) -> PdfRect {
|
||||
let rect = self
|
||||
.transform
|
||||
.pixel_rect_to_page(PixelRect::new(x, y, width, height));
|
||||
PdfRect {
|
||||
x: rect.left as f32,
|
||||
y: rect.bottom as f32,
|
||||
width: rect.width() as f32,
|
||||
height: rect.height() as f32,
|
||||
page: self.page,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a PDF-space rectangle to bitmap coordinates
|
||||
/// `(x, y, width, height)` with a top-left origin.
|
||||
pub fn pdf_rect_to_pixel(&self, rect: &PdfRect) -> (f64, f64, f64, f64) {
|
||||
let rect = self.transform.page_rect_to_pixel(PdfiumPageRect::new(
|
||||
f64::from(rect.x),
|
||||
f64::from(rect.y),
|
||||
f64::from(rect.x + rect.width),
|
||||
f64::from(rect.y + rect.height),
|
||||
));
|
||||
(rect.x, rect.y, rect.width, rect.height)
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors produced by the optional local renderer.
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum RenderError {
|
||||
/// Page numbers in pdf-inspector APIs are 1-indexed, so zero is invalid.
|
||||
#[error("page numbers are 1-indexed; page 0 is invalid")]
|
||||
InvalidPageNumber,
|
||||
/// The requested 1-indexed page is not present in the document.
|
||||
#[error("page {page} is out of bounds for a {page_count}-page document")]
|
||||
PageOutOfBounds {
|
||||
/// Requested 1-indexed page.
|
||||
page: u32,
|
||||
/// Number of pages in the document.
|
||||
page_count: usize,
|
||||
},
|
||||
/// PDFium loading, document parsing, form setup, or rendering failed.
|
||||
#[error(transparent)]
|
||||
Pdfium(#[from] firecrawl_pdfium::Error),
|
||||
}
|
||||
|
||||
/// Loaded PDFium renderer used to prepare pages for OCR.
|
||||
///
|
||||
/// PDFium calls are safe from concurrent threads but serialize inside the
|
||||
/// underlying binding. Returned [`RenderedPage`] values are ordinary owned
|
||||
/// data and can be processed concurrently after rendering.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PdfiumRenderer {
|
||||
pdfium: Pdfium,
|
||||
}
|
||||
|
||||
impl PdfiumRenderer {
|
||||
/// Loads PDFium using `firecrawl-pdfium`'s documented discovery chain.
|
||||
pub fn load() -> Result<Self, RenderError> {
|
||||
Ok(Self {
|
||||
pdfium: Pdfium::load()?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Loads PDFium from an explicit native library path.
|
||||
pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, RenderError> {
|
||||
Ok(Self {
|
||||
pdfium: Pdfium::load_from_path(path)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Path of the active PDFium library, if it was loaded from a concrete
|
||||
/// file rather than through the system loader.
|
||||
pub fn loaded_from(&self) -> Option<&Path> {
|
||||
self.pdfium.loaded_from()
|
||||
}
|
||||
|
||||
/// Renders selected 1-indexed pages in the same order as `pages`.
|
||||
///
|
||||
/// The PDF is parsed once for the full batch. Passing an empty page list
|
||||
/// returns immediately without parsing or allocating.
|
||||
pub fn render_pages(
|
||||
&self,
|
||||
pdf_bytes: &[u8],
|
||||
pages: &[u32],
|
||||
password: Option<&str>,
|
||||
options: &RenderOptions,
|
||||
) -> Result<Vec<RenderedPage>, RenderError> {
|
||||
if pages.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
if pages.contains(&0) {
|
||||
return Err(RenderError::InvalidPageNumber);
|
||||
}
|
||||
|
||||
let document = self.pdfium.load_document(pdf_bytes.to_vec(), password)?;
|
||||
let page_count = document.page_count();
|
||||
|
||||
if let Some(&page) = pages.iter().find(|&&page| page as usize > page_count) {
|
||||
return Err(RenderError::PageOutOfBounds { page, page_count });
|
||||
}
|
||||
|
||||
if options.form_fields {
|
||||
document.enable_form_rendering()?;
|
||||
}
|
||||
|
||||
let config = options.pdfium_config();
|
||||
let mut rendered_pages = Vec::with_capacity(pages.len());
|
||||
for &page_number in pages {
|
||||
let page = document.page(page_number as usize - 1)?;
|
||||
let size = page.size();
|
||||
let rendered = page.render(&config)?;
|
||||
rendered_pages.push(RenderedPage::from_pdfium(
|
||||
page_number,
|
||||
size.width,
|
||||
size.height,
|
||||
options.pixel_format,
|
||||
rendered,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(rendered_pages)
|
||||
}
|
||||
}
|
||||
|
||||
fn bgr_to_rgb_in_place(pixels: &mut [u8], width: u32, height: u32, stride: usize) {
|
||||
let row_bytes = width as usize * RenderPixelFormat::Rgb8.bytes_per_pixel();
|
||||
assert!(stride >= row_bytes, "pixel stride is shorter than one row");
|
||||
assert_eq!(
|
||||
pixels.len(),
|
||||
stride * height as usize,
|
||||
"pixel buffer length does not match stride and height"
|
||||
);
|
||||
|
||||
for row in pixels.chunks_exact_mut(stride) {
|
||||
for pixel in row[..row_bytes].chunks_exact_mut(3) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn defaults_are_ocr_oriented_and_bounded() {
|
||||
let options = RenderOptions::default();
|
||||
assert_eq!(options.dpi, 150.0);
|
||||
assert_eq!(options.pixel_format, RenderPixelFormat::Rgb8);
|
||||
assert!(options.annotations);
|
||||
assert!(options.form_fields);
|
||||
assert_eq!(options.max_output_bytes_per_page, 256 * 1024 * 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bgr_pixels_are_converted_to_rgb_in_place() {
|
||||
let mut pixels = vec![1, 2, 3, 4, 5, 6];
|
||||
bgr_to_rgb_in_place(&mut pixels, 2, 1, 6);
|
||||
assert_eq!(pixels, [3, 2, 1, 6, 5, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bgr_conversion_skips_row_padding() {
|
||||
let mut pixels = vec![1, 2, 3, 9, 7, 8, 9, 6];
|
||||
bgr_to_rgb_in_place(&mut pixels, 1, 2, 4);
|
||||
assert_eq!(pixels, [3, 2, 1, 9, 9, 8, 7, 6]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#![cfg(all(feature = "render-pdfium", not(target_arch = "wasm32")))]
|
||||
|
||||
use pdf_inspector::vision::{PdfiumRenderer, RenderError, RenderOptions, RenderPixelFormat};
|
||||
|
||||
fn load_renderer() -> Option<PdfiumRenderer> {
|
||||
match PdfiumRenderer::load() {
|
||||
Ok(renderer) => Some(renderer),
|
||||
Err(RenderError::Pdfium(firecrawl_pdfium::Error::Load(
|
||||
firecrawl_pdfium::LoadError::LibraryNotFound { .. },
|
||||
))) => {
|
||||
eprintln!("skipping PDFium runtime test because no native library is installed");
|
||||
None
|
||||
}
|
||||
Err(error) => panic!("failed to load PDFium: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_owned_rgb_page_and_round_trips_coordinates() {
|
||||
let Some(renderer) = load_renderer() else {
|
||||
return;
|
||||
};
|
||||
let bytes = std::fs::read("tests/fixtures/thermo-freon12.pdf").unwrap();
|
||||
let pages = renderer
|
||||
.render_pages(
|
||||
&bytes,
|
||||
&[1],
|
||||
None,
|
||||
&RenderOptions::new().dpi(150.0).form_fields(false),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(pages.len(), 1);
|
||||
let page = &pages[0];
|
||||
assert_eq!(page.page(), 1);
|
||||
assert_eq!(page.format(), RenderPixelFormat::Rgb8);
|
||||
assert_eq!(page.stride(), page.width() as usize * 3);
|
||||
assert_eq!(page.pixels().len(), page.stride() * page.height() as usize);
|
||||
assert!((page.width() as f32 - page.page_width()).abs() > 1.0);
|
||||
|
||||
let pdf_rect = page.pixel_rect_to_pdf_rect(10.0, 10.0, 20.0, 12.0);
|
||||
let pixel_rect = page.pdf_rect_to_pixel(&pdf_rect);
|
||||
assert!((pixel_rect.0 - 10.0).abs() < 0.01);
|
||||
assert!((pixel_rect.1 - 10.0).abs() < 0.01);
|
||||
assert!((pixel_rect.2 - 20.0).abs() < 0.01);
|
||||
assert!((pixel_rect.3 - 12.0).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_and_out_of_range_page_numbers() {
|
||||
let Some(renderer) = load_renderer() else {
|
||||
return;
|
||||
};
|
||||
let bytes = std::fs::read("tests/fixtures/thermo-freon12.pdf").unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
renderer.render_pages(&bytes, &[0], None, &RenderOptions::new()),
|
||||
Err(RenderError::InvalidPageNumber)
|
||||
));
|
||||
|
||||
assert!(matches!(
|
||||
renderer.render_pages(&bytes, &[u32::MAX], None, &RenderOptions::new()),
|
||||
Err(RenderError::PageOutOfBounds { .. })
|
||||
));
|
||||
}
|
||||
Reference in New Issue
Block a user