Skip to main content

pdfium_render/pdf/document/page/
struct_element.rs

1//! Defines the [PdfStructElement] struct, exposing functionality related to a single
2//! structure element in a PDF structure tree.
3
4use crate::bindgen::FPDF_STRUCTELEMENT;
5use crate::bindings::PdfiumLibraryBindings;
6use crate::utils::mem::create_byte_buffer;
7use crate::utils::utf16le::get_string_from_pdfium_utf16le_bytes;
8use std::os::raw::{c_int, c_ulong, c_void};
9
10/// The type of a PDF structure element, corresponding to the standard structure types
11/// defined in the PDF specification (ISO 32000).
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum PdfStructElementType {
14    Document,
15    Part,
16    Div,
17    Span,
18    P,
19    H,
20    H1,
21    H2,
22    H3,
23    H4,
24    H5,
25    H6,
26    Table,
27    TR,
28    TH,
29    TD,
30    THead,
31    TBody,
32    TFoot,
33    L,
34    LI,
35    Lbl,
36    LBody,
37    Figure,
38    Formula,
39    Form,
40    Code,
41    BlockQuote,
42    Caption,
43    Link,
44    Note,
45    TOC,
46    TOCI,
47    Sect,
48    Art,
49    Reference,
50    BibEntry,
51    Quote,
52    Index,
53    NonStruct,
54    Other(String),
55}
56
57impl PdfStructElementType {
58    /// Parses a PDF structure type string (the /S entry) into a [PdfStructElementType].
59    pub fn from_pdf_type_string(s: &str) -> Self {
60        match s {
61            "Document" => Self::Document,
62            "Part" => Self::Part,
63            "Div" => Self::Div,
64            "Span" => Self::Span,
65            "P" => Self::P,
66            "H" => Self::H,
67            "H1" => Self::H1,
68            "H2" => Self::H2,
69            "H3" => Self::H3,
70            "H4" => Self::H4,
71            "H5" => Self::H5,
72            "H6" => Self::H6,
73            "Table" => Self::Table,
74            "TR" => Self::TR,
75            "TH" => Self::TH,
76            "TD" => Self::TD,
77            "THead" => Self::THead,
78            "TBody" => Self::TBody,
79            "TFoot" => Self::TFoot,
80            "L" => Self::L,
81            "LI" => Self::LI,
82            "Lbl" => Self::Lbl,
83            "LBody" => Self::LBody,
84            "Figure" => Self::Figure,
85            "Formula" => Self::Formula,
86            "Form" => Self::Form,
87            "Code" => Self::Code,
88            "BlockQuote" => Self::BlockQuote,
89            "Caption" => Self::Caption,
90            "Link" => Self::Link,
91            "Note" => Self::Note,
92            "TOC" => Self::TOC,
93            "TOCI" => Self::TOCI,
94            "Sect" => Self::Sect,
95            "Art" => Self::Art,
96            "Reference" => Self::Reference,
97            "BibEntry" => Self::BibEntry,
98            "Quote" => Self::Quote,
99            "Index" => Self::Index,
100            "NonStruct" => Self::NonStruct,
101            other => Self::Other(other.to_string()),
102        }
103    }
104
105    /// Returns `true` if this element type is a heading (H, H1-H6).
106    pub fn is_heading(&self) -> bool {
107        matches!(
108            self,
109            Self::H | Self::H1 | Self::H2 | Self::H3 | Self::H4 | Self::H5 | Self::H6
110        )
111    }
112
113    /// Returns the heading level (1-6) for H1-H6 elements, or `None` for non-heading elements
114    /// and the generic H element.
115    pub fn heading_level(&self) -> Option<u8> {
116        match self {
117            Self::H1 => Some(1),
118            Self::H2 => Some(2),
119            Self::H3 => Some(3),
120            Self::H4 => Some(4),
121            Self::H5 => Some(5),
122            Self::H6 => Some(6),
123            _ => None,
124        }
125    }
126
127    /// Returns `true` if this element type is a table-related element
128    /// (Table, TR, TH, TD, THead, TBody, TFoot).
129    pub fn is_table_element(&self) -> bool {
130        matches!(
131            self,
132            Self::Table | Self::TR | Self::TH | Self::TD | Self::THead | Self::TBody | Self::TFoot
133        )
134    }
135
136    /// Returns `true` if this element type is a list-related element (L, LI, Lbl, LBody).
137    pub fn is_list_element(&self) -> bool {
138        matches!(self, Self::L | Self::LI | Self::Lbl | Self::LBody)
139    }
140
141    /// Returns `true` if this element type is a block-level element.
142    pub fn is_block_level(&self) -> bool {
143        matches!(
144            self,
145            Self::Document
146                | Self::Part
147                | Self::Div
148                | Self::Sect
149                | Self::Art
150                | Self::P
151                | Self::H
152                | Self::H1
153                | Self::H2
154                | Self::H3
155                | Self::H4
156                | Self::H5
157                | Self::H6
158                | Self::Table
159                | Self::L
160                | Self::Figure
161                | Self::Formula
162                | Self::Form
163                | Self::BlockQuote
164                | Self::Code
165                | Self::TOC
166                | Self::TOCI
167                | Self::Index
168                | Self::Caption
169                | Self::Note
170        )
171    }
172}
173
174/// A single element in the structure tree of a tagged PDF page.
175///
176/// Structure elements carry semantic information such as element type (paragraph,
177/// heading, table cell, etc.), alternative text, actual text, language, and
178/// marked content identifiers that associate the element with content on the page.
179///
180/// The element handle is not owned by this struct; it is owned by the parent
181/// `FPDF_STRUCTTREE` and remains valid for as long as that tree is open.
182#[derive(Clone)]
183pub struct PdfStructElement<'a> {
184    element_handle: FPDF_STRUCTELEMENT,
185    bindings: &'a dyn PdfiumLibraryBindings,
186}
187
188impl<'a> PdfStructElement<'a> {
189    pub(crate) fn from_pdfium(element_handle: FPDF_STRUCTELEMENT, bindings: &'a dyn PdfiumLibraryBindings) -> Self {
190        Self {
191            element_handle,
192            bindings,
193        }
194    }
195
196    /// Extracts a UTF-16LE string from pdfium using the standard two-call buffer pattern.
197    /// The `get_fn` is called first with a null buffer to determine the required size,
198    /// then again with a properly sized buffer.
199    fn extract_utf16_string<F>(&self, get_fn: F) -> Option<String>
200    where
201        F: Fn(FPDF_STRUCTELEMENT, *mut c_void, c_ulong) -> c_ulong,
202    {
203        let buffer_length = get_fn(self.element_handle, std::ptr::null_mut(), 0);
204
205        if buffer_length == 0 {
206            return None;
207        }
208
209        let mut buffer = create_byte_buffer(buffer_length as usize);
210
211        let result = get_fn(self.element_handle, buffer.as_mut_ptr() as *mut c_void, buffer_length);
212
213        assert_eq!(result, buffer_length);
214
215        get_string_from_pdfium_utf16le_bytes(buffer)
216    }
217
218    /// Returns the parsed [PdfStructElementType] of this element.
219    pub fn element_type(&self) -> PdfStructElementType {
220        match self.element_type_raw() {
221            Some(raw) => PdfStructElementType::from_pdf_type_string(&raw),
222            None => PdfStructElementType::Other(String::new()),
223        }
224    }
225
226    /// Returns the raw type string (/S entry) of this element, if any.
227    pub fn element_type_raw(&self) -> Option<String> {
228        self.extract_utf16_string(|handle, buf, len| self.bindings.FPDF_StructElement_GetType(handle, buf, len))
229    }
230
231    /// Returns the title (/T entry) of this element, if any.
232    pub fn title(&self) -> Option<String> {
233        self.extract_utf16_string(|handle, buf, len| self.bindings.FPDF_StructElement_GetTitle(handle, buf, len))
234    }
235
236    /// Returns the alternative text (/Alt entry) of this element, if any.
237    /// This is typically used for accessibility purposes.
238    pub fn alt_text(&self) -> Option<String> {
239        self.extract_utf16_string(|handle, buf, len| self.bindings.FPDF_StructElement_GetAltText(handle, buf, len))
240    }
241
242    /// Returns the actual text (/ActualText entry) of this element, if any.
243    pub fn actual_text(&self) -> Option<String> {
244        self.extract_utf16_string(|handle, buf, len| self.bindings.FPDF_StructElement_GetActualText(handle, buf, len))
245    }
246
247    /// Returns the ID of this element, if any.
248    pub fn id(&self) -> Option<String> {
249        self.extract_utf16_string(|handle, buf, len| self.bindings.FPDF_StructElement_GetID(handle, buf, len))
250    }
251
252    /// Returns the language (IETF BCP 47 code) of this element, if any.
253    pub fn lang(&self) -> Option<String> {
254        self.extract_utf16_string(|handle, buf, len| self.bindings.FPDF_StructElement_GetLang(handle, buf, len))
255    }
256
257    /// Returns the primary marked content ID of this element, or `None` if no ID exists.
258    ///
259    /// Consider using [PdfStructElement::all_marked_content_ids] to retrieve all MCIDs,
260    /// as an element may have more than one.
261    pub fn marked_content_id(&self) -> Option<i32> {
262        let id = self.bindings.FPDF_StructElement_GetMarkedContentID(self.element_handle);
263        if id == -1 { None } else { Some(id) }
264    }
265
266    /// Returns the count of marked content IDs associated with this element.
267    pub fn marked_content_id_count(&self) -> usize {
268        let count = self
269            .bindings
270            .FPDF_StructElement_GetMarkedContentIdCount(self.element_handle);
271        if count < 0 { 0 } else { count as usize }
272    }
273
274    /// Returns the marked content ID at the given index, or `None` if the index is
275    /// out of bounds or no ID exists at that index.
276    pub fn marked_content_id_at_index(&self, index: usize) -> Option<i32> {
277        let id = self
278            .bindings
279            .FPDF_StructElement_GetMarkedContentIdAtIndex(self.element_handle, index as c_int);
280        if id == -1 { None } else { Some(id) }
281    }
282
283    /// Returns all marked content IDs associated with this element.
284    pub fn all_marked_content_ids(&self) -> Vec<i32> {
285        let count = self.marked_content_id_count();
286        let mut ids = Vec::with_capacity(count);
287        for i in 0..count {
288            if let Some(id) = self.marked_content_id_at_index(i) {
289                ids.push(id);
290            }
291        }
292        ids
293    }
294
295    /// Returns the parent structure element, or `None` if this is a root element.
296    pub fn parent(&self) -> Option<PdfStructElement<'a>> {
297        let handle = self.bindings.FPDF_StructElement_GetParent(self.element_handle);
298        if handle.is_null() {
299            None
300        } else {
301            Some(PdfStructElement::from_pdfium(handle, self.bindings))
302        }
303    }
304
305    /// Returns the number of direct children of this element.
306    pub fn children_count(&self) -> usize {
307        let count = self.bindings.FPDF_StructElement_CountChildren(self.element_handle);
308        if count < 0 { 0 } else { count as usize }
309    }
310
311    /// Returns the child element at the given index, or `None` if the index is out of
312    /// bounds or the child at that index is not a structure element (e.g. it is a
313    /// marked-content reference).
314    pub fn child_at_index(&self, index: usize) -> Option<PdfStructElement<'a>> {
315        let handle = self
316            .bindings
317            .FPDF_StructElement_GetChildAtIndex(self.element_handle, index as c_int);
318        if handle.is_null() {
319            None
320        } else {
321            Some(PdfStructElement::from_pdfium(handle, self.bindings))
322        }
323    }
324
325    /// Returns an iterator over the direct children of this element.
326    pub fn children(&self) -> PdfStructElementChildrenIterator<'a> {
327        PdfStructElementChildrenIterator {
328            element: self.clone(),
329            count: self.children_count(),
330            index: 0,
331        }
332    }
333
334    /// Returns the number of attributes on this element.
335    pub fn attribute_count(&self) -> usize {
336        let count = self.bindings.FPDF_StructElement_GetAttributeCount(self.element_handle);
337        if count < 0 { 0 } else { count as usize }
338    }
339
340    /// Returns the value of a string attribute with the given name, if any.
341    pub fn string_attribute(&self, name: &str) -> Option<String> {
342        let buffer_length =
343            self.bindings
344                .FPDF_StructElement_GetStringAttribute(self.element_handle, name, std::ptr::null_mut(), 0);
345
346        if buffer_length == 0 {
347            return None;
348        }
349
350        let mut buffer = create_byte_buffer(buffer_length as usize);
351
352        let result = self.bindings.FPDF_StructElement_GetStringAttribute(
353            self.element_handle,
354            name,
355            buffer.as_mut_ptr() as *mut c_void,
356            buffer_length,
357        );
358
359        assert_eq!(result, buffer_length);
360
361        get_string_from_pdfium_utf16le_bytes(buffer)
362    }
363}
364
365/// An iterator over the direct children of a [PdfStructElement].
366pub struct PdfStructElementChildrenIterator<'a> {
367    element: PdfStructElement<'a>,
368    count: usize,
369    index: usize,
370}
371
372impl<'a> Iterator for PdfStructElementChildrenIterator<'a> {
373    type Item = PdfStructElement<'a>;
374
375    fn next(&mut self) -> Option<Self::Item> {
376        while self.index < self.count {
377            let current = self.index;
378            self.index += 1;
379            if let Some(child) = self.element.child_at_index(current) {
380                return Some(child);
381            }
382        }
383        None
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    #[test]
392    fn test_from_pdf_type_string_all_standard_types() {
393        assert_eq!(
394            PdfStructElementType::from_pdf_type_string("Document"),
395            PdfStructElementType::Document
396        );
397        assert_eq!(
398            PdfStructElementType::from_pdf_type_string("Part"),
399            PdfStructElementType::Part
400        );
401        assert_eq!(
402            PdfStructElementType::from_pdf_type_string("Div"),
403            PdfStructElementType::Div
404        );
405        assert_eq!(
406            PdfStructElementType::from_pdf_type_string("Span"),
407            PdfStructElementType::Span
408        );
409        assert_eq!(PdfStructElementType::from_pdf_type_string("P"), PdfStructElementType::P);
410        assert_eq!(PdfStructElementType::from_pdf_type_string("H"), PdfStructElementType::H);
411        assert_eq!(
412            PdfStructElementType::from_pdf_type_string("H1"),
413            PdfStructElementType::H1
414        );
415        assert_eq!(
416            PdfStructElementType::from_pdf_type_string("H2"),
417            PdfStructElementType::H2
418        );
419        assert_eq!(
420            PdfStructElementType::from_pdf_type_string("H3"),
421            PdfStructElementType::H3
422        );
423        assert_eq!(
424            PdfStructElementType::from_pdf_type_string("H4"),
425            PdfStructElementType::H4
426        );
427        assert_eq!(
428            PdfStructElementType::from_pdf_type_string("H5"),
429            PdfStructElementType::H5
430        );
431        assert_eq!(
432            PdfStructElementType::from_pdf_type_string("H6"),
433            PdfStructElementType::H6
434        );
435        assert_eq!(
436            PdfStructElementType::from_pdf_type_string("Table"),
437            PdfStructElementType::Table
438        );
439        assert_eq!(
440            PdfStructElementType::from_pdf_type_string("TR"),
441            PdfStructElementType::TR
442        );
443        assert_eq!(
444            PdfStructElementType::from_pdf_type_string("TH"),
445            PdfStructElementType::TH
446        );
447        assert_eq!(
448            PdfStructElementType::from_pdf_type_string("TD"),
449            PdfStructElementType::TD
450        );
451        assert_eq!(
452            PdfStructElementType::from_pdf_type_string("THead"),
453            PdfStructElementType::THead
454        );
455        assert_eq!(
456            PdfStructElementType::from_pdf_type_string("TBody"),
457            PdfStructElementType::TBody
458        );
459        assert_eq!(
460            PdfStructElementType::from_pdf_type_string("TFoot"),
461            PdfStructElementType::TFoot
462        );
463        assert_eq!(PdfStructElementType::from_pdf_type_string("L"), PdfStructElementType::L);
464        assert_eq!(
465            PdfStructElementType::from_pdf_type_string("LI"),
466            PdfStructElementType::LI
467        );
468        assert_eq!(
469            PdfStructElementType::from_pdf_type_string("Lbl"),
470            PdfStructElementType::Lbl
471        );
472        assert_eq!(
473            PdfStructElementType::from_pdf_type_string("LBody"),
474            PdfStructElementType::LBody
475        );
476        assert_eq!(
477            PdfStructElementType::from_pdf_type_string("Figure"),
478            PdfStructElementType::Figure
479        );
480        assert_eq!(
481            PdfStructElementType::from_pdf_type_string("Formula"),
482            PdfStructElementType::Formula
483        );
484        assert_eq!(
485            PdfStructElementType::from_pdf_type_string("Form"),
486            PdfStructElementType::Form
487        );
488        assert_eq!(
489            PdfStructElementType::from_pdf_type_string("Code"),
490            PdfStructElementType::Code
491        );
492        assert_eq!(
493            PdfStructElementType::from_pdf_type_string("BlockQuote"),
494            PdfStructElementType::BlockQuote
495        );
496        assert_eq!(
497            PdfStructElementType::from_pdf_type_string("Caption"),
498            PdfStructElementType::Caption
499        );
500        assert_eq!(
501            PdfStructElementType::from_pdf_type_string("Link"),
502            PdfStructElementType::Link
503        );
504        assert_eq!(
505            PdfStructElementType::from_pdf_type_string("Note"),
506            PdfStructElementType::Note
507        );
508        assert_eq!(
509            PdfStructElementType::from_pdf_type_string("TOC"),
510            PdfStructElementType::TOC
511        );
512        assert_eq!(
513            PdfStructElementType::from_pdf_type_string("TOCI"),
514            PdfStructElementType::TOCI
515        );
516        assert_eq!(
517            PdfStructElementType::from_pdf_type_string("Sect"),
518            PdfStructElementType::Sect
519        );
520        assert_eq!(
521            PdfStructElementType::from_pdf_type_string("Art"),
522            PdfStructElementType::Art
523        );
524        assert_eq!(
525            PdfStructElementType::from_pdf_type_string("Reference"),
526            PdfStructElementType::Reference
527        );
528        assert_eq!(
529            PdfStructElementType::from_pdf_type_string("BibEntry"),
530            PdfStructElementType::BibEntry
531        );
532        assert_eq!(
533            PdfStructElementType::from_pdf_type_string("Quote"),
534            PdfStructElementType::Quote
535        );
536        assert_eq!(
537            PdfStructElementType::from_pdf_type_string("Index"),
538            PdfStructElementType::Index
539        );
540        assert_eq!(
541            PdfStructElementType::from_pdf_type_string("NonStruct"),
542            PdfStructElementType::NonStruct
543        );
544    }
545
546    #[test]
547    fn test_from_pdf_type_string_unknown() {
548        assert_eq!(
549            PdfStructElementType::from_pdf_type_string("CustomType"),
550            PdfStructElementType::Other("CustomType".to_string())
551        );
552        assert_eq!(
553            PdfStructElementType::from_pdf_type_string(""),
554            PdfStructElementType::Other(String::new())
555        );
556    }
557
558    #[test]
559    fn test_is_heading() {
560        assert!(PdfStructElementType::H.is_heading());
561        assert!(PdfStructElementType::H1.is_heading());
562        assert!(PdfStructElementType::H2.is_heading());
563        assert!(PdfStructElementType::H3.is_heading());
564        assert!(PdfStructElementType::H4.is_heading());
565        assert!(PdfStructElementType::H5.is_heading());
566        assert!(PdfStructElementType::H6.is_heading());
567
568        assert!(!PdfStructElementType::P.is_heading());
569        assert!(!PdfStructElementType::Table.is_heading());
570        assert!(!PdfStructElementType::Document.is_heading());
571        assert!(!PdfStructElementType::Other("H7".to_string()).is_heading());
572    }
573
574    #[test]
575    fn test_heading_level() {
576        assert_eq!(PdfStructElementType::H1.heading_level(), Some(1));
577        assert_eq!(PdfStructElementType::H2.heading_level(), Some(2));
578        assert_eq!(PdfStructElementType::H3.heading_level(), Some(3));
579        assert_eq!(PdfStructElementType::H4.heading_level(), Some(4));
580        assert_eq!(PdfStructElementType::H5.heading_level(), Some(5));
581        assert_eq!(PdfStructElementType::H6.heading_level(), Some(6));
582
583        assert_eq!(PdfStructElementType::H.heading_level(), None);
584        assert_eq!(PdfStructElementType::P.heading_level(), None);
585    }
586
587    #[test]
588    fn test_is_table_element() {
589        assert!(PdfStructElementType::Table.is_table_element());
590        assert!(PdfStructElementType::TR.is_table_element());
591        assert!(PdfStructElementType::TH.is_table_element());
592        assert!(PdfStructElementType::TD.is_table_element());
593        assert!(PdfStructElementType::THead.is_table_element());
594        assert!(PdfStructElementType::TBody.is_table_element());
595        assert!(PdfStructElementType::TFoot.is_table_element());
596
597        assert!(!PdfStructElementType::P.is_table_element());
598        assert!(!PdfStructElementType::L.is_table_element());
599    }
600
601    #[test]
602    fn test_is_list_element() {
603        assert!(PdfStructElementType::L.is_list_element());
604        assert!(PdfStructElementType::LI.is_list_element());
605        assert!(PdfStructElementType::Lbl.is_list_element());
606        assert!(PdfStructElementType::LBody.is_list_element());
607
608        assert!(!PdfStructElementType::P.is_list_element());
609        assert!(!PdfStructElementType::Table.is_list_element());
610    }
611
612    #[test]
613    fn test_is_block_level() {
614        assert!(PdfStructElementType::Document.is_block_level());
615        assert!(PdfStructElementType::Part.is_block_level());
616        assert!(PdfStructElementType::P.is_block_level());
617        assert!(PdfStructElementType::H1.is_block_level());
618        assert!(PdfStructElementType::Table.is_block_level());
619        assert!(PdfStructElementType::L.is_block_level());
620        assert!(PdfStructElementType::Figure.is_block_level());
621        assert!(PdfStructElementType::BlockQuote.is_block_level());
622        assert!(PdfStructElementType::Code.is_block_level());
623
624        assert!(!PdfStructElementType::Span.is_block_level());
625        assert!(!PdfStructElementType::Link.is_block_level());
626        assert!(!PdfStructElementType::TD.is_block_level());
627        assert!(!PdfStructElementType::TH.is_block_level());
628        assert!(!PdfStructElementType::TR.is_block_level());
629        assert!(!PdfStructElementType::LI.is_block_level());
630        assert!(!PdfStructElementType::Lbl.is_block_level());
631        assert!(!PdfStructElementType::LBody.is_block_level());
632    }
633
634    #[test]
635    fn test_element_type_equality() {
636        assert_eq!(PdfStructElementType::P, PdfStructElementType::P);
637        assert_ne!(PdfStructElementType::P, PdfStructElementType::H1);
638        assert_eq!(
639            PdfStructElementType::Other("X".to_string()),
640            PdfStructElementType::Other("X".to_string())
641        );
642        assert_ne!(
643            PdfStructElementType::Other("X".to_string()),
644            PdfStructElementType::Other("Y".to_string())
645        );
646    }
647
648    #[test]
649    fn test_element_type_clone() {
650        let t = PdfStructElementType::H1;
651        let cloned = t.clone();
652        assert_eq!(t, cloned);
653
654        let other = PdfStructElementType::Other("Custom".to_string());
655        let cloned = other.clone();
656        assert_eq!(other, cloned);
657    }
658
659    #[test]
660    fn test_element_type_hash() {
661        use std::collections::HashSet;
662        let mut set = HashSet::new();
663        set.insert(PdfStructElementType::P);
664        set.insert(PdfStructElementType::H1);
665        set.insert(PdfStructElementType::P);
666        assert_eq!(set.len(), 2);
667    }
668}