rust_pdf_box/text/
stripper.rs1use crate::cos::lexer::{Lexer, Token};
2use crate::cos::object::COSObject;
3use crate::error::PdfResult;
4use crate::pd::document::PDDocument;
5
6pub struct PDFTextStripper;
8
9impl PDFTextStripper {
10 pub fn extract_text(doc: &PDDocument) -> PdfResult<String> {
12 let mut full_text = String::new();
13 for i in 0..doc.page_count() {
14 let page_text = Self::extract_page_text(doc, i)?;
15 if !page_text.is_empty() {
16 if !full_text.is_empty() {
17 full_text.push_str("\n\n");
18 }
19 full_text.push_str(&page_text);
20 }
21 }
22 Ok(full_text)
23 }
24
25 pub fn extract_page_text(doc: &PDDocument, page_index: usize) -> PdfResult<String> {
27 let page = doc.get_page(page_index)?;
28 let content_ids = page.content_stream_ids();
29
30 let mut page_text = String::new();
31
32 for id in content_ids {
33 if let Some(COSObject::Stream(stream)) = doc.cos_doc.get_object(id) {
34 if let Ok(data) = stream.decode() {
35 let stream_text = Self::extract_text_from_stream(&data);
36 if !stream_text.is_empty() {
37 if !page_text.is_empty() {
38 page_text.push('\n');
39 }
40 page_text.push_str(&stream_text);
41 }
42 }
43 }
44 }
45
46 Ok(page_text)
47 }
48
49 fn extract_text_from_stream(stream_data: &[u8]) -> String {
51 let mut lexer = Lexer::new(stream_data);
52 let mut extracted = String::new();
53 let mut token_stack: Vec<Token> = Vec::new();
54 let mut in_text_object = false;
55
56 while let Ok(token) = lexer.next_token() {
57 match token {
58 Token::EOF => break,
59 Token::Keyword(ref k) => match k.as_str() {
60 "BT" => {
61 in_text_object = true;
62 token_stack.clear();
63 }
64 "ET" => {
65 in_text_object = false;
66 token_stack.clear();
67 if !extracted.ends_with('\n') && !extracted.is_empty() {
68 extracted.push('\n');
69 }
70 }
71 "T*" => {
72 extracted.push('\n');
73 token_stack.clear();
74 }
75 "Td" | "TD" => {
76 if token_stack.len() >= 2 {
78 if let Token::Real(ty) = token_stack[token_stack.len() - 1] {
79 if ty < 0.0 && !extracted.ends_with('\n') {
80 extracted.push('\n');
81 }
82 } else if let Token::Integer(ty) = token_stack[token_stack.len() - 1] {
83 if ty < 0 && !extracted.ends_with('\n') {
84 extracted.push('\n');
85 }
86 }
87 }
88 token_stack.clear();
89 }
90 "Tj" => {
91 if let Some(Token::String(bytes, _)) = token_stack.pop() {
92 let text = String::from_utf8_lossy(&bytes);
93 extracted.push_str(&text);
94 }
95 token_stack.clear();
96 }
97 "'" => {
98 extracted.push('\n');
99 if let Some(Token::String(bytes, _)) = token_stack.pop() {
100 let text = String::from_utf8_lossy(&bytes);
101 extracted.push_str(&text);
102 }
103 token_stack.clear();
104 }
105 "\"" => {
106 extracted.push('\n');
107 if let Some(Token::String(bytes, _)) = token_stack.pop() {
108 let text = String::from_utf8_lossy(&bytes);
109 extracted.push_str(&text);
110 }
111 token_stack.clear();
112 }
113 "TJ" => {
114 if let Some(Token::ArrayEnd) = token_stack.pop() {
116 let mut elements = Vec::new();
117 while let Some(top) = token_stack.pop() {
118 if top == Token::ArrayStart {
119 break;
120 }
121 elements.push(top);
122 }
123 elements.reverse();
124
125 for el in elements {
126 match el {
127 Token::String(bytes, _) => {
128 let text = String::from_utf8_lossy(&bytes);
129 extracted.push_str(&text);
130 }
131 Token::Integer(kern) if kern < -150 => {
132 if !extracted.ends_with(' ') {
134 extracted.push(' ');
135 }
136 }
137 Token::Real(kern) if kern < -150.0 => {
138 if !extracted.ends_with(' ') {
139 extracted.push(' ');
140 }
141 }
142 _ => {}
143 }
144 }
145 }
146 token_stack.clear();
147 }
148 _ => {
149 token_stack.clear();
150 }
151 },
152 other => {
153 if in_text_object {
154 token_stack.push(other);
155 }
156 }
157 }
158 }
159
160 extracted.trim().to_string()
161 }
162}