1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
//! Defines the [PdfPages] struct, a collection of all the `PdfPage` objects in a
//! `PdfDocument`.
use crate::bindgen::{
size_t, FPDF_DOCUMENT, FPDF_PAGE, PAGEMODE_FULLSCREEN, PAGEMODE_UNKNOWN,
PAGEMODE_USEATTACHMENTS, PAGEMODE_USENONE, PAGEMODE_USEOC, PAGEMODE_USEOUTLINES,
PAGEMODE_USETHUMBS,
};
use crate::bindings::PdfiumLibraryBindings;
use crate::document::PdfDocument;
use crate::error::{PdfiumError, PdfiumInternalError};
use crate::page::{PdfPage, PdfPageContentRegenerationStrategy, PdfPoints};
use crate::page_index_cache::PdfPageIndexCache;
use crate::page_object_group::PdfPageGroupObject;
use crate::page_size::PdfPagePaperSize;
use crate::utils::mem::create_byte_buffer;
use crate::utils::utf16le::get_string_from_pdfium_utf16le_bytes;
use std::ops::{Range, RangeInclusive};
use std::os::raw::{c_double, c_int, c_void};
/// The zero-based index of a single [PdfPage] inside its containing [PdfPages] collection.
pub type PdfPageIndex = u16;
/// A hint to a PDF document reader (such as Adobe Acrobat) as to how the creator intended
/// the [PdfPage] objects in a [PdfDocument] to be displayed to the viewer when the document is opened.
#[derive(Debug, Copy, Clone)]
pub enum PdfPageMode {
/// No known page mode is set for this [PdfDocument].
UnsetOrUnknown = PAGEMODE_UNKNOWN as isize,
/// No page mode, i.e. neither the document outline nor thumbnail images should be visible,
/// no side panels should be visible, and the document should not be displayed in full screen mode.
None = PAGEMODE_USENONE as isize,
/// Outline page mode: the document outline should be visible.
ShowDocumentOutline = PAGEMODE_USEOUTLINES as isize,
/// Thumbnail page mode: page thumbnails should be visible.
ShowPageThumbnails = PAGEMODE_USETHUMBS as isize,
/// Fullscreen page mode: no menu bar, window controls, or other windows should be visible.
Fullscreen = PAGEMODE_FULLSCREEN as isize,
/// The optional content group panel should be visible.
ShowContentGroupPanel = PAGEMODE_USEOC as isize,
/// The attachments panel should be visible.
ShowAttachmentsPanel = PAGEMODE_USEATTACHMENTS as isize,
}
impl PdfPageMode {
#[inline]
pub(crate) fn from_pdfium(page_mode: i32) -> Option<Self> {
// The PAGEMODE_* enum constants are a mixture of i32 and u32 values :/
if page_mode == PAGEMODE_UNKNOWN {
return Some(PdfPageMode::UnsetOrUnknown);
}
match page_mode as u32 {
PAGEMODE_USENONE => Some(PdfPageMode::None),
PAGEMODE_USEOUTLINES => Some(PdfPageMode::ShowDocumentOutline),
PAGEMODE_USETHUMBS => Some(PdfPageMode::ShowPageThumbnails),
PAGEMODE_FULLSCREEN => Some(PdfPageMode::Fullscreen),
PAGEMODE_USEOC => Some(PdfPageMode::ShowContentGroupPanel),
PAGEMODE_USEATTACHMENTS => Some(PdfPageMode::ShowAttachmentsPanel),
_ => None,
}
}
}
/// The collection of [PdfPage] objects inside a [PdfDocument].
pub struct PdfPages<'a> {
document: &'a PdfDocument<'a>,
}
impl<'a> PdfPages<'a> {
/// Creates a new [PdfPages] collection from the given [PdfDocument].
#[inline]
pub(crate) fn new(document: &'a PdfDocument<'a>) -> Self {
PdfPages { document }
}
/// Returns a reference to the [PdfDocument] that contains this [PdfPages] collection.
#[inline]
pub(crate) fn document(&self) -> &'a PdfDocument<'a> {
self.document
}
/// Returns the [PdfiumLibraryBindings] used by the containing [PdfDocument].
#[inline]
pub fn bindings(&self) -> &'a dyn PdfiumLibraryBindings {
self.document().bindings()
}
/// Returns the number of pages in this [PdfPages] collection.
pub fn len(&self) -> PdfPageIndex {
self.bindings().FPDF_GetPageCount(*self.document().handle()) as PdfPageIndex
}
/// Returns `true` if this [PdfPages] collection is empty.
#[inline]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Returns a Range from `0..(number of pages)` for this [PdfPages] collection.
#[inline]
pub fn as_range(&self) -> Range<PdfPageIndex> {
0..self.len()
}
/// Returns an inclusive Range from `0..=(number of pages - 1)` for this [PdfPages] collection.
#[inline]
pub fn as_range_inclusive(&self) -> RangeInclusive<PdfPageIndex> {
if self.is_empty() {
0..=0
} else {
0..=(self.len() - 1)
}
}
/// Returns a single [PdfPage] from this [PdfPages] collection.
pub fn get(&self, index: PdfPageIndex) -> Result<PdfPage<'a>, PdfiumError> {
if index >= self.len() {
return Err(PdfiumError::PageIndexOutOfBounds);
}
let handle = self
.bindings()
.FPDF_LoadPage(*self.document().handle(), index as c_int);
let result = self.pdfium_page_handle_to_result(index, handle);
if result.is_ok() {
PdfPageIndexCache::set_index_for_page(*self.document.handle(), handle, index);
}
result
}
/// Returns the first [PdfPage] in this [PdfPages] collection.
#[inline]
pub fn first(&self) -> Result<PdfPage<'a>, PdfiumError> {
if !self.is_empty() {
self.get(0)
} else {
Err(PdfiumError::NoPagesInDocument)
}
}
/// Returns the last [PdfPage] in this [PdfPages] collection.
#[inline]
pub fn last(&self) -> Result<PdfPage<'a>, PdfiumError> {
if !self.is_empty() {
self.get(self.len() - 1)
} else {
Err(PdfiumError::NoPagesInDocument)
}
}
/// Creates a new, empty [PdfPage] with the given [PdfPagePaperSize] and inserts it
/// at the start of this [PdfPages] collection, shuffling down all other pages.
#[inline]
pub fn create_page_at_start(
&mut self,
size: PdfPagePaperSize,
) -> Result<PdfPage<'a>, PdfiumError> {
self.create_page_at_index(size, 0)
}
/// Creates a new, empty [PdfPage] with the given [PdfPagePaperSize] and adds it
/// to the end of this [PdfPages] collection.
#[inline]
pub fn create_page_at_end(
&mut self,
size: PdfPagePaperSize,
) -> Result<PdfPage<'a>, PdfiumError> {
self.create_page_at_index(size, self.len())
}
/// Creates a new, empty [PdfPage] with the given [PdfPagePaperSize] and inserts it
/// into this [PdfPages] collection at the given page index.
pub fn create_page_at_index(
&mut self,
size: PdfPagePaperSize,
index: PdfPageIndex,
) -> Result<PdfPage<'a>, PdfiumError> {
let result = self.pdfium_page_handle_to_result(
index,
self.bindings().FPDFPage_New(
*self.document().handle(),
index as c_int,
size.width().value as c_double,
size.height().value as c_double,
),
);
if let Ok(page) = result.as_ref() {
PdfPageIndexCache::insert_pages_at_index(*self.document.handle(), index, 1);
PdfPageIndexCache::set_index_for_page(*self.document.handle(), *page.handle(), index);
}
result
}
// TODO: AJRC - 5/2/23 - remove deprecated PdfPages::delete_page_range() function in 0.9.0
// as part of tracking issue: https://github.com/ajrcarey/pdfium-render/issues/36
// TODO: AJRC - 5/2/23 - if PdfDocument::pages() returned a &PdfPages reference (rather than an
// owned PdfPages instance), and if PdfPages::get() returned a &PdfPage reference (rather than an
// owned PdfPage instance), then it might be possible to reinstate this function, as Rust
// would be able to manage the reference lifetimes safely. Tracking issue:
// https://github.com/ajrcarey/pdfium-render/issues/47
/// Deletes the page at the given index from this [PdfPages] collection.
#[deprecated(
since = "0.7.30",
note = "This function has been deprecated. Use the PdfPage::delete() function instead."
)]
#[doc(hidden)]
pub fn delete_page_at_index(&mut self, index: PdfPageIndex) -> Result<(), PdfiumError> {
if index >= self.len() {
return Err(PdfiumError::PageIndexOutOfBounds);
}
self.bindings()
.FPDFPage_Delete(*self.document().handle(), index as c_int);
if let Some(error) = self.bindings().get_pdfium_last_error() {
Err(PdfiumError::PdfiumLibraryInternalError(error))
} else {
PdfPageIndexCache::delete_pages_at_index(*self.document.handle(), index, 1);
Ok(())
}
}
// TODO: AJRC - 5/2/23 - remove deprecated PdfPages::delete_page_range() function in 0.9.0
// as part of tracking issue: https://github.com/ajrcarey/pdfium-render/issues/36
// TODO: AJRC - 5/2/23 - if PdfDocument::pages() returned a &PdfPages reference (rather than an
// owned PdfPages instance), and if PdfPages::get() returned a &PdfPage reference (rather than an
// owned PdfPage instance), then it might be possible to reinstate this function, as Rust
// would be able to manage the reference lifetimes safely. Tracking issue:
// https://github.com/ajrcarey/pdfium-render/issues/47
/// Deletes all pages in the given range from this [PdfPages] collection.
#[deprecated(
since = "0.7.30",
note = "This function has been deprecated. Use the PdfPage::delete() function instead."
)]
#[doc(hidden)]
pub fn delete_page_range(&mut self, range: Range<PdfPageIndex>) -> Result<(), PdfiumError> {
for index in range.rev() {
#[allow(deprecated)] // Both functions will be removed at the same time.
self.delete_page_at_index(index)?;
}
Ok(())
}
/// Copies a single page with the given source page index from the given
/// source [PdfDocument], inserting it at the given destination page index
/// in this [PdfPages] collection.
pub fn copy_page_from_document(
&mut self,
source: &PdfDocument,
source_page_index: PdfPageIndex,
destination_page_index: PdfPageIndex,
) -> Result<(), PdfiumError> {
self.copy_page_range_from_document(
source,
source_page_index..=source_page_index,
destination_page_index,
)
}
/// Copies one or more pages, specified using a user-friendly page range string,
/// from the given source [PdfDocument], inserting the pages sequentially starting at the given
/// destination page index in this [PdfPages] collection.
///
/// The page range string should be in a comma-separated list of indexes and ranges,
/// for example \"1,3,5-7\". Pages are indexed starting at one, not zero.
#[inline]
pub fn copy_pages_from_document(
&mut self,
source: &PdfDocument,
pages: &str,
destination_page_index: PdfPageIndex,
) -> Result<(), PdfiumError> {
Self::copy_pages_between_documents(
*source.handle(),
pages,
*self.document.handle(),
destination_page_index,
self.bindings(),
)
}
/// Copies one or more pages, specified using a user-friendly page range string,
/// from one raw document handle to another, inserting the pages sequentially
/// starting at the given destination page index.
pub(crate) fn copy_pages_between_documents(
source: FPDF_DOCUMENT,
pages: &str,
destination: FPDF_DOCUMENT,
destination_page_index: PdfPageIndex,
bindings: &dyn PdfiumLibraryBindings,
) -> Result<(), PdfiumError> {
let destination_page_count_before_import = bindings.FPDF_GetPageCount(destination);
if bindings.is_true(bindings.FPDF_ImportPages(
destination,
source,
pages,
destination_page_index as c_int,
)) {
let destination_page_count_after_import = bindings.FPDF_GetPageCount(destination);
PdfPageIndexCache::insert_pages_at_index(
destination,
destination_page_index,
(destination_page_count_after_import - destination_page_count_before_import)
as PdfPageIndex,
);
Ok(())
} else if let Some(error) = bindings.get_pdfium_last_error() {
Err(PdfiumError::PdfiumLibraryInternalError(error))
} else {
// This would be an unusual situation; a null handle indicating failure,
// yet Pdfium's error code indicates success.
Err(PdfiumError::PdfiumLibraryInternalError(
PdfiumInternalError::Unknown,
))
}
}
/// Copies one or more pages with the given range of indices from the given
/// source [PdfDocument], inserting the pages sequentially starting at the given
/// destination page index in this [PdfPages] collection.
#[inline]
pub fn copy_page_range_from_document(
&mut self,
source: &PdfDocument,
source_page_range: RangeInclusive<PdfPageIndex>,
destination_page_index: PdfPageIndex,
) -> Result<(), PdfiumError> {
Self::copy_page_range_between_documents(
*source.handle(),
source_page_range,
*self.document.handle(),
destination_page_index,
self.bindings(),
)
}
/// Copies one or more pages with the given range of indices from one raw document handle
/// to another, inserting the pages sequentially starting at the given destination page index.
pub(crate) fn copy_page_range_between_documents(
source: FPDF_DOCUMENT,
source_page_range: RangeInclusive<PdfPageIndex>,
destination: FPDF_DOCUMENT,
destination_page_index: PdfPageIndex,
bindings: &dyn PdfiumLibraryBindings,
) -> Result<(), PdfiumError> {
let no_of_pages_to_import = source_page_range.len() as PdfPageIndex;
if bindings.is_true(
bindings.FPDF_ImportPagesByIndex_vec(
destination,
source,
source_page_range
.map(|index| index as c_int)
.collect::<Vec<_>>(),
destination_page_index as c_int,
),
) {
PdfPageIndexCache::insert_pages_at_index(
destination,
destination_page_index,
no_of_pages_to_import,
);
Ok(())
} else if let Some(error) = bindings.get_pdfium_last_error() {
Err(PdfiumError::PdfiumLibraryInternalError(error))
} else {
// This would be an unusual situation; a null handle indicating failure,
// yet Pdfium's error code indicates success.
Err(PdfiumError::PdfiumLibraryInternalError(
PdfiumInternalError::Unknown,
))
}
}
/// Copies all pages in the given source [PdfDocument], appending them sequentially
/// to the end of this [PdfPages] collection.
///
/// For finer control over which pages are imported, and where they should be inserted,
/// use one of the [PdfPages::copy_page_from_document()], [PdfPages::copy_pages_from_document()],
/// or [PdfPages::copy_page_range_from_document()] functions.
#[inline]
pub fn append(&mut self, document: &PdfDocument) -> Result<(), PdfiumError> {
self.copy_page_range_from_document(
document,
document.pages().as_range_inclusive(),
self.len(),
)
}
/// Creates a new [PdfDocument] by copying the pages in this [PdfPages] collection
/// into tiled grids, the size of each tile shrinking or expanding as necessary to fit
/// the given [PdfPagePaperSize].
///
/// For example, to output all pages in a [PdfPages] collection into a new
/// A3 landscape document with six source pages tiled on each destination page arranged
/// into a 2 row x 3 column grid, you would call:
///
/// ```
/// PdfPages::tile_into_new_document(2, 3, PdfPagePaperSize::a3().to_landscape())
/// ```
pub fn tile_into_new_document(
&self,
rows_per_page: u8,
columns_per_row: u8,
size: PdfPagePaperSize,
) -> Result<PdfDocument, PdfiumError> {
let handle = self.bindings().FPDF_ImportNPagesToOne(
*self.document().handle(),
size.width().value,
size.height().value,
columns_per_row as size_t,
rows_per_page as size_t,
);
if handle.is_null() {
if let Some(error) = self.bindings().get_pdfium_last_error() {
Err(PdfiumError::PdfiumLibraryInternalError(error))
} else {
// This would be an unusual situation; a null handle indicating failure,
// yet Pdfium's error code indicates success.
Err(PdfiumError::PdfiumLibraryInternalError(
PdfiumInternalError::Unknown,
))
}
} else {
Ok(PdfDocument::from_pdfium(handle, self.bindings()))
}
}
/// Returns a [PdfPage] from the given `FPDF_PAGE` handle, if possible.
pub(crate) fn pdfium_page_handle_to_result(
&self,
index: PdfPageIndex,
handle: FPDF_PAGE,
) -> Result<PdfPage<'a>, PdfiumError> {
if handle.is_null() {
if let Some(error) = self.bindings().get_pdfium_last_error() {
Err(PdfiumError::PdfiumLibraryInternalError(error))
} else {
// This would be an unusual situation; a null handle indicating failure,
// yet Pdfium's error code indicates success.
Err(PdfiumError::PdfiumLibraryInternalError(
PdfiumInternalError::Unknown,
))
}
} else {
// The page's label (if any) is retrieved by index rather than by using the
// FPDF_PAGE handle. Since the index of any particular page can change
// (if other pages are inserted or removed), it's better if we don't treat the
// page index as an immutable property of the PdfPage; instead, we look up the label now.
// (Pdfium does not currently include an FPDF_SetPageLabel() function, so the label
// _will_ be an immutable property of the PdfPage for its entire lifetime.)
let label = {
// Retrieving the label text from Pdfium is a two-step operation. First, we call
// FPDF_GetPageLabel() with a null buffer; this will retrieve the length of
// the label text in bytes. If the length is zero, then there is no such tag.
// If the length is non-zero, then we reserve a byte buffer of the given
// length and call FPDF_GetPageLabel() again with a pointer to the buffer;
// this will write the label text to the buffer in UTF16LE format.
let buffer_length = self.bindings().FPDF_GetPageLabel(
*self.document().handle(),
index as c_int,
std::ptr::null_mut(),
0,
);
if buffer_length == 0 {
// The label is not present.
None
} else {
let mut buffer = create_byte_buffer(buffer_length as usize);
let result = self.bindings().FPDF_GetPageLabel(
*self.document().handle(),
index as c_int,
buffer.as_mut_ptr() as *mut c_void,
buffer_length,
);
debug_assert_eq!(result, buffer_length);
get_string_from_pdfium_utf16le_bytes(buffer)
}
};
Ok(PdfPage::from_pdfium(handle, label, self.document()))
}
}
/// Returns the [PdfPageMode] setting embedded in the containing [PdfDocument].
pub fn page_mode(&self) -> PdfPageMode {
PdfPageMode::from_pdfium(
self.bindings()
.FPDFDoc_GetPageMode(*self.document().handle()),
)
.unwrap_or(PdfPageMode::UnsetOrUnknown)
}
/// Applies the given watermarking closure to each [PdfPage] in this [PdfPages] collection.
///
/// The closure receives four arguments:
/// * An empty [PdfPageGroupObject] for you to populate with the page objects that make up your watermark.
/// * The zero-based index of the [PdfPage] currently being processed.
/// * The width of the [PdfPage] currently being processed, in [PdfPoints].
/// * The height of the [PdfPage] currently being processed, in [PdfPoints].
///
/// If the current page should not be watermarked, simply leave the group empty.
///
/// The closure can return a `Result<(), PdfiumError>`; this makes it easy to use the `?` unwrapping
/// operator within the closure.
///
/// For example, the following snippet adds a page number to the very top of every page in a document
/// except for the first page.
///
/// ```
/// document.pages().watermark(|group, index, width, height| {
/// if index == 0 {
/// // Don't watermark the first page.
///
/// Ok(())
/// } else {
/// let mut page_number = PdfPageTextObject::new(
/// &document,
/// format!("Page {}", index + 1),
/// &PdfFont::helvetica(&document),
/// PdfPoints::new(14.0),
/// )?;
///
/// page_number.translate(
/// (width - page_number.width()?) / 2.0, // Horizontally center the page number...
/// height - page_number.height()?, // ... and vertically position it at the page top.
/// )?;
///
/// group.push(&mut page_number.into())
/// }
/// })?;
/// ```
pub fn watermark<F>(&self, watermarker: F) -> Result<(), PdfiumError>
where
F: Fn(
&mut PdfPageGroupObject<'a>,
PdfPageIndex,
PdfPoints,
PdfPoints,
) -> Result<(), PdfiumError>,
{
for (index, page) in self.iter().enumerate() {
let mut group = PdfPageGroupObject::from_pdfium(
*page.handle(),
*page.document().handle(),
self.bindings(),
page.content_regeneration_strategy()
== PdfPageContentRegenerationStrategy::AutomaticOnEveryChange,
);
watermarker(
&mut group,
index as PdfPageIndex,
page.width(),
page.height(),
)?;
}
Ok(())
}
/// Returns an iterator over all the pages in this [PdfPages] collection.
#[inline]
pub fn iter(&self) -> PdfPagesIterator {
PdfPagesIterator::new(self)
}
}
/// An iterator over all the [PdfPage] objects in a [PdfPages] collection.
pub struct PdfPagesIterator<'a> {
pages: &'a PdfPages<'a>,
next_index: PdfPageIndex,
}
impl<'a> PdfPagesIterator<'a> {
#[inline]
pub(crate) fn new(pages: &'a PdfPages<'a>) -> Self {
PdfPagesIterator {
pages,
next_index: 0,
}
}
}
impl<'a> Iterator for PdfPagesIterator<'a> {
type Item = PdfPage<'a>;
fn next(&mut self) -> Option<Self::Item> {
let next = self.pages.get(self.next_index);
self.next_index += 1;
next.ok()
}
}