Skip to main content

pdfium_render/pdf/document/page/object/
text.rs

1//! Defines the [PdfPageTextObject] struct, exposing functionality related to a single
2//! page object defining a piece of formatted text.
3
4use crate::bindgen::{
5    FPDF_DOCUMENT, FPDF_FONT, FPDF_PAGEOBJECT, FPDF_TEXT_RENDERMODE,
6    FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_CLIP, FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_FILL,
7    FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_FILL_CLIP,
8    FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_FILL_STROKE,
9    FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_FILL_STROKE_CLIP,
10    FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_INVISIBLE,
11    FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_STROKE,
12    FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_STROKE_CLIP,
13    FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_UNKNOWN, FPDF_WCHAR,
14};
15use crate::bindings::PdfiumLibraryBindings;
16use crate::error::{PdfiumError, PdfiumInternalError};
17use crate::pdf::document::fonts::ToPdfFontToken;
18use crate::pdf::document::page::object::private::internal::PdfPageObjectPrivate;
19use crate::pdf::document::page::object::PdfPageObjectOwnership;
20use crate::pdf::document::PdfDocument;
21use crate::pdf::font::PdfFont;
22use crate::pdf::matrix::{PdfMatrix, PdfMatrixValue};
23use crate::pdf::points::PdfPoints;
24use crate::pdfium::PdfiumLibraryBindingsAccessor;
25use crate::utils::mem::create_byte_buffer;
26use crate::utils::utf16le::get_string_from_pdfium_utf16le_bytes;
27use crate::{create_transform_getters, create_transform_setters};
28use std::marker::PhantomData;
29
30#[cfg(any(
31    feature = "pdfium_future",
32    feature = "pdfium_7881",
33    feature = "pdfium_7763",
34    feature = "pdfium_7543",
35    feature = "pdfium_7350",
36    feature = "pdfium_7215",
37    feature = "pdfium_7123",
38    feature = "pdfium_6996",
39    feature = "pdfium_6721",
40    feature = "pdfium_6666",
41    feature = "pdfium_6611",
42))]
43use {
44    crate::pdf::document::page::text::chars::PdfPageTextChars,
45    crate::pdf::document::page::text::PdfPageText,
46};
47
48#[cfg(doc)]
49use {
50    crate::pdf::document::page::object::PdfPageObject,
51    crate::pdf::document::page::object::PdfPageObjectType,
52    crate::pdf::document::page::objects::common::PdfPageObjectsCommon,
53    crate::pdf::document::page::PdfPage,
54};
55
56/// The text rendering modes supported by the PDF standard, as listed in table 5.3
57/// on page 402 in the PDF Reference manual version 1.7.
58#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
59pub enum PdfPageTextRenderMode {
60    /// The text render mode is not recognized by Pdfium.
61    Unknown = FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_UNKNOWN as isize,
62
63    /// The text will be filled, but not stroked.
64    FilledUnstroked = FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_FILL as isize,
65
66    /// The text will be stroked, but not filled.
67    StrokedUnfilled = FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_STROKE as isize,
68
69    /// The text will be filled, then stroked.
70    FilledThenStroked = FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_FILL_STROKE as isize,
71
72    /// The text will be neither filled nor stroked. It will still take up size in the layout, however.
73    Invisible = FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_INVISIBLE as isize,
74
75    /// The text will be filled and added to the path for clipping.
76    FilledUnstrokedClipping = FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_FILL_CLIP as isize,
77
78    /// The text will be stroked and added to the path for clipping.
79    StrokedUnfilledClipping = FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_STROKE_CLIP as isize,
80
81    /// The text will be filled, then stroked, and added to the path for clipping.
82    FilledThenStrokedClipping = FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_FILL_STROKE_CLIP as isize,
83
84    /// The text will be neither filled nor stroked, only added to the path for clipping.
85    InvisibleClipping = FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_CLIP as isize,
86}
87
88impl PdfPageTextRenderMode {
89    #[inline]
90    pub(crate) fn from_pdfium(value: i32) -> Result<PdfPageTextRenderMode, PdfiumError> {
91        match value {
92            FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_UNKNOWN => Ok(PdfPageTextRenderMode::Unknown),
93            FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_FILL => {
94                Ok(PdfPageTextRenderMode::FilledUnstroked)
95            }
96            FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_STROKE => {
97                Ok(PdfPageTextRenderMode::StrokedUnfilled)
98            }
99            FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_FILL_STROKE => {
100                Ok(PdfPageTextRenderMode::FilledThenStroked)
101            }
102            FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_INVISIBLE => {
103                Ok(PdfPageTextRenderMode::Invisible)
104            }
105            FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_FILL_CLIP => {
106                Ok(PdfPageTextRenderMode::FilledUnstrokedClipping)
107            }
108            FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_STROKE_CLIP => {
109                Ok(PdfPageTextRenderMode::StrokedUnfilledClipping)
110            }
111            FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_FILL_STROKE_CLIP => {
112                Ok(PdfPageTextRenderMode::FilledThenStrokedClipping)
113            }
114            FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_CLIP => {
115                Ok(PdfPageTextRenderMode::InvisibleClipping)
116            }
117            _ => Err(PdfiumError::UnknownPdfPageTextRenderMode),
118        }
119    }
120
121    #[inline]
122    #[allow(dead_code)]
123    // The as_pdfium() function is not currently used, but we expect it to be in future
124    pub(crate) fn as_pdfium(&self) -> FPDF_TEXT_RENDERMODE {
125        match self {
126            PdfPageTextRenderMode::Unknown => FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_UNKNOWN,
127            PdfPageTextRenderMode::FilledUnstroked => FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_FILL,
128            PdfPageTextRenderMode::StrokedUnfilled => {
129                FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_STROKE
130            }
131            PdfPageTextRenderMode::FilledThenStroked => {
132                FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_FILL_STROKE
133            }
134            PdfPageTextRenderMode::Invisible => FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_INVISIBLE,
135            PdfPageTextRenderMode::FilledUnstrokedClipping => {
136                FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_FILL_CLIP
137            }
138            PdfPageTextRenderMode::StrokedUnfilledClipping => {
139                FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_STROKE_CLIP
140            }
141            PdfPageTextRenderMode::FilledThenStrokedClipping => {
142                FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_FILL_STROKE_CLIP
143            }
144            PdfPageTextRenderMode::InvisibleClipping => {
145                FPDF_TEXT_RENDERMODE_FPDF_TEXTRENDERMODE_CLIP
146            }
147        }
148    }
149}
150
151/// A single [PdfPageObject] of type [PdfPageObjectType::Text]. The page object defines a single
152/// piece of formatted text.
153///
154/// Page objects can be created either attached to a [PdfPage] (in which case the page object's
155/// memory is owned by the containing page) or detached from any page (in which case the page
156/// object's memory is owned by the object). Page objects are not rendered until they are
157/// attached to a page; page objects that are never attached to a page will be lost when they
158/// fall out of scope.
159///
160/// The simplest way to create a page text object that is immediately attached to a page
161/// is to call the [PdfPageObjectsCommon::create_text_object()] function.
162///
163/// Creating a detached page text object offers more scope for customization, but you must
164/// add the object to a containing [PdfPage] manually. To create a detached page text object,
165/// use the [PdfPageTextObject::new()] function. The detached page text object can later
166/// be attached to a page by using the [PdfPageObjectsCommon::add_text_object()] function.
167pub struct PdfPageTextObject<'a> {
168    object_handle: FPDF_PAGEOBJECT,
169    ownership: PdfPageObjectOwnership,
170    lifetime: PhantomData<&'a FPDF_PAGEOBJECT>,
171}
172
173impl<'a> PdfPageTextObject<'a> {
174    #[inline]
175    pub(crate) fn from_pdfium(
176        object_handle: FPDF_PAGEOBJECT,
177        ownership: PdfPageObjectOwnership,
178    ) -> Self {
179        PdfPageTextObject {
180            object_handle,
181            ownership,
182            lifetime: PhantomData,
183        }
184    }
185
186    /// Creates a new [PdfPageTextObject] from the given arguments. The returned page object
187    /// will not be rendered until it is added to a [PdfPage] using the
188    /// [PdfPageObjectsCommon::add_text_object()] function.
189    ///
190    /// A single space will be used if the given text is empty, in order to avoid
191    /// unexpected behaviour from Pdfium when dealing with empty strings.
192    // Specifically, `FPDFPageObj_SetText()` will crash if we try to apply an empty string to a
193    // text object, and `FPDFText_LoadPage()` will crash if any text object on the page contains
194    // an empty string (so it isn't enough to avoid calling `FPDFPageObj_SetText()` for an empty
195    // text object, we _have_ to set a non-empty string to avoid segfaults).
196    #[inline]
197    pub fn new(
198        document: &PdfDocument<'a>,
199        text: impl ToString,
200        font: impl ToPdfFontToken,
201        font_size: PdfPoints,
202    ) -> Result<Self, PdfiumError> {
203        Self::new_from_handles(
204            document.handle(),
205            text,
206            font.token().handle(),
207            font_size,
208            document.bindings(),
209        )
210    }
211
212    // Take raw `FPDF_DOCUMENT` and `FPDF_FONT` handles to avoid cascading lifetime problems
213    // associated with borrowing `PdfDocument<'a>` and/or `PdfFont<'a>`.
214    pub(crate) fn new_from_handles(
215        document: FPDF_DOCUMENT,
216        text: impl ToString,
217        font: FPDF_FONT,
218        font_size: PdfPoints,
219        bindings: &'a dyn PdfiumLibraryBindings,
220    ) -> Result<Self, PdfiumError> {
221        let handle = unsafe { bindings.FPDFPageObj_CreateTextObj(document, font, font_size.value) };
222
223        if handle.is_null() {
224            Err(PdfiumError::PdfiumLibraryInternalError(
225                PdfiumInternalError::Unknown,
226            ))
227        } else {
228            let mut result = PdfPageTextObject {
229                object_handle: handle,
230                ownership: PdfPageObjectOwnership::unowned(),
231                lifetime: PhantomData,
232            };
233
234            result.set_text(text)?;
235
236            Ok(result)
237        }
238    }
239
240    /// Returns the text rendering mode for the text contained within this [PdfPageTextObject].
241    pub fn render_mode(&self) -> PdfPageTextRenderMode {
242        PdfPageTextRenderMode::from_pdfium(unsafe {
243            self.bindings()
244                .FPDFTextObj_GetTextRenderMode(self.object_handle)
245        })
246        .unwrap_or(PdfPageTextRenderMode::Unknown)
247    }
248
249    /// Returns `true` if the text rendering mode for the text contained within this
250    /// [PdfPageTextObject] is set to any value other than [PdfPageTextRenderMode::Invisible]
251    /// or [PdfPageTextRenderMode::InvisibleClipping].
252    #[inline]
253    pub fn is_visible(&self) -> bool {
254        match self.render_mode() {
255            PdfPageTextRenderMode::Invisible | PdfPageTextRenderMode::InvisibleClipping => false,
256            _ => true,
257        }
258    }
259
260    /// Returns the effective size of the text when rendered, taking into account both the
261    /// font size specified in this text object as well as any vertical scale factor applied
262    /// to the text object's transformation matrix.
263    ///
264    /// To retrieve only the specified font size, ignoring any vertical scaling, use the
265    /// [PdfPageTextObject::unscaled_font_size()] function.
266    #[inline]
267    pub fn scaled_font_size(&self) -> PdfPoints {
268        PdfPoints::new(self.unscaled_font_size().value * self.get_vertical_scale())
269    }
270
271    /// Returns the font size of the text specified in this [PdfPageTextObject].
272    ///
273    /// Note that the effective size of the text when rendered may differ from the font size
274    /// if a scaling factor has been applied to this text object's transformation matrix.
275    /// To retrieve the effective font size, taking vertical scaling into account, use the
276    /// [PdfPageTextObject::scaled_font_size()] function.
277    pub fn unscaled_font_size(&self) -> PdfPoints {
278        let mut result = 0.0;
279
280        if self.bindings().is_true(unsafe {
281            self.bindings()
282                .FPDFTextObj_GetFontSize(self.object_handle(), &mut result)
283        }) {
284            PdfPoints::new(result)
285        } else {
286            PdfPoints::ZERO
287        }
288    }
289
290    #[cfg(any(feature = "pdfium_future", feature = "pdfium_7881"))]
291    /// Sets the font size of the text specified in this [PdfPageTextObject].
292    ///
293    /// Note that the effective size of the text when rendered may differ from the font size
294    /// if a scaling factor has been applied to this text object's transformation matrix.
295    pub fn set_unscaled_font_size(&self, points: PdfPoints) -> Result<(), PdfiumError> {
296        if self.bindings().is_true(unsafe {
297            self.bindings()
298                .FPDFTextObj_SetFontSize(self.object_handle(), points.value)
299        }) {
300            Ok(())
301        } else {
302            Err(PdfiumError::InvalidFontSize)
303        }
304    }
305
306    /// Returns the [PdfFont] used to render the text contained within this [PdfPageTextObject].
307    #[inline]
308    pub fn font(&self) -> PdfFont<'_> {
309        PdfFont::from_pdfium(
310            unsafe { self.bindings().FPDFTextObj_GetFont(self.object_handle) },
311            None,
312            false,
313        )
314    }
315
316    /// Returns the text contained within this [PdfPageTextObject].
317    ///
318    /// Text retrieval in Pdfium is handled by the [PdfPageText] object owned by the [PdfPage]
319    /// containing this [PdfPageTextObject]. If this text object has not been attached to a page
320    /// then text retrieval will be unavailable and an empty string will be returned.
321    ///
322    /// When retrieving the text from many [PdfPageTextObject] objects (for instance, as part of
323    /// a loop or an iterator), it may be faster to open the [PdfPageText] object once and keep
324    /// it open while processing the text objects, like so:
325    ///
326    /// ```
327    /// let text_page = page.text()?; // Opens the text page once.
328    ///
329    /// for object in <some object iterator> {
330    ///     let object_text = text_page.for_object(object)?;
331    /// }
332    /// ```
333    ///
334    /// The [PdfPageText] object will be closed when the binding to it (`text_page` in the example above)
335    /// falls out of scope.
336    pub fn text(&self) -> String {
337        // Retrieving the text from Pdfium is a two-step operation. First, we call
338        // FPDFTextObj_GetText() with a null buffer; this will retrieve the length of
339        // the text in bytes. If the length is zero, then there is no text associated
340        // with the page object.
341
342        // If the length is non-zero, then we reserve a byte buffer of the given
343        // length and call FPDFTextObj_GetText() again with a pointer to the buffer;
344        // this will write the text to the buffer in UTF16-LE format.
345
346        let page_handle = match self.ownership() {
347            PdfPageObjectOwnership::Page(ownership) => Some(ownership.page_handle()),
348            PdfPageObjectOwnership::AttachedAnnotation(ownership) => Some(ownership.page_handle()),
349            _ => None,
350        };
351
352        if let Some(page_handle) = page_handle {
353            let text_handle = unsafe { self.bindings().FPDFText_LoadPage(page_handle) };
354
355            if !text_handle.is_null() {
356                let buffer_length = unsafe {
357                    self.bindings().FPDFTextObj_GetText(
358                        self.object_handle(),
359                        text_handle,
360                        std::ptr::null_mut(),
361                        0,
362                    )
363                };
364
365                if buffer_length == 0 {
366                    // There is no text.
367
368                    return String::new();
369                }
370
371                let mut buffer = create_byte_buffer(buffer_length as usize);
372
373                let result = unsafe {
374                    self.bindings().FPDFTextObj_GetText(
375                        self.object_handle(),
376                        text_handle,
377                        buffer.as_mut_ptr() as *mut FPDF_WCHAR,
378                        buffer_length,
379                    )
380                };
381
382                assert_eq!(result, buffer_length);
383
384                unsafe {
385                    self.bindings().FPDFText_ClosePage(text_handle);
386                }
387
388                get_string_from_pdfium_utf16le_bytes(buffer).unwrap_or_default()
389            } else {
390                // The PdfPage containing this page object does not have an associated
391                // FPDF_TEXTPAGE object.
392
393                String::new()
394            }
395        } else {
396            // This page object is not contained by a PdfPage.
397
398            String::new()
399        }
400    }
401
402    /// Sets the text contained within this [PdfPageTextObject], replacing any existing text.
403    ///
404    /// A single space will be used if the given text is empty, in order to avoid
405    /// unexpected behaviour from Pdfium when dealing with an empty string.
406    pub fn set_text(&mut self, text: impl ToString) -> Result<(), PdfiumError> {
407        let text = text.to_string();
408
409        let text = if text.is_empty() { " " } else { text.as_str() };
410
411        if self.bindings().is_true(unsafe {
412            self.bindings()
413                .FPDFText_SetText_str(self.object_handle(), text)
414        }) {
415            Ok(())
416        } else {
417            Err(PdfiumError::PdfiumLibraryInternalError(
418                PdfiumInternalError::Unknown,
419            ))
420        }
421    }
422
423    /// Sets the text rendering mode for the text contained within this [PdfPageTextObject].
424    pub fn set_render_mode(
425        &mut self,
426        render_mode: PdfPageTextRenderMode,
427    ) -> Result<(), PdfiumError> {
428        if self.bindings().is_true(unsafe {
429            self.bindings()
430                .FPDFTextObj_SetTextRenderMode(self.object_handle(), render_mode.as_pdfium())
431        }) {
432            Ok(())
433        } else {
434            Err(PdfiumError::PdfiumLibraryInternalError(
435                PdfiumInternalError::Unknown,
436            ))
437        }
438    }
439
440    #[cfg(any(
441        feature = "pdfium_future",
442        feature = "pdfium_7881",
443        feature = "pdfium_7763",
444        feature = "pdfium_7543",
445        feature = "pdfium_7350",
446        feature = "pdfium_7215",
447        feature = "pdfium_7123",
448        feature = "pdfium_6996",
449        feature = "pdfium_6721",
450        feature = "pdfium_6666",
451        feature = "pdfium_6611",
452    ))]
453    /// Returns a collection of the characters contained within this [PdfPageTextObject],
454    /// using character retrieval functionality provided by the given [PdfPageText] object.
455    #[inline]
456    pub fn chars(&self, text: &'a PdfPageText<'a>) -> Result<PdfPageTextChars<'a>, PdfiumError> {
457        text.chars_for_object(self)
458    }
459
460    #[cfg(any(
461        feature = "pdfium_future",
462        feature = "pdfium_7881",
463        feature = "pdfium_7763",
464        feature = "pdfium_7543",
465        feature = "pdfium_7350",
466        feature = "pdfium_7215",
467        feature = "pdfium_7123",
468        feature = "pdfium_6996",
469        feature = "pdfium_6721",
470        feature = "pdfium_6666",
471        feature = "pdfium_6611",
472    ))]
473    /// Returns `true` if any of the characters contained within this [PdfPageTextObject] have a
474    /// glyph shape that descends below the font baseline.
475    ///
476    /// Character retrieval functionality is provided by the given [PdfPageText] object.
477    #[inline]
478    pub fn has_descenders(&self, text: &PdfPageText) -> Result<bool, PdfiumError> {
479        self.chars(text)
480            .map(|chars| chars.iter().any(|char| char.has_descender()))
481    }
482
483    #[cfg(any(
484        feature = "pdfium_future",
485        feature = "pdfium_7881",
486        feature = "pdfium_7763",
487        feature = "pdfium_7543",
488        feature = "pdfium_7350",
489        feature = "pdfium_7215",
490        feature = "pdfium_7123",
491        feature = "pdfium_6996",
492        feature = "pdfium_6721",
493        feature = "pdfium_6666",
494        feature = "pdfium_6611",
495    ))]
496    /// Returns the descent of this [PdfPageTextObject]. The descent is the maximum distance below
497    /// the baseline reached by any glyph in any of the characters contained in this text object,
498    /// expressed as a negative points value.
499    ///
500    /// Character retrieval and bounds measurement is provided by the given [PdfPageText] object.
501    pub fn descent(&self, text: &PdfPageText) -> Result<PdfPoints, PdfiumError> {
502        let object_bottom = self.get_vertical_translation();
503
504        let mut maximum_descent = object_bottom;
505
506        for char in self.chars(text)?.iter() {
507            let char_bottom = char.tight_bounds()?.bottom();
508
509            if char_bottom < maximum_descent {
510                maximum_descent = char_bottom;
511            }
512        }
513
514        Ok(maximum_descent - object_bottom)
515    }
516
517    create_transform_setters!(
518        &mut Self,
519        Result<(), PdfiumError>,
520        "this [PdfPageTextObject]",
521        "this [PdfPageTextObject].",
522        "this [PdfPageTextObject],"
523    );
524
525    // The transform_impl() function required by the create_transform_setters!() macro
526    // is provided by the PdfPageObjectPrivate trait.
527
528    create_transform_getters!(
529        "this [PdfPageTextObject]",
530        "this [PdfPageTextObject].",
531        "this [PdfPageTextObject],"
532    );
533
534    // The get_matrix_impl() function required by the create_transform_getters!() macro
535    // is provided by the PdfPageObjectPrivate trait.
536}
537
538impl<'a> PdfPageObjectPrivate<'a> for PdfPageTextObject<'a> {
539    #[inline]
540    fn object_handle(&self) -> FPDF_PAGEOBJECT {
541        self.object_handle
542    }
543
544    #[inline]
545    fn ownership(&self) -> &PdfPageObjectOwnership {
546        &self.ownership
547    }
548
549    #[inline]
550    fn set_ownership(&mut self, ownership: PdfPageObjectOwnership) {
551        self.ownership = ownership;
552    }
553}
554
555impl<'a> Drop for PdfPageTextObject<'a> {
556    /// Closes this [PdfPageTextObject], releasing held memory.
557    fn drop(&mut self) {
558        self.drop_impl();
559    }
560}
561
562impl<'a> PdfiumLibraryBindingsAccessor<'a> for PdfPageTextObject<'a> {}
563
564#[cfg(feature = "thread_safe")]
565unsafe impl<'a> Send for PdfPageTextObject<'a> {}
566
567#[cfg(feature = "thread_safe")]
568unsafe impl<'a> Sync for PdfPageTextObject<'a> {}