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