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::annotation::PdfPageAnnotation;
14use crate::pdf::document::page::annotation::PdfPageAnnotationCommon;
15use crate::pdf::document::page::object::private::internal::PdfPageObjectPrivate;
16use crate::pdf::document::page::object::text::PdfPageTextObject;
17use crate::pdf::document::page::text::chars::{PdfPageTextCharIndex, PdfPageTextChars};
18use crate::pdf::document::page::text::search::{PdfPageTextSearch, PdfSearchOptions};
19use crate::pdf::document::page::text::segments::PdfPageTextSegments;
20use crate::pdf::document::page::PdfPage;
21use crate::pdf::points::PdfPoints;
22use crate::pdf::rect::PdfRect;
23use crate::pdfium::PdfiumLibraryBindingsAccessor;
24use crate::utils::mem::{create_byte_buffer, create_sized_buffer};
25use crate::utils::utf16le::{
26 get_pdfium_utf16le_bytes_from_str, get_string_from_pdfium_utf16le_bytes,
27};
28use bytemuck::cast_slice;
29use std::fmt::{Display, Formatter};
30use std::marker::PhantomData;
31use std::os::raw::{c_double, c_int};
32use std::ptr::null_mut;
33
34pub struct PdfPageText<'a> {
48 text_page_handle: FPDF_TEXTPAGE,
49 page: &'a PdfPage<'a>,
50 lifetime: PhantomData<&'a FPDF_TEXTPAGE>,
51}
52
53impl<'a> PdfPageText<'a> {
54 pub(crate) fn from_pdfium(text_page_handle: FPDF_TEXTPAGE, page: &'a PdfPage<'a>) -> Self {
55 PdfPageText {
56 text_page_handle,
57 page,
58 lifetime: PhantomData,
59 }
60 }
61
62 #[inline]
64 pub(crate) fn text_page_handle(&self) -> FPDF_TEXTPAGE {
65 self.text_page_handle
66 }
67
68 #[inline]
73 pub fn len(&self) -> i32 {
74 unsafe { self.bindings().FPDFText_CountChars(self.text_page_handle()) }
75 }
76
77 #[inline]
79 pub fn is_empty(&self) -> bool {
80 self.len() == 0
81 }
82
83 #[inline]
85 pub fn segments(&self) -> PdfPageTextSegments<'_> {
86 PdfPageTextSegments::new(self, 0, self.len(), self.bindings())
87 }
88
89 #[inline]
92 pub fn segments_subset(
93 &self,
94 start: PdfPageTextCharIndex,
95 count: PdfPageTextCharIndex,
96 ) -> PdfPageTextSegments<'_> {
97 PdfPageTextSegments::new(self, start as i32, count as i32, self.bindings())
98 }
99
100 #[inline]
102 pub fn chars(&self) -> PdfPageTextChars<'_> {
103 PdfPageTextChars::new(
104 self.page.document_handle(),
105 self.page.page_handle(),
106 self.text_page_handle(),
107 (0..self.len()).collect(),
108 )
109 }
110
111 #[cfg(any(
112 feature = "pdfium_future",
113 feature = "pdfium_7881",
114 feature = "pdfium_7763",
115 feature = "pdfium_7543",
116 feature = "pdfium_7350",
117 feature = "pdfium_7215",
118 feature = "pdfium_7123",
119 feature = "pdfium_6996",
120 feature = "pdfium_6721",
121 feature = "pdfium_6666",
122 feature = "pdfium_6611",
123 ))]
124 #[inline]
129 pub fn chars_for_object(
130 &self,
131 object: &PdfPageTextObject,
132 ) -> Result<PdfPageTextChars<'_>, PdfiumError> {
133 Ok(PdfPageTextChars::new(
134 self.page.document_handle(),
135 self.page.page_handle(),
136 self.text_page_handle(),
137 self.chars()
138 .iter()
139 .filter(|char| {
140 (unsafe {
141 self.bindings()
142 .FPDFText_GetTextObject(self.text_page_handle(), char.index() as i32)
143 }) == object.object_handle()
144 })
145 .map(|char| char.index() as i32)
146 .collect(),
147 ))
148 }
149
150 #[inline]
155 pub fn chars_for_annotation(
156 &self,
157 annotation: &PdfPageAnnotation,
158 ) -> Result<PdfPageTextChars<'_>, PdfiumError> {
159 self.chars_inside_rect(annotation.bounds()?)
160 .map_err(|_| PdfiumError::NoCharsInAnnotation)
161 }
162
163 #[inline]
166 pub fn chars_inside_rect<'b>(
167 &'b self,
168 rect: PdfRect,
169 ) -> Result<PdfPageTextChars<'a>, PdfiumError> {
170 let tolerance_x = rect.width() / 2.0;
171 let tolerance_y = rect.height() / 2.0;
172 let center_height = rect.bottom() + tolerance_y;
173
174 match (
175 Self::get_char_index_near_point(
176 self.text_page_handle(),
177 rect.left(),
178 tolerance_x,
179 center_height,
180 tolerance_y,
181 self.bindings(),
182 ),
183 Self::get_char_index_near_point(
184 self.text_page_handle(),
185 rect.right(),
186 tolerance_x,
187 center_height,
188 tolerance_y,
189 self.bindings(),
190 ),
191 ) {
192 (Some(start), Some(end)) => Ok(PdfPageTextChars::new(
193 self.page.document_handle(),
194 self.page.page_handle(),
195 self.text_page_handle(),
196 (start as i32..=end as i32 + 1).collect(),
197 )),
198 (Some(start), None) => Ok(PdfPageTextChars::new(
199 self.page.document_handle(),
200 self.page.page_handle(),
201 self.text_page_handle(),
202 (start as i32..=start as i32 + 1).collect(),
203 )),
204 (None, Some(end)) => Ok(PdfPageTextChars::new(
205 self.page.document_handle(),
206 self.page.page_handle(),
207 self.text_page_handle(),
208 (end as i32..=end as i32 + 1).collect(),
209 )),
210 _ => Err(PdfiumError::NoCharsInRect),
211 }
212 }
213
214 pub(crate) fn get_char_index_near_point(
218 text_page_handle: FPDF_TEXTPAGE,
219 x: PdfPoints,
220 tolerance_x: PdfPoints,
221 y: PdfPoints,
222 tolerance_y: PdfPoints,
223 bindings: &dyn PdfiumLibraryBindings,
224 ) -> Option<PdfPageTextCharIndex> {
225 match unsafe {
226 bindings.FPDFText_GetCharIndexAtPos(
227 text_page_handle,
228 x.value as c_double,
229 y.value as c_double,
230 tolerance_x.value as c_double,
231 tolerance_y.value as c_double,
232 )
233 } {
234 -1 => None, -3 => None, index => Some(index as PdfPageTextCharIndex),
237 }
238 }
239
240 pub fn all(&self) -> String {
247 self.inside_rect(self.page.page_size())
248 }
249
250 pub fn inside_rect(&self, rect: PdfRect) -> String {
258 let left = rect.left().value as f64;
268
269 let top = rect.top().value as f64;
270
271 let right = rect.right().value as f64;
272
273 let bottom = rect.bottom().value as f64;
274
275 let chars_count = unsafe {
276 self.bindings().FPDFText_GetBoundedText(
277 self.text_page_handle(),
278 left,
279 top,
280 right,
281 bottom,
282 null_mut(),
283 0,
284 )
285 };
286
287 if chars_count == 0 {
288 return String::new();
291 }
292
293 let mut buffer = create_sized_buffer(chars_count as usize);
294
295 let result = unsafe {
296 self.bindings().FPDFText_GetBoundedText(
297 self.text_page_handle(),
298 left,
299 top,
300 right,
301 bottom,
302 buffer.as_mut_ptr(),
303 chars_count,
304 )
305 };
306
307 assert_eq!(result, chars_count);
308
309 get_string_from_pdfium_utf16le_bytes(cast_slice(buffer.as_slice()).to_vec())
310 .unwrap_or_default()
311 }
312
313 pub fn for_object(&self, object: &PdfPageTextObject) -> String {
316 let buffer_length = unsafe {
326 self.bindings().FPDFTextObj_GetText(
327 object.object_handle(),
328 self.text_page_handle(),
329 null_mut(),
330 0,
331 )
332 };
333
334 if buffer_length == 0 {
335 return String::new();
338 }
339
340 let mut buffer = create_byte_buffer(buffer_length as usize);
341
342 let result = unsafe {
343 self.bindings().FPDFTextObj_GetText(
344 object.object_handle(),
345 self.text_page_handle(),
346 buffer.as_mut_ptr() as *mut FPDF_WCHAR,
347 buffer_length,
348 )
349 };
350
351 assert_eq!(result, buffer_length);
352
353 get_string_from_pdfium_utf16le_bytes(buffer).unwrap_or_default()
354 }
355
356 #[inline]
364 pub fn for_annotation(&self, annotation: &PdfPageAnnotation) -> Result<String, PdfiumError> {
365 let bounds = annotation.bounds()?;
366
367 Ok(self.inside_rect(bounds))
368 }
369
370 #[inline]
373 pub fn search(
374 &self,
375 text: &str,
376 options: &PdfSearchOptions,
377 ) -> Result<PdfPageTextSearch<'_>, PdfiumError> {
378 self.search_from(text, options, 0)
379 }
380
381 pub fn search_from(
385 &self,
386 text: &str,
387 options: &PdfSearchOptions,
388 index: PdfPageTextCharIndex,
389 ) -> Result<PdfPageTextSearch<'_>, PdfiumError> {
390 if text.is_empty() {
391 Err(PdfiumError::TextSearchTargetIsEmpty)
392 } else {
393 Ok(PdfPageTextSearch::from_pdfium(
394 unsafe {
395 self.bindings().FPDFText_FindStart(
396 self.text_page_handle(),
397 get_pdfium_utf16le_bytes_from_str(text).as_ptr() as FPDF_WIDESTRING,
398 options.as_pdfium(),
399 index as c_int,
400 )
401 },
402 self,
403 ))
404 }
405 }
406}
407
408impl<'a> Display for PdfPageText<'a> {
409 #[inline]
410 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
411 f.write_str(self.all().as_str())
412 }
413}
414
415impl<'a> Drop for PdfPageText<'a> {
416 #[inline]
418 fn drop(&mut self) {
419 unsafe {
420 self.bindings().FPDFText_ClosePage(self.text_page_handle());
421 }
422 }
423}
424
425impl<'a> PdfiumLibraryBindingsAccessor<'a> for PdfPageText<'a> {}
426
427#[cfg(feature = "thread_safe")]
428unsafe impl<'a> Send for PdfPageText<'a> {}
429
430#[cfg(feature = "thread_safe")]
431unsafe impl<'a> Sync for PdfPageText<'a> {}
432
433#[cfg(test)]
434mod tests {
435 use itertools::Itertools;
436 use std::ffi::OsStr;
437 use std::fs;
438
439 use crate::prelude::*;
440 use crate::utils::test::test_bind_to_pdfium;
441
442 #[test]
443 fn test_overlapping_chars_results() -> Result<(), PdfiumError> {
444 let pdfium = test_bind_to_pdfium();
449
450 let mut document = pdfium.create_new_pdf()?;
453
454 let mut page = document
455 .pages_mut()
456 .create_page_at_start(PdfPagePaperSize::a4())?;
457
458 let font = document.fonts_mut().courier();
459
460 let txt1 = page.objects_mut().create_text_object(
461 PdfPoints::ZERO,
462 PdfPoints::ZERO,
463 "AAAAAA",
464 font,
465 PdfPoints::new(10.0),
466 )?;
467
468 let txt2 = page.objects_mut().create_text_object(
469 PdfPoints::ZERO,
470 PdfPoints::ZERO,
471 "BBBBBB",
472 font,
473 PdfPoints::new(10.0),
474 )?;
475
476 let txt3 = page.objects_mut().create_text_object(
477 PdfPoints::ZERO,
478 PdfPoints::ZERO,
479 "CDCDCDE",
480 font,
481 PdfPoints::new(10.0),
482 )?;
483
484 let page_text = page.text()?;
485
486 assert!(test_one_overlapping_text_object_results(
489 &txt1, &page_text, "AAAAAA"
490 )?);
491 assert!(test_one_overlapping_text_object_results(
492 &txt2, &page_text, "BBBBBB"
493 )?);
494 assert!(test_one_overlapping_text_object_results(
495 &txt3, &page_text, "CDCDCDE"
496 )?);
497
498 Ok(())
499 }
500
501 fn test_one_overlapping_text_object_results(
502 object: &PdfPageObject,
503 page_text: &PdfPageText,
504 expected: &str,
505 ) -> Result<bool, PdfiumError> {
506 if let Some(txt) = object.as_text_object() {
507 assert_eq!(txt.text().trim(), expected);
508 assert_eq!(page_text.for_object(txt).trim(), expected);
509
510 for (index, char) in txt.chars(&page_text)?.iter().enumerate() {
511 assert_eq!(txt.text().chars().nth(index), char.unicode_char());
512 assert_eq!(expected.chars().nth(index), char.unicode_char());
513 }
514
515 Ok(true)
516 } else {
517 Ok(false)
518 }
519 }
520
521 #[test]
522 fn test_text_chars_results_equality() -> Result<(), PdfiumError> {
523 let pdfium = test_bind_to_pdfium();
527
528 let samples = fs::read_dir("./test/")
529 .unwrap()
530 .filter_map(|entry| match entry {
531 Ok(e) => Some(e.path()),
532 Err(_) => None,
533 })
534 .filter(|path| path.extension() == Some(OsStr::new("pdf")))
535 .collect::<Vec<_>>();
536
537 assert!(samples.len() > 0);
538
539 for sample in samples {
540 println!("Testing all text objects in file {}", sample.display());
541
542 let document = pdfium.load_pdf_from_file(&sample, None)?;
543
544 for page in document.pages().iter() {
545 let text = page.text()?;
546
547 for object in page.objects().iter() {
548 if let Some(obj) = object.as_text_object() {
549 let chars = obj
550 .chars(&text)?
551 .iter()
552 .filter_map(|char| char.unicode_string())
553 .join("");
554
555 assert_eq!(obj.text().trim(), chars.replace("\0", "").trim());
556 }
557 }
558 }
559 }
560
561 Ok(())
562 }
563
564 #[test]
565 fn test_text_segment_chars_char_lifetimes() -> Result<(), PdfiumError> {
566 let pdfium = test_bind_to_pdfium();
571 let document = pdfium.load_pdf_from_file("./test/export-test.pdf", None)?;
572 let page = document.pages().first()?;
573 let text = page.text()?;
574
575 let _char = {
576 let chars = {
577 let segment = text.segments().first()?;
578
579 segment.chars()?
580 }; chars.first()?
583 }; Ok(())
586 }
587}