1pub 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::PdfPage;
14use crate::pdf::document::page::annotation::PdfPageAnnotation;
15use crate::pdf::document::page::annotation::PdfPageAnnotationCommon;
16use crate::pdf::document::page::object::private::internal::PdfPageObjectPrivate;
17use crate::pdf::document::page::object::text::PdfPageTextObject;
18use crate::pdf::document::page::text::chars::{PdfPageTextCharIndex, PdfPageTextChars};
19use crate::pdf::document::page::text::search::{PdfPageTextSearch, PdfSearchOptions};
20use crate::pdf::document::page::text::segments::PdfPageTextSegments;
21use crate::pdf::points::PdfPoints;
22use crate::pdf::rect::PdfRect;
23use crate::utils::mem::{create_byte_buffer, create_sized_buffer};
24use crate::utils::utf16le::{get_pdfium_utf16le_bytes_from_str, get_string_from_pdfium_utf16le_bytes};
25use bytemuck::cast_slice;
26use std::fmt::{Display, Formatter};
27use std::os::raw::{c_double, c_int};
28use std::ptr::null_mut;
29
30pub(super) fn filter_generated_spaces_direct(
45 text_page_handle: crate::bindgen::FPDF_TEXTPAGE,
46 start: i32,
47 count: i32,
48 space_ratio: f32,
49 bindings: &dyn PdfiumLibraryBindings,
50) -> String {
51 if count <= 0 {
52 return String::new();
53 }
54
55 let end = start + count;
56 let mut result = String::with_capacity(count as usize);
57 let mut prev_right_x: Option<f32> = None;
58 let mut prev_font_size: f32 = 12.0;
59
60 let mut i = start;
61 while i < end {
62 let idx = i as std::os::raw::c_int;
63
64 if bindings.FPDFText_IsGenerated(text_page_handle, idx) != 0 {
65 if let Some(prev_r) = prev_right_x {
66 let mut j = i + 1;
67 while j < end {
68 let jdx = j as std::os::raw::c_int;
69 if bindings.FPDFText_IsGenerated(text_page_handle, jdx) == 0 {
70 let mut left = 0.0_f64;
71 let mut bottom = 0.0_f64;
72 let mut right = 0.0_f64;
73 let mut top = 0.0_f64;
74 if bindings.FPDFText_GetCharBox(
75 text_page_handle,
76 jdx,
77 &mut left,
78 &mut right,
79 &mut bottom,
80 &mut top,
81 ) != 0
82 {
83 let gap = left as f32 - prev_r;
84 let next_fs = bindings.FPDFText_GetFontSize(text_page_handle, jdx) as f32;
85 let ref_fs = if next_fs > 0.0 { next_fs } else { prev_font_size };
86 if gap > ref_fs * space_ratio {
87 result.push(' ');
88 }
89 } else {
90 result.push(' ');
91 }
92 break;
93 }
94 j += 1;
95 }
96 if j >= end {
97 result.push(' ');
98 }
99 }
100 i += 1;
101 continue;
102 }
103
104 let unicode_val = bindings.FPDFText_GetUnicode(text_page_handle, idx);
105 if let Some(uc) = char::from_u32(unicode_val) {
106 if uc == '\r' {
107 result.push('\n');
108 prev_right_x = None;
109 i += 1;
110 continue;
111 }
112 if !uc.is_control() || uc == '\n' || uc == '\t' {
113 result.push(uc);
114 }
115 }
116
117 let mut left = 0.0_f64;
118 let mut bottom = 0.0_f64;
119 let mut right = 0.0_f64;
120 let mut top = 0.0_f64;
121 if bindings.FPDFText_GetCharBox(text_page_handle, idx, &mut left, &mut right, &mut bottom, &mut top) != 0 {
122 prev_right_x = Some(right as f32);
123 }
124
125 let fs = bindings.FPDFText_GetFontSize(text_page_handle, idx) as f32;
126 if fs > 0.0 {
127 prev_font_size = fs;
128 }
129
130 i += 1;
131 }
132
133 result
134}
135
136pub struct PdfPageText<'a> {
150 text_page_handle: FPDF_TEXTPAGE,
151 page: &'a PdfPage<'a>,
152 bindings: &'a dyn PdfiumLibraryBindings,
153}
154
155impl<'a> PdfPageText<'a> {
156 pub(crate) fn from_pdfium(
157 text_page_handle: FPDF_TEXTPAGE,
158 page: &'a PdfPage<'a>,
159 bindings: &'a dyn PdfiumLibraryBindings,
160 ) -> Self {
161 PdfPageText {
162 text_page_handle,
163 page,
164 bindings,
165 }
166 }
167
168 #[inline]
170 pub(crate) fn text_page_handle(&self) -> FPDF_TEXTPAGE {
171 self.text_page_handle
172 }
173
174 #[inline]
176 pub fn bindings(&self) -> &'a dyn PdfiumLibraryBindings {
177 self.bindings
178 }
179
180 #[inline]
185 pub fn len(&self) -> i32 {
186 self.bindings.FPDFText_CountChars(self.text_page_handle())
187 }
188
189 #[inline]
191 pub fn is_empty(&self) -> bool {
192 self.len() == 0
193 }
194
195 #[inline]
197 pub fn segments(&self) -> PdfPageTextSegments<'_> {
198 PdfPageTextSegments::new(self, 0, self.len(), self.bindings())
199 }
200
201 #[inline]
204 pub fn segments_subset(&self, start: PdfPageTextCharIndex, count: PdfPageTextCharIndex) -> PdfPageTextSegments<'_> {
205 PdfPageTextSegments::new(self, start as i32, count as i32, self.bindings())
206 }
207
208 #[inline]
210 pub fn chars(&self) -> PdfPageTextChars<'_> {
211 PdfPageTextChars::new(
212 self.page.document_handle(),
213 self.page.page_handle(),
214 self.text_page_handle(),
215 (0..self.len()).collect(),
216 self.bindings(),
217 )
218 }
219
220 #[inline]
225 pub fn chars_for_object(&self, object: &PdfPageTextObject) -> Result<PdfPageTextChars<'_>, PdfiumError> {
226 Ok(PdfPageTextChars::new(
227 self.page.document_handle(),
228 self.page.page_handle(),
229 self.text_page_handle(),
230 self.chars()
231 .iter()
232 .filter(|char| {
233 self.bindings
234 .FPDFText_GetTextObject(self.text_page_handle(), char.index() as i32)
235 == object.object_handle()
236 })
237 .map(|char| char.index() as i32)
238 .collect(),
239 self.bindings(),
240 ))
241 }
242
243 #[inline]
248 pub fn chars_for_annotation(&self, annotation: &PdfPageAnnotation) -> Result<PdfPageTextChars<'_>, PdfiumError> {
249 self.chars_inside_rect(annotation.bounds()?)
250 .map_err(|_| PdfiumError::NoCharsInAnnotation)
251 }
252
253 #[inline]
256 pub fn chars_inside_rect(&self, rect: PdfRect) -> Result<PdfPageTextChars<'_>, PdfiumError> {
257 let tolerance_x = rect.width() / 2.0;
258 let tolerance_y = rect.height() / 2.0;
259 let center_height = rect.bottom() + tolerance_y;
260
261 match (
262 Self::get_char_index_near_point(
263 self.text_page_handle(),
264 rect.left(),
265 tolerance_x,
266 center_height,
267 tolerance_y,
268 self.bindings(),
269 ),
270 Self::get_char_index_near_point(
271 self.text_page_handle(),
272 rect.right(),
273 tolerance_x,
274 center_height,
275 tolerance_y,
276 self.bindings(),
277 ),
278 ) {
279 (Some(start), Some(end)) => Ok(PdfPageTextChars::new(
280 self.page.document_handle(),
281 self.page.page_handle(),
282 self.text_page_handle(),
283 (start as i32..=end as i32 + 1).collect(),
284 self.bindings,
285 )),
286 (Some(start), None) => Ok(PdfPageTextChars::new(
287 self.page.document_handle(),
288 self.page.page_handle(),
289 self.text_page_handle(),
290 (start as i32..=start as i32 + 1).collect(),
291 self.bindings,
292 )),
293 (None, Some(end)) => Ok(PdfPageTextChars::new(
294 self.page.document_handle(),
295 self.page.page_handle(),
296 self.text_page_handle(),
297 (end as i32..=end as i32 + 1).collect(),
298 self.bindings,
299 )),
300 _ => Err(PdfiumError::NoCharsInRect),
301 }
302 }
303
304 pub(crate) fn get_char_index_near_point(
308 text_page_handle: FPDF_TEXTPAGE,
309 x: PdfPoints,
310 tolerance_x: PdfPoints,
311 y: PdfPoints,
312 tolerance_y: PdfPoints,
313 bindings: &dyn PdfiumLibraryBindings,
314 ) -> Option<PdfPageTextCharIndex> {
315 match bindings.FPDFText_GetCharIndexAtPos(
316 text_page_handle,
317 x.value as c_double,
318 y.value as c_double,
319 tolerance_x.value as c_double,
320 tolerance_y.value as c_double,
321 ) {
322 -1 => None,
323 -3 => None,
324 index => Some(index as PdfPageTextCharIndex),
325 }
326 }
327
328 pub fn all(&self) -> String {
335 self.inside_rect(self.page.page_size())
336 }
337
338 pub fn all_respaced(&self, space_ratio: f32) -> String {
348 let count = self.len();
349 filter_generated_spaces_direct(self.text_page_handle(), 0, count, space_ratio, self.bindings)
350 }
351
352 pub fn inside_rect(&self, rect: PdfRect) -> String {
360 let left = rect.left().value as f64;
361
362 let top = rect.top().value as f64;
363
364 let right = rect.right().value as f64;
365
366 let bottom = rect.bottom().value as f64;
367
368 let chars_count =
369 self.bindings()
370 .FPDFText_GetBoundedText(self.text_page_handle(), left, top, right, bottom, null_mut(), 0);
371
372 if chars_count == 0 {
373 return String::new();
374 }
375
376 let mut buffer = create_sized_buffer(chars_count as usize);
377
378 let result = self.bindings().FPDFText_GetBoundedText(
379 self.text_page_handle(),
380 left,
381 top,
382 right,
383 bottom,
384 buffer.as_mut_ptr(),
385 chars_count,
386 );
387
388 assert_eq!(result, chars_count);
389
390 get_string_from_pdfium_utf16le_bytes(cast_slice(buffer.as_slice()).to_vec()).unwrap_or_default()
391 }
392
393 pub fn inside_rect_respaced(&self, rect: PdfRect, space_ratio: f32) -> String {
399 let chars = match self.chars_inside_rect(rect) {
400 Ok(c) => c,
401 Err(_) => {
402 log::warn!("chars_inside_rect failed, falling back to unrespaced text");
403 return self.inside_rect(rect);
404 }
405 };
406 let count = chars.len();
407 if count == 0 {
408 return String::new();
409 }
410 let start = chars.first_char_index().unwrap_or(0) as i32;
411 filter_generated_spaces_direct(self.text_page_handle(), start, count as i32, space_ratio, self.bindings)
412 }
413
414 pub fn for_object(&self, object: &PdfPageTextObject) -> String {
417 let buffer_length =
418 self.bindings()
419 .FPDFTextObj_GetText(object.object_handle(), self.text_page_handle(), null_mut(), 0);
420
421 if buffer_length == 0 {
422 return String::new();
423 }
424
425 let mut buffer = create_byte_buffer(buffer_length as usize);
426
427 let result = self.bindings().FPDFTextObj_GetText(
428 object.object_handle(),
429 self.text_page_handle(),
430 buffer.as_mut_ptr() as *mut FPDF_WCHAR,
431 buffer_length,
432 );
433
434 assert_eq!(result, buffer_length);
435
436 get_string_from_pdfium_utf16le_bytes(buffer).unwrap_or_default()
437 }
438
439 pub fn text_object_for_char_index(&self, index: usize) -> Option<usize> {
446 let handle = self
447 .bindings()
448 .FPDFText_GetTextObject(self.text_page_handle(), index as std::ffi::c_int);
449 if handle.is_null() { None } else { Some(handle as usize) }
450 }
451
452 #[inline]
460 pub fn for_annotation(&self, annotation: &PdfPageAnnotation) -> Result<String, PdfiumError> {
461 let bounds = annotation.bounds()?;
462
463 Ok(self.inside_rect(bounds))
464 }
465
466 #[inline]
469 pub fn search(&self, text: &str, options: &PdfSearchOptions) -> Result<PdfPageTextSearch<'_>, PdfiumError> {
470 self.search_from(text, options, 0)
471 }
472
473 pub fn search_from(
477 &self,
478 text: &str,
479 options: &PdfSearchOptions,
480 index: PdfPageTextCharIndex,
481 ) -> Result<PdfPageTextSearch<'_>, PdfiumError> {
482 if text.is_empty() {
483 Err(PdfiumError::TextSearchTargetIsEmpty)
484 } else {
485 Ok(PdfPageTextSearch::from_pdfium(
486 self.bindings().FPDFText_FindStart(
487 self.text_page_handle(),
488 get_pdfium_utf16le_bytes_from_str(text).as_ptr() as FPDF_WIDESTRING,
489 options.as_pdfium(),
490 index as c_int,
491 ),
492 self,
493 self.bindings(),
494 ))
495 }
496 }
497}
498
499impl<'a> Display for PdfPageText<'a> {
500 #[inline]
501 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
502 f.write_str(self.all().as_str())
503 }
504}
505
506impl<'a> Drop for PdfPageText<'a> {
507 #[inline]
509 fn drop(&mut self) {
510 self.bindings().FPDFText_ClosePage(self.text_page_handle());
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use itertools::Itertools;
517 use std::ffi::OsStr;
518 use std::fs;
519
520 use crate::prelude::*;
521 use crate::utils::test::{test_bind_to_pdfium, test_fixture_path};
522
523 #[test]
524 fn test_overlapping_chars_results() -> Result<(), PdfiumError> {
525 let pdfium = test_bind_to_pdfium();
526
527 let mut document = pdfium.create_new_pdf()?;
528
529 let mut page = document.pages_mut().create_page_at_start(PdfPagePaperSize::a4())?;
530
531 let font = document.fonts_mut().courier();
532
533 let txt1 = page.objects_mut().create_text_object(
534 PdfPoints::ZERO,
535 PdfPoints::ZERO,
536 "AAAAAA",
537 font,
538 PdfPoints::new(10.0),
539 )?;
540
541 let txt2 = page.objects_mut().create_text_object(
542 PdfPoints::ZERO,
543 PdfPoints::ZERO,
544 "BBBBBB",
545 font,
546 PdfPoints::new(10.0),
547 )?;
548
549 let txt3 = page.objects_mut().create_text_object(
550 PdfPoints::ZERO,
551 PdfPoints::ZERO,
552 "CDCDCDE",
553 font,
554 PdfPoints::new(10.0),
555 )?;
556
557 let page_text = page.text()?;
558
559 assert!(test_one_overlapping_text_object_results(&txt1, &page_text, "AAAAAA")?);
560 assert!(test_one_overlapping_text_object_results(&txt2, &page_text, "BBBBBB")?);
561 assert!(test_one_overlapping_text_object_results(&txt3, &page_text, "CDCDCDE")?);
562
563 Ok(())
564 }
565
566 fn test_one_overlapping_text_object_results(
567 object: &PdfPageObject,
568 page_text: &PdfPageText,
569 expected: &str,
570 ) -> Result<bool, PdfiumError> {
571 if let Some(txt) = object.as_text_object() {
572 assert_eq!(txt.text().trim(), expected);
573 assert_eq!(page_text.for_object(txt).trim(), expected);
574
575 for (index, char) in txt.chars(page_text)?.iter().enumerate() {
576 assert_eq!(txt.text().chars().nth(index), char.unicode_char());
577 assert_eq!(expected.chars().nth(index), char.unicode_char());
578 }
579
580 Ok(true)
581 } else {
582 Ok(false)
583 }
584 }
585
586 #[test]
587 fn test_text_chars_results_equality() -> Result<(), PdfiumError> {
588 let pdfium = test_bind_to_pdfium();
589
590 let fixture_dir = test_fixture_path("");
591 let samples = fs::read_dir(&fixture_dir)
592 .unwrap()
593 .filter_map(|entry| match entry {
594 Ok(e) => Some(e.path()),
595 Err(_) => None,
596 })
597 .filter(|path| path.extension() == Some(OsStr::new("pdf")))
598 .collect::<Vec<_>>();
599
600 assert!(!samples.is_empty());
601
602 for sample in samples {
603 println!("Testing all text objects in file {}", sample.display());
604
605 let document = pdfium.load_pdf_from_file(&sample, None)?;
606
607 for page in document.pages().iter() {
608 let text = page.text()?;
609
610 for object in page.objects().iter() {
611 if let Some(obj) = object.as_text_object() {
612 let chars = obj
613 .chars(&text)?
614 .iter()
615 .filter_map(|char| char.unicode_string())
616 .join("");
617
618 assert_eq!(obj.text().trim(), chars.replace("\0", "").trim());
619 }
620 }
621 }
622 }
623
624 Ok(())
625 }
626}