1#![forbid(unsafe_code)]
12
13use otf_pdf_content::{Device, Glyph, GraphicsState, run_page};
14use otf_pdf_doc::{Document, Page, Rect, decode_text_string};
15use otf_pdf_font::FontCache;
16use otf_pdf_syntax::{Dict, Error, Name, Result};
17
18#[derive(Debug, Clone, PartialEq)]
20pub struct TextChar {
21 pub text: String,
23 pub origin: (f64, f64),
25 pub end: (f64, f64),
27 pub size: f64,
29 pub font: String,
31 pub invisible: bool,
33}
34
35#[derive(Debug, Clone, PartialEq)]
37pub struct Word {
38 pub text: String,
40 pub bbox: Rect,
43}
44
45#[derive(Debug, Clone, PartialEq)]
47pub struct TextLine {
48 pub text: String,
51 pub bbox: Rect,
53 pub chars: Vec<TextChar>,
55}
56
57impl TextLine {
58 pub fn words(&self) -> Vec<Word> {
60 let mut words = Vec::new();
61 let mut current: Vec<&TextChar> = Vec::new();
62 let flush = |current: &mut Vec<&TextChar>, words: &mut Vec<Word>| {
63 if !current.is_empty() {
64 words.push(Word {
65 text: current.iter().map(|c| c.text.as_str()).collect(),
66 bbox: bounds(current.iter().copied()),
67 });
68 current.clear();
69 }
70 };
71 for c in &self.chars {
72 if c.text.trim().is_empty() || c.text == " " {
73 flush(&mut current, &mut words);
74 } else {
75 current.push(c);
76 }
77 }
78 flush(&mut current, &mut words);
79 words
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Default)]
85pub struct PageText {
86 pub lines: Vec<TextLine>,
88}
89
90impl PageText {
91 pub fn text(&self) -> String {
93 let mut out = String::new();
94 for line in &self.lines {
95 out.push_str(&line.text);
96 out.push('\n');
97 }
98 out
99 }
100
101 pub fn words(&self) -> Vec<Word> {
103 self.lines.iter().flat_map(TextLine::words).collect()
104 }
105}
106
107pub struct TextExtractor<'a> {
110 doc: &'a Document,
111 fonts: FontCache,
112}
113
114impl<'a> TextExtractor<'a> {
115 pub fn new(doc: &'a Document) -> Self {
117 Self {
118 doc,
119 fonts: FontCache::new(),
120 }
121 }
122
123 pub fn page(&self, index: usize) -> Result<PageText> {
125 let pages = self.doc.pages()?;
126 let page = pages
127 .get(index)
128 .ok_or(Error::syntax(0, "page index out of range"))?;
129 let mut device = Collector::new(page);
130 run_page(self.doc.store(), page, &self.fonts, &mut device)?;
131 Ok(PageText {
132 lines: build_lines(device.chars),
133 })
134 }
135
136 pub fn page_text(&self, index: usize) -> Result<String> {
138 self.page(index).map(|p| p.text())
139 }
140
141 pub fn text(&self) -> Result<String> {
145 let count = self.doc.page_count()?;
146 let mut out = String::new();
147 for i in 0..count {
148 match self.page_text(i) {
149 Ok(t) => out.push_str(&t),
150 Err(e @ Error::Limit(_)) => return Err(e),
151 Err(_) => {}
152 }
153 out.push('\x0c');
154 }
155 Ok(out)
156 }
157}
158
159#[derive(Debug, Clone, Copy)]
161struct Display {
162 crop: Rect,
163 rotate: u16,
164}
165
166impl Display {
167 fn point(&self, (x, y): (f64, f64)) -> (f64, f64) {
168 let Rect { llx, lly, urx, ury } = self.crop;
169 match self.rotate {
170 90 => (y - lly, urx - x),
171 180 => (urx - x, ury - y),
172 270 => (ury - y, x - llx),
173 _ => (x - llx, y - lly),
174 }
175 }
176}
177
178struct Collector {
180 display: Display,
181 chars: Vec<TextChar>,
182 marked: Vec<Option<(String, bool)>>,
185}
186
187impl Collector {
188 fn new(page: &Page) -> Self {
189 Self {
190 display: Display {
191 crop: page.crop_box,
192 rotate: page.rotate,
193 },
194 chars: Vec::new(),
195 marked: Vec::new(),
196 }
197 }
198}
199
200impl Device for Collector {
201 fn draw_glyph(&mut self, glyph: &Glyph, state: &GraphicsState) {
202 if !state.in_clip(glyph.origin, 0.5) {
205 return;
206 }
207 let origin = self.display.point(glyph.origin);
208 let end = self.display.point(glyph.end);
209 if let Some(Some((text, emitted))) = self.marked.iter_mut().rev().find(|m| m.is_some()) {
212 if *emitted {
213 if let Some(last) = self.chars.last_mut() {
214 last.end = end;
215 }
216 return;
217 }
218 *emitted = true;
219 let text = text.clone();
220 self.chars.push(TextChar {
221 text,
222 origin,
223 end,
224 size: glyph.size,
225 font: glyph.font.name().to_owned(),
226 invisible: glyph.render_mode == 3,
227 });
228 return;
229 }
230 let Some(text) = glyph.unicode.clone() else {
231 return;
232 };
233 let text: String = text
235 .chars()
236 .filter(|c| !c.is_control() || *c == '\t')
237 .collect();
238 if text.is_empty() {
239 return;
240 }
241 self.chars.push(TextChar {
242 text,
243 origin,
244 end,
245 size: glyph.size,
246 font: glyph.font.name().to_owned(),
247 invisible: glyph.render_mode == 3,
248 });
249 }
250
251 fn begin_marked_content(&mut self, _tag: &Name, properties: Option<&Dict>) {
252 let actual = properties
253 .and_then(|p| p.get(b"ActualText"))
254 .and_then(|t| t.as_string())
255 .map(decode_text_string);
256 self.marked.push(actual.map(|t| (t, false)));
257 }
258
259 fn end_marked_content(&mut self) {
260 self.marked.pop();
261 }
262}
263
264fn bounds<'c>(chars: impl Iterator<Item = &'c TextChar>) -> Rect {
265 let mut r = Rect {
266 llx: f64::INFINITY,
267 lly: f64::INFINITY,
268 urx: f64::NEG_INFINITY,
269 ury: f64::NEG_INFINITY,
270 };
271 for c in chars {
272 let (dx, dy) = direction(c);
273 let (ux, uy) = (-dy, dx);
275 for (x, y) in [c.origin, c.end] {
276 for k in [0.8, -0.2] {
277 let px = x + ux * k * c.size;
278 let py = y + uy * k * c.size;
279 r.llx = r.llx.min(px);
280 r.lly = r.lly.min(py);
281 r.urx = r.urx.max(px);
282 r.ury = r.ury.max(py);
283 }
284 }
285 }
286 if !r.llx.is_finite() {
287 return Rect {
288 llx: 0.0,
289 lly: 0.0,
290 urx: 0.0,
291 ury: 0.0,
292 };
293 }
294 r
295}
296
297fn direction(c: &TextChar) -> (f64, f64) {
299 let (dx, dy) = (c.end.0 - c.origin.0, c.end.1 - c.origin.1);
300 let len = (dx * dx + dy * dy).sqrt();
301 if len < 1e-9 {
302 (1.0, 0.0)
303 } else {
304 (dx / len, dy / len)
305 }
306}
307
308const SPACE_GAP: f64 = 0.2;
310
311fn build_lines(chars: Vec<TextChar>) -> Vec<TextLine> {
313 let mut lines: Vec<TextLine> = Vec::new();
314 let mut current: Vec<TextChar> = Vec::new();
315 let mut text = String::new();
316 let mut dir = (1.0, 0.0);
317
318 let finish = |current: &mut Vec<TextChar>, text: &mut String, lines: &mut Vec<TextLine>| {
319 let t = text.trim_end().to_owned();
320 if !current.is_empty() && !t.is_empty() {
321 lines.push(TextLine {
322 text: t,
323 bbox: bounds(current.iter()),
324 chars: std::mem::take(current),
325 });
326 }
327 current.clear();
328 text.clear();
329 };
330
331 for c in chars {
332 let d = direction(&c);
333 if let Some(last) = current.last() {
334 let size = c.size.max(last.size).max(1e-6);
335 let same_direction = d.0 * dir.0 + d.1 * dir.1 > 0.985;
336 let (gx, gy) = (c.origin.0 - last.end.0, c.origin.1 - last.end.1);
337 let along = gx * dir.0 + gy * dir.1;
338 let across = (-gx * dir.1 + gy * dir.0).abs();
339 let continues = same_direction && across < 0.5 * size && along > -0.5 * size;
340 if !continues {
341 finish(&mut current, &mut text, &mut lines);
342 } else if along > SPACE_GAP * size
343 && !text.ends_with(char::is_whitespace)
344 && !c.text.starts_with(char::is_whitespace)
345 {
346 text.push(' ');
347 current.push(TextChar {
348 text: " ".into(),
349 origin: last.end,
350 end: c.origin,
351 size: c.size,
352 font: c.font.clone(),
353 invisible: c.invisible,
354 });
355 }
356 }
357 if current.is_empty() {
358 dir = d;
359 }
360 text.push_str(&c.text);
361 current.push(c);
362 }
363 finish(&mut current, &mut text, &mut lines);
364 lines
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 fn ch(text: &str, x: f64, y: f64, w: f64) -> TextChar {
372 TextChar {
373 text: text.into(),
374 origin: (x, y),
375 end: (x + w, y),
376 size: 10.0,
377 font: "F".into(),
378 invisible: false,
379 }
380 }
381
382 #[test]
383 fn lines_spaces_and_words() {
384 let chars = vec![
385 ch("H", 0.0, 100.0, 6.0),
386 ch("i", 6.0, 100.0, 3.0),
387 ch("y", 14.0, 100.0, 5.0),
389 ch("o", 19.0, 100.0, 5.0),
390 ch("N", 0.0, 85.0, 7.0),
392 ch(" ", 7.0, 85.0, 3.0),
393 ch("o", 10.0, 85.0, 5.0),
394 ];
395 let lines = build_lines(chars);
396 assert_eq!(lines.len(), 2);
397 assert_eq!(lines[0].text, "Hi yo");
398 assert_eq!(lines[1].text, "N o");
399 let words = lines[0].words();
400 assert_eq!(
401 words.iter().map(|w| w.text.as_str()).collect::<Vec<_>>(),
402 ["Hi", "yo"]
403 );
404 assert_eq!(words[0].bbox.llx, 0.0);
405 assert_eq!(words[0].bbox.urx, 9.0);
406 assert!((words[0].bbox.ury - 108.0).abs() < 1e-9);
407 let page = PageText { lines };
408 assert_eq!(page.text(), "Hi yo\nN o\n");
409 assert_eq!(page.words().len(), 4);
410 }
411
412 #[test]
413 fn going_backwards_starts_a_line() {
414 let lines = build_lines(vec![ch("a", 50.0, 10.0, 5.0), ch("b", 0.0, 10.0, 5.0)]);
415 assert_eq!(lines.len(), 2);
416 }
417
418 #[test]
419 fn vertical_text_is_its_own_line() {
420 let down = |t: &str, y: f64| TextChar {
421 text: t.into(),
422 origin: (0.0, y),
423 end: (0.0, y - 10.0),
424 size: 10.0,
425 font: "F".into(),
426 invisible: false,
427 };
428 let lines = build_lines(vec![
429 down("縦", 100.0),
430 down("書", 90.0),
431 ch("x", 50.0, 50.0, 5.0),
432 ]);
433 assert_eq!(lines.len(), 2);
434 assert_eq!(lines[0].text, "縦書");
435 }
436
437 #[test]
438 fn rotation_to_display_space() {
439 let crop = Rect {
440 llx: 0.0,
441 lly: 0.0,
442 urx: 100.0,
443 ury: 200.0,
444 };
445 let at = |rotate| Display { crop, rotate }.point((0.0, 200.0));
446 assert_eq!(at(0), (0.0, 200.0));
447 assert_eq!(at(90), (200.0, 100.0));
448 assert_eq!(at(180), (100.0, 0.0));
449 assert_eq!(at(270), (0.0, 0.0));
450 }
451}