1use crate::engine::{OcrEngine, OcrPageResult};
13use crate::error::{OcrError, Result};
14use lopdf::content::{Content, Operation};
15use lopdf::{dictionary, Document, Object, ObjectId, Stream};
16
17#[derive(Debug, Clone)]
19pub struct OcrConfig {
20 pub dpi: u32,
22 pub text_threshold: usize,
24 pub pages: Vec<u32>,
26}
27
28impl Default for OcrConfig {
29 fn default() -> Self {
30 Self {
31 dpi: 300,
32 text_threshold: 10,
33 pages: Vec::new(),
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
40pub struct OcrReport {
41 pub pages: Vec<OcrPageReport>,
43 pub pages_processed: usize,
45 pub total_words: usize,
47}
48
49#[derive(Debug, Clone)]
51pub struct OcrPageReport {
52 pub page: u32,
54 pub ocr_needed: bool,
56 pub words_recognized: usize,
58 pub confidence: f32,
60}
61
62pub fn make_searchable<
70 E: OcrEngine,
71 R: Fn(&Document, u32, u32) -> std::result::Result<(Vec<u8>, u32, u32), String>,
72>(
73 doc: &mut Document,
74 engine: &E,
75 config: &OcrConfig,
76 render_fn: R,
77) -> Result<OcrReport> {
78 let pages = doc.get_pages();
79 let total = pages.len() as u32;
80
81 let page_nums: Vec<u32> = if config.pages.is_empty() {
83 (1..=total).collect()
84 } else {
85 for &p in &config.pages {
87 if p == 0 || p > total {
88 return Err(OcrError::PageOutOfRange(p, total));
89 }
90 }
91 config.pages.clone()
92 };
93
94 let mut report = OcrReport {
95 pages: Vec::new(),
96 pages_processed: 0,
97 total_words: 0,
98 };
99
100 for &page_num in &page_nums {
101 let page_id = match pages.get(&page_num) {
102 Some(&id) => id,
103 None => continue,
104 };
105
106 let needs_ocr = page_needs_ocr(doc, page_id, config.text_threshold);
107
108 if !needs_ocr {
109 report.pages.push(OcrPageReport {
110 page: page_num,
111 ocr_needed: false,
112 words_recognized: 0,
113 confidence: 1.0,
114 });
115 continue;
116 }
117
118 let (image_data, width, height) =
120 render_fn(doc, page_num, config.dpi).map_err(OcrError::Render)?;
121
122 let ocr_result = engine
124 .recognize(&image_data, width, height, config.dpi)
125 .map_err(OcrError::Engine)?;
126
127 let words_count = ocr_result.words.len();
128 let confidence = ocr_result.confidence;
129
130 if !ocr_result.words.is_empty() {
132 let media_box = get_media_box(doc, page_id);
133 insert_invisible_text_layer(doc, page_id, &ocr_result, &media_box, config.dpi)?;
134 }
135
136 report.pages.push(OcrPageReport {
137 page: page_num,
138 ocr_needed: true,
139 words_recognized: words_count,
140 confidence,
141 });
142 report.pages_processed += 1;
143 report.total_words += words_count;
144 }
145
146 Ok(report)
147}
148
149fn page_needs_ocr(doc: &Document, page_id: ObjectId, threshold: usize) -> bool {
151 let content_bytes = match get_page_content_bytes(doc, page_id) {
152 Some(bytes) => bytes,
153 None => return true, };
155
156 let content = match Content::decode(&content_bytes) {
157 Ok(c) => c,
158 Err(_) => return true,
159 };
160
161 let mut char_count = 0;
162 for op in &content.operations {
163 match op.operator.as_str() {
164 "Tj" => {
165 for operand in &op.operands {
166 if let Object::String(bytes, _) = operand {
167 char_count += bytes.len();
168 }
169 }
170 }
171 "TJ" => {
172 for operand in &op.operands {
173 if let Object::Array(arr) = operand {
174 for item in arr {
175 if let Object::String(bytes, _) = item {
176 char_count += bytes.len();
177 }
178 }
179 }
180 }
181 }
182 "'" | "\"" => {
183 for operand in &op.operands {
184 if let Object::String(bytes, _) = operand {
185 char_count += bytes.len();
186 }
187 }
188 }
189 _ => {}
190 }
191 }
192
193 char_count < threshold
194}
195
196pub const OCR_FONT_NAME: &[u8] = b"PdfOcrFont";
203
204fn insert_invisible_text_layer(
214 doc: &mut Document,
215 page_id: ObjectId,
216 ocr_result: &OcrPageResult,
217 media_box: &[f64; 4],
218 _dpi: u32,
219) -> Result<()> {
220 let page_width = media_box[2] - media_box[0];
221 let page_height = media_box[3] - media_box[1];
222 let img_w = ocr_result.image_width as f64;
223 let img_h = ocr_result.image_height as f64;
224
225 let scale_x = page_width / img_w;
226 let scale_y = page_height / img_h;
227
228 let mut ops = vec![
229 Operation::new("BT", vec![]),
230 Operation::new("Tr", vec![Object::Integer(3)]),
232 Operation::new(
233 "Tf",
234 vec![Object::Name(OCR_FONT_NAME.to_vec()), Object::Real(10.0)],
235 ),
236 ];
237
238 for word in &ocr_result.words {
239 let [px0, py0, px1, _py1] = word.bbox_px;
240
241 let pdf_x = media_box[0] + (px0 as f64) * scale_x;
243 let pdf_y = media_box[3] - (py0 as f64) * scale_y;
245
246 let word_width_px = (px1 - px0) as f64;
247 let word_width_pdf = word_width_px * scale_x;
248
249 let natural_width = word.text.chars().count() as f64 * 10.0 * 0.5;
251 let h_scale = if natural_width > 0.0 {
252 (word_width_pdf / natural_width) * 100.0
253 } else {
254 100.0
255 };
256
257 ops.push(Operation::new(
259 "Tm",
260 vec![
261 Object::Real(1.0),
262 Object::Real(0.0),
263 Object::Real(0.0),
264 Object::Real(1.0),
265 Object::Real(pdf_x as f32),
266 Object::Real(pdf_y as f32),
267 ],
268 ));
269 ops.push(Operation::new("Tz", vec![Object::Real(h_scale as f32)]));
271 ops.push(Operation::new(
273 "Tj",
274 vec![Object::String(
275 encode_layer_string(&word.text),
276 lopdf::StringFormat::Literal,
277 )],
278 ));
279 }
280
281 ops.push(Operation::new("ET", vec![]));
282
283 let content = Content { operations: ops };
284 let encoded = content
285 .encode()
286 .map_err(|e| OcrError::Other(format!("failed to encode text layer: {e}")))?;
287
288 let text_stream = Stream::new(dictionary! {}, encoded);
289 let text_id = doc.add_object(Object::Stream(text_stream));
290
291 let existing = {
293 let page_obj = match doc.get_object(page_id) {
294 Ok(obj) => obj,
295 Err(_) => return Ok(()),
296 };
297 let page_dict = match page_obj {
298 Object::Dictionary(ref d) => d,
299 _ => return Ok(()),
300 };
301 page_dict.get(b"Contents").ok().cloned()
302 };
303
304 let new_contents = match existing {
305 Some(Object::Reference(existing_id)) => Object::Array(vec![
306 Object::Reference(existing_id),
307 Object::Reference(text_id),
308 ]),
309 Some(Object::Array(mut arr)) => {
310 arr.push(Object::Reference(text_id));
311 Object::Array(arr)
312 }
313 _ => Object::Reference(text_id),
314 };
315
316 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
317 d.set("Contents", new_contents);
318 }
319
320 ensure_ocr_font_resource(doc, page_id)?;
321
322 Ok(())
323}
324
325fn encode_layer_string(text: &str) -> Vec<u8> {
330 text.chars().map(winansi_byte).collect()
331}
332
333fn winansi_byte(c: char) -> u8 {
341 match c {
342 c if (c as u32) < 0x80 => c as u8,
343 '€' => 0x80,
344 '‚' => 0x82,
345 'ƒ' => 0x83,
346 '„' => 0x84,
347 '…' => 0x85,
348 '†' => 0x86,
349 '‡' => 0x87,
350 'ˆ' => 0x88,
351 '‰' => 0x89,
352 'Š' => 0x8A,
353 '‹' => 0x8B,
354 'Œ' => 0x8C,
355 'Ž' => 0x8E,
356 '‘' => 0x91,
357 '’' => 0x92,
358 '“' => 0x93,
359 '”' => 0x94,
360 '•' => 0x95,
361 '–' => 0x96,
362 '—' => 0x97,
363 '˜' => 0x98,
364 '™' => 0x99,
365 'š' => 0x9A,
366 '›' => 0x9B,
367 'œ' => 0x9C,
368 'ž' => 0x9E,
369 'Ÿ' => 0x9F,
370 c if (c as u32) < 0x100 => c as u8,
371 _ => b'?',
372 }
373}
374
375fn ensure_ocr_font_resource(doc: &mut Document, page_id: ObjectId) -> Result<()> {
384 let font_id = doc.add_object(Object::Dictionary(dictionary! {
385 "Type" => "Font",
386 "Subtype" => "Type1",
387 "BaseFont" => "Helvetica",
388 "Encoding" => "WinAnsiEncoding",
389 }));
390 let font_ref = Object::Reference(font_id);
391
392 let resources = {
393 let page_obj = match doc.get_object(page_id) {
394 Ok(obj) => obj,
395 Err(_) => return Ok(()),
396 };
397 match page_obj {
398 Object::Dictionary(ref d) => d.get(b"Resources").ok().cloned(),
399 _ => return Ok(()),
400 }
401 };
402
403 match resources {
404 Some(Object::Reference(resources_id)) => {
406 let fonts = match doc.get_object(resources_id) {
407 Ok(Object::Dictionary(d)) => d.get(b"Font").ok().cloned(),
408 _ => {
409 set_fresh_resources(doc, page_id, font_ref);
410 return Ok(());
411 }
412 };
413 match fonts {
414 Some(Object::Reference(fonts_id)) => {
415 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(fonts_id) {
416 d.set(OCR_FONT_NAME, font_ref);
417 } else if let Ok(Object::Dictionary(d)) = doc.get_object_mut(resources_id) {
418 d.set("Font", fresh_fonts(font_ref));
419 }
420 }
421 Some(Object::Dictionary(_)) => {
422 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(resources_id) {
423 if let Ok(Object::Dictionary(fonts)) = d.get_mut(b"Font") {
424 fonts.set(OCR_FONT_NAME, font_ref);
425 }
426 }
427 }
428 _ => {
429 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(resources_id) {
430 d.set("Font", fresh_fonts(font_ref));
431 }
432 }
433 }
434 }
435 Some(Object::Dictionary(mut dict)) => {
437 let write_back = match dict.get(b"Font").ok().cloned() {
438 Some(Object::Reference(fonts_id)) => {
439 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(fonts_id) {
440 d.set(OCR_FONT_NAME, font_ref);
441 false
442 } else {
443 dict.set("Font", fresh_fonts(font_ref));
444 true
445 }
446 }
447 Some(Object::Dictionary(_)) => {
448 if let Ok(Object::Dictionary(fonts)) = dict.get_mut(b"Font") {
449 fonts.set(OCR_FONT_NAME, font_ref);
450 }
451 true
452 }
453 _ => {
454 dict.set("Font", fresh_fonts(font_ref));
455 true
456 }
457 };
458 if write_back {
459 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(page_id) {
460 d.set("Resources", Object::Dictionary(dict));
461 }
462 }
463 }
464 _ => set_fresh_resources(doc, page_id, font_ref),
465 }
466 Ok(())
467}
468
469fn fresh_fonts(font_ref: Object) -> Object {
471 Object::Dictionary(dictionary! {
472 OCR_FONT_NAME => font_ref,
473 })
474}
475
476fn set_fresh_resources(doc: &mut Document, page_id: ObjectId, font_ref: Object) {
479 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(page_id) {
480 d.set(
481 "Resources",
482 Object::Dictionary(dictionary! {
483 "Font" => fresh_fonts(font_ref),
484 }),
485 );
486 }
487}
488
489fn get_media_box(doc: &Document, page_id: ObjectId) -> [f64; 4] {
491 let default_box = [0.0, 0.0, 612.0, 792.0];
492
493 let page_obj = match doc.get_object(page_id) {
494 Ok(obj) => obj,
495 Err(_) => return default_box,
496 };
497
498 let page_dict = match page_obj {
499 Object::Dictionary(ref d) => d,
500 _ => return default_box,
501 };
502
503 match page_dict.get(b"MediaBox") {
504 Ok(Object::Array(arr)) => {
505 if arr.len() >= 4 {
506 let vals: Vec<f64> = arr
507 .iter()
508 .filter_map(|v| match v {
509 Object::Integer(i) => Some(*i as f64),
510 Object::Real(f) => Some(*f as f64),
511 _ => None,
512 })
513 .collect();
514 if vals.len() >= 4 {
515 [vals[0], vals[1], vals[2], vals[3]]
516 } else {
517 default_box
518 }
519 } else {
520 default_box
521 }
522 }
523 _ => default_box,
524 }
525}
526
527fn get_page_content_bytes(doc: &Document, page_id: ObjectId) -> Option<Vec<u8>> {
529 Some(doc.get_page_content(page_id))
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use crate::engine::{NoOpEngine, OcrPageResult, OcrWord};
536
537 struct MockEngine {
539 result: OcrPageResult,
540 }
541
542 impl MockEngine {
543 fn new(words: Vec<OcrWord>) -> Self {
544 let confidence = if words.is_empty() {
545 0.0
546 } else {
547 words.iter().map(|w| w.confidence).sum::<f32>() / words.len() as f32
548 };
549 Self {
550 result: OcrPageResult {
551 words,
552 confidence,
553 image_width: 600,
554 image_height: 800,
555 },
556 }
557 }
558 }
559
560 impl OcrEngine for MockEngine {
561 fn recognize(
562 &self,
563 _image_data: &[u8],
564 _width: u32,
565 _height: u32,
566 _dpi: u32,
567 ) -> std::result::Result<OcrPageResult, String> {
568 Ok(self.result.clone())
569 }
570
571 fn supported_languages(&self) -> Vec<String> {
572 vec!["eng".to_string()]
573 }
574 }
575
576 fn make_scanned_doc() -> Document {
578 let mut doc = Document::with_version("1.7");
579
580 let content_stream =
582 Stream::new(dictionary! {}, b"q 612 0 0 792 0 0 cm /Im0 Do Q".to_vec());
583 let content_id = doc.add_object(Object::Stream(content_stream));
584
585 let page_dict = dictionary! {
586 "Type" => "Page",
587 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
588 "Contents" => Object::Reference(content_id),
589 };
590 let page_id = doc.add_object(Object::Dictionary(page_dict));
591
592 let pages_dict = dictionary! {
593 "Type" => "Pages",
594 "Kids" => vec![Object::Reference(page_id)],
595 "Count" => 1_i64,
596 };
597 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
598
599 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
600 d.set("Parent", Object::Reference(pages_id));
601 }
602
603 let catalog = dictionary! {
604 "Type" => "Catalog",
605 "Pages" => Object::Reference(pages_id),
606 };
607 let catalog_id = doc.add_object(Object::Dictionary(catalog));
608 doc.trailer.set("Root", Object::Reference(catalog_id));
609
610 doc
611 }
612
613 fn make_text_doc() -> Document {
615 let mut doc = Document::with_version("1.7");
616
617 let content_stream = Stream::new(
618 dictionary! {},
619 b"BT /F1 12 Tf (This is a text page with enough characters to pass threshold) Tj ET"
620 .to_vec(),
621 );
622 let content_id = doc.add_object(Object::Stream(content_stream));
623
624 let page_dict = dictionary! {
625 "Type" => "Page",
626 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
627 "Contents" => Object::Reference(content_id),
628 };
629 let page_id = doc.add_object(Object::Dictionary(page_dict));
630
631 let pages_dict = dictionary! {
632 "Type" => "Pages",
633 "Kids" => vec![Object::Reference(page_id)],
634 "Count" => 1_i64,
635 };
636 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
637
638 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
639 d.set("Parent", Object::Reference(pages_id));
640 }
641
642 let catalog = dictionary! {
643 "Type" => "Catalog",
644 "Pages" => Object::Reference(pages_id),
645 };
646 let catalog_id = doc.add_object(Object::Dictionary(catalog));
647 doc.trailer.set("Root", Object::Reference(catalog_id));
648
649 doc
650 }
651
652 fn make_mixed_doc() -> Document {
654 let mut doc = Document::with_version("1.7");
655 let mut page_ids = Vec::new();
656
657 let content1 = Stream::new(dictionary! {}, b"q 612 0 0 792 0 0 cm /Im0 Do Q".to_vec());
659 let c1 = doc.add_object(Object::Stream(content1));
660 let p1 = dictionary! {
661 "Type" => "Page",
662 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
663 "Contents" => Object::Reference(c1),
664 };
665 let p1_id = doc.add_object(Object::Dictionary(p1));
666 page_ids.push(p1_id);
667
668 let content2 = Stream::new(
670 dictionary! {},
671 b"BT /F1 12 Tf (Enough text content to pass the threshold) Tj ET".to_vec(),
672 );
673 let c2 = doc.add_object(Object::Stream(content2));
674 let p2 = dictionary! {
675 "Type" => "Page",
676 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
677 "Contents" => Object::Reference(c2),
678 };
679 let p2_id = doc.add_object(Object::Dictionary(p2));
680 page_ids.push(p2_id);
681
682 let content3 = Stream::new(dictionary! {}, b"q 612 0 0 792 0 0 cm /Im1 Do Q".to_vec());
684 let c3 = doc.add_object(Object::Stream(content3));
685 let p3 = dictionary! {
686 "Type" => "Page",
687 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
688 "Contents" => Object::Reference(c3),
689 };
690 let p3_id = doc.add_object(Object::Dictionary(p3));
691 page_ids.push(p3_id);
692
693 let kids: Vec<Object> = page_ids.iter().map(|id| Object::Reference(*id)).collect();
694 let pages_dict = dictionary! {
695 "Type" => "Pages",
696 "Kids" => kids,
697 "Count" => Object::Integer(page_ids.len() as i64),
698 };
699 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
700
701 for &pid in &page_ids {
702 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(pid) {
703 d.set("Parent", Object::Reference(pages_id));
704 }
705 }
706
707 let catalog = dictionary! {
708 "Type" => "Catalog",
709 "Pages" => Object::Reference(pages_id),
710 };
711 let catalog_id = doc.add_object(Object::Dictionary(catalog));
712 doc.trailer.set("Root", Object::Reference(catalog_id));
713
714 doc
715 }
716
717 fn dummy_render(
719 _doc: &Document,
720 _page_num: u32,
721 _dpi: u32,
722 ) -> std::result::Result<(Vec<u8>, u32, u32), String> {
723 Ok((vec![0u8; 600 * 800 * 3], 600, 800))
724 }
725
726 #[test]
727 fn scanned_page_needs_ocr() {
728 let doc = make_scanned_doc();
729 let pages = doc.get_pages();
730 let page_id = *pages.get(&1).unwrap();
731 assert!(page_needs_ocr(&doc, page_id, 10));
732 }
733
734 #[test]
735 fn text_page_does_not_need_ocr() {
736 let doc = make_text_doc();
737 let pages = doc.get_pages();
738 let page_id = *pages.get(&1).unwrap();
739 assert!(!page_needs_ocr(&doc, page_id, 10));
740 }
741
742 #[test]
743 fn noop_engine_processes_scanned() {
744 let mut doc = make_scanned_doc();
745 let engine = NoOpEngine;
746 let config = OcrConfig::default();
747
748 let report = make_searchable(&mut doc, &engine, &config, dummy_render).unwrap();
749 assert_eq!(report.pages.len(), 1);
750 assert!(report.pages[0].ocr_needed);
751 assert_eq!(report.pages[0].words_recognized, 0);
752 }
753
754 #[test]
755 fn text_page_skipped_by_pipeline() {
756 let mut doc = make_text_doc();
757 let engine = NoOpEngine;
758 let config = OcrConfig::default();
759
760 let report = make_searchable(&mut doc, &engine, &config, dummy_render).unwrap();
761 assert_eq!(report.pages.len(), 1);
762 assert!(!report.pages[0].ocr_needed);
763 assert_eq!(report.pages_processed, 0);
764 }
765
766 #[test]
767 fn mock_engine_inserts_invisible_text() {
768 let mut doc = make_scanned_doc();
769 let engine = MockEngine::new(vec![
770 OcrWord {
771 text: "Hello".to_string(),
772 bbox_px: [10, 20, 100, 40],
773 confidence: 0.95,
774 },
775 OcrWord {
776 text: "World".to_string(),
777 bbox_px: [110, 20, 200, 40],
778 confidence: 0.90,
779 },
780 ]);
781 let config = OcrConfig::default();
782
783 let report = make_searchable(&mut doc, &engine, &config, dummy_render).unwrap();
784 assert_eq!(report.pages_processed, 1);
785 assert_eq!(report.total_words, 2);
786 assert!(report.pages[0].ocr_needed);
787 assert_eq!(report.pages[0].words_recognized, 2);
788 }
789
790 #[test]
791 fn ocr_specific_pages() {
792 let mut doc = make_mixed_doc();
793 let engine = NoOpEngine;
794 let config = OcrConfig {
795 pages: vec![1],
796 ..Default::default()
797 };
798
799 let report = make_searchable(&mut doc, &engine, &config, dummy_render).unwrap();
800 assert_eq!(report.pages.len(), 1);
802 assert_eq!(report.pages[0].page, 1);
803 }
804
805 #[test]
806 fn ocr_page_out_of_range() {
807 let mut doc = make_scanned_doc();
808 let engine = NoOpEngine;
809 let config = OcrConfig {
810 pages: vec![5],
811 ..Default::default()
812 };
813
814 let result = make_searchable(&mut doc, &engine, &config, dummy_render);
815 assert!(result.is_err());
816 }
817
818 fn layer_font(doc: &Document, page_num: u32) -> lopdf::Dictionary {
820 let pages = doc.get_pages();
821 let page_id = *pages.get(&page_num).unwrap();
822 let page = doc.get_object(page_id).unwrap();
823 let resources = match page {
824 Object::Dictionary(d) => d.get(b"Resources").unwrap().clone(),
825 _ => panic!("page is no dictionary"),
826 };
827 let fonts = match &resources {
828 Object::Dictionary(d) => d.get(b"Font").unwrap().clone(),
829 Object::Reference(id) => match doc.get_object(*id).unwrap() {
830 Object::Dictionary(d) => d.get(b"Font").unwrap().clone(),
831 other => panic!("shared resources are no dictionary: {other:?}"),
832 },
833 other => panic!("resources are no dictionary: {other:?}"),
834 };
835 let font_ref = match &fonts {
836 Object::Dictionary(d) => d.get(OCR_FONT_NAME).unwrap().clone(),
837 other => panic!("/Font is no dictionary: {other:?}"),
838 };
839 match font_ref {
840 Object::Reference(id) => match doc.get_object(id).unwrap() {
841 Object::Dictionary(d) => d.clone(),
842 other => panic!("font is no dictionary: {other:?}"),
843 },
844 other => panic!("font is no reference: {other:?}"),
845 }
846 }
847
848 #[test]
849 fn the_layer_declares_the_font_it_sets() {
850 let mut doc = make_scanned_doc();
851 let engine = MockEngine::new(vec![OcrWord {
852 text: "Hello".to_string(),
853 bbox_px: [10, 20, 100, 40],
854 confidence: 0.95,
855 }]);
856 make_searchable(&mut doc, &engine, &OcrConfig::default(), dummy_render).unwrap();
857
858 let font = layer_font(&doc, 1);
859 assert_eq!(font.get(b"BaseFont").unwrap(), &Object::from("Helvetica"));
860 assert_eq!(
861 font.get(b"Encoding").unwrap(),
862 &Object::from("WinAnsiEncoding")
863 );
864 let pages = doc.get_pages();
866 let content = doc.get_page_content(*pages.get(&1).unwrap());
867 assert!(content.windows(11).any(|w| w == b"/PdfOcrFont"));
868 }
869
870 #[test]
871 fn the_layer_keeps_a_page_own_font() {
872 let mut doc = make_scanned_doc();
873 let own_font = doc.add_object(Object::Dictionary(dictionary! {
875 "Type" => "Font",
876 "Subtype" => "Type1",
877 "BaseFont" => "Times-Roman",
878 }));
879 let pages = doc.get_pages();
880 let page_id = *pages.get(&1).unwrap();
881 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(page_id) {
882 d.set(
883 "Resources",
884 Object::Dictionary(dictionary! {
885 "Font" => Object::Dictionary(dictionary! {
886 "F1" => Object::Reference(own_font),
887 }),
888 }),
889 );
890 }
891 let engine = MockEngine::new(vec![OcrWord {
892 text: "Hello".to_string(),
893 bbox_px: [10, 20, 100, 40],
894 confidence: 0.95,
895 }]);
896 make_searchable(&mut doc, &engine, &OcrConfig::default(), dummy_render).unwrap();
897
898 let page = doc.get_object(page_id).unwrap().clone();
899 let fonts = match &page {
900 Object::Dictionary(d) => match d.get(b"Resources").unwrap() {
901 Object::Dictionary(r) => match r.get(b"Font").unwrap() {
902 Object::Dictionary(f) => f.clone(),
903 other => panic!("/Font is no dictionary: {other:?}"),
904 },
905 other => panic!("resources are no dictionary: {other:?}"),
906 },
907 _ => panic!("page is no dictionary"),
908 };
909 assert_eq!(
910 fonts.get(b"F1").unwrap(),
911 &Object::Reference(own_font),
912 "the page's own font survives the layer"
913 );
914 assert!(
915 fonts.get(OCR_FONT_NAME).is_ok(),
916 "the layer's font is declared beside it"
917 );
918 }
919
920 #[test]
921 fn parentheses_and_backslashes_in_words_do_not_break_the_layer() {
922 let mut doc = make_scanned_doc();
923 let engine = MockEngine::new(vec![OcrWord {
924 text: "a(b)\\c".to_string(),
925 bbox_px: [10, 20, 100, 40],
926 confidence: 0.95,
927 }]);
928 make_searchable(&mut doc, &engine, &OcrConfig::default(), dummy_render).unwrap();
929
930 let pages = doc.get_pages();
931 let content = doc.get_page_content(*pages.get(&1).unwrap());
932 assert!(
935 content.windows(9).any(|w| w == b"(a(b)\\\\c)"),
936 "the word lands in the content: {content:?}"
937 );
938 let decoded = Content::decode(&content).unwrap();
939 let shown: Vec<Vec<u8>> = decoded
940 .operations
941 .iter()
942 .filter(|op| op.operator == "Tj")
943 .filter_map(|op| op.operands.first())
944 .filter_map(|o| match o {
945 Object::String(bytes, _) => Some(bytes.clone()),
946 _ => None,
947 })
948 .collect();
949 assert!(
950 shown.iter().any(|s| s == b"a(b)\\c"),
951 "the word decodes whole: {shown:?}"
952 );
953 }
954
955 #[test]
956 fn winansi_spelling_and_question_marks() {
957 assert_eq!(winansi_byte('A'), b'A');
958 assert_eq!(winansi_byte('é'), 0xE9);
959 assert_eq!(winansi_byte('€'), 0x80);
960 assert_eq!(winansi_byte('中'), b'?');
961 assert_eq!(encode_layer_string("café"), b"caf\xE9".to_vec());
962 }
963}