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}