Skip to main content

pdfium_render/pdf/document/
attachments.rs

1//! Defines the [PdfAttachments] struct, a collection of all the `PdfAttachment` objects in a
2//! `PdfDocument`.
3
4use crate::bindgen::FPDF_DOCUMENT;
5use crate::error::{PdfiumError, PdfiumInternalError};
6use crate::pdf::document::attachment::PdfAttachment;
7use crate::pdfium::PdfiumLibraryBindingsAccessor;
8use std::io::Read;
9use std::marker::PhantomData;
10use std::ops::{Range, RangeInclusive};
11use std::os::raw::{c_int, c_ulong, c_void};
12
13#[cfg(not(target_arch = "wasm32"))]
14use {std::fs::File, std::path::Path};
15
16#[cfg(target_arch = "wasm32")]
17use {
18    js_sys::{ArrayBuffer, Uint8Array},
19    wasm_bindgen::JsCast,
20    wasm_bindgen_futures::JsFuture,
21    web_sys::{window, Blob, Response},
22};
23
24// The following dummy declaration is used only when running cargo doc.
25// It allows documentation of WASM-specific functionality to be included
26// in documentation generated on non-WASM targets.
27
28#[cfg(doc)]
29struct Blob;
30
31#[cfg(doc)]
32use crate::pdf::document::PdfDocument;
33
34/// The zero-based index of a single attachment inside its containing [PdfAttachments] collection.
35pub type PdfAttachmentIndex = u16;
36
37/// The collection of [PdfAttachment] objects embedded in a [PdfDocument].
38pub struct PdfAttachments<'a> {
39    document_handle: FPDF_DOCUMENT,
40    lifetime: PhantomData<&'a FPDF_DOCUMENT>,
41}
42
43impl<'a> PdfAttachments<'a> {
44    #[inline]
45    pub(crate) fn from_pdfium(document_handle: FPDF_DOCUMENT) -> Self {
46        PdfAttachments {
47            document_handle,
48            lifetime: PhantomData,
49        }
50    }
51
52    /// Returns the number of attachments in this [PdfAttachments] collection.
53    pub fn len(&self) -> PdfAttachmentIndex {
54        (unsafe {
55            self.bindings()
56                .FPDFDoc_GetAttachmentCount(self.document_handle)
57        }) as PdfAttachmentIndex
58    }
59
60    /// Returns `true` if this [PdfAttachments] collection is empty.
61    #[inline]
62    pub fn is_empty(&self) -> bool {
63        self.len() == 0
64    }
65
66    /// Returns a Range from `0..(number of attachments)` for this [PdfAttachments] collection.
67    #[inline]
68    pub fn as_range(&self) -> Range<PdfAttachmentIndex> {
69        0..self.len()
70    }
71
72    /// Returns an inclusive Range from `0..=(number of attachments - 1)`
73    /// for this [PdfAttachments] collection.
74    #[inline]
75    pub fn as_range_inclusive(&self) -> RangeInclusive<PdfAttachmentIndex> {
76        if self.is_empty() {
77            0..=0
78        } else {
79            0..=(self.len() - 1)
80        }
81    }
82
83    /// Returns a single [PdfAttachment] from this [PdfAttachments] collection.
84    pub fn get(&self, index: PdfAttachmentIndex) -> Result<PdfAttachment<'a>, PdfiumError> {
85        if index >= self.len() {
86            return Err(PdfiumError::AttachmentIndexOutOfBounds);
87        }
88
89        let handle = unsafe {
90            self.bindings()
91                .FPDFDoc_GetAttachment(self.document_handle, index as c_int)
92        };
93
94        if handle.is_null() {
95            Err(PdfiumError::PdfiumLibraryInternalError(
96                PdfiumInternalError::Unknown,
97            ))
98        } else {
99            Ok(PdfAttachment::from_pdfium(handle))
100        }
101    }
102
103    /// Attempts to add a new [PdfAttachment] to this collection, using the given name and the
104    /// data in the given byte buffer. An error will be returned if the given name is not
105    /// unique in the list of attachments already present in the containing PDF document.
106    pub fn create_attachment_from_bytes(
107        &mut self,
108        name: &str,
109        bytes: &[u8],
110    ) -> Result<PdfAttachment<'_>, PdfiumError> {
111        // Creating the attachment is a two step operation. First, we create the FPDF_ATTACHMENT
112        // handle using the given name. Then, we add the given byte data to the FPDF_ATTACHMENT.
113
114        let handle = unsafe {
115            self.bindings()
116                .FPDFDoc_AddAttachment_str(self.document_handle, name)
117        };
118
119        if handle.is_null() {
120            Err(PdfiumError::PdfiumLibraryInternalError(
121                PdfiumInternalError::Unknown,
122            ))
123        } else {
124            // With the FPDF_ATTACHMENT correctly created, we can now apply the byte data to the attachment.
125
126            if self.bindings().is_true(unsafe {
127                self.bindings().FPDFAttachment_SetFile(
128                    handle,
129                    self.document_handle,
130                    bytes.as_ptr() as *const c_void,
131                    bytes.len() as c_ulong,
132                )
133            }) {
134                Ok(PdfAttachment::from_pdfium(handle))
135            } else {
136                // The return value from FPDFAttachment_SetFile() indicates failure.
137
138                Err(PdfiumError::PdfiumLibraryInternalError(
139                    PdfiumInternalError::Unknown,
140                ))
141            }
142        }
143    }
144
145    /// Attempts to add a new [PdfAttachment] to this collection, using the given name and file path.
146    /// Byte data from the given file path will be embedded directly into the containing document.
147    /// An error will be returned if the given name is not unique in the list of attachments
148    /// already present in the containing PDF document.
149    ///
150    /// This function is not available when compiling to WASM. You have several options for
151    /// loading attachment data in WASM:
152    /// * Use the [PdfAttachments::create_attachment_from_fetch()] function to download attachment data
153    ///   from a URL using the browser's built-in `fetch()` API. This function is only available when
154    ///   compiling to WASM.
155    /// * Use the [PdfAttachments::create_attachment_from_blob()] function to load attachment data
156    ///   from a Javascript `File` or `Blob` object (such as a `File` object returned from an HTML
157    ///   `<input type="file">` element). This function is only available when compiling to WASM.
158    /// * Use another method to retrieve the bytes of the target attachment over the network,
159    ///   then load those bytes into Pdfium using the [PdfAttachments::create_attachment_from_bytes()] function.
160    /// * Embed the bytes of the target attachment directly into the compiled WASM module
161    ///   using the `include_bytes!()` macro.
162    #[cfg(not(target_arch = "wasm32"))]
163    pub fn create_attachment_from_file(
164        &mut self,
165        name: &str,
166        path: &(impl AsRef<Path> + ?Sized),
167    ) -> Result<PdfAttachment<'_>, PdfiumError> {
168        self.create_attachment_from_reader(name, File::open(path).map_err(PdfiumError::IoError)?)
169    }
170
171    /// Attempts to add a new [PdfAttachment] to this collection, using the given name
172    /// and the given reader. Byte data from the given reader will be embedded directly into
173    /// the containing document. An error will be returned if the given name is not
174    /// unique in the list of attachments already present in the containing PDF document.
175    pub fn create_attachment_from_reader<R: Read>(
176        &mut self,
177        name: &str,
178        mut reader: R,
179    ) -> Result<PdfAttachment<'_>, PdfiumError> {
180        let mut bytes = Vec::new();
181
182        reader
183            .read_to_end(&mut bytes)
184            .map_err(PdfiumError::IoError)?;
185
186        self.create_attachment_from_bytes(name, bytes.as_slice())
187    }
188
189    /// Attempts to add a new [PdfAttachment] to this collection by loading attachment data
190    /// from the given URL. The Javascript `fetch()` API is used to download data over the network.
191    /// Byte data retrieved from the given URL will be embedded directly into the containing document.
192    /// An error will be returned if the given name is not unique in the list of attachments
193    /// already present in the containing PDF document.
194    ///
195    /// This function is only available when compiling to WASM.
196    #[cfg(any(doc, target_arch = "wasm32"))]
197    pub async fn create_attachment_from_fetch(
198        &'a mut self,
199        name: &str,
200        url: impl ToString,
201    ) -> Result<PdfAttachment<'a>, PdfiumError> {
202        if let Some(window) = window() {
203            let fetch_result = JsFuture::from(window.fetch_with_str(url.to_string().as_str()))
204                .await
205                .map_err(PdfiumError::WebSysFetchError)?;
206
207            debug_assert!(fetch_result.is_instance_of::<Response>());
208
209            let response: Response = fetch_result
210                .dyn_into()
211                .map_err(|_| PdfiumError::WebSysInvalidResponseError)?;
212
213            let blob: Blob =
214                JsFuture::from(response.blob().map_err(PdfiumError::WebSysFetchError)?)
215                    .await
216                    .map_err(PdfiumError::WebSysFetchError)?
217                    .into();
218
219            self.create_attachment_from_blob(name, blob).await
220        } else {
221            Err(PdfiumError::WebSysWindowObjectNotAvailable)
222        }
223    }
224
225    /// Attempts to create a new [PdfAttachment] to this collection, using the given name and
226    /// the given `Blob`. Byte data from the given `Blob` will be embedded directly into
227    /// the containing document. An error will be returned if the given name is not
228    /// unique in the list of attachments already present in the containing PDF document.
229    /// A `File` object returned from a `FileList` is a suitable `Blob`:
230    ///
231    /// ```text
232    /// <input id="filePicker" type="file">
233    ///
234    /// const file = document.getElementById('filePicker').files[0];
235    /// ```
236    ///
237    /// This function is only available when compiling to WASM.
238    #[cfg(any(doc, target_arch = "wasm32"))]
239    pub async fn create_attachment_from_blob(
240        &'a mut self,
241        name: &str,
242        blob: Blob,
243    ) -> Result<PdfAttachment<'a>, PdfiumError> {
244        let array_buffer: ArrayBuffer = JsFuture::from(blob.array_buffer())
245            .await
246            .map_err(PdfiumError::WebSysFetchError)?
247            .into();
248
249        let u8_array: Uint8Array = Uint8Array::new(&array_buffer);
250
251        let bytes: Vec<u8> = u8_array.to_vec();
252
253        self.create_attachment_from_bytes(name, bytes.as_slice())
254    }
255
256    /// Deletes the attachment at the given index from this [PdfAttachments] collection.
257    ///
258    /// Pdfium's current implementation of this action does not remove the attachment data
259    /// from the document; it simply removes the attachment's index entry from the document,
260    /// so that the attachment no longer appears in the list of attachments.
261    /// This behavior may change in the future.
262    pub fn delete_at_index(&mut self, index: PdfAttachmentIndex) -> Result<(), PdfiumError> {
263        if index >= self.len() {
264            return Err(PdfiumError::AttachmentIndexOutOfBounds);
265        }
266
267        if self.bindings().is_true(unsafe {
268            self.bindings()
269                .FPDFDoc_DeleteAttachment(self.document_handle, index as c_int)
270        }) {
271            Ok(())
272        } else {
273            Err(PdfiumError::PdfiumLibraryInternalError(
274                PdfiumInternalError::Unknown,
275            ))
276        }
277    }
278
279    /// Returns an iterator over all the attachments in this [PdfAttachments] collection.
280    #[inline]
281    pub fn iter(&self) -> PdfAttachmentsIterator<'_> {
282        PdfAttachmentsIterator::new(self)
283    }
284}
285
286impl<'a> PdfiumLibraryBindingsAccessor<'a> for PdfAttachments<'a> {}
287
288#[cfg(feature = "thread_safe")]
289unsafe impl<'a> Send for PdfAttachments<'a> {}
290
291#[cfg(feature = "thread_safe")]
292unsafe impl<'a> Sync for PdfAttachments<'a> {}
293
294/// An iterator over all the [PdfAttachment] objects in a [PdfAttachments] collection.
295pub struct PdfAttachmentsIterator<'a> {
296    attachments: &'a PdfAttachments<'a>,
297    next_index: PdfAttachmentIndex,
298}
299
300impl<'a> PdfAttachmentsIterator<'a> {
301    #[inline]
302    pub(crate) fn new(signatures: &'a PdfAttachments<'a>) -> Self {
303        PdfAttachmentsIterator {
304            attachments: signatures,
305            next_index: 0,
306        }
307    }
308}
309
310impl<'a> Iterator for PdfAttachmentsIterator<'a> {
311    type Item = PdfAttachment<'a>;
312
313    fn next(&mut self) -> Option<Self::Item> {
314        let next = self.attachments.get(self.next_index);
315
316        self.next_index += 1;
317
318        next.ok()
319    }
320}