Skip to main content

pdfium_render/
pdfium.rs

1//! Defines the [Pdfium] struct, a high-level idiomatic Rust wrapper around Pdfium.
2
3use crate::bindings::PdfiumLibraryBindings;
4use crate::error::{PdfiumError, PdfiumInternalError};
5use crate::font_provider::FontDescriptor;
6use crate::pdf::document::{PdfDocument, PdfDocumentVersion};
7use once_cell::sync::OnceCell;
8use std::ffi::CString;
9use std::fmt::{Debug, Formatter};
10
11#[cfg(all(not(target_arch = "wasm32"), not(pdfium_use_static)))]
12use {
13    crate::bindings::dynamic_bindings::DynamicPdfiumBindings, libloading::Library, std::ffi::OsString,
14    std::path::PathBuf,
15};
16
17#[cfg(all(not(target_arch = "wasm32"), pdfium_use_static))]
18use crate::bindings::static_bindings::StaticPdfiumBindings;
19
20#[cfg(not(target_arch = "wasm32"))]
21use {
22    crate::utils::files::get_pdfium_file_accessor_from_reader,
23    std::fs::File,
24    std::io::{Read, Seek},
25    std::path::Path,
26};
27
28#[cfg(target_arch = "wasm32")]
29use {
30    js_sys::{ArrayBuffer, Uint8Array},
31    wasm_bindgen::JsCast,
32    wasm_bindgen_futures::JsFuture,
33    web_sys::{Blob, Response, window},
34};
35
36#[cfg(doc)]
37struct Blob;
38
39static BINDINGS: OnceCell<Box<dyn PdfiumLibraryBindings>> = OnceCell::new();
40
41#[cfg(feature = "thread_safe")]
42pub(crate) trait PdfiumLibraryBindingsAccessor: Send + Sync {
43    fn bindings(&self) -> &dyn PdfiumLibraryBindings {
44        BINDINGS.wait().as_ref()
45    }
46}
47
48#[cfg(not(feature = "thread_safe"))]
49pub(crate) trait PdfiumLibraryBindingsAccessor {
50    fn bindings(&self) -> &dyn PdfiumLibraryBindings {
51        BINDINGS.get().unwrap().as_ref()
52    }
53}
54
55/// Configuration options for initializing the Pdfium library.
56#[derive(Debug, Default, Clone)]
57pub struct PdfiumConfig {
58    user_font_paths: Option<Vec<String>>,
59    font_provider: Option<Vec<FontDescriptor>>,
60}
61
62impl PdfiumConfig {
63    /// Creates a new [PdfiumConfig] with default settings.
64    pub fn new() -> Self {
65        Self::default()
66    }
67
68    /// Sets the paths to scan for fonts in addition to the default system paths.
69    ///
70    /// This is useful when you want to use custom fonts that are not installed on the system.
71    pub fn set_user_font_paths(mut self, paths: Vec<String>) -> Self {
72        self.user_font_paths = Some(paths);
73        self
74    }
75
76    /// Sets a custom font provider with pre-loaded font data.
77    ///
78    /// This bypasses all filesystem scanning and serves fonts directly from memory.
79    /// Fonts are matched by family name, weight, italic style, and charset.
80    ///
81    /// # Example
82    /// ```rust,no_run
83    /// use pdfium_render::prelude::*;
84    /// use std::sync::Arc;
85    ///
86    /// let fonts = vec![
87    ///     FontDescriptor {
88    ///         family: "Arial".to_string(),
89    ///         weight: 400,
90    ///         is_italic: false,
91    ///         charset: 0,
92    ///         data: Arc::from(std::fs::read("/fonts/Arial.ttf")?),
93    ///     },
94    /// ];
95    ///
96    /// let config = PdfiumConfig::new()
97    ///     .set_font_provider(fonts);
98    /// # Ok::<(), pdfium_render::error::PdfiumError>(())
99    /// ```
100    pub fn set_font_provider(mut self, fonts: Vec<FontDescriptor>) -> Self {
101        self.font_provider = Some(fonts);
102        self
103    }
104}
105
106/// A high-level idiomatic Rust wrapper around Pdfium, the C++ PDF library used by
107/// the Google Chromium project.
108#[derive(Clone)]
109pub struct Pdfium;
110
111impl Pdfium {
112    /// Binds to a Pdfium library that was statically linked into the currently running
113    /// executable, returning a new [PdfiumLibraryBindings] object that contains bindings to the
114    /// functions exposed by the library. The application will immediately crash if Pdfium
115    /// was not correctly statically linked into the executable at compile time.
116    ///
117    /// This function is only available when this crate's `static` feature is enabled.
118    #[cfg(not(target_arch = "wasm32"))]
119    #[cfg(any(doc, pdfium_use_static))]
120    #[inline]
121    pub fn bind_to_statically_linked_library() -> Result<Box<dyn PdfiumLibraryBindings>, PdfiumError> {
122        if BINDINGS.get().is_none() {
123            let bindings = StaticPdfiumBindings::new();
124
125            Ok(Box::new(bindings))
126        } else {
127            Err(PdfiumError::PdfiumLibraryBindingsAlreadyInitialized)
128        }
129    }
130
131    /// Initializes the external Pdfium library, loading it from the system libraries.
132    /// Returns a new [PdfiumLibraryBindings] object that contains bindings to the functions exposed
133    /// by the library, or an error if the library could not be loaded.
134    #[cfg(not(target_arch = "wasm32"))]
135    #[cfg(not(pdfium_use_static))]
136    #[inline]
137    pub fn bind_to_system_library() -> Result<Box<dyn PdfiumLibraryBindings>, PdfiumError> {
138        if BINDINGS.get().is_none() {
139            let bindings = DynamicPdfiumBindings::new(
140                unsafe { Library::new(Self::pdfium_platform_library_name()) }.map_err(PdfiumError::LoadLibraryError)?,
141            )?;
142
143            Ok(Box::new(bindings))
144        } else {
145            Err(PdfiumError::PdfiumLibraryBindingsAlreadyInitialized)
146        }
147    }
148
149    /// Initializes the external pdfium library, loading it from the given path.
150    /// Returns a new [PdfiumLibraryBindings] object that contains bindings to the functions
151    /// exposed by the library, or an error if the library could not be loaded.
152    #[cfg(not(target_arch = "wasm32"))]
153    #[cfg(not(pdfium_use_static))]
154    #[inline]
155    pub fn bind_to_library(path: impl AsRef<Path>) -> Result<Box<dyn PdfiumLibraryBindings>, PdfiumError> {
156        if BINDINGS.get().is_none() {
157            let bindings = DynamicPdfiumBindings::new(
158                unsafe { Library::new(path.as_ref().as_os_str()) }.map_err(PdfiumError::LoadLibraryError)?,
159            )?;
160
161            Ok(Box::new(bindings))
162        } else {
163            Err(PdfiumError::PdfiumLibraryBindingsAlreadyInitialized)
164        }
165    }
166
167    /// Returns the name of the external Pdfium library on the currently running platform.
168    /// On Linux and Android, this will be `libpdfium.so` or similar; on Windows, this will
169    /// be `pdfium.dll` or similar; on MacOS, this will be `libpdfium.dylib` or similar.
170    #[cfg(not(target_arch = "wasm32"))]
171    #[cfg(not(pdfium_use_static))]
172    #[inline]
173    pub fn pdfium_platform_library_name() -> OsString {
174        libloading::library_filename("pdfium")
175    }
176
177    /// Returns the name of the external Pdfium library on the currently running platform,
178    /// prefixed with the given path string.
179    #[cfg(not(target_arch = "wasm32"))]
180    #[cfg(not(pdfium_use_static))]
181    #[inline]
182    pub fn pdfium_platform_library_name_at_path(path: &(impl AsRef<Path> + ?Sized)) -> PathBuf {
183        path.as_ref().join(Pdfium::pdfium_platform_library_name())
184    }
185
186    /// Creates a new [Pdfium] instance from the given external Pdfium library bindings.
187    #[inline]
188    pub fn new(bindings: Box<dyn PdfiumLibraryBindings>) -> Self {
189        Pdfium::new_with_config(bindings, &PdfiumConfig::default())
190    }
191
192    /// Creates a new [Pdfium] instance from the given external Pdfium library bindings and configuration.
193    ///
194    /// # Performance Note
195    ///
196    /// When no configuration is provided (default `PdfiumConfig`), this method uses the simple
197    /// `FPDF_InitLibrary()` function to avoid potential font enumeration overhead that can occur
198    /// with `FPDF_InitLibraryWithConfig()`. This optimization eliminates ~52ms overhead on
199    /// documents with many fonts (e.g., academic PDFs with 18+ custom Type 1 fonts).
200    ///
201    /// Configuration features (`user_font_paths` and `font_provider`) are only initialized when
202    /// explicitly set, ensuring zero overhead when not in use.
203    #[inline]
204    pub fn new_with_config(bindings: Box<dyn PdfiumLibraryBindings>, config: &PdfiumConfig) -> Self {
205        assert!(BINDINGS.get().is_none());
206
207        let has_user_font_paths =
208            config.user_font_paths.is_some() && !config.user_font_paths.as_ref().unwrap().is_empty();
209        let has_font_provider = config.font_provider.is_some() && !config.font_provider.as_ref().unwrap().is_empty();
210
211        if !has_user_font_paths && !has_font_provider {
212            bindings.FPDF_InitLibrary();
213        } else {
214            let mut c_strings = Vec::new();
215            let mut c_ptrs = Vec::new();
216
217            if let Some(paths) = &config.user_font_paths {
218                for path in paths {
219                    if let Ok(c_str) = CString::new(path.as_str()) {
220                        c_ptrs.push(c_str.as_ptr());
221                        c_strings.push(c_str);
222                    }
223                }
224                c_ptrs.push(std::ptr::null());
225            }
226
227            let font_paths_ptr = if c_ptrs.is_empty() {
228                std::ptr::null_mut()
229            } else {
230                Box::leak(c_strings.into_boxed_slice());
231
232                let leaked_ptrs = Box::leak(c_ptrs.into_boxed_slice());
233                leaked_ptrs.as_mut_ptr()
234            };
235
236            let library_config = crate::bindgen::FPDF_LIBRARY_CONFIG_ {
237                version: 2,
238                m_pUserFontPaths: font_paths_ptr,
239                m_pIsolate: std::ptr::null_mut(),
240                m_v8EmbedderSlot: 0,
241                m_pPlatform: std::ptr::null_mut(),
242                m_RendererType: 0,
243            };
244
245            bindings
246                .FPDF_InitLibraryWithConfig(&library_config as *const _ as *const crate::bindgen::FPDF_LIBRARY_CONFIG);
247
248            if let Some(font_descriptors) = &config.font_provider
249                && !font_descriptors.is_empty()
250            {
251                use crate::font_provider::MemoryFontProvider;
252
253                let provider = MemoryFontProvider::new(font_descriptors.clone());
254                let mut boxed_provider = Box::new(provider);
255
256                let provider_ptr = boxed_provider.as_mut_ptr();
257
258                let _leaked_provider = Box::leak(boxed_provider);
259
260                bindings.FPDF_SetSystemFontInfo(provider_ptr);
261            }
262        }
263
264        assert!(BINDINGS.set(bindings).is_ok());
265
266        Self {}
267    }
268
269    /// Attempts to open a [PdfDocument] from the given static byte buffer.
270    ///
271    /// If the document is password protected, the given password will be used to unlock it.
272    pub fn load_pdf_from_byte_slice<'a>(
273        &'a self,
274        bytes: &'a [u8],
275        password: Option<&str>,
276    ) -> Result<PdfDocument<'a>, PdfiumError> {
277        Self::pdfium_document_handle_to_result(self.bindings().FPDF_LoadMemDocument64(bytes, password), self.bindings())
278    }
279
280    /// Attempts to open a [PdfDocument] from the given owned byte buffer.
281    ///
282    /// If the document is password protected, the given password will be used to unlock it.
283    ///
284    /// `pdfium-render` will take ownership of the given byte buffer, ensuring its lifetime lasts
285    /// as long as the [PdfDocument] opened from it.
286    pub fn load_pdf_from_byte_vec(
287        &self,
288        bytes: Vec<u8>,
289        password: Option<&str>,
290    ) -> Result<PdfDocument<'_>, PdfiumError> {
291        Self::pdfium_document_handle_to_result(
292            self.bindings().FPDF_LoadMemDocument64(bytes.as_slice(), password),
293            self.bindings(),
294        )
295        .map(|mut document| {
296            document.set_source_byte_buffer(bytes);
297
298            document
299        })
300    }
301
302    /// Attempts to open a [PdfDocument] from the given file path.
303    ///
304    /// If the document is password protected, the given password will be used
305    /// to unlock it.
306    ///
307    /// This function is not available when compiling to WASM. You have several options for
308    /// loading your PDF document data in WASM:
309    /// * Use the [Pdfium::load_pdf_from_fetch()] function to download document data from a
310    ///   URL using the browser's built-in `fetch` API. This function is only available when
311    ///   compiling to WASM.
312    /// * Use the [Pdfium::load_pdf_from_blob()] function to load document data from a
313    ///   Javascript `File` or `Blob` object (such as a `File` object returned from an HTML
314    ///   `<input type="file">` element). This function is only available when compiling to WASM.
315    /// * Use another method to retrieve the bytes of the target document over the network,
316    ///   then load those bytes into Pdfium using either the [Pdfium::load_pdf_from_byte_slice()]
317    ///   function or the [Pdfium::load_pdf_from_byte_vec()] function.
318    /// * Embed the bytes of the target document directly into the compiled WASM module
319    ///   using the `include_bytes!` macro.
320    #[cfg(not(target_arch = "wasm32"))]
321    pub fn load_pdf_from_file<'a>(
322        &'a self,
323        path: &(impl AsRef<Path> + ?Sized),
324        password: Option<&'a str>,
325    ) -> Result<PdfDocument<'a>, PdfiumError> {
326        self.load_pdf_from_reader(File::open(path).map_err(PdfiumError::IoError)?, password)
327    }
328
329    /// Attempts to open a [PdfDocument] from the given reader.
330    ///
331    /// Pdfium will only load the portions of the document it actually needs into memory.
332    /// This is more efficient than loading the entire document into memory, especially when
333    /// working with large documents, and allows for working with documents larger than the
334    /// amount of available memory.
335    ///
336    /// Because Pdfium must know the total content length in advance prior to loading
337    /// any portion of it, the given reader must implement the [Seek] trait as well as
338    /// the [Read] trait.
339    ///
340    /// If the document is password protected, the given password will be used
341    /// to unlock it.
342    ///
343    /// This function is not available when compiling to WASM. You have several options for
344    /// loading your PDF document data in WASM:
345    /// * Use the [Pdfium::load_pdf_from_fetch()] function to download document data from a
346    ///   URL using the browser's built-in `fetch` API. This function is only available when
347    ///   compiling to WASM.
348    /// * Use the [Pdfium::load_pdf_from_blob()] function to load document data from a
349    ///   Javascript `File` or `Blob` object (such as a `File` object returned from an HTML
350    ///   `<input type="file">` element). This function is only available when compiling to WASM.
351    /// * Use another method to retrieve the bytes of the target document over the network,
352    ///   then load those bytes into Pdfium using either the [Pdfium::load_pdf_from_byte_slice()]
353    ///   function or the [Pdfium::load_pdf_from_byte_vec()] function.
354    /// * Embed the bytes of the target document directly into the compiled WASM module
355    ///   using the `include_bytes!` macro.
356    #[cfg(not(target_arch = "wasm32"))]
357    pub fn load_pdf_from_reader<'a, R: Read + Seek + 'a>(
358        &'a self,
359        reader: R,
360        password: Option<&'a str>,
361    ) -> Result<PdfDocument<'a>, PdfiumError> {
362        let mut reader = get_pdfium_file_accessor_from_reader(reader);
363
364        Pdfium::pdfium_document_handle_to_result(
365            self.bindings()
366                .FPDF_LoadCustomDocument(reader.as_fpdf_file_access_mut_ptr(), password),
367            self.bindings(),
368        )
369        .map(|mut document| {
370            document.set_file_access_reader(reader);
371
372            document
373        })
374    }
375
376    /// Attempts to open a [PdfDocument] by loading document data from the given URL.
377    /// The Javascript `fetch` API is used to download data over the network.
378    ///
379    /// If the document is password protected, the given password will be used to unlock it.
380    ///
381    /// This function is only available when compiling to WASM.
382    #[cfg(any(doc, target_arch = "wasm32"))]
383    pub async fn load_pdf_from_fetch<'a>(
384        &'a self,
385        url: impl ToString,
386        password: Option<&str>,
387    ) -> Result<PdfDocument<'a>, PdfiumError> {
388        if let Some(window) = window() {
389            let fetch_result = JsFuture::from(window.fetch_with_str(url.to_string().as_str()))
390                .await
391                .map_err(PdfiumError::WebSysFetchError)?;
392
393            debug_assert!(fetch_result.is_instance_of::<Response>());
394
395            let response: Response = fetch_result
396                .dyn_into()
397                .map_err(|_| PdfiumError::WebSysInvalidResponseError)?;
398
399            let blob: Blob = JsFuture::from(response.blob().map_err(PdfiumError::WebSysFetchError)?)
400                .await
401                .map_err(PdfiumError::WebSysFetchError)?
402                .into();
403
404            self.load_pdf_from_blob(blob, password).await
405        } else {
406            Err(PdfiumError::WebSysWindowObjectNotAvailable)
407        }
408    }
409
410    /// Attempts to open a [PdfDocument] by loading document data from the given `Blob`.
411    /// A `File` object returned from a `FileList` is a suitable `Blob`:
412    ///
413    /// ```text
414    /// <input id="filePicker" type="file">
415    ///
416    /// const file = document.getElementById('filePicker').files[0];
417    /// ```
418    ///
419    /// If the document is password protected, the given password will be used to unlock it.
420    ///
421    /// This function is only available when compiling to WASM.
422    #[cfg(any(doc, target_arch = "wasm32"))]
423    pub async fn load_pdf_from_blob<'a>(
424        &'a self,
425        blob: Blob,
426        password: Option<&str>,
427    ) -> Result<PdfDocument<'a>, PdfiumError> {
428        let array_buffer: ArrayBuffer = JsFuture::from(blob.array_buffer())
429            .await
430            .map_err(PdfiumError::WebSysFetchError)?
431            .into();
432
433        let u8_array: Uint8Array = Uint8Array::new(&array_buffer);
434
435        let bytes: Vec<u8> = u8_array.to_vec();
436
437        self.load_pdf_from_byte_vec(bytes, password)
438    }
439
440    /// Creates a new, empty [PdfDocument] in memory.
441    pub fn create_new_pdf(&self) -> Result<PdfDocument<'_>, PdfiumError> {
442        Self::pdfium_document_handle_to_result(self.bindings().FPDF_CreateNewDocument(), self.bindings()).map(
443            |mut document| {
444                document.set_version(PdfDocumentVersion::DEFAULT_VERSION);
445
446                document
447            },
448        )
449    }
450
451    /// Returns a [PdfDocument] from the given `FPDF_DOCUMENT` handle, if possible.
452    pub(crate) fn pdfium_document_handle_to_result(
453        handle: crate::bindgen::FPDF_DOCUMENT,
454        bindings: &dyn PdfiumLibraryBindings,
455    ) -> Result<PdfDocument<'_>, PdfiumError> {
456        if handle.is_null() {
457            #[allow(clippy::unnecessary_cast)]
458            if let Some(error) = match bindings.FPDF_GetLastError() as u32 {
459                crate::bindgen::FPDF_ERR_SUCCESS => None,
460                crate::bindgen::FPDF_ERR_UNKNOWN => Some(PdfiumInternalError::Unknown),
461                crate::bindgen::FPDF_ERR_FILE => Some(PdfiumInternalError::FileError),
462                crate::bindgen::FPDF_ERR_FORMAT => Some(PdfiumInternalError::FormatError),
463                crate::bindgen::FPDF_ERR_PASSWORD => Some(PdfiumInternalError::PasswordError),
464                crate::bindgen::FPDF_ERR_SECURITY => Some(PdfiumInternalError::SecurityError),
465                crate::bindgen::FPDF_ERR_PAGE => Some(PdfiumInternalError::PageError),
466                _ => None,
467            } {
468                Err(PdfiumError::PdfiumLibraryInternalError(error))
469            } else {
470                Err(PdfiumError::PdfiumLibraryInternalError(PdfiumInternalError::Unknown))
471            }
472        } else {
473            Ok(PdfDocument::from_pdfium(handle, bindings))
474        }
475    }
476}
477
478impl PdfiumLibraryBindingsAccessor for Pdfium {}
479
480impl Default for Pdfium {
481    /// Binds to a Pdfium library that was statically linked into the currently running
482    /// executable by calling [Pdfium::bind_to_statically_linked_library]. This function
483    /// will panic if no statically linked Pdfium functions can be located.
484    #[cfg(pdfium_use_static)]
485    #[inline]
486    fn default() -> Self {
487        Pdfium::new(Pdfium::bind_to_statically_linked_library().unwrap())
488    }
489
490    /// Binds to an external Pdfium library by first attempting to bind to a Pdfium library
491    /// in the current working directory; if that fails, then a system-provided library
492    /// will be used as a fall back.
493    ///
494    /// This function will panic if no suitable Pdfium library can be loaded.
495    #[cfg(not(pdfium_use_static))]
496    #[cfg(not(target_arch = "wasm32"))]
497    #[inline]
498    fn default() -> Self {
499        match Pdfium::bind_to_library(Pdfium::pdfium_platform_library_name_at_path("./")) {
500            Ok(bindings) => Pdfium::new(bindings),
501            Err(PdfiumError::PdfiumLibraryBindingsAlreadyInitialized) => Pdfium {},
502            Err(PdfiumError::LoadLibraryError(err)) => match err {
503                libloading::Error::DlOpen { .. } => Pdfium::new(Pdfium::bind_to_system_library().unwrap()),
504                _ => panic!("Failed to load Pdfium library: {:?}", err),
505            },
506            Err(err) => panic!("Failed to initialize Pdfium: {:?}", err),
507        }
508    }
509}
510
511impl Debug for Pdfium {
512    #[inline]
513    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
514        f.debug_struct("Pdfium").finish()
515    }
516}
517
518#[cfg(feature = "thread_safe")]
519unsafe impl Sync for Pdfium {}
520
521#[cfg(feature = "thread_safe")]
522unsafe impl Send for Pdfium {}