1use 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#[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 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 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 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 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 pub fn is_list_element(&self) -> bool {
138 matches!(self, Self::L | Self::LI | Self::Lbl | Self::LBody)
139 }
140
141 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#[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn children(&self) -> PdfStructElementChildrenIterator<'a> {
327 PdfStructElementChildrenIterator {
328 element: self.clone(),
329 count: self.children_count(),
330 index: 0,
331 }
332 }
333
334 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 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
365pub 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}