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}