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