Skip to main content

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

1//! Defines the [PdfPageTextChar] struct, exposing functionality related to a single character
2//! in a [PdfPageTextChars] collection.
3
4use crate::bindgen::{FPDF_DOCUMENT, FPDF_PAGE, FPDF_TEXTPAGE, FS_MATRIX, FS_RECTF};
5use crate::bindings::PdfiumLibraryBindings;
6use crate::create_transform_getters;
7use crate::error::{PdfiumError, PdfiumInternalError};
8use crate::pdf::color::PdfColor;
9use crate::pdf::document::page::object::text::PdfPageTextRenderMode;
10use crate::pdf::document::page::text::chars::PdfPageTextCharIndex;
11use crate::pdf::font::{FpdfFontDescriptorFlags, PdfFontWeight};
12use crate::pdf::matrix::{PdfMatrix, PdfMatrixValue};
13use crate::pdf::points::PdfPoints;
14use crate::pdf::rect::PdfRect;
15use crate::utils::mem::create_byte_buffer;
16use std::convert::TryInto;
17use std::ffi::c_void;
18
19use {crate::pdf::document::page::PdfPageObjectOwnership, crate::pdf::document::page::object::text::PdfPageTextObject};
20
21#[cfg(doc)]
22use crate::pdf::document::page::text::PdfPageTextChars;
23
24/// A single character in a [PdfPageTextChars] collection.
25pub struct PdfPageTextChar<'a> {
26    document_handle: FPDF_DOCUMENT,
27    page_handle: FPDF_PAGE,
28    text_page_handle: FPDF_TEXTPAGE,
29    index: i32,
30    bindings: &'a dyn PdfiumLibraryBindings,
31}
32
33impl<'a> PdfPageTextChar<'a> {
34    #[inline]
35    pub(crate) fn from_pdfium(
36        document_handle: FPDF_DOCUMENT,
37        page_handle: FPDF_PAGE,
38        text_page_handle: FPDF_TEXTPAGE,
39        index: i32,
40        bindings: &'a dyn PdfiumLibraryBindings,
41    ) -> Self {
42        PdfPageTextChar {
43            document_handle,
44            page_handle,
45            text_page_handle,
46            index,
47            bindings,
48        }
49    }
50
51    /// Returns the internal `FPDF_DOCUMENT` handle of the [PdfDocument] containing this [PdfPageTextChar].
52    #[inline]
53    pub(crate) fn document_handle(&self) -> FPDF_DOCUMENT {
54        self.document_handle
55    }
56
57    /// Returns the internal `FPDF_PAGE` handle of the [PdfPage] containing this [PdfPageTextChar].
58    #[inline]
59    pub(crate) fn page_handle(&self) -> FPDF_PAGE {
60        self.page_handle
61    }
62
63    /// Returns the internal `FPDF_TEXTPAGE` handle for this [PdfPageTextChar].
64    #[inline]
65    pub(crate) fn text_page_handle(&self) -> FPDF_TEXTPAGE {
66        self.text_page_handle
67    }
68
69    /// Returns the [PdfiumLibraryBindings] used by this [PdfPageTextChar].
70    #[inline]
71    pub fn bindings(&self) -> &'a dyn PdfiumLibraryBindings {
72        self.bindings
73    }
74
75    #[inline]
76    pub fn index(&self) -> PdfPageTextCharIndex {
77        self.index as PdfPageTextCharIndex
78    }
79
80    /// Returns the raw Unicode literal value for this character.
81    ///
82    /// To return Rust's Unicode `char` representation of this Unicode literal, use the
83    /// [PdfPageTextChar::unicode_char] function. To return the string representation of this
84    /// Unicode literal, use the [PdfPageTextChar::unicode_string] function.
85    #[inline]
86    pub fn unicode_value(&self) -> u32 {
87        self.bindings.FPDFText_GetUnicode(self.text_page_handle, self.index)
88    }
89
90    /// Returns Rust's Unicode `char` representation for this character, if available.
91    ///
92    /// To return the raw Unicode literal value for this character,
93    /// use the [PdfPageTextChar::unicode_value] function. To return the string representation of
94    /// this `char`, use the [PdfPageTextChar::unicode_string] function.
95    #[inline]
96    pub fn unicode_char(&self) -> Option<char> {
97        char::from_u32(self.unicode_value())
98    }
99
100    /// Returns a string containing Rust's Unicode `char` representation for this character,
101    /// if available.
102    ///
103    /// To return the raw Unicode literal value for this character,
104    /// use the [PdfPageTextChar::unicode_value] function. To return Rust's Unicode `char`
105    /// representation of this Unicode literal, use the [PdfPageTextChar::unicode_char] function.
106    #[inline]
107    pub fn unicode_string(&self) -> Option<String> {
108        self.unicode_char().map(|char| char.to_string())
109    }
110
111    /// Returns the effective size of this character when rendered, taking into account both the
112    /// font size applied to the character as well as any vertical scale factor applied
113    /// to the character's transformation matrix.
114    ///
115    /// To retrieve only the specified font size, ignoring any vertical scaling, use the
116    /// [PdfPageTextChar::unscaled_font_size] function.
117    #[inline]
118    pub fn scaled_font_size(&self) -> PdfPoints {
119        PdfPoints::new(self.unscaled_font_size().value * self.get_vertical_scale())
120    }
121
122    /// Returns the font size applied to this character.
123    ///
124    /// Note that the effective size of the character when rendered may differ from the font size
125    /// if a scaling factor has been applied to this character's transformation matrix.
126    /// To retrieve the effective font size, taking vertical scaling into account, use the
127    /// [PdfPageTextChar::scaled_font_size] function.
128    #[inline]
129    pub fn unscaled_font_size(&self) -> PdfPoints {
130        PdfPoints::new(self.bindings.FPDFText_GetFontSize(self.text_page_handle, self.index) as f32)
131    }
132
133    /// Returns the font name and raw font descriptor flags for the font applied to this character.
134    fn font(&self) -> (Option<String>, FpdfFontDescriptorFlags) {
135        let mut flags = 0;
136
137        let buffer_length =
138            self.bindings
139                .FPDFText_GetFontInfo(self.text_page_handle, self.index, std::ptr::null_mut(), 0, &mut flags);
140
141        if buffer_length == 0 {
142            return (None, FpdfFontDescriptorFlags::from_bits_truncate(flags as u32));
143        }
144
145        let mut buffer = create_byte_buffer(buffer_length as usize);
146
147        let result = self.bindings.FPDFText_GetFontInfo(
148            self.text_page_handle,
149            self.index,
150            buffer.as_mut_ptr() as *mut c_void,
151            buffer_length,
152            &mut flags,
153        );
154
155        assert_eq!(result, buffer_length);
156
157        (
158            String::from_utf8(buffer)
159                .map(|str| str.trim_end_matches(char::from(0)).to_owned())
160                .ok(),
161            FpdfFontDescriptorFlags::from_bits_truncate(flags as u32),
162        )
163    }
164
165    /// Returns the name of the font applied to this character.
166    #[inline]
167    pub fn font_name(&self) -> String {
168        self.font().0.unwrap_or_default()
169    }
170
171    /// Returns the weight of the font applied to this character.
172    ///
173    /// Pdfium may not reliably return the correct value of this property for built-in fonts.
174    #[inline]
175    pub fn font_weight(&self) -> Option<PdfFontWeight> {
176        PdfFontWeight::from_pdfium(self.bindings.FPDFText_GetFontWeight(self.text_page_handle, self.index))
177    }
178
179    /// Returns the raw font descriptor bitflags for the font applied to this character.
180    #[inline]
181    fn font_flags_bits(&self) -> FpdfFontDescriptorFlags {
182        self.font().1
183    }
184
185    /// Returns `true` if all the glyphs in the font applied to this character have the same width.
186    ///
187    /// Pdfium may not reliably return the correct value of this flag for built-in fonts.
188    pub fn font_is_fixed_pitch(&self) -> bool {
189        self.font_flags_bits()
190            .contains(FpdfFontDescriptorFlags::FIXED_PITCH_BIT_1)
191    }
192
193    /// Returns `true` if the glyphs in the font applied to this character have variable widths.
194    ///
195    /// Pdfium may not reliably return the correct value of this flag for built-in fonts.
196    #[inline]
197    pub fn font_is_proportional_pitch(&self) -> bool {
198        !self.font_is_fixed_pitch()
199    }
200
201    /// Returns `true` if one or more glyphs in the font applied to this character have serifs -
202    /// short strokes drawn at an angle on the top or bottom of glyph stems to decorate the glyphs.
203    /// For example, Times New Roman is a serif font.
204    ///
205    /// Pdfium may not reliably return the correct value of this flag for built-in fonts.
206    pub fn font_is_serif(&self) -> bool {
207        self.font_flags_bits().contains(FpdfFontDescriptorFlags::SERIF_BIT_2)
208    }
209
210    /// Returns `true` if no glyphs in the font applied to this character have serifs -
211    /// short strokes drawn at an angle on the top or bottom of glyph stems to decorate the glyphs.
212    /// For example, Helvetica is a sans-serif font.
213    ///
214    /// Pdfium may not reliably return the correct value of this flag for built-in fonts.
215    #[inline]
216    pub fn font_is_sans_serif(&self) -> bool {
217        !self.font_is_serif()
218    }
219
220    /// Returns `true` if the font applied to this character contains glyphs outside the
221    /// Adobe standard Latin character set.
222    ///
223    /// This classification of non-symbolic and symbolic fonts is peculiar to PDF. A font may
224    /// contain additional characters that are used in Latin writing systems but are outside the
225    /// Adobe standard Latin character set; PDF considers such a font to be symbolic.
226    ///
227    /// Pdfium may not reliably return the correct value of this flag for built-in fonts.
228    pub fn font_is_symbolic(&self) -> bool {
229        self.font_flags_bits().contains(FpdfFontDescriptorFlags::SYMBOLIC_BIT_3)
230    }
231
232    /// Returns `true` if the font applied to this character does not contain glyphs outside the
233    /// Adobe standard Latin character set.
234    ///
235    /// This classification of non-symbolic and symbolic fonts is peculiar to PDF. A font may
236    /// contain additional characters that are used in Latin writing systems but are outside the
237    /// Adobe standard Latin character set; PDF considers such a font to be symbolic.
238    ///
239    /// Pdfium may not reliably return the correct value of this flag for built-in fonts.
240    pub fn font_is_non_symbolic(&self) -> bool {
241        self.font_flags_bits()
242            .contains(FpdfFontDescriptorFlags::NON_SYMBOLIC_BIT_6)
243    }
244
245    /// Returns `true` if the glyphs in the font applied to this character are designed to resemble
246    /// cursive handwriting.
247    ///
248    /// Pdfium may not reliably return the correct value of this flag for built-in fonts.
249    pub fn font_is_cursive(&self) -> bool {
250        self.font_flags_bits().contains(FpdfFontDescriptorFlags::SCRIPT_BIT_4)
251    }
252
253    /// Returns `true` if the glyphs in the font applied to this character include dominant
254    /// vertical strokes that are slanted.
255    ///
256    /// Pdfium may not reliably return the correct value of this flag for built-in fonts.
257    pub fn font_is_italic(&self) -> bool {
258        self.font_flags_bits().contains(FpdfFontDescriptorFlags::ITALIC_BIT_7)
259    }
260
261    /// Returns `true` if the font applied to this character contains no lowercase letters by design.
262    ///
263    /// Pdfium may not reliably return the correct value of this flag for built-in fonts.
264    pub fn font_is_all_caps(&self) -> bool {
265        self.font_flags_bits().contains(FpdfFontDescriptorFlags::ALL_CAP_BIT_17)
266    }
267
268    /// Returns `true` if the lowercase letters in the font applied to this character have the
269    /// same shapes as the corresponding uppercase letters but are sized proportionally
270    /// so they have the same size and stroke weight as lowercase glyphs in the same typeface family.
271    ///
272    /// Pdfium may not reliably return the correct value of this flag for built-in fonts.
273    pub fn font_is_small_caps(&self) -> bool {
274        self.font_flags_bits()
275            .contains(FpdfFontDescriptorFlags::SMALL_CAP_BIT_18)
276    }
277
278    /// Returns `true` if bold glyphs in the font applied to this character are painted with
279    /// extra pixels at very small font sizes.
280    ///
281    /// Typically when glyphs are painted at small sizes on low-resolution devices, individual strokes
282    /// of bold glyphs may appear only one pixel wide. Because this is the minimum width of a pixel
283    /// based device, individual strokes of non-bold glyphs may also appear as one pixel wide
284    /// and therefore cannot be distinguished from bold glyphs. If this flag is set, individual
285    /// strokes of bold glyphs may be thickened at small font sizes.
286    ///
287    /// Pdfium may not reliably return the correct value of this flag for built-in fonts.
288    pub fn font_is_bold_reenforced(&self) -> bool {
289        self.font_flags_bits()
290            .contains(FpdfFontDescriptorFlags::FORCE_BOLD_BIT_19)
291    }
292
293    /// Returns combined font style information from a single FFI call.
294    ///
295    /// This is more efficient than calling `font_name()`, `font_is_bold_reenforced()`,
296    /// and `font_is_italic()` separately, as those each independently invoke the
297    /// underlying `FPDFText_GetFontInfo()` FFI function.
298    ///
299    /// Returns a tuple of (font_name, is_bold, is_italic).
300    pub fn font_info(&self) -> (String, bool, bool) {
301        let (name, flags) = self.font();
302        let name = name.unwrap_or_default();
303        let is_bold =
304            flags.contains(FpdfFontDescriptorFlags::FORCE_BOLD_BIT_19) || name.to_ascii_lowercase().contains("bold");
305        let is_italic = flags.contains(FpdfFontDescriptorFlags::ITALIC_BIT_7);
306        (name, is_bold, is_italic)
307    }
308
309    /// Returns the page text object that contains this character.
310    pub fn text_object(&self) -> Result<PdfPageTextObject<'_>, PdfiumError> {
311        let object_handle = self
312            .bindings()
313            .FPDFText_GetTextObject(self.text_page_handle(), self.index);
314
315        if object_handle.is_null() {
316            Err(PdfiumError::PdfiumLibraryInternalError(PdfiumInternalError::Unknown))
317        } else {
318            Ok(PdfPageTextObject::from_pdfium(
319                object_handle,
320                PdfPageObjectOwnership::owned_by_page(self.document_handle(), self.page_handle()),
321                self.bindings(),
322            ))
323        }
324    }
325
326    /// Returns the text rendering mode for this character.
327    pub fn render_mode(&self) -> Result<PdfPageTextRenderMode, PdfiumError> {
328        self.text_object().map(|text_object| text_object.render_mode())
329    }
330
331    /// Returns the fill color applied to this character.
332    pub fn fill_color(&self) -> Result<PdfColor, PdfiumError> {
333        let mut r = 0;
334
335        let mut g = 0;
336
337        let mut b = 0;
338
339        let mut a = 0;
340
341        if self.bindings.is_true(self.bindings.FPDFText_GetFillColor(
342            self.text_page_handle,
343            self.index,
344            &mut r,
345            &mut g,
346            &mut b,
347            &mut a,
348        )) {
349            Ok(PdfColor::new(
350                r.try_into()
351                    .map_err(PdfiumError::UnableToConvertPdfiumColorValueToRustu8)?,
352                g.try_into()
353                    .map_err(PdfiumError::UnableToConvertPdfiumColorValueToRustu8)?,
354                b.try_into()
355                    .map_err(PdfiumError::UnableToConvertPdfiumColorValueToRustu8)?,
356                a.try_into()
357                    .map_err(PdfiumError::UnableToConvertPdfiumColorValueToRustu8)?,
358            ))
359        } else {
360            Err(PdfiumError::PdfiumFunctionReturnValueIndicatedFailure)
361        }
362    }
363
364    /// Returns the stroke color applied to this character.
365    pub fn stroke_color(&self) -> Result<PdfColor, PdfiumError> {
366        let mut r = 0;
367
368        let mut g = 0;
369
370        let mut b = 0;
371
372        let mut a = 0;
373
374        if self.bindings().is_true(self.bindings.FPDFText_GetStrokeColor(
375            self.text_page_handle(),
376            self.index,
377            &mut r,
378            &mut g,
379            &mut b,
380            &mut a,
381        )) {
382            Ok(PdfColor::new(
383                r.try_into()
384                    .map_err(PdfiumError::UnableToConvertPdfiumColorValueToRustu8)?,
385                g.try_into()
386                    .map_err(PdfiumError::UnableToConvertPdfiumColorValueToRustu8)?,
387                b.try_into()
388                    .map_err(PdfiumError::UnableToConvertPdfiumColorValueToRustu8)?,
389                a.try_into()
390                    .map_err(PdfiumError::UnableToConvertPdfiumColorValueToRustu8)?,
391            ))
392        } else {
393            Err(PdfiumError::PdfiumFunctionReturnValueIndicatedFailure)
394        }
395    }
396
397    /// Returns the rotation angle of this character, expressed in degrees.
398    #[inline]
399    pub fn angle_degrees(&self) -> Result<f32, PdfiumError> {
400        self.angle_radians().map(|result| result.to_degrees())
401    }
402
403    /// Returns the rotation angle of this character, expressed in radians.
404    #[inline]
405    pub fn angle_radians(&self) -> Result<f32, PdfiumError> {
406        let result = self.bindings.FPDFText_GetCharAngle(self.text_page_handle, self.index);
407
408        if result == -1.0 {
409            Err(PdfiumError::PdfiumFunctionReturnValueIndicatedFailure)
410        } else {
411            Ok(result)
412        }
413    }
414
415    /// Returns a precise bounding box for this character, taking the character's specific
416    /// shape into account.
417    ///
418    /// To return a loose bounding box that contains the entire glyph bounds, use the
419    /// [PdfPageTextChar::loose_bounds] function.
420    pub fn tight_bounds(&self) -> Result<PdfRect, PdfiumError> {
421        let mut left = 0.0;
422
423        let mut bottom = 0.0;
424
425        let mut right = 0.0;
426
427        let mut top = 0.0;
428
429        let result = self.bindings().FPDFText_GetCharBox(
430            self.text_page_handle(),
431            self.index,
432            &mut left,
433            &mut right,
434            &mut bottom,
435            &mut top,
436        );
437
438        PdfRect::from_pdfium_as_result(
439            result,
440            FS_RECTF {
441                left: left as f32,
442                top: top as f32,
443                right: right as f32,
444                bottom: bottom as f32,
445            },
446            self.bindings(),
447        )
448    }
449
450    /// Returns a loose bounding box for this character, containing the entire glyph bounds.
451    ///
452    /// To return a tight bounding box that takes this character's specific shape into
453    /// account, use the [PdfPageTextChar::tight_bounds] function.
454    pub fn loose_bounds(&self) -> Result<PdfRect, PdfiumError> {
455        let mut bounds = FS_RECTF {
456            left: 0.0,
457            top: 0.0,
458            right: 0.0,
459            bottom: 0.0,
460        };
461
462        let result = self
463            .bindings
464            .FPDFText_GetLooseCharBox(self.text_page_handle(), self.index, &mut bounds);
465
466        PdfRect::from_pdfium_as_result(result, bounds, self.bindings())
467    }
468
469    /// Returns the current raw transformation matrix for this character.
470    fn get_matrix_impl(&self) -> Result<PdfMatrix, PdfiumError> {
471        let mut matrix = FS_MATRIX {
472            a: 0.0,
473            b: 0.0,
474            c: 0.0,
475            d: 0.0,
476            e: 0.0,
477            f: 0.0,
478        };
479
480        if self.bindings().is_true(
481            self.bindings()
482                .FPDFText_GetMatrix(self.text_page_handle(), self.index, &mut matrix),
483        ) {
484            Ok(PdfMatrix::from_pdfium(matrix))
485        } else {
486            Err(PdfiumError::PdfiumLibraryInternalError(PdfiumInternalError::Unknown))
487        }
488    }
489
490    create_transform_getters!(
491        "this [PdfPageTextChar]",
492        "this [PdfPageTextChar].",
493        "this [PdfPageTextChar],"
494    );
495
496    /// Returns the origin x and y positions of this character relative to its containing page.
497    pub fn origin(&self) -> Result<(PdfPoints, PdfPoints), PdfiumError> {
498        let mut x = 0.0;
499
500        let mut y = 0.0;
501
502        if self.bindings().is_true(self.bindings().FPDFText_GetCharOrigin(
503            self.text_page_handle(),
504            self.index,
505            &mut x,
506            &mut y,
507        )) {
508            Ok((PdfPoints::new(x as f32), PdfPoints::new(y as f32)))
509        } else {
510            Err(PdfiumError::PdfiumLibraryInternalError(PdfiumInternalError::Unknown))
511        }
512    }
513
514    /// Returns the origin x position of this character relative to its containing page.
515    #[inline]
516    pub fn origin_x(&self) -> Result<PdfPoints, PdfiumError> {
517        self.origin().map(|result| result.0)
518    }
519
520    /// Returns the origin y position of this character relative to its containing page.
521    #[inline]
522    pub fn origin_y(&self) -> Result<PdfPoints, PdfiumError> {
523        self.origin().map(|result| result.1)
524    }
525
526    /// Returns `true` if the glyph shape of this character descends below the font baseline.
527    #[inline]
528    pub fn has_descender(&self) -> bool {
529        self.tight_bounds().map(|bounds| bounds.bottom().value).unwrap_or(0.0)
530            < self.loose_bounds().map(|bounds| bounds.bottom().value).unwrap_or(0.0)
531    }
532
533    /// Returns `true` if this character was generated by Pdfium. This can be the case for
534    /// certain spacing, breaking, and justification-related characters.
535    #[inline]
536    pub fn is_generated(&self) -> Result<bool, PdfiumError> {
537        match self
538            .bindings()
539            .FPDFText_IsGenerated(self.text_page_handle(), self.index)
540        {
541            1 => Ok(true),
542            0 => Ok(false),
543            _ => Err(PdfiumError::PdfiumLibraryInternalError(PdfiumInternalError::Unknown)),
544        }
545    }
546
547    /// Returns `true` if this character is recognized as a hyphen by Pdfium.
548    #[inline]
549    pub fn is_hyphen(&self) -> Result<bool, PdfiumError> {
550        match self.bindings().FPDFText_IsHyphen(self.text_page_handle(), self.index) {
551            1 => Ok(true),
552            0 => Ok(false),
553            _ => Err(PdfiumError::PdfiumLibraryInternalError(PdfiumInternalError::Unknown)),
554        }
555    }
556
557    /// Returns `true` if this character has an invalid unicode mapping in the PDF font.
558    ///
559    /// This indicates the font's ToUnicode CMap is broken or missing for this glyph,
560    /// meaning the extracted text for this character is unreliable (tofu/garbage).
561    #[inline]
562    pub fn has_unicode_map_error(&self) -> Result<bool, PdfiumError> {
563        match self
564            .bindings()
565            .FPDFText_HasUnicodeMapError(self.text_page_handle(), self.index)
566        {
567            1 => Ok(true),
568            0 => Ok(false),
569            _ => Err(PdfiumError::PdfiumLibraryInternalError(PdfiumInternalError::Unknown)),
570        }
571    }
572}