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