Skip to main content

pdfium_render/pdf/document/page/
text.rs

1//! Defines the [PdfPageText] struct, exposing functionality related to the
2//! collection of Unicode characters visible on a single [PdfPage].
3
4pub mod char;
5pub mod chars;
6pub mod search;
7pub mod segment;
8pub mod segments;
9
10use crate::bindgen::{FPDF_TEXTPAGE, FPDF_WCHAR, FPDF_WIDESTRING};
11use crate::bindings::PdfiumLibraryBindings;
12use crate::error::PdfiumError;
13use crate::pdf::document::page::annotation::PdfPageAnnotation;
14use crate::pdf::document::page::annotation::PdfPageAnnotationCommon;
15use crate::pdf::document::page::object::private::internal::PdfPageObjectPrivate;
16use crate::pdf::document::page::object::text::PdfPageTextObject;
17use crate::pdf::document::page::text::chars::{PdfPageTextCharIndex, PdfPageTextChars};
18use crate::pdf::document::page::text::search::{PdfPageTextSearch, PdfSearchOptions};
19use crate::pdf::document::page::text::segments::PdfPageTextSegments;
20use crate::pdf::document::page::PdfPage;
21use crate::pdf::points::PdfPoints;
22use crate::pdf::rect::PdfRect;
23use crate::pdfium::PdfiumLibraryBindingsAccessor;
24use crate::utils::mem::{create_byte_buffer, create_sized_buffer};
25use crate::utils::utf16le::{
26    get_pdfium_utf16le_bytes_from_str, get_string_from_pdfium_utf16le_bytes,
27};
28use bytemuck::cast_slice;
29use std::fmt::{Display, Formatter};
30use std::marker::PhantomData;
31use std::os::raw::{c_double, c_int};
32use std::ptr::null_mut;
33
34/// A collection of all Unicode characters on a single [PdfPage].
35///
36/// Use the [PdfPageText::all()] function to easily return all characters in the containing
37/// [PdfPage] in the order in which they are defined in the PDF file.
38///
39/// Use the [PdfPageText::search()] function to initialize a new [PdfPageTextSearch] object,
40/// yielding the results of searching for a target string within the character collection.
41///
42/// In complex custom layouts, the order in which characters are defined in the document
43/// and the order in which they appear visually during rendering (and thus the order in
44/// which they are read by a user) may not necessarily match.
45///
46/// [PdfPageText] implements both the [ToString] and the [Display] traits.
47pub struct PdfPageText<'a> {
48    text_page_handle: FPDF_TEXTPAGE,
49    page: &'a PdfPage<'a>,
50    lifetime: PhantomData<&'a FPDF_TEXTPAGE>,
51}
52
53impl<'a> PdfPageText<'a> {
54    pub(crate) fn from_pdfium(text_page_handle: FPDF_TEXTPAGE, page: &'a PdfPage<'a>) -> Self {
55        PdfPageText {
56            text_page_handle,
57            page,
58            lifetime: PhantomData,
59        }
60    }
61
62    /// Returns the internal `FPDF_TEXTPAGE` handle for this [PdfPageText].
63    #[inline]
64    pub(crate) fn text_page_handle(&self) -> FPDF_TEXTPAGE {
65        self.text_page_handle
66    }
67
68    /// Returns the total number of characters in all text segments in the containing [PdfPage].
69    ///
70    /// The character count includes whitespace and newlines, and so may differ slightly
71    /// from the result of calling `PdfPageText::all().len()`.
72    #[inline]
73    pub fn len(&self) -> i32 {
74        unsafe { self.bindings().FPDFText_CountChars(self.text_page_handle()) }
75    }
76
77    /// Returns `true` if there are no characters in any text box collection in the containing [PdfPage].
78    #[inline]
79    pub fn is_empty(&self) -> bool {
80        self.len() == 0
81    }
82
83    /// Returns a collection of all the `PdfPageTextSegment` text segments in the containing [PdfPage].
84    #[inline]
85    pub fn segments(&self) -> PdfPageTextSegments<'_> {
86        PdfPageTextSegments::new(self, 0, self.len(), self.bindings())
87    }
88
89    /// Returns a subset of the `PdfPageTextSegment` text segments in the containing [PdfPage].
90    /// Only text segments containing characters in the given index range will be included.
91    #[inline]
92    pub fn segments_subset(
93        &self,
94        start: PdfPageTextCharIndex,
95        count: PdfPageTextCharIndex,
96    ) -> PdfPageTextSegments<'_> {
97        PdfPageTextSegments::new(self, start as i32, count as i32, self.bindings())
98    }
99
100    /// Returns a collection of all the `PdfPageTextChar` characters in the containing [PdfPage].
101    #[inline]
102    pub fn chars(&self) -> PdfPageTextChars<'_> {
103        PdfPageTextChars::new(
104            self.page.document_handle(),
105            self.page.page_handle(),
106            self.text_page_handle(),
107            (0..self.len()).collect(),
108        )
109    }
110
111    #[cfg(any(
112        feature = "pdfium_future",
113        feature = "pdfium_8125",
114        feature = "pdfium_7881",
115        feature = "pdfium_7763",
116        feature = "pdfium_7543",
117        feature = "pdfium_7350",
118        feature = "pdfium_7215",
119        feature = "pdfium_7123",
120        feature = "pdfium_6996",
121        feature = "pdfium_6721",
122        feature = "pdfium_6666",
123        feature = "pdfium_6611",
124    ))]
125    /// Returns a collection of all the `PdfPageTextChar` characters in the given [PdfPageTextObject].
126    ///
127    /// The return result will be empty if the given [PdfPageTextObject] is not attached to the
128    /// containing [PdfPage].
129    #[inline]
130    pub fn chars_for_object(
131        &self,
132        object: &PdfPageTextObject,
133    ) -> Result<PdfPageTextChars<'_>, PdfiumError> {
134        Ok(PdfPageTextChars::new(
135            self.page.document_handle(),
136            self.page.page_handle(),
137            self.text_page_handle(),
138            self.chars()
139                .iter()
140                .filter(|char| {
141                    (unsafe {
142                        self.bindings()
143                            .FPDFText_GetTextObject(self.text_page_handle(), char.index() as i32)
144                    }) == object.object_handle()
145                })
146                .map(|char| char.index() as i32)
147                .collect(),
148        ))
149    }
150
151    /// Returns a collection of all the `PdfPageTextChar` characters in the given [PdfPageAnnotation].
152    ///
153    /// The return result will be empty if the given [PdfPageAnnotation] is not attached to the
154    /// containing [PdfPage].
155    #[inline]
156    pub fn chars_for_annotation(
157        &self,
158        annotation: &PdfPageAnnotation,
159    ) -> Result<PdfPageTextChars<'_>, PdfiumError> {
160        self.chars_inside_rect(annotation.bounds()?)
161            .map_err(|_| PdfiumError::NoCharsInAnnotation)
162    }
163
164    /// Returns a collection of all the `PdfPageTextChar` characters that lie within the bounds of
165    /// the given [PdfRect] in the containing [PdfPage].
166    #[inline]
167    pub fn chars_inside_rect<'b>(
168        &'b self,
169        rect: PdfRect,
170    ) -> Result<PdfPageTextChars<'a>, PdfiumError> {
171        let tolerance_x = rect.width() / 2.0;
172        let tolerance_y = rect.height() / 2.0;
173        let center_height = rect.bottom() + tolerance_y;
174
175        match (
176            Self::get_char_index_near_point(
177                self.text_page_handle(),
178                rect.left(),
179                tolerance_x,
180                center_height,
181                tolerance_y,
182                self.bindings(),
183            ),
184            Self::get_char_index_near_point(
185                self.text_page_handle(),
186                rect.right(),
187                tolerance_x,
188                center_height,
189                tolerance_y,
190                self.bindings(),
191            ),
192        ) {
193            (Some(start), Some(end)) => Ok(PdfPageTextChars::new(
194                self.page.document_handle(),
195                self.page.page_handle(),
196                self.text_page_handle(),
197                (start as i32..=end as i32 + 1).collect(),
198            )),
199            (Some(start), None) => Ok(PdfPageTextChars::new(
200                self.page.document_handle(),
201                self.page.page_handle(),
202                self.text_page_handle(),
203                (start as i32..=start as i32 + 1).collect(),
204            )),
205            (None, Some(end)) => Ok(PdfPageTextChars::new(
206                self.page.document_handle(),
207                self.page.page_handle(),
208                self.text_page_handle(),
209                (end as i32..=end as i32 + 1).collect(),
210            )),
211            _ => Err(PdfiumError::NoCharsInRect),
212        }
213    }
214
215    /// Returns the character near to the given x and y positions on the containing [PdfPage],
216    /// if any. The returned character will be no further from the given positions than the given
217    /// tolerance values.
218    pub(crate) fn get_char_index_near_point(
219        text_page_handle: FPDF_TEXTPAGE,
220        x: PdfPoints,
221        tolerance_x: PdfPoints,
222        y: PdfPoints,
223        tolerance_y: PdfPoints,
224        bindings: &dyn PdfiumLibraryBindings,
225    ) -> Option<PdfPageTextCharIndex> {
226        match unsafe {
227            bindings.FPDFText_GetCharIndexAtPos(
228                text_page_handle,
229                x.value as c_double,
230                y.value as c_double,
231                tolerance_x.value as c_double,
232                tolerance_y.value as c_double,
233            )
234        } {
235            -1 => None, // No character at position within tolerances
236            -3 => None, // An error occurred, but we'll eat it
237            index => Some(index as PdfPageTextCharIndex),
238        }
239    }
240
241    /// Returns all characters that lie within the containing [PdfPage], in the order in which
242    /// they are defined in the document, concatenated into a single string.
243    ///
244    /// In complex custom layouts, the order in which characters are defined in the document
245    /// and the order in which they appear visually during rendering (and thus the order in
246    /// which they are read by a user) may not necessarily match.
247    pub fn all(&self) -> String {
248        self.inside_rect(self.page.page_size())
249    }
250
251    /// Returns all characters that lie within the bounds of the given [PdfRect] in the
252    /// containing [PdfPage], in the order in which they are defined in the document,
253    /// concatenated into a single string.
254    ///
255    /// In complex custom layouts, the order in which characters are defined in the document
256    /// and the order in which they appear visually during rendering (and thus the order in
257    /// which they are read by a user) may not necessarily match.
258    pub fn inside_rect(&self, rect: PdfRect) -> String {
259        // Retrieving the bounded text from Pdfium is a two-step operation. First, we call
260        // FPDFText_GetBoundedText() with a null buffer; this will retrieve the length of
261        // the bounded text in _characters_ (not _bytes_!). If the length is zero, then there is
262        // no text within the given rectangle's boundaries.
263
264        // If the length is non-zero, then we reserve a buffer (sized in words rather than bytes,
265        // to allow for two bytes per character) and call FPDFText_GetBoundedText() again with a
266        // pointer to the buffer; this will write the bounded text to the buffer in UTF16-LE format.
267
268        let left = rect.left().value as f64;
269
270        let top = rect.top().value as f64;
271
272        let right = rect.right().value as f64;
273
274        let bottom = rect.bottom().value as f64;
275
276        let chars_count = unsafe {
277            self.bindings().FPDFText_GetBoundedText(
278                self.text_page_handle(),
279                left,
280                top,
281                right,
282                bottom,
283                null_mut(),
284                0,
285            )
286        };
287
288        if chars_count == 0 {
289            // No text lies within the given rectangle.
290
291            return String::new();
292        }
293
294        let mut buffer = create_sized_buffer(chars_count as usize);
295
296        let result = unsafe {
297            self.bindings().FPDFText_GetBoundedText(
298                self.text_page_handle(),
299                left,
300                top,
301                right,
302                bottom,
303                buffer.as_mut_ptr(),
304                chars_count,
305            )
306        };
307
308        assert_eq!(result, chars_count);
309
310        get_string_from_pdfium_utf16le_bytes(cast_slice(buffer.as_slice()).to_vec())
311            .unwrap_or_default()
312    }
313
314    /// Returns all characters assigned to the given [PdfPageTextObject] in this [PdfPageText] object,
315    /// concatenated into a single string.
316    pub fn for_object(&self, object: &PdfPageTextObject) -> String {
317        // Retrieving the string value from Pdfium is a two-step operation. First, we call
318        // FPDFTextObj_GetText() with a null buffer; this will retrieve the length of
319        // the text in bytes, assuming the page object exists. If the length is zero,
320        // then there is no text.
321
322        // If the length is non-zero, then we reserve a byte buffer of the given
323        // length and call FPDFTextObj_GetText() again with a pointer to the buffer;
324        // this will write the text for the page object into the buffer.
325
326        let buffer_length = unsafe {
327            self.bindings().FPDFTextObj_GetText(
328                object.object_handle(),
329                self.text_page_handle(),
330                null_mut(),
331                0,
332            )
333        };
334
335        if buffer_length == 0 {
336            // There is no text.
337
338            return String::new();
339        }
340
341        let mut buffer = create_byte_buffer(buffer_length as usize);
342
343        let result = unsafe {
344            self.bindings().FPDFTextObj_GetText(
345                object.object_handle(),
346                self.text_page_handle(),
347                buffer.as_mut_ptr() as *mut FPDF_WCHAR,
348                buffer_length,
349            )
350        };
351
352        assert_eq!(result, buffer_length);
353
354        get_string_from_pdfium_utf16le_bytes(buffer).unwrap_or_default()
355    }
356
357    /// Returns all characters that lie within the bounds of the given [PdfPageAnnotation] in the
358    /// containing [PdfPage], in the order in which they are defined in the document,
359    /// concatenated into a single string.
360    ///
361    /// In complex custom layouts, the order in which characters are defined in the document
362    /// and the order in which they appear visually during rendering (and thus the order in
363    /// which they are read by a user) may not necessarily match.
364    #[inline]
365    pub fn for_annotation(&self, annotation: &PdfPageAnnotation) -> Result<String, PdfiumError> {
366        let bounds = annotation.bounds()?;
367
368        Ok(self.inside_rect(bounds))
369    }
370
371    /// Starts a search for the given text string, returning a new [PdfPageTextSearch]
372    /// object that can be used to step through the search results.
373    #[inline]
374    pub fn search(
375        &self,
376        text: &str,
377        options: &PdfSearchOptions,
378    ) -> Result<PdfPageTextSearch<'_>, PdfiumError> {
379        self.search_from(text, options, 0)
380    }
381
382    /// Starts a search for the given test string from the given character position,
383    /// returning a new [PdfPageTextSearch] object that can be used to step through
384    /// the search results.
385    pub fn search_from(
386        &self,
387        text: &str,
388        options: &PdfSearchOptions,
389        index: PdfPageTextCharIndex,
390    ) -> Result<PdfPageTextSearch<'_>, PdfiumError> {
391        if text.is_empty() {
392            Err(PdfiumError::TextSearchTargetIsEmpty)
393        } else {
394            Ok(PdfPageTextSearch::from_pdfium(
395                unsafe {
396                    self.bindings().FPDFText_FindStart(
397                        self.text_page_handle(),
398                        get_pdfium_utf16le_bytes_from_str(text).as_ptr() as FPDF_WIDESTRING,
399                        options.as_pdfium(),
400                        index as c_int,
401                    )
402                },
403                self,
404            ))
405        }
406    }
407}
408
409impl<'a> Display for PdfPageText<'a> {
410    #[inline]
411    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
412        f.write_str(self.all().as_str())
413    }
414}
415
416impl<'a> Drop for PdfPageText<'a> {
417    /// Closes the [PdfPageText] collection, releasing held memory.
418    #[inline]
419    fn drop(&mut self) {
420        unsafe {
421            self.bindings().FPDFText_ClosePage(self.text_page_handle());
422        }
423    }
424}
425
426impl<'a> PdfiumLibraryBindingsAccessor<'a> for PdfPageText<'a> {}
427
428#[cfg(feature = "thread_safe")]
429unsafe impl<'a> Send for PdfPageText<'a> {}
430
431#[cfg(feature = "thread_safe")]
432unsafe impl<'a> Sync for PdfPageText<'a> {}
433
434#[cfg(test)]
435mod tests {
436    use itertools::Itertools;
437    use std::ffi::OsStr;
438    use std::fs;
439
440    use crate::prelude::*;
441    use crate::utils::test::test_bind_to_pdfium;
442
443    #[test]
444    fn test_overlapping_chars_results() -> Result<(), PdfiumError> {
445        // Test to make sure the result of the .chars_for_object() function returns the
446        // correct results in the event of overlapping text objects.
447        // For more details, see: https://github.com/ajrcarey/pdfium-render/issues/98
448
449        let pdfium = test_bind_to_pdfium();
450
451        // Create a new document with three overlapping text objects.
452
453        let mut document = pdfium.create_new_pdf()?;
454
455        let mut page = document
456            .pages_mut()
457            .create_page_at_start(PdfPagePaperSize::a4())?;
458
459        let font = document.fonts_mut().courier();
460
461        let txt1 = page.objects_mut().create_text_object(
462            PdfPoints::ZERO,
463            PdfPoints::ZERO,
464            "AAAAAA",
465            font,
466            PdfPoints::new(10.0),
467        )?;
468
469        let txt2 = page.objects_mut().create_text_object(
470            PdfPoints::ZERO,
471            PdfPoints::ZERO,
472            "BBBBBB",
473            font,
474            PdfPoints::new(10.0),
475        )?;
476
477        let txt3 = page.objects_mut().create_text_object(
478            PdfPoints::ZERO,
479            PdfPoints::ZERO,
480            "CDCDCDE",
481            font,
482            PdfPoints::new(10.0),
483        )?;
484
485        let page_text = page.text()?;
486
487        // Check the results for all three objects are not affected by overlapping.
488
489        assert!(test_one_overlapping_text_object_results(
490            &txt1, &page_text, "AAAAAA"
491        )?);
492        assert!(test_one_overlapping_text_object_results(
493            &txt2, &page_text, "BBBBBB"
494        )?);
495        assert!(test_one_overlapping_text_object_results(
496            &txt3, &page_text, "CDCDCDE"
497        )?);
498
499        Ok(())
500    }
501
502    fn test_one_overlapping_text_object_results(
503        object: &PdfPageObject,
504        page_text: &PdfPageText,
505        expected: &str,
506    ) -> Result<bool, PdfiumError> {
507        if let Some(txt) = object.as_text_object() {
508            assert_eq!(txt.text().trim(), expected);
509            assert_eq!(page_text.for_object(txt).trim(), expected);
510
511            for (index, char) in txt.chars(&page_text)?.iter().enumerate() {
512                assert_eq!(txt.text().chars().nth(index), char.unicode_char());
513                assert_eq!(expected.chars().nth(index), char.unicode_char());
514            }
515
516            Ok(true)
517        } else {
518            Ok(false)
519        }
520    }
521
522    #[test]
523    fn test_text_chars_results_equality() -> Result<(), PdfiumError> {
524        // For all available test documents, check that the results of
525        // PdfPageObjectText::text() and PdfPageObjectText::chars() match.
526
527        let pdfium = test_bind_to_pdfium();
528
529        let samples = fs::read_dir("./test/")
530            .unwrap()
531            .filter_map(|entry| match entry {
532                Ok(e) => Some(e.path()),
533                Err(_) => None,
534            })
535            .filter(|path| path.extension() == Some(OsStr::new("pdf")))
536            .collect::<Vec<_>>();
537
538        assert!(samples.len() > 0);
539
540        for sample in samples {
541            println!("Testing all text objects in file {}", sample.display());
542
543            let document = pdfium.load_pdf_from_file(&sample, None)?;
544
545            for page in document.pages().iter() {
546                let text = page.text()?;
547
548                for object in page.objects().iter() {
549                    if let Some(obj) = object.as_text_object() {
550                        let chars = obj
551                            .chars(&text)?
552                            .iter()
553                            .filter_map(|char| char.unicode_string())
554                            .join("");
555
556                        assert_eq!(obj.text().trim(), chars.replace("\0", "").trim());
557                    }
558                }
559            }
560        }
561
562        Ok(())
563    }
564
565    #[test]
566    fn test_text_segment_chars_char_lifetimes() -> Result<(), PdfiumError> {
567        // Lifetimes of segments, text chars, and text char should be bound to the
568        // lifetime of page text, but not necessarily to one another. See:
569        // https://github.com/ajrcarey/pdfium-render/pull/248
570
571        let pdfium = test_bind_to_pdfium();
572        let document = pdfium.load_pdf_from_file("./test/export-test.pdf", None)?;
573        let page = document.pages().first()?;
574        let text = page.text()?;
575
576        let _char = {
577            let chars = {
578                let segment = text.segments().first()?;
579
580                segment.chars()?
581            }; // PdfPageTextSegment object is dropped here
582
583            chars.first()?
584        }; // PdfPageTextChars object is dropped here
585
586        Ok(())
587    }
588}