1use crate::error::{ExtractError, Result};
13use lopdf::content::{Content, Operation};
14use lopdf::{Document, Object, ObjectId};
15use std::collections::HashMap;
16use std::sync::OnceLock;
17
18const APPROX_CHAR_WIDTH: f64 = 0.5;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum WidthSource {
24 Metric,
26 #[default]
28 Estimate,
29}
30
31#[derive(Debug, Clone, Default)]
33pub struct TextBlock {
34 pub text: String,
36 pub page: u32,
38 pub bbox: [f64; 4],
40 pub font_name: String,
42 pub font_size: f64,
44 pub actual_text: Option<String>,
49
50 pub base_font: Option<String>,
55 pub is_bold: bool,
61 pub is_italic: bool,
65 pub color: Option<[u8; 4]>,
69
70 pub width_source: WidthSource,
73 pub char_bounds: Vec<[f64; 4]>,
77}
78
79#[derive(Debug, Clone)]
81pub struct PositionedChar {
82 pub ch: char,
84 pub page: u32,
86 pub bbox: [f64; 4],
88}
89
90#[derive(Debug, Clone)]
92struct GraphicsState {
93 ctm: [f64; 6],
94 fill_color: Option<[u8; 4]>,
96}
97
98#[derive(Debug, Clone)]
100struct TextState {
101 tm: [f64; 6],
103 tlm: [f64; 6],
105 font_name: String,
107 font_size: f64,
109 tc: f64,
111 tw: f64,
113 th: f64,
115 tl: f64,
117 ts: f64,
119 gs_stack: Vec<GraphicsState>,
121 ctm: [f64; 6],
123 fill_color: Option<[u8; 4]>,
129}
130
131impl Default for TextState {
132 fn default() -> Self {
133 Self {
134 tm: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
135 tlm: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
136 font_name: String::new(),
137 font_size: 12.0,
138 tc: 0.0,
139 tw: 0.0,
140 th: 100.0,
141 tl: 0.0,
142 ts: 0.0,
143 gs_stack: Vec::new(),
144 ctm: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
145 fill_color: Some([0, 0, 0, 255]),
146 }
147 }
148}
149
150#[derive(Clone)]
152struct FontInfo {
153 is_cid: bool,
155 to_unicode: HashMap<u32, String>,
158 encoding_map: [Option<char>; 256],
161 ct_codes: [bool; 256],
169 base_font: Option<String>,
171 is_bold: bool,
174 is_italic: bool,
177
178 simple_widths: Box<[Option<f32>; 256]>,
183 cid_default_width: f32,
186 cid_widths: HashMap<u32, f32>,
189}
190
191fn strip_subset_prefix(name: &str) -> &str {
193 match name.split_once('+') {
194 Some((prefix, rest)) if prefix.len() == 6 => rest,
195 _ => name,
196 }
197}
198
199const FONT_FLAG_ITALIC: u32 = 1 << 6;
203const FONT_FLAG_FORCE_BOLD: u32 = 1 << 18;
204
205fn derive_font_style(doc: &Document, font: &lopdf::Dictionary) -> (Option<String>, bool, bool) {
209 let base_font_raw = font.get(b"BaseFont").ok().and_then(|o| match o {
210 Object::Name(n) => Some(String::from_utf8_lossy(n).to_string()),
211 _ => None,
212 });
213 let base_font = base_font_raw
214 .as_deref()
215 .map(|s| strip_subset_prefix(s).to_string());
216
217 let (name_bold, name_italic) = base_font
218 .as_deref()
219 .map(name_style_hints)
220 .unwrap_or((false, false));
221
222 let descriptor = resolve_dict(doc, font, b"FontDescriptor");
223 let (desc_bold, desc_italic) = descriptor
224 .as_ref()
225 .map(|d| {
226 let weight_bold = d
227 .get(b"FontWeight")
228 .ok()
229 .and_then(|o| match o {
230 Object::Integer(i) => Some(*i as u32),
231 Object::Real(r) => Some(*r as u32),
232 _ => None,
233 })
234 .is_some_and(|w| w >= 700);
235 let flags = d
236 .get(b"Flags")
237 .ok()
238 .and_then(|o| match o {
239 Object::Integer(i) => Some(*i as u32),
240 Object::Real(r) => Some(*r as u32),
241 _ => None,
242 })
243 .unwrap_or(0);
244 let flag_italic = (flags & FONT_FLAG_ITALIC) != 0;
245 let flag_force_bold = (flags & FONT_FLAG_FORCE_BOLD) != 0;
246 (weight_bold || flag_force_bold, flag_italic)
247 })
248 .unwrap_or((false, false));
249
250 (
251 base_font,
252 desc_bold || name_bold,
253 desc_italic || name_italic,
254 )
255}
256
257fn clamp_unit_to_u8(v: f64) -> u8 {
259 (v.clamp(0.0, 1.0) * 255.0).round() as u8
260}
261
262fn cmyk_to_rgba(c: f64, m: f64, y: f64, k: f64) -> [u8; 4] {
266 let c = c.clamp(0.0, 1.0);
267 let m = m.clamp(0.0, 1.0);
268 let y = y.clamp(0.0, 1.0);
269 let k = k.clamp(0.0, 1.0);
270 let r = (1.0 - c) * (1.0 - k);
271 let g = (1.0 - m) * (1.0 - k);
272 let b = (1.0 - y) * (1.0 - k);
273 [
274 clamp_unit_to_u8(r),
275 clamp_unit_to_u8(g),
276 clamp_unit_to_u8(b),
277 255,
278 ]
279}
280
281fn name_style_hints(name: &str) -> (bool, bool) {
284 let lower = name.to_ascii_lowercase();
285 let italic = lower.contains("italic") || lower.contains("oblique") || lower.contains("slant");
286 let bold = lower.contains("bold")
287 || lower.contains("demi")
288 || lower.contains("semibold")
289 || lower.contains("heavy")
290 || lower.contains("black");
291 (bold, italic)
292}
293
294fn font_glyph_advance(fi: &FontInfo, char_code: u32) -> Option<f32> {
300 if fi.is_cid {
301 fi.cid_widths
302 .get(&char_code)
303 .copied()
304 .or(if fi.cid_default_width > 0.0 {
305 Some(fi.cid_default_width)
306 } else {
307 None
308 })
309 } else {
310 fi.simple_widths.get(char_code as usize).and_then(|w| *w)
311 }
312}
313
314fn fi_has_metrics(fi: &FontInfo) -> bool {
317 if fi.is_cid {
318 true
320 } else {
321 fi.simple_widths.iter().any(|w| w.is_some())
322 }
323}
324
325fn compute_glyph_advances(
333 bytes: &[u8],
334 fi: &FontInfo,
335 font_size: f64,
336 tc: f64,
337 tw: f64,
338 th: f64,
339) -> (Vec<f64>, WidthSource) {
340 let has_metrics = fi_has_metrics(fi);
341 let width_source = if has_metrics {
342 WidthSource::Metric
343 } else {
344 WidthSource::Estimate
345 };
346 let approx = APPROX_CHAR_WIDTH * 1000.0; let mut advances = Vec::new();
349
350 if fi.is_cid {
351 let mut i = 0;
352 while i + 1 < bytes.len() {
353 let code = u16::from_be_bytes([bytes[i], bytes[i + 1]]) as u32;
354 i += 2;
355 let glyph_w = font_glyph_advance(fi, code).unwrap_or(approx as f32);
356 let adv = glyph_w as f64 / 1000.0 * font_size * (th / 100.0) + tc;
357 advances.push(adv);
358 }
359 } else {
360 for &b in bytes {
361 let glyph_w = if has_metrics {
362 font_glyph_advance(fi, b as u32).unwrap_or(approx as f32)
363 } else {
364 approx as f32
365 };
366 let extra_word = if b == 0x20 { tw } else { 0.0 };
367 let adv = glyph_w as f64 / 1000.0 * font_size * (th / 100.0) + tc + extra_word;
368 advances.push(adv);
369 }
370 }
371
372 (advances, width_source)
373}
374
375fn hscale(tm: &[f64; 6]) -> f64 {
377 let s = tm[0].hypot(tm[1]);
378 if s.is_finite() && s > 0.0 {
379 s
380 } else {
381 1.0
382 }
383}
384
385fn vscale(tm: &[f64; 6]) -> f64 {
387 let s = tm[2].hypot(tm[3]);
388 if s.is_finite() && s > 0.0 {
389 s
390 } else {
391 1.0
392 }
393}
394
395struct GlyphCtx {
397 font_size: f64,
398 hscale: f64,
402 vscale: f64,
403 tc: f64,
404 tw: f64,
405 th: f64,
406}
407
408fn char_bounds_from_bytes(
410 bytes: &[u8],
411 fi: &FontInfo,
412 x: f64,
413 y: f64,
414 ctx: &GlyphCtx,
415) -> (Vec<[f64; 4]>, WidthSource, f64) {
416 let (advances, ws) = compute_glyph_advances(
417 bytes,
418 fi,
419 ctx.font_size * ctx.hscale,
420 ctx.tc,
421 ctx.tw,
422 ctx.th,
423 );
424 let mut cx = x;
425 let mut bounds = Vec::with_capacity(advances.len());
426 for adv in &advances {
427 bounds.push([cx, y, cx + adv, y + ctx.font_size * ctx.vscale]);
428 cx += adv;
429 }
430 (bounds, ws, cx - x)
431}
432
433fn char_bounds_from_tj_array(
439 arr: &[Object],
440 fi: &FontInfo,
441 x_start: f64,
442 y: f64,
443 ctx: &GlyphCtx,
444) -> (Vec<[f64; 4]>, WidthSource, f64) {
445 let font_size = ctx.font_size * ctx.hscale;
446 let height = ctx.font_size * ctx.vscale;
447 let tc = ctx.tc;
448 let tw = ctx.tw;
449 let th = ctx.th;
450 let has_metrics = fi_has_metrics(fi);
451 let width_source = if has_metrics {
452 WidthSource::Metric
453 } else {
454 WidthSource::Estimate
455 };
456 let approx = APPROX_CHAR_WIDTH * 1000.0_f64;
457
458 let mut bounds = Vec::new();
459 let mut cx = x_start;
460
461 for item in arr {
462 match item {
463 Object::String(bytes, _) => {
464 if fi.is_cid {
465 let mut i = 0;
466 while i + 1 < bytes.len() {
467 let code = u16::from_be_bytes([bytes[i], bytes[i + 1]]) as u32;
468 i += 2;
469 let glyph_w = font_glyph_advance(fi, code).unwrap_or(approx as f32) as f64;
470 let adv = glyph_w / 1000.0 * font_size * (th / 100.0) + tc;
471 bounds.push([cx, y, cx + adv, y + height]);
472 cx += adv;
473 }
474 } else {
475 for &b in bytes.iter() {
476 let glyph_w = if has_metrics {
477 font_glyph_advance(fi, b as u32).unwrap_or(approx as f32) as f64
478 } else {
479 approx
480 };
481 let extra_word = if b == 0x20 { tw } else { 0.0 };
482 let adv = glyph_w / 1000.0 * font_size * (th / 100.0) + tc + extra_word;
483 bounds.push([cx, y, cx + adv, y + height]);
484 cx += adv;
485 }
486 }
487 }
488 _ => {
489 if let Some(adj) = as_number(item) {
490 cx -= adj / 1000.0 * font_size * (th / 100.0);
492 }
493 }
494 }
495 }
496
497 (bounds, width_source, cx)
498}
499
500fn build_font_map(doc: &Document, page_id: ObjectId) -> HashMap<String, FontInfo> {
502 let mut map = HashMap::new();
503
504 let resources = get_page_resources(doc, page_id);
506 let font_dict = match resources.and_then(|res| match res.get(b"Font").ok()? {
507 Object::Dictionary(d) => Some(d.clone()),
508 Object::Reference(r) => match doc.get_object(*r).ok()? {
509 Object::Dictionary(d) => Some(d.clone()),
510 _ => None,
511 },
512 _ => None,
513 }) {
514 Some(d) => d,
515 None => return map,
516 };
517
518 for (name_bytes, value) in font_dict.iter() {
519 let font_name = String::from_utf8_lossy(name_bytes).to_string();
520
521 let font = match value {
523 Object::Reference(r) => match doc.get_object(*r).ok() {
524 Some(Object::Dictionary(d)) => d.clone(),
525 _ => continue,
526 },
527 Object::Dictionary(d) => d.clone(),
528 _ => continue,
529 };
530
531 let subtype = font
532 .get(b"Subtype")
533 .ok()
534 .and_then(|o| match o {
535 Object::Name(n) => Some(String::from_utf8_lossy(n).to_string()),
536 _ => None,
537 })
538 .unwrap_or_default();
539
540 let is_cid = subtype == "Type0";
541
542 let to_unicode = parse_to_unicode_from_font(doc, &font);
544
545 let to_unicode = if to_unicode.is_empty() && is_cid {
547 if let Ok(Object::Array(descendants)) = font.get(b"DescendantFonts") {
548 descendants
549 .iter()
550 .find_map(|d| {
551 let desc_dict = match d {
552 Object::Reference(r) => match doc.get_object(*r).ok()? {
553 Object::Dictionary(d) => d,
554 _ => return None,
555 },
556 Object::Dictionary(d) => d,
557 _ => return None,
558 };
559 let tu = parse_to_unicode_from_font(doc, desc_dict);
560 if tu.is_empty() {
561 None
562 } else {
563 Some(tu)
564 }
565 })
566 .unwrap_or_default()
567 } else {
568 HashMap::new()
569 }
570 } else {
571 to_unicode
572 };
573
574 let (encoding_map, ct_codes) = if !is_cid {
576 build_encoding_map(doc, &font)
577 } else {
578 ([None; 256], [false; 256])
579 };
580
581 let (base_font, is_bold, is_italic) = derive_font_style(doc, &font);
582
583 let (simple_widths, cid_default_width, cid_widths) = parse_font_widths(doc, &font, is_cid);
584
585 map.insert(
586 font_name,
587 FontInfo {
588 is_cid,
589 to_unicode,
590 encoding_map,
591 ct_codes,
592 base_font,
593 is_bold,
594 is_italic,
595 simple_widths,
596 cid_default_width,
597 cid_widths,
598 },
599 );
600 }
601
602 map
603}
604
605fn parse_font_widths(
612 doc: &Document,
613 font: &lopdf::Dictionary,
614 is_cid: bool,
615) -> (Box<[Option<f32>; 256]>, f32, HashMap<u32, f32>) {
616 if is_cid {
617 let (dw, cid_map) = parse_cid_widths(doc, font);
618 return (Box::new([None; 256]), dw, cid_map);
619 }
620
621 let first_char = match font.get(b"FirstChar").ok() {
623 Some(Object::Integer(n)) => *n as usize,
624 _ => return (Box::new([None; 256]), 1000.0, HashMap::new()),
625 };
626
627 let widths_arr = match font.get(b"Widths").ok() {
628 Some(Object::Array(a)) => a.clone(),
629 Some(Object::Reference(r)) => match doc.get_object(*r).ok() {
630 Some(Object::Array(a)) => a.clone(),
631 _ => return (Box::new([None; 256]), 1000.0, HashMap::new()),
632 },
633 _ => return (Box::new([None; 256]), 1000.0, HashMap::new()),
634 };
635
636 let mut simple_widths = Box::new([None::<f32>; 256]);
637 for (i, obj) in widths_arr.iter().enumerate() {
638 let code = first_char + i;
639 if code > 255 {
640 break;
641 }
642 let w = match obj {
643 Object::Integer(n) => *n as f32,
644 Object::Real(n) => *n,
645 _ => continue,
646 };
647 simple_widths[code] = Some(w);
648 }
649
650 (simple_widths, 1000.0, HashMap::new())
651}
652
653fn parse_cid_widths(doc: &Document, font: &lopdf::Dictionary) -> (f32, HashMap<u32, f32>) {
656 let descendants = match font.get(b"DescendantFonts").ok() {
657 Some(Object::Array(a)) => a.clone(),
658 Some(Object::Reference(r)) => match doc.get_object(*r).ok() {
659 Some(Object::Array(a)) => a.clone(),
660 _ => return (1000.0, HashMap::new()),
661 },
662 _ => return (1000.0, HashMap::new()),
663 };
664
665 let desc_dict = match descendants.first() {
666 Some(Object::Reference(r)) => match doc.get_object(*r).ok() {
667 Some(Object::Dictionary(d)) => d.clone(),
668 _ => return (1000.0, HashMap::new()),
669 },
670 Some(Object::Dictionary(d)) => d.clone(),
671 _ => return (1000.0, HashMap::new()),
672 };
673
674 let dw = match desc_dict.get(b"DW").ok() {
675 Some(Object::Integer(n)) => *n as f32,
676 Some(Object::Real(n)) => *n,
677 _ => 1000.0,
678 };
679
680 let w_arr = match desc_dict.get(b"W").ok() {
681 Some(Object::Array(a)) => a.clone(),
682 Some(Object::Reference(r)) => match doc.get_object(*r).ok() {
683 Some(Object::Array(a)) => a.clone(),
684 _ => return (dw, HashMap::new()),
685 },
686 _ => return (dw, HashMap::new()),
687 };
688
689 let mut map = HashMap::new();
691 let mut idx = 0;
692 while idx < w_arr.len() {
693 let cid_start = match &w_arr[idx] {
694 Object::Integer(n) => *n as u32,
695 _ => {
696 idx += 1;
697 continue;
698 }
699 };
700 idx += 1;
701 if idx >= w_arr.len() {
702 break;
703 }
704 match &w_arr[idx] {
705 Object::Array(widths) => {
706 for (i, wobj) in widths.iter().enumerate() {
708 let w = match wobj {
709 Object::Integer(n) => *n as f32,
710 Object::Real(n) => *n,
711 _ => continue,
712 };
713 map.insert(cid_start + i as u32, w);
714 }
715 idx += 1;
716 }
717 Object::Integer(c2) => {
718 let cid_end = *c2 as u32;
720 idx += 1;
721 if idx >= w_arr.len() {
722 break;
723 }
724 let w = match &w_arr[idx] {
725 Object::Integer(n) => *n as f32,
726 Object::Real(n) => *n,
727 _ => {
728 idx += 1;
729 continue;
730 }
731 };
732 for cid in cid_start..=cid_end {
733 map.insert(cid, w);
734 }
735 idx += 1;
736 }
737 _ => {
738 idx += 1;
739 }
740 }
741 }
742
743 (dw, map)
744}
745
746fn parse_to_unicode_from_font(doc: &Document, font: &lopdf::Dictionary) -> HashMap<u32, String> {
748 let tu_obj = match font.get(b"ToUnicode").ok() {
749 Some(Object::Reference(r)) => doc.get_object(*r).ok(),
750 Some(obj) => Some(obj),
751 None => return HashMap::new(),
752 };
753
754 let stream_bytes = match tu_obj {
755 Some(Object::Stream(ref s)) => s
756 .decompressed_content()
757 .ok()
758 .unwrap_or_else(|| s.content.clone()),
759 _ => return HashMap::new(),
760 };
761
762 parse_to_unicode_cmap(&stream_bytes)
763}
764
765fn parse_to_unicode_cmap(data: &[u8]) -> HashMap<u32, String> {
769 let text = String::from_utf8_lossy(data);
770 let mut map = HashMap::new();
771
772 for section in text.split("beginbfchar") {
774 let section = match section.split("endbfchar").next() {
775 Some(s) => s,
776 None => continue,
777 };
778 let tokens = extract_hex_tokens(section);
779 for pair in tokens.chunks(2) {
780 if pair.len() == 2 {
781 let code = parse_hex_u32(&pair[0]);
782 let unicode = hex_to_unicode_string(&pair[1]);
783 map.insert(code, unicode);
784 }
785 }
786 }
787
788 for section in text.split("beginbfrange") {
790 let section = match section.split("endbfrange").next() {
791 Some(s) => s,
792 None => continue,
793 };
794
795 let mut chars = section.chars().peekable();
797 let mut tokens: Vec<String> = Vec::new();
798 let mut arrays: Vec<Vec<String>> = Vec::new();
799 let mut in_array = false;
800 let mut current_array: Vec<String> = Vec::new();
801
802 while let Some(&ch) = chars.peek() {
803 if ch == '<' {
804 chars.next();
805 let hex: String = chars
806 .by_ref()
807 .take_while(|&c| c != '>')
808 .filter(|c| !c.is_whitespace())
809 .collect();
810 if in_array {
811 current_array.push(hex);
812 } else {
813 tokens.push(hex);
814 }
815 } else if ch == '[' {
816 chars.next();
817 in_array = true;
818 current_array = Vec::new();
819 } else if ch == ']' {
820 chars.next();
821 in_array = false;
822 arrays.push(std::mem::take(&mut current_array));
823 tokens.push(String::new()); } else {
825 chars.next();
826 }
827 }
828
829 let mut array_idx = 0;
831 let mut i = 0;
832 while i + 2 < tokens.len() {
833 let lo = parse_hex_u32(&tokens[i]);
834 let hi = parse_hex_u32(&tokens[i + 1]);
835
836 if tokens[i + 2].is_empty() {
837 if array_idx < arrays.len() {
839 let arr = &arrays[array_idx];
840 for (offset, dst) in arr.iter().enumerate() {
841 let code = lo + offset as u32;
842 if code <= hi {
843 map.insert(code, hex_to_unicode_string(dst));
844 }
845 }
846 array_idx += 1;
847 }
848 } else {
849 let dst_start = parse_hex_u32(&tokens[i + 2]);
851 let dst_len = tokens[i + 2].len();
852 for code in lo..=hi {
853 let dst_val = dst_start + (code - lo);
854 let s = if dst_len <= 4 {
855 char::from_u32(dst_val)
857 .map(|c| c.to_string())
858 .unwrap_or_default()
859 } else {
860 let hex = format!("{:0>width$X}", dst_val, width = dst_len);
862 hex_to_unicode_string(&hex)
863 };
864 map.insert(code, s);
865 }
866 }
867 i += 3;
868 }
869 }
870
871 map
872}
873
874fn extract_hex_tokens(text: &str) -> Vec<String> {
876 let mut tokens = Vec::new();
877 let mut in_hex = false;
878 let mut current = String::new();
879 for ch in text.chars() {
880 if ch == '<' {
881 in_hex = true;
882 current.clear();
883 } else if ch == '>' && in_hex {
884 in_hex = false;
885 tokens.push(current.clone());
886 } else if in_hex && !ch.is_whitespace() {
887 current.push(ch);
888 }
889 }
890 tokens
891}
892
893fn parse_hex_u32(hex: &str) -> u32 {
895 u32::from_str_radix(hex, 16).unwrap_or(0)
896}
897
898fn hex_to_unicode_string(hex: &str) -> String {
900 let bytes: Vec<u8> = (0..hex.len())
901 .step_by(2)
902 .filter_map(|i| u8::from_str_radix(&hex[i..i + 2.min(hex.len() - i)], 16).ok())
903 .collect();
904
905 if bytes.len() >= 2 && bytes.len().is_multiple_of(2) {
906 let u16s: Vec<u16> = bytes
908 .chunks(2)
909 .map(|c| u16::from_be_bytes([c[0], c[1]]))
910 .collect();
911 String::from_utf16_lossy(&u16s)
912 } else if bytes.len() == 1 {
913 char::from_u32(bytes[0] as u32)
914 .map(|c| c.to_string())
915 .unwrap_or_default()
916 } else {
917 String::new()
918 }
919}
920
921fn get_page_resources(doc: &Document, page_id: ObjectId) -> Option<lopdf::Dictionary> {
923 let page = match doc.get_object(page_id).ok()? {
924 Object::Dictionary(d) => d.clone(),
925 _ => return None,
926 };
927
928 if let Some(res) = resolve_dict(doc, &page, b"Resources") {
930 return Some(res);
931 }
932
933 let mut current = page;
935 for _ in 0..20 {
936 let parent_ref = match current.get(b"Parent").ok()? {
937 Object::Reference(r) => *r,
938 _ => break,
939 };
940 let parent = match doc.get_object(parent_ref).ok()? {
941 Object::Dictionary(d) => d.clone(),
942 _ => break,
943 };
944 if let Some(res) = resolve_dict(doc, &parent, b"Resources") {
945 return Some(res);
946 }
947 current = parent;
948 }
949
950 None
951}
952
953fn resolve_dict(doc: &Document, dict: &lopdf::Dictionary, key: &[u8]) -> Option<lopdf::Dictionary> {
955 match dict.get(key).ok()? {
956 Object::Dictionary(d) => Some(d.clone()),
957 Object::Reference(r) => match doc.get_object(*r).ok()? {
958 Object::Dictionary(d) => Some(d.clone()),
959 _ => None,
960 },
961 _ => None,
962 }
963}
964
965fn build_encoding_map(
972 doc: &Document,
973 font: &lopdf::Dictionary,
974) -> ([Option<char>; 256], [bool; 256]) {
975 let mut table = [None::<char>; 256];
976 let mut ct_codes = [false; 256];
977
978 let encoding = match font.get(b"Encoding").ok() {
979 Some(obj) => obj,
980 None => return (table, ct_codes),
981 };
982
983 match encoding {
984 Object::Name(name) => {
985 let name_str = String::from_utf8_lossy(name);
987 apply_base_encoding(&mut table, &name_str);
988 }
989 Object::Reference(r) => match doc.get_object(*r) {
990 Ok(Object::Dictionary(enc_dict)) => {
991 parse_encoding_dict(doc, enc_dict, &mut table, &mut ct_codes);
992 }
993 Ok(Object::Name(name)) => {
994 let name_str = String::from_utf8_lossy(name);
995 apply_base_encoding(&mut table, &name_str);
996 }
997 _ => {}
998 },
999 Object::Dictionary(enc_dict) => {
1000 parse_encoding_dict(doc, enc_dict, &mut table, &mut ct_codes);
1001 }
1002 _ => {}
1003 }
1004
1005 (table, ct_codes)
1006}
1007
1008fn parse_encoding_dict(
1010 doc: &Document,
1011 enc_dict: &lopdf::Dictionary,
1012 table: &mut [Option<char>; 256],
1013 ct_codes: &mut [bool; 256],
1014) {
1015 if let Ok(Object::Name(base)) = enc_dict.get(b"BaseEncoding") {
1017 let base_str = String::from_utf8_lossy(base);
1018 apply_base_encoding(table, &base_str);
1019 }
1020
1021 let diffs = match enc_dict.get(b"Differences").ok() {
1023 Some(Object::Array(arr)) => arr.clone(),
1024 Some(Object::Reference(r)) => match doc.get_object(*r).ok() {
1025 Some(Object::Array(arr)) => arr.clone(),
1026 _ => return,
1027 },
1028 _ => return,
1029 };
1030
1031 let mut code: Option<u32> = None;
1032 for item in &diffs {
1033 match item {
1034 Object::Integer(n) => {
1035 code = Some(*n as u32);
1036 }
1037 Object::Name(name) => {
1038 if let Some(c) = code {
1039 if c < 256 {
1040 let glyph = String::from_utf8_lossy(name);
1041 apply_glyph_name(&glyph, c as usize, table, ct_codes);
1042 }
1043 code = Some(c + 1);
1044 }
1045 }
1046 Object::Reference(r) => {
1047 if let Ok(Object::Name(name)) = doc.get_object(*r) {
1049 if let Some(c) = code {
1050 if c < 256 {
1051 let glyph = String::from_utf8_lossy(name);
1052 apply_glyph_name(&glyph, c as usize, table, ct_codes);
1053 }
1054 code = Some(c + 1);
1055 }
1056 }
1057 }
1058 _ => {}
1059 }
1060 }
1061}
1062
1063fn apply_glyph_name(
1069 glyph: &str,
1070 code: usize,
1071 table: &mut [Option<char>; 256],
1072 ct_codes: &mut [bool; 256],
1073) {
1074 if glyph == "ct" {
1075 table[code] = None;
1078 ct_codes[code] = true;
1079 return;
1080 }
1081 if let Some(ch) = glyph_name_to_unicode(glyph) {
1082 table[code] = Some(ch);
1083 ct_codes[code] = false;
1084 }
1085}
1086
1087fn apply_base_encoding(table: &mut [Option<char>; 256], name: &str) {
1089 let source = match name {
1090 "WinAnsiEncoding" => winansi_encoding(),
1091 "MacRomanEncoding" => mac_roman_encoding(),
1092 _ => return,
1093 };
1094 for (i, &ch) in source.iter().enumerate() {
1095 if ch != '\0' {
1096 table[i] = Some(ch);
1097 }
1098 }
1099}
1100
1101fn winansi_encoding() -> &'static [char; 256] {
1103 static TABLE: OnceLock<[char; 256]> = OnceLock::new();
1104 TABLE.get_or_init(|| {
1105 let mut t = ['\0'; 256];
1106 for i in 0x20..=0x7Eu8 {
1108 t[i as usize] = i as char;
1109 }
1110 t[0x09] = '\t';
1112 t[0x0A] = '\n';
1113 t[0x0D] = '\r';
1114 let cp1252: [(u8, char); 27] = [
1116 (0x80, '\u{20AC}'), (0x82, '\u{201A}'), (0x83, '\u{0192}'), (0x84, '\u{201E}'), (0x85, '\u{2026}'), (0x86, '\u{2020}'), (0x87, '\u{2021}'), (0x88, '\u{02C6}'), (0x89, '\u{2030}'), (0x8A, '\u{0160}'), (0x8B, '\u{2039}'), (0x8C, '\u{0152}'), (0x8E, '\u{017D}'), (0x91, '\u{2018}'), (0x92, '\u{2019}'), (0x93, '\u{201C}'), (0x94, '\u{201D}'), (0x95, '\u{2022}'), (0x96, '\u{2013}'), (0x97, '\u{2014}'), (0x98, '\u{02DC}'), (0x99, '\u{2122}'), (0x9A, '\u{0161}'), (0x9B, '\u{203A}'), (0x9C, '\u{0153}'), (0x9E, '\u{017E}'), (0x9F, '\u{0178}'), ];
1144 for (code, ch) in cp1252 {
1145 t[code as usize] = ch;
1146 }
1147 for i in 0xA0..=0xFFu16 {
1149 t[i as usize] = char::from_u32(i as u32).unwrap_or('\0');
1150 }
1151 t
1152 })
1153}
1154
1155fn mac_roman_encoding() -> &'static [char; 256] {
1157 static TABLE: OnceLock<[char; 256]> = OnceLock::new();
1158 TABLE.get_or_init(|| {
1159 let mut t = ['\0'; 256];
1160 for i in 0x20..=0x7Eu8 {
1162 t[i as usize] = i as char;
1163 }
1164 t[0x09] = '\t';
1165 t[0x0A] = '\n';
1166 t[0x0D] = '\r';
1167 let mac_upper: [char; 128] = [
1169 '\u{00C4}', '\u{00C5}', '\u{00C7}', '\u{00C9}', '\u{00D1}', '\u{00D6}', '\u{00DC}',
1170 '\u{00E1}', '\u{00E0}', '\u{00E2}', '\u{00E4}', '\u{00E3}', '\u{00E5}', '\u{00E7}',
1171 '\u{00E9}', '\u{00E8}', '\u{00EA}', '\u{00EB}', '\u{00ED}', '\u{00EC}', '\u{00EE}',
1172 '\u{00EF}', '\u{00F1}', '\u{00F3}', '\u{00F2}', '\u{00F4}', '\u{00F6}', '\u{00F5}',
1173 '\u{00FA}', '\u{00F9}', '\u{00FB}', '\u{00FC}', '\u{2020}', '\u{00B0}', '\u{00A2}',
1174 '\u{00A3}', '\u{00A7}', '\u{2022}', '\u{00B6}', '\u{00DF}', '\u{00AE}', '\u{00A9}',
1175 '\u{2122}', '\u{00B4}', '\u{00A8}', '\u{2260}', '\u{00C6}', '\u{00D8}', '\u{221E}',
1176 '\u{00B1}', '\u{2264}', '\u{2265}', '\u{00A5}', '\u{00B5}', '\u{2202}', '\u{2211}',
1177 '\u{220F}', '\u{03C0}', '\u{222B}', '\u{00AA}', '\u{00BA}', '\u{03A9}', '\u{00E6}',
1178 '\u{00F8}', '\u{00BF}', '\u{00A1}', '\u{00AC}', '\u{221A}', '\u{0192}', '\u{2248}',
1179 '\u{2206}', '\u{00AB}', '\u{00BB}', '\u{2026}', '\u{00A0}', '\u{00C0}', '\u{00C3}',
1180 '\u{00D5}', '\u{0152}', '\u{0153}', '\u{2013}', '\u{2014}', '\u{201C}', '\u{201D}',
1181 '\u{2018}', '\u{2019}', '\u{00F7}', '\u{25CA}', '\u{00FF}', '\u{0178}', '\u{2044}',
1182 '\u{20AC}', '\u{2039}', '\u{203A}', '\u{FB01}', '\u{FB02}', '\u{2021}', '\u{00B7}',
1183 '\u{201A}', '\u{201E}', '\u{2030}', '\u{00C2}', '\u{00CA}', '\u{00C1}', '\u{00CB}',
1184 '\u{00C8}', '\u{00CD}', '\u{00CE}', '\u{00CF}', '\u{00CC}', '\u{00D3}', '\u{00D4}',
1185 '\u{F8FF}', '\u{00D2}', '\u{00DA}', '\u{00DB}', '\u{00D9}', '\u{0131}', '\u{02C6}',
1186 '\u{02DC}', '\u{00AF}', '\u{02D8}', '\u{02D9}', '\u{02DA}', '\u{00B8}', '\u{02DD}',
1187 '\u{02DB}', '\u{02C7}',
1188 ];
1189 for (i, &ch) in mac_upper.iter().enumerate() {
1190 t[0x80 + i] = ch;
1191 }
1192 t
1193 })
1194}
1195
1196fn glyph_name_to_unicode(name: &str) -> Option<char> {
1207 if name.starts_with("uni") && name.len() >= 7 {
1209 return u32::from_str_radix(&name[3..7], 16)
1210 .ok()
1211 .and_then(char::from_u32);
1212 }
1213 if name.starts_with('u') && name.len() >= 5 && name[1..].chars().all(|c| c.is_ascii_hexdigit())
1214 {
1215 return u32::from_str_radix(&name[1..], 16)
1216 .ok()
1217 .and_then(char::from_u32);
1218 }
1219
1220 if let Some(c) = agl_table().get(name).copied() {
1221 return Some(c);
1222 }
1223
1224 match name {
1225 "st" => Some('\u{FB06}'),
1226 "longst" => Some('\u{FB05}'),
1227 _ => None,
1228 }
1229}
1230
1231fn agl_table() -> &'static HashMap<&'static str, char> {
1233 static TABLE: OnceLock<HashMap<&'static str, char>> = OnceLock::new();
1234 TABLE.get_or_init(|| {
1235 let entries: &[(&str, char)] = &[
1236 ("space", ' '),
1237 ("exclam", '!'),
1238 ("quotedbl", '"'),
1239 ("numbersign", '#'),
1240 ("dollar", '$'),
1241 ("percent", '%'),
1242 ("ampersand", '&'),
1243 ("quotesingle", '\''),
1244 ("parenleft", '('),
1245 ("parenright", ')'),
1246 ("asterisk", '*'),
1247 ("plus", '+'),
1248 ("comma", ','),
1249 ("hyphen", '-'),
1250 ("period", '.'),
1251 ("slash", '/'),
1252 ("zero", '0'),
1253 ("one", '1'),
1254 ("two", '2'),
1255 ("three", '3'),
1256 ("four", '4'),
1257 ("five", '5'),
1258 ("six", '6'),
1259 ("seven", '7'),
1260 ("eight", '8'),
1261 ("nine", '9'),
1262 ("colon", ':'),
1263 ("semicolon", ';'),
1264 ("less", '<'),
1265 ("equal", '='),
1266 ("greater", '>'),
1267 ("question", '?'),
1268 ("at", '@'),
1269 ("A", 'A'),
1270 ("B", 'B'),
1271 ("C", 'C'),
1272 ("D", 'D'),
1273 ("E", 'E'),
1274 ("F", 'F'),
1275 ("G", 'G'),
1276 ("H", 'H'),
1277 ("I", 'I'),
1278 ("J", 'J'),
1279 ("K", 'K'),
1280 ("L", 'L'),
1281 ("M", 'M'),
1282 ("N", 'N'),
1283 ("O", 'O'),
1284 ("P", 'P'),
1285 ("Q", 'Q'),
1286 ("R", 'R'),
1287 ("S", 'S'),
1288 ("T", 'T'),
1289 ("U", 'U'),
1290 ("V", 'V'),
1291 ("W", 'W'),
1292 ("X", 'X'),
1293 ("Y", 'Y'),
1294 ("Z", 'Z'),
1295 ("bracketleft", '['),
1296 ("backslash", '\\'),
1297 ("bracketright", ']'),
1298 ("asciicircum", '^'),
1299 ("underscore", '_'),
1300 ("grave", '`'),
1301 ("a", 'a'),
1302 ("b", 'b'),
1303 ("c", 'c'),
1304 ("d", 'd'),
1305 ("e", 'e'),
1306 ("f", 'f'),
1307 ("g", 'g'),
1308 ("h", 'h'),
1309 ("i", 'i'),
1310 ("j", 'j'),
1311 ("k", 'k'),
1312 ("l", 'l'),
1313 ("m", 'm'),
1314 ("n", 'n'),
1315 ("o", 'o'),
1316 ("p", 'p'),
1317 ("q", 'q'),
1318 ("r", 'r'),
1319 ("s", 's'),
1320 ("t", 't'),
1321 ("u", 'u'),
1322 ("v", 'v'),
1323 ("w", 'w'),
1324 ("x", 'x'),
1325 ("y", 'y'),
1326 ("z", 'z'),
1327 ("braceleft", '{'),
1328 ("bar", '|'),
1329 ("braceright", '}'),
1330 ("asciitilde", '~'),
1331 ("Agrave", '\u{00C0}'),
1333 ("Aacute", '\u{00C1}'),
1334 ("Acircumflex", '\u{00C2}'),
1335 ("Atilde", '\u{00C3}'),
1336 ("Adieresis", '\u{00C4}'),
1337 ("Aring", '\u{00C5}'),
1338 ("AE", '\u{00C6}'),
1339 ("Ccedilla", '\u{00C7}'),
1340 ("Egrave", '\u{00C8}'),
1341 ("Eacute", '\u{00C9}'),
1342 ("Ecircumflex", '\u{00CA}'),
1343 ("Edieresis", '\u{00CB}'),
1344 ("Igrave", '\u{00CC}'),
1345 ("Iacute", '\u{00CD}'),
1346 ("Icircumflex", '\u{00CE}'),
1347 ("Idieresis", '\u{00CF}'),
1348 ("Eth", '\u{00D0}'),
1349 ("Ntilde", '\u{00D1}'),
1350 ("Ograve", '\u{00D2}'),
1351 ("Oacute", '\u{00D3}'),
1352 ("Ocircumflex", '\u{00D4}'),
1353 ("Otilde", '\u{00D5}'),
1354 ("Odieresis", '\u{00D6}'),
1355 ("Ugrave", '\u{00D9}'),
1356 ("Uacute", '\u{00DA}'),
1357 ("Ucircumflex", '\u{00DB}'),
1358 ("Udieresis", '\u{00DC}'),
1359 ("Yacute", '\u{00DD}'),
1360 ("Thorn", '\u{00DE}'),
1361 ("germandbls", '\u{00DF}'),
1362 ("agrave", '\u{00E0}'),
1363 ("aacute", '\u{00E1}'),
1364 ("acircumflex", '\u{00E2}'),
1365 ("atilde", '\u{00E3}'),
1366 ("adieresis", '\u{00E4}'),
1367 ("aring", '\u{00E5}'),
1368 ("ae", '\u{00E6}'),
1369 ("ccedilla", '\u{00E7}'),
1370 ("egrave", '\u{00E8}'),
1371 ("eacute", '\u{00E9}'),
1372 ("ecircumflex", '\u{00EA}'),
1373 ("edieresis", '\u{00EB}'),
1374 ("igrave", '\u{00EC}'),
1375 ("iacute", '\u{00ED}'),
1376 ("icircumflex", '\u{00EE}'),
1377 ("idieresis", '\u{00EF}'),
1378 ("eth", '\u{00F0}'),
1379 ("ntilde", '\u{00F1}'),
1380 ("ograve", '\u{00F2}'),
1381 ("oacute", '\u{00F3}'),
1382 ("ocircumflex", '\u{00F4}'),
1383 ("otilde", '\u{00F5}'),
1384 ("odieresis", '\u{00F6}'),
1385 ("ugrave", '\u{00F9}'),
1386 ("uacute", '\u{00FA}'),
1387 ("ucircumflex", '\u{00FB}'),
1388 ("udieresis", '\u{00FC}'),
1389 ("yacute", '\u{00FD}'),
1390 ("thorn", '\u{00FE}'),
1391 ("ydieresis", '\u{00FF}'),
1392 ("fi", '\u{FB01}'),
1394 ("fl", '\u{FB02}'),
1395 ("ff", '\u{FB00}'),
1396 ("ffi", '\u{FB03}'),
1397 ("ffl", '\u{FB04}'),
1398 ("endash", '\u{2013}'),
1400 ("emdash", '\u{2014}'),
1401 ("bullet", '\u{2022}'),
1402 ("ellipsis", '\u{2026}'),
1403 ("quoteleft", '\u{2018}'),
1404 ("quoteright", '\u{2019}'),
1405 ("quotedblleft", '\u{201C}'),
1406 ("quotedblright", '\u{201D}'),
1407 ("quotesinglebase", '\u{201A}'),
1408 ("quotesinglbase", '\u{201A}'),
1409 ("quotedblbase", '\u{201E}'),
1410 ("dagger", '\u{2020}'),
1411 ("daggerdbl", '\u{2021}'),
1412 ("perthousand", '\u{2030}'),
1413 ("guilsinglleft", '\u{2039}'),
1414 ("guilsinglright", '\u{203A}'),
1415 ("guillemotleft", '\u{00AB}'),
1416 ("guillemotright", '\u{00BB}'),
1417 ("trademark", '\u{2122}'),
1418 ("copyright", '\u{00A9}'),
1419 ("registered", '\u{00AE}'),
1420 ("degree", '\u{00B0}'),
1421 ("plusminus", '\u{00B1}'),
1422 ("multiply", '\u{00D7}'),
1423 ("divide", '\u{00F7}'),
1424 ("fraction", '\u{2044}'),
1425 ("Euro", '\u{20AC}'),
1426 ("sterling", '\u{00A3}'),
1427 ("yen", '\u{00A5}'),
1428 ("cent", '\u{00A2}'),
1429 ("currency", '\u{00A4}'),
1430 ("section", '\u{00A7}'),
1431 ("paragraph", '\u{00B6}'),
1432 ("brokenbar", '\u{00A6}'),
1433 ("ordfeminine", '\u{00AA}'),
1434 ("ordmasculine", '\u{00BA}'),
1435 ("exclamdown", '\u{00A1}'),
1436 ("questiondown", '\u{00BF}'),
1437 ("logicalnot", '\u{00AC}'),
1438 ("mu", '\u{00B5}'),
1439 ("macron", '\u{00AF}'),
1440 ("acute", '\u{00B4}'),
1441 ("cedilla", '\u{00B8}'),
1442 ("dieresis", '\u{00A8}'),
1443 ("circumflex", '\u{02C6}'),
1444 ("tilde", '\u{02DC}'),
1445 ("caron", '\u{02C7}'),
1446 ("ring", '\u{02DA}'),
1447 ("breve", '\u{02D8}'),
1448 ("dotaccent", '\u{02D9}'),
1449 ("hungarumlaut", '\u{02DD}'),
1450 ("ogonek", '\u{02DB}'),
1451 ("nbspace", '\u{00A0}'),
1452 ("nonbreakingspace", '\u{00A0}'),
1453 ("softhyphen", '\u{00AD}'),
1454 ("periodcentered", '\u{00B7}'),
1455 ("middot", '\u{00B7}'),
1456 ("florin", '\u{0192}'),
1457 ("OE", '\u{0152}'),
1458 ("oe", '\u{0153}'),
1459 ("Scaron", '\u{0160}'),
1460 ("scaron", '\u{0161}'),
1461 ("Zcaron", '\u{017D}'),
1462 ("zcaron", '\u{017E}'),
1463 ("Ydieresis", '\u{0178}'),
1464 ("Lslash", '\u{0141}'),
1465 ("lslash", '\u{0142}'),
1466 ("Oslash", '\u{00D8}'),
1467 ("oslash", '\u{00F8}'),
1468 ("dotlessi", '\u{0131}'),
1469 ("onesuperior", '\u{00B9}'),
1471 ("twosuperior", '\u{00B2}'),
1472 ("threesuperior", '\u{00B3}'),
1473 ("onequarter", '\u{00BC}'),
1474 ("onehalf", '\u{00BD}'),
1475 ("threequarters", '\u{00BE}'),
1476 ("minus", '\u{2212}'),
1478 ("notequal", '\u{2260}'),
1479 ("lessequal", '\u{2264}'),
1480 ("greaterequal", '\u{2265}'),
1481 ("infinity", '\u{221E}'),
1482 ("partialdiff", '\u{2202}'),
1483 ("summation", '\u{2211}'),
1484 ("product", '\u{220F}'),
1485 ("integral", '\u{222B}'),
1486 ("radical", '\u{221A}'),
1487 ("approxequal", '\u{2248}'),
1488 ("Delta", '\u{0394}'),
1489 ("lozenge", '\u{25CA}'),
1490 ("pi", '\u{03C0}'),
1491 ("Omega", '\u{03A9}'),
1492 ];
1493 entries.iter().cloned().collect()
1494 })
1495}
1496
1497const CT_LIGATURE_MARKER: char = '\u{E007}';
1498
1499#[derive(Clone, Default)]
1500struct DecodedPdfString {
1501 text: String,
1502 ct_origins: Vec<bool>,
1503}
1504
1505impl DecodedPdfString {
1506 fn from_text(text: String) -> Self {
1507 let ct_origins = vec![false; text.chars().count()];
1508 Self { text, ct_origins }
1509 }
1510
1511 fn push_char(&mut self, ch: char, is_ct_origin: bool) {
1512 self.text.push(ch);
1513 self.ct_origins.push(is_ct_origin);
1514 }
1515
1516 fn push_str(&mut self, s: &str) {
1517 for ch in s.chars() {
1518 self.push_char(ch, false);
1519 }
1520 }
1521
1522 fn extend(&mut self, other: DecodedPdfString) {
1523 self.text.push_str(&other.text);
1524 self.ct_origins.extend(other.ct_origins);
1525 }
1526
1527 fn is_empty(&self) -> bool {
1528 self.text.is_empty()
1529 }
1530
1531 fn glyph_count(&self) -> usize {
1532 self.ct_origins.len()
1533 }
1534
1535 fn iter(&self) -> impl Iterator<Item = (char, bool)> + '_ {
1536 self.text.chars().zip(self.ct_origins.iter().copied())
1537 }
1538}
1539
1540fn decode_pdf_string_with_font_marked(
1543 bytes: &[u8],
1544 font_info: Option<&FontInfo>,
1545) -> DecodedPdfString {
1546 if bytes.len() >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF {
1548 let chars: Vec<u16> = bytes[2..]
1549 .chunks(2)
1550 .filter_map(|chunk| {
1551 if chunk.len() == 2 {
1552 Some(u16::from_be_bytes([chunk[0], chunk[1]]))
1553 } else {
1554 None
1555 }
1556 })
1557 .collect();
1558 return DecodedPdfString::from_text(String::from_utf16_lossy(&chars));
1559 }
1560
1561 if let Some(info) = font_info {
1562 if info.is_cid && !info.to_unicode.is_empty() {
1563 let mut result = DecodedPdfString::default();
1565 let mut i = 0;
1566 while i + 1 < bytes.len() {
1567 let code = u16::from_be_bytes([bytes[i], bytes[i + 1]]) as u32;
1568 if let Some(s) = info.to_unicode.get(&code) {
1569 result.push_str(s);
1570 } else {
1571 if let Some(ch) = char::from_u32(code) {
1573 if !ch.is_control() || ch == ' ' || ch == '\t' || ch == '\n' {
1574 result.push_char(ch, false);
1575 }
1576 }
1577 }
1578 i += 2;
1579 }
1580 return result;
1581 }
1582
1583 if !info.is_cid && !info.to_unicode.is_empty() {
1584 let mut result = DecodedPdfString::default();
1586 for &b in bytes {
1587 if let Some(s) = info.to_unicode.get(&(b as u32)) {
1588 result.push_str(s);
1589 } else if info.ct_codes[b as usize] {
1590 result.push_char(CT_LIGATURE_MARKER, true);
1591 } else if let Some(ch) = info.encoding_map[b as usize] {
1592 result.push_char(ch, false);
1593 } else {
1594 let ch = b as char;
1595 if is_printable_or_space(ch) {
1596 result.push_char(ch, false);
1597 }
1598 }
1599 }
1600 return result;
1601 }
1602
1603 if !info.is_cid
1605 && (info.encoding_map.iter().any(|c| c.is_some()) || info.ct_codes.iter().any(|f| *f))
1606 {
1607 let mut result = DecodedPdfString::default();
1608 for &b in bytes {
1609 if info.ct_codes[b as usize] {
1610 result.push_char(CT_LIGATURE_MARKER, true);
1611 } else if let Some(ch) = info.encoding_map[b as usize] {
1612 result.push_char(ch, false);
1613 } else {
1614 let ch = b as char;
1615 if is_printable_or_space(ch) {
1616 result.push_char(ch, false);
1617 }
1618 }
1619 }
1620 return result;
1621 }
1622 }
1623
1624 let mut result = DecodedPdfString::default();
1626 for &b in bytes {
1627 let ch = b as char;
1628 if is_printable_or_space(ch) {
1629 result.push_char(ch, false);
1630 }
1631 }
1632 result
1633}
1634
1635fn is_printable_or_space(ch: char) -> bool {
1638 let cp = ch as u32;
1639 cp >= 0x20 || cp == 0x09 || cp == 0x0A || cp == 0x0D
1640}
1641
1642const LIGATURE_DECOMP: bool = true;
1650
1651fn decompose_ligature_char(c: char) -> Option<&'static str> {
1659 Some(match c {
1660 '\u{FB00}' => "ff",
1661 '\u{FB01}' => "fi",
1662 '\u{FB02}' => "fl",
1663 '\u{FB03}' => "ffi",
1664 '\u{FB04}' => "ffl",
1665 '\u{FB05}' | '\u{FB06}' => "st",
1669 _ => return None,
1670 })
1671}
1672
1673fn decompose_ligature_char_with_origin(c: char, is_ct_origin: bool) -> Option<&'static str> {
1674 if is_ct_origin && c == CT_LIGATURE_MARKER {
1675 Some("ct")
1676 } else {
1677 decompose_ligature_char(c)
1678 }
1679}
1680
1681fn decompose_glyph_to_string(c: char) -> Option<String> {
1689 use unicode_normalization::UnicodeNormalization;
1690 if let Some(s) = decompose_ligature_char(c) {
1691 return Some(s.to_string());
1692 }
1693 if matches!(c, '\u{FB00}'..='\u{FB4F}') {
1694 let nfkd: String = c.nfkd().collect();
1695 if nfkd != c.to_string() {
1700 return Some(nfkd);
1701 }
1702 }
1703 None
1704}
1705
1706fn decompose_ligatures(s: &str) -> String {
1713 let mut out = String::with_capacity(s.len());
1714 for c in s.chars() {
1715 if let Some(replacement) = decompose_glyph_to_string(c) {
1716 out.push_str(&replacement);
1717 } else {
1718 out.push(c);
1719 }
1720 }
1721 out
1722}
1723
1724fn decompose_decoded_ligatures(s: &DecodedPdfString) -> String {
1725 let mut out = String::with_capacity(s.text.len());
1726 for (c, is_ct_origin) in s.iter() {
1727 if let Some(replacement) = decompose_ligature_char_with_origin(c, is_ct_origin) {
1728 out.push_str(replacement);
1729 } else if let Some(replacement) = decompose_glyph_to_string(c) {
1730 out.push_str(&replacement);
1731 } else {
1732 out.push(c);
1733 }
1734 }
1735 out
1736}
1737
1738fn maybe_decompose_decoded(s: &DecodedPdfString) -> String {
1742 if LIGATURE_DECOMP {
1743 if s.ct_origins.iter().any(|origin| *origin) {
1744 decompose_decoded_ligatures(s)
1745 } else {
1746 decompose_ligatures(&s.text)
1747 }
1748 } else {
1749 s.text.clone()
1750 }
1751}
1752
1753pub fn extract_page_blocks(doc: &Document, page_num: u32) -> Vec<TextBlock> {
1755 let pages = doc.get_pages();
1756 let Some(&page_id) = pages.get(&page_num) else {
1757 return Vec::new();
1758 };
1759
1760 let font_map = build_font_map(doc, page_id);
1761 let resources = get_page_resources(doc, page_id).unwrap_or_default();
1762 if let Ok(content_bytes) = get_page_content_bytes(doc, page_id) {
1763 if let Ok(content) = Content::decode(&content_bytes) {
1764 return extract_blocks_from_ops_inner(
1765 &content.operations,
1766 page_num,
1767 &font_map,
1768 Some((doc, &resources)),
1769 0,
1770 None,
1771 );
1772 }
1773 }
1774
1775 Vec::new()
1776}
1777
1778pub fn extract_blocks_from_page_id(
1784 doc: &Document,
1785 page_id: lopdf::ObjectId,
1786 page_num: u32,
1787) -> Vec<TextBlock> {
1788 let font_map = build_font_map(doc, page_id);
1789 let resources = get_page_resources(doc, page_id).unwrap_or_default();
1790 if let Ok(content_bytes) = get_page_content_bytes(doc, page_id) {
1791 if let Ok(content) = Content::decode(&content_bytes) {
1792 return extract_blocks_from_ops_inner(
1793 &content.operations,
1794 page_num,
1795 &font_map,
1796 Some((doc, &resources)),
1797 0,
1798 None,
1799 );
1800 }
1801 }
1802 Vec::new()
1803}
1804
1805pub fn extract_text(doc: &Document) -> Vec<TextBlock> {
1807 let pages = doc.get_pages();
1808 let mut blocks = Vec::new();
1809
1810 for (&page_num, &page_id) in &pages {
1811 let font_map = build_font_map(doc, page_id);
1812 let resources = get_page_resources(doc, page_id).unwrap_or_default();
1813 if let Ok(content_bytes) = get_page_content_bytes(doc, page_id) {
1814 if let Ok(content) = Content::decode(&content_bytes) {
1815 let page_blocks = extract_blocks_from_ops_inner(
1816 &content.operations,
1817 page_num,
1818 &font_map,
1819 Some((doc, &resources)),
1820 0,
1821 None,
1822 );
1823 blocks.extend(page_blocks);
1824 }
1825 }
1826 }
1827
1828 blocks
1829}
1830
1831pub fn extract_page_text(doc: &Document, page_num: u32) -> Result<String> {
1833 let pages = doc.get_pages();
1834 let total = pages.len() as u32;
1835
1836 if page_num == 0 || page_num > total {
1837 return Err(ExtractError::PageOutOfRange(page_num, total));
1838 }
1839
1840 let page_id = *pages
1841 .get(&page_num)
1842 .ok_or(ExtractError::PageOutOfRange(page_num, total))?;
1843
1844 let font_map = build_font_map(doc, page_id);
1845 let resources = get_page_resources(doc, page_id).unwrap_or_default();
1846 let content_bytes = get_page_content_bytes(doc, page_id).unwrap_or_default();
1847 let content = match Content::decode(&content_bytes) {
1848 Ok(c) => c,
1849 Err(_) => return Ok(String::new()),
1850 };
1851
1852 let blocks = extract_blocks_from_ops_inner(
1853 &content.operations,
1854 page_num,
1855 &font_map,
1856 Some((doc, &resources)),
1857 0,
1858 None,
1859 );
1860 let text = blocks
1861 .iter()
1862 .map(|b| b.text.as_str())
1863 .collect::<Vec<_>>()
1864 .join("");
1865
1866 Ok(text)
1867}
1868
1869pub fn extract_positioned_chars(doc: &Document, page_num: u32) -> Result<Vec<PositionedChar>> {
1871 let pages = doc.get_pages();
1872 let total = pages.len() as u32;
1873
1874 if page_num == 0 || page_num > total {
1875 return Err(ExtractError::PageOutOfRange(page_num, total));
1876 }
1877
1878 let page_id = *pages
1879 .get(&page_num)
1880 .ok_or(ExtractError::PageOutOfRange(page_num, total))?;
1881
1882 let font_map = build_font_map(doc, page_id);
1883 let content_bytes = get_page_content_bytes(doc, page_id).unwrap_or_default();
1884 let content = match Content::decode(&content_bytes) {
1885 Ok(c) => c,
1886 Err(_) => return Ok(Vec::new()),
1887 };
1888
1889 let chars = extract_chars_from_ops(&content.operations, page_num, &font_map);
1890 Ok(chars)
1891}
1892
1893fn get_page_content_bytes(doc: &Document, page_id: ObjectId) -> std::result::Result<Vec<u8>, ()> {
1895 Ok(doc.get_page_content(page_id))
1896}
1897
1898#[derive(Debug, Clone, Default)]
1903struct MarkedContentEntry {
1904 actual_text: Option<String>,
1907}
1908
1909fn resolve_bdc_properties(
1913 doc_and_resources: Option<(&Document, &lopdf::Dictionary)>,
1914 operand: &Object,
1915) -> Option<lopdf::Dictionary> {
1916 match operand {
1917 Object::Dictionary(d) => Some(d.clone()),
1918 Object::Reference(r) => doc_and_resources.and_then(|(doc, _)| {
1919 doc.get_object(*r).ok().and_then(|o| match o {
1920 Object::Dictionary(d) => Some(d.clone()),
1921 _ => None,
1922 })
1923 }),
1924 Object::Name(n) => {
1925 let (doc, resources) = doc_and_resources?;
1926 let props = resolve_dict(doc, resources, b"Properties")?;
1927 match props.get(n.as_slice()).ok()? {
1928 Object::Dictionary(d) => Some(d.clone()),
1929 Object::Reference(r) => match doc.get_object(*r).ok()? {
1930 Object::Dictionary(d) => Some(d.clone()),
1931 _ => None,
1932 },
1933 _ => None,
1934 }
1935 }
1936 _ => None,
1937 }
1938}
1939
1940fn decode_pdf_text_string(bytes: &[u8]) -> String {
1944 if bytes.len() >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF {
1945 let u16s: Vec<u16> = bytes[2..]
1946 .chunks_exact(2)
1947 .map(|c| u16::from_be_bytes([c[0], c[1]]))
1948 .collect();
1949 return String::from_utf16_lossy(&u16s);
1950 }
1951 if bytes.len() >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF {
1952 return String::from_utf8_lossy(&bytes[3..]).into_owned();
1953 }
1954 bytes
1955 .iter()
1956 .filter_map(|&b| {
1957 let ch = b as char;
1958 if is_printable_or_space(ch) {
1959 Some(ch)
1960 } else {
1961 None
1962 }
1963 })
1964 .collect()
1965}
1966
1967fn extract_actual_text(props: &lopdf::Dictionary) -> Option<String> {
1969 let obj = props.get(b"ActualText").ok()?;
1970 match obj {
1971 Object::String(bytes, _) => Some(decode_pdf_text_string(bytes)),
1972 _ => None,
1973 }
1974}
1975
1976fn current_actual_text(stack: &[MarkedContentEntry], inherited: Option<&str>) -> Option<String> {
1982 stack
1983 .iter()
1984 .rev()
1985 .find_map(|e| e.actual_text.clone())
1986 .or_else(|| inherited.map(|s| s.to_string()))
1987}
1988
1989fn extract_blocks_from_ops_inner(
1995 ops: &[Operation],
1996 page: u32,
1997 font_map: &HashMap<String, FontInfo>,
1998 doc_and_resources: Option<(&Document, &lopdf::Dictionary)>,
1999 depth: u32,
2000 inherited_actual_text: Option<&str>,
2001) -> Vec<TextBlock> {
2002 let mut state = TextState::default();
2003 let mut blocks = Vec::new();
2004 let mut mc_stack: Vec<MarkedContentEntry> = Vec::new();
2005
2006 for op in ops {
2007 match op.operator.as_str() {
2008 "q" => {
2009 state.gs_stack.push(GraphicsState {
2010 ctm: state.ctm,
2011 fill_color: state.fill_color,
2012 });
2013 }
2014 "Q" => {
2015 if let Some(gs) = state.gs_stack.pop() {
2016 state.ctm = gs.ctm;
2017 state.fill_color = gs.fill_color;
2018 }
2019 }
2020 "cm" => {
2021 if let Some(m) = extract_matrix(&op.operands) {
2022 state.ctm = concat_cm(&m, &state.ctm);
2023 }
2024 }
2025 "rg" if op.operands.len() >= 3 => {
2032 if let (Some(r), Some(g), Some(b)) = (
2033 op.operands.first().and_then(as_number),
2034 op.operands.get(1).and_then(as_number),
2035 op.operands.get(2).and_then(as_number),
2036 ) {
2037 state.fill_color = Some([
2038 clamp_unit_to_u8(r),
2039 clamp_unit_to_u8(g),
2040 clamp_unit_to_u8(b),
2041 255,
2042 ]);
2043 }
2044 }
2045 "g" => {
2046 if let Some(v) = op.operands.first().and_then(as_number) {
2047 let byte = clamp_unit_to_u8(v);
2048 state.fill_color = Some([byte, byte, byte, 255]);
2049 }
2050 }
2051 "k" if op.operands.len() >= 4 => {
2052 if let (Some(c), Some(m), Some(y), Some(kk)) = (
2053 op.operands.first().and_then(as_number),
2054 op.operands.get(1).and_then(as_number),
2055 op.operands.get(2).and_then(as_number),
2056 op.operands.get(3).and_then(as_number),
2057 ) {
2058 state.fill_color = Some(cmyk_to_rgba(c, m, y, kk));
2059 }
2060 }
2061 "RG" | "G" | "K" => {}
2062 "sc" | "scn" => {
2063 state.fill_color = match op.operands.len() {
2064 1 => op.operands.first().and_then(as_number).map(|v| {
2065 let b = clamp_unit_to_u8(v);
2066 [b, b, b, 255]
2067 }),
2068 3 => {
2069 let r = op.operands.first().and_then(as_number);
2070 let g = op.operands.get(1).and_then(as_number);
2071 let b = op.operands.get(2).and_then(as_number);
2072 match (r, g, b) {
2073 (Some(r), Some(g), Some(b)) => Some([
2074 clamp_unit_to_u8(r),
2075 clamp_unit_to_u8(g),
2076 clamp_unit_to_u8(b),
2077 255,
2078 ]),
2079 _ => None,
2080 }
2081 }
2082 4 => {
2083 let c = op.operands.first().and_then(as_number);
2084 let m = op.operands.get(1).and_then(as_number);
2085 let y = op.operands.get(2).and_then(as_number);
2086 let kk = op.operands.get(3).and_then(as_number);
2087 match (c, m, y, kk) {
2088 (Some(c), Some(m), Some(y), Some(kk)) => {
2089 Some(cmyk_to_rgba(c, m, y, kk))
2090 }
2091 _ => None,
2092 }
2093 }
2094 _ => None,
2095 };
2096 }
2097 "cs" | "CS" => {
2098 state.fill_color = None;
2099 }
2100 "BT" => {
2101 state.tm = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
2102 state.tlm = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
2103 }
2104 "Tf" if op.operands.len() >= 2 => {
2105 if let Object::Name(ref name) = op.operands[0] {
2106 state.font_name = String::from_utf8_lossy(name).to_string();
2107 }
2108 if let Some(size) = as_number(&op.operands[1]) {
2109 state.font_size = size;
2110 }
2111 }
2112 "Tc" => {
2113 if let Some(v) = op.operands.first().and_then(as_number) {
2114 state.tc = v;
2115 }
2116 }
2117 "Tw" => {
2118 if let Some(v) = op.operands.first().and_then(as_number) {
2119 state.tw = v;
2120 }
2121 }
2122 "Tz" => {
2123 if let Some(v) = op.operands.first().and_then(as_number) {
2124 state.th = v;
2125 }
2126 }
2127 "TL" => {
2128 if let Some(v) = op.operands.first().and_then(as_number) {
2129 state.tl = v;
2130 }
2131 }
2132 "Ts" => {
2133 if let Some(v) = op.operands.first().and_then(as_number) {
2134 state.ts = v;
2135 }
2136 }
2137 "Td" if op.operands.len() >= 2 => {
2138 let tx = as_number(&op.operands[0]).unwrap_or(0.0);
2139 let ty = as_number(&op.operands[1]).unwrap_or(0.0);
2140 let new_tlm = offset_text_line(&state.tlm, tx, ty);
2141 state.tlm = new_tlm;
2142 state.tm = new_tlm;
2143 }
2144 "TD" if op.operands.len() >= 2 => {
2145 let tx = as_number(&op.operands[0]).unwrap_or(0.0);
2146 let ty = as_number(&op.operands[1]).unwrap_or(0.0);
2147 state.tl = -ty;
2148 let new_tlm = offset_text_line(&state.tlm, tx, ty);
2149 state.tlm = new_tlm;
2150 state.tm = new_tlm;
2151 }
2152 "Tm" => {
2153 if let Some(m) = extract_matrix(&op.operands) {
2154 state.tm = m;
2155 state.tlm = m;
2156 }
2157 }
2158 "T*" => {
2159 let new_tlm = offset_text_line(&state.tlm, 0.0, -state.tl);
2160 state.tlm = new_tlm;
2161 state.tm = new_tlm;
2162 }
2163 "Tj" => {
2164 let fi = font_map.get(&state.font_name);
2165 if let Some(text) = extract_decoded_string_operand_with_font(&op.operands, fi) {
2166 let raw = op.operands.iter().find_map(|o| {
2167 if let Object::String(b, _) = o {
2168 Some(b.as_slice())
2169 } else {
2170 None
2171 }
2172 });
2173
2174 let x = state.tm[4];
2176 let y = state.tm[5];
2177 let gctx = GlyphCtx {
2178 font_size: state.font_size,
2179 tc: state.tc,
2180 tw: state.tw,
2181 th: state.th,
2182 hscale: hscale(&state.tm),
2183 vscale: vscale(&state.tm),
2184 };
2185 let (char_bounds, width_source, total_adv) = match (raw, fi) {
2186 (Some(raw), Some(fi)) => {
2187 let (cb, ws, adv) = char_bounds_from_bytes(raw, fi, x, y, &gctx);
2188 (cb, ws, adv)
2189 }
2190 _ => {
2191 let cw = state.font_size
2192 * hscale(&state.tm)
2193 * APPROX_CHAR_WIDTH
2194 * (state.th / 100.0);
2195 let n = text.glyph_count();
2196 let cb = (0..n)
2197 .map(|i| {
2198 let cx = x + i as f64 * cw;
2199 [cx, y, cx + cw, y + state.font_size * vscale(&state.tm)]
2200 })
2201 .collect();
2202 (cb, WidthSource::Estimate, n as f64 * cw)
2203 }
2204 };
2205
2206 if !text.is_empty() {
2209 let display_text = maybe_decompose_decoded(&text);
2210 blocks.push(TextBlock {
2211 text: display_text,
2212 page,
2213 bbox: [x, y, x + total_adv, y + state.font_size * vscale(&state.tm)],
2214 font_name: state.font_name.clone(),
2215 font_size: state.font_size,
2216 actual_text: current_actual_text(&mc_stack, inherited_actual_text),
2217 base_font: font_map
2218 .get(&state.font_name)
2219 .and_then(|fi| fi.base_font.clone()),
2220 is_bold: font_map.get(&state.font_name).is_some_and(|fi| fi.is_bold),
2221 is_italic: font_map
2222 .get(&state.font_name)
2223 .is_some_and(|fi| fi.is_italic),
2224 color: state.fill_color,
2225 width_source,
2226 char_bounds,
2227 });
2228 }
2229
2230 state.tm[4] = x + total_adv;
2231 }
2232 }
2233 "TJ" => {
2234 if let Some(Object::Array(ref arr)) = op.operands.first() {
2235 let x_start = state.tm[4];
2236 let y = state.tm[5];
2237 let fi = font_map.get(&state.font_name);
2238
2239 let gctx = GlyphCtx {
2241 font_size: state.font_size,
2242 tc: state.tc,
2243 tw: state.tw,
2244 th: state.th,
2245 hscale: hscale(&state.tm),
2246 vscale: vscale(&state.tm),
2247 };
2248 let (char_bounds, width_source, x_end) = match fi {
2249 Some(fi) => char_bounds_from_tj_array(arr, fi, x_start, y, &gctx),
2250 None => {
2251 let cw = state.font_size
2252 * hscale(&state.tm)
2253 * APPROX_CHAR_WIDTH
2254 * (state.th / 100.0);
2255 let mut cx = x_start;
2257 let mut cb = Vec::new();
2258 for item in arr {
2259 if let Object::String(bytes, _) = item {
2260 let n = if fi.map(|f| f.is_cid).unwrap_or(false) {
2261 bytes.len() / 2
2262 } else {
2263 bytes.len()
2264 };
2265 for _ in 0..n {
2266 cb.push([
2267 cx,
2268 y,
2269 cx + cw,
2270 y + state.font_size * vscale(&state.tm),
2271 ]);
2272 cx += cw + state.tc;
2273 }
2274 } else if let Some(adj) = as_number(item) {
2275 cx -= adj / 1000.0 * state.font_size;
2276 }
2277 }
2278 (cb, WidthSource::Estimate, cx)
2279 }
2280 };
2281
2282 let mut combined_text = DecodedPdfString::default();
2284 for item in arr {
2285 if let Object::String(bytes, _) = item {
2286 combined_text.extend(decode_pdf_string_with_font_marked(bytes, fi));
2287 }
2288 }
2289
2290 state.tm[4] = x_end;
2291
2292 if !combined_text.is_empty() {
2293 blocks.push(TextBlock {
2294 text: maybe_decompose_decoded(&combined_text),
2295 page,
2296 bbox: [x_start, y, x_end, y + state.font_size * vscale(&state.tm)],
2297 font_name: state.font_name.clone(),
2298 font_size: state.font_size,
2299 actual_text: current_actual_text(&mc_stack, inherited_actual_text),
2300 base_font: font_map
2301 .get(&state.font_name)
2302 .and_then(|fi| fi.base_font.clone()),
2303 is_bold: font_map.get(&state.font_name).is_some_and(|fi| fi.is_bold),
2304 is_italic: font_map
2305 .get(&state.font_name)
2306 .is_some_and(|fi| fi.is_italic),
2307 color: state.fill_color,
2308 width_source,
2309 char_bounds,
2310 });
2311 }
2312 }
2313 }
2314 "'" => {
2315 let new_tlm = offset_text_line(&state.tlm, 0.0, -state.tl);
2317 state.tlm = new_tlm;
2318 state.tm = new_tlm;
2319
2320 let fi = font_map.get(&state.font_name);
2321 if let Some(text) = extract_decoded_string_operand_with_font(&op.operands, fi) {
2322 let raw = op.operands.iter().find_map(|o| {
2323 if let Object::String(b, _) = o {
2324 Some(b.as_slice())
2325 } else {
2326 None
2327 }
2328 });
2329 let x = state.tm[4];
2330 let y = state.tm[5];
2331 let gctx = GlyphCtx {
2332 font_size: state.font_size,
2333 tc: state.tc,
2334 tw: state.tw,
2335 th: state.th,
2336 hscale: hscale(&state.tm),
2337 vscale: vscale(&state.tm),
2338 };
2339 let (char_bounds, width_source, total_adv) = match (raw, fi) {
2340 (Some(raw), Some(fi)) => {
2341 let (cb, ws, adv) = char_bounds_from_bytes(raw, fi, x, y, &gctx);
2342 (cb, ws, adv)
2343 }
2344 _ => {
2345 let cw = state.font_size
2346 * hscale(&state.tm)
2347 * APPROX_CHAR_WIDTH
2348 * (state.th / 100.0);
2349 let n = text.glyph_count();
2350 let cb = (0..n)
2351 .map(|i| {
2352 let cx = x + i as f64 * cw;
2353 [cx, y, cx + cw, y + state.font_size * vscale(&state.tm)]
2354 })
2355 .collect();
2356 (cb, WidthSource::Estimate, n as f64 * cw)
2357 }
2358 };
2359
2360 if !text.is_empty() {
2361 let display_text = maybe_decompose_decoded(&text);
2362 blocks.push(TextBlock {
2363 text: display_text,
2364 page,
2365 bbox: [x, y, x + total_adv, y + state.font_size * vscale(&state.tm)],
2366 font_name: state.font_name.clone(),
2367 font_size: state.font_size,
2368 actual_text: current_actual_text(&mc_stack, inherited_actual_text),
2369 base_font: font_map
2370 .get(&state.font_name)
2371 .and_then(|fi| fi.base_font.clone()),
2372 is_bold: font_map.get(&state.font_name).is_some_and(|fi| fi.is_bold),
2373 is_italic: font_map
2374 .get(&state.font_name)
2375 .is_some_and(|fi| fi.is_italic),
2376 color: state.fill_color,
2377 width_source,
2378 char_bounds,
2379 });
2380 }
2381
2382 state.tm[4] = x + total_adv;
2383 }
2384 }
2385 "\"" if op.operands.len() >= 3 => {
2386 if let Some(tw) = as_number(&op.operands[0]) {
2388 state.tw = tw;
2389 }
2390 if let Some(tc) = as_number(&op.operands[1]) {
2391 state.tc = tc;
2392 }
2393
2394 let new_tlm = offset_text_line(&state.tlm, 0.0, -state.tl);
2395 state.tlm = new_tlm;
2396 state.tm = new_tlm;
2397
2398 let fi = font_map.get(&state.font_name);
2399 if let Some(text) = extract_decoded_string_operand_with_font(&op.operands[2..], fi)
2400 {
2401 let raw = op.operands[2..].iter().find_map(|o| {
2402 if let Object::String(b, _) = o {
2403 Some(b.as_slice())
2404 } else {
2405 None
2406 }
2407 });
2408 let x = state.tm[4];
2409 let y = state.tm[5];
2410 let gctx = GlyphCtx {
2411 font_size: state.font_size,
2412 tc: state.tc,
2413 tw: state.tw,
2414 th: state.th,
2415 hscale: hscale(&state.tm),
2416 vscale: vscale(&state.tm),
2417 };
2418 let (char_bounds, width_source, total_adv) = match (raw, fi) {
2419 (Some(raw), Some(fi)) => {
2420 let (cb, ws, adv) = char_bounds_from_bytes(raw, fi, x, y, &gctx);
2421 (cb, ws, adv)
2422 }
2423 _ => {
2424 let cw = state.font_size
2425 * hscale(&state.tm)
2426 * APPROX_CHAR_WIDTH
2427 * (state.th / 100.0);
2428 let n = text.glyph_count();
2429 let cb = (0..n)
2430 .map(|i| {
2431 let cx = x + i as f64 * cw;
2432 [cx, y, cx + cw, y + state.font_size * vscale(&state.tm)]
2433 })
2434 .collect();
2435 (cb, WidthSource::Estimate, n as f64 * cw)
2436 }
2437 };
2438
2439 if !text.is_empty() {
2440 let display_text = maybe_decompose_decoded(&text);
2441 blocks.push(TextBlock {
2442 text: display_text,
2443 page,
2444 bbox: [x, y, x + total_adv, y + state.font_size * vscale(&state.tm)],
2445 font_name: state.font_name.clone(),
2446 font_size: state.font_size,
2447 actual_text: current_actual_text(&mc_stack, inherited_actual_text),
2448 base_font: font_map
2449 .get(&state.font_name)
2450 .and_then(|fi| fi.base_font.clone()),
2451 is_bold: font_map.get(&state.font_name).is_some_and(|fi| fi.is_bold),
2452 is_italic: font_map
2453 .get(&state.font_name)
2454 .is_some_and(|fi| fi.is_italic),
2455 color: state.fill_color,
2456 width_source,
2457 char_bounds,
2458 });
2459 }
2460
2461 state.tm[4] = x + total_adv;
2462 }
2463 }
2464 "BMC" => {
2465 mc_stack.push(MarkedContentEntry::default());
2469 }
2470 "BDC" => {
2471 let actual_text = op
2475 .operands
2476 .get(1)
2477 .and_then(|o| resolve_bdc_properties(doc_and_resources, o))
2478 .as_ref()
2479 .and_then(extract_actual_text);
2480 mc_stack.push(MarkedContentEntry { actual_text });
2481 }
2482 "EMC" => {
2483 mc_stack.pop();
2486 }
2487 "Do" if depth < 5 => {
2488 if let Some((doc, resources)) = doc_and_resources {
2493 if let Some(Object::Name(ref xobj_name)) = op.operands.first() {
2494 let xobj_name_str = String::from_utf8_lossy(xobj_name);
2495 let inherited_for_child =
2496 current_actual_text(&mc_stack, inherited_actual_text);
2497 if let Some(xobj_blocks) = extract_form_xobject_text(
2498 doc,
2499 resources,
2500 &xobj_name_str,
2501 page,
2502 font_map,
2503 depth,
2504 inherited_for_child.as_deref(),
2505 ) {
2506 blocks.extend(xobj_blocks);
2507 }
2508 }
2509 }
2510 }
2511 _ => {}
2512 }
2513 }
2514
2515 blocks
2516}
2517
2518fn extract_form_xobject_text(
2525 doc: &Document,
2526 resources: &lopdf::Dictionary,
2527 name: &str,
2528 page: u32,
2529 font_map: &HashMap<String, FontInfo>,
2530 depth: u32,
2531 inherited_actual_text: Option<&str>,
2532) -> Option<Vec<TextBlock>> {
2533 let xobj_dict = match resources.get(b"XObject").ok()? {
2535 Object::Dictionary(d) => d.clone(),
2536 Object::Reference(r) => match doc.get_object(*r).ok()? {
2537 Object::Dictionary(d) => d.clone(),
2538 _ => return None,
2539 },
2540 _ => return None,
2541 };
2542
2543 let xobj_ref = match xobj_dict.get(name.as_bytes()).ok()? {
2544 Object::Reference(r) => *r,
2545 _ => return None,
2546 };
2547
2548 let stream = match doc.get_object(xobj_ref).ok()? {
2549 Object::Stream(s) => s.clone(),
2550 _ => return None,
2551 };
2552
2553 let subtype = stream
2555 .dict
2556 .get(b"Subtype")
2557 .ok()
2558 .and_then(|o| match o {
2559 Object::Name(n) => Some(String::from_utf8_lossy(n).to_string()),
2560 _ => None,
2561 })
2562 .unwrap_or_default();
2563 if subtype != "Form" {
2564 return None;
2565 }
2566
2567 let content_bytes = stream
2569 .decompressed_content()
2570 .ok()
2571 .unwrap_or_else(|| stream.content.clone());
2572 let content = Content::decode(&content_bytes).ok()?;
2573
2574 let mut xobj_font_map = font_map.clone();
2577 if let Some(xobj_resources) = resolve_dict(doc, &stream.dict, b"Resources") {
2578 if let Some(xobj_fonts) = resolve_dict(doc, &xobj_resources, b"Font") {
2579 for (name_bytes, value) in xobj_fonts.iter() {
2580 let fname = String::from_utf8_lossy(name_bytes).to_string();
2581 if let Some(fi) = build_font_info_from_value(doc, value) {
2582 xobj_font_map.insert(fname, fi);
2583 }
2584 }
2585 }
2586 }
2587
2588 let xobj_resources =
2590 resolve_dict(doc, &stream.dict, b"Resources").unwrap_or_else(|| resources.clone());
2591
2592 Some(extract_blocks_from_ops_inner(
2593 &content.operations,
2594 page,
2595 &xobj_font_map,
2596 Some((doc, &xobj_resources)),
2597 depth + 1,
2598 inherited_actual_text,
2599 ))
2600}
2601
2602fn build_font_info_from_value(doc: &Document, value: &Object) -> Option<FontInfo> {
2604 let font = match value {
2605 Object::Reference(r) => match doc.get_object(*r).ok()? {
2606 Object::Dictionary(d) => d.clone(),
2607 _ => return None,
2608 },
2609 Object::Dictionary(d) => d.clone(),
2610 _ => return None,
2611 };
2612
2613 let subtype = font
2614 .get(b"Subtype")
2615 .ok()
2616 .and_then(|o| match o {
2617 Object::Name(n) => Some(String::from_utf8_lossy(n).to_string()),
2618 _ => None,
2619 })
2620 .unwrap_or_default();
2621
2622 let is_cid = subtype == "Type0";
2623 let mut to_unicode = parse_to_unicode_from_font(doc, &font);
2624
2625 if to_unicode.is_empty() && is_cid {
2626 if let Ok(Object::Array(descendants)) = font.get(b"DescendantFonts") {
2627 for d in descendants {
2628 let desc_dict = match d {
2629 Object::Reference(r) => {
2630 let Some(Object::Dictionary(d)) = doc.get_object(*r).ok() else {
2633 continue;
2634 };
2635 d
2636 }
2637 Object::Dictionary(d) => d,
2638 _ => continue,
2639 };
2640 let tu = parse_to_unicode_from_font(doc, desc_dict);
2641 if !tu.is_empty() {
2642 to_unicode = tu;
2643 break;
2644 }
2645 }
2646 }
2647 }
2648
2649 let (encoding_map, ct_codes) = if !is_cid {
2650 build_encoding_map(doc, &font)
2651 } else {
2652 ([None; 256], [false; 256])
2653 };
2654
2655 let (base_font, is_bold, is_italic) = derive_font_style(doc, &font);
2656 let (simple_widths, cid_default_width, cid_widths) = parse_font_widths(doc, &font, is_cid);
2657
2658 Some(FontInfo {
2659 is_cid,
2660 to_unicode,
2661 encoding_map,
2662 ct_codes,
2663 base_font,
2664 is_bold,
2665 is_italic,
2666 simple_widths,
2667 cid_default_width,
2668 cid_widths,
2669 })
2670}
2671
2672fn push_glyph_positioned(
2686 chars: &mut Vec<PositionedChar>,
2687 state: &mut TextState,
2688 page: u32,
2689 glyph: char,
2690 is_ct_origin: bool,
2691 char_w: f64,
2692) {
2693 let constituents: Option<String> = if LIGATURE_DECOMP {
2694 if let Some(s) = decompose_ligature_char_with_origin(glyph, is_ct_origin) {
2698 Some(s.to_string())
2699 } else {
2700 decompose_glyph_to_string(glyph)
2701 }
2702 } else {
2703 None
2704 };
2705 match constituents {
2706 None => {
2707 chars.push(PositionedChar {
2708 ch: glyph,
2709 page,
2710 bbox: glyph_box(state, 0.0, char_w, state.font_size),
2711 });
2712 }
2713 Some(s) => {
2714 let n = s.chars().count() as f64;
2715 let part_w = char_w / n;
2716 for (i, c) in s.chars().enumerate() {
2717 let x0 = part_w * i as f64;
2718 chars.push(PositionedChar {
2719 ch: c,
2720 page,
2721 bbox: glyph_box(state, x0, x0 + part_w, state.font_size),
2722 });
2723 }
2724 }
2725 }
2726 advance_text_origin(state, char_w + state.tc);
2727}
2728
2729fn advance_text_origin(state: &mut TextState, d: f64) {
2734 state.tm[4] += d * state.tm[0];
2735 state.tm[5] += d * state.tm[1];
2736}
2737
2738fn glyph_box(state: &TextState, x0: f64, x1: f64, h: f64) -> [f64; 4] {
2742 let tm = &state.tm;
2743 let c = &state.ctm;
2744 let mut lo = [f64::INFINITY; 2];
2745 let mut hi = [f64::NEG_INFINITY; 2];
2746 for (tx, ty) in [(x0, 0.0), (x1, 0.0), (x0, h), (x1, h)] {
2747 let ux = tx * tm[0] + ty * tm[2] + tm[4];
2748 let uy = tx * tm[1] + ty * tm[3] + tm[5];
2749 let px = c[0] * ux + c[2] * uy + c[4];
2750 let py = c[1] * ux + c[3] * uy + c[5];
2751 lo[0] = lo[0].min(px);
2752 lo[1] = lo[1].min(py);
2753 hi[0] = hi[0].max(px);
2754 hi[1] = hi[1].max(py);
2755 }
2756 [lo[0], lo[1], hi[0], hi[1]]
2757}
2758
2759fn extract_chars_from_ops(
2761 ops: &[Operation],
2762 page: u32,
2763 font_map: &HashMap<String, FontInfo>,
2764) -> Vec<PositionedChar> {
2765 let mut state = TextState::default();
2766 let mut chars = Vec::new();
2767
2768 for op in ops {
2769 match op.operator.as_str() {
2770 "q" => {
2771 state.gs_stack.push(GraphicsState {
2772 ctm: state.ctm,
2773 fill_color: state.fill_color,
2774 });
2775 }
2776 "Q" => {
2777 if let Some(gs) = state.gs_stack.pop() {
2778 state.ctm = gs.ctm;
2779 state.fill_color = gs.fill_color;
2780 }
2781 }
2782 "cm" => {
2783 if let Some(m) = extract_matrix(&op.operands) {
2784 state.ctm = concat_cm(&m, &state.ctm);
2785 }
2786 }
2787 "rg" if op.operands.len() >= 3 => {
2794 if let (Some(r), Some(g), Some(b)) = (
2795 op.operands.first().and_then(as_number),
2796 op.operands.get(1).and_then(as_number),
2797 op.operands.get(2).and_then(as_number),
2798 ) {
2799 state.fill_color = Some([
2800 clamp_unit_to_u8(r),
2801 clamp_unit_to_u8(g),
2802 clamp_unit_to_u8(b),
2803 255,
2804 ]);
2805 }
2806 }
2807 "g" => {
2808 if let Some(v) = op.operands.first().and_then(as_number) {
2809 let byte = clamp_unit_to_u8(v);
2810 state.fill_color = Some([byte, byte, byte, 255]);
2811 }
2812 }
2813 "k" if op.operands.len() >= 4 => {
2814 if let (Some(c), Some(m), Some(y), Some(kk)) = (
2815 op.operands.first().and_then(as_number),
2816 op.operands.get(1).and_then(as_number),
2817 op.operands.get(2).and_then(as_number),
2818 op.operands.get(3).and_then(as_number),
2819 ) {
2820 state.fill_color = Some(cmyk_to_rgba(c, m, y, kk));
2821 }
2822 }
2823 "RG" | "G" | "K" => {}
2824 "sc" | "scn" => {
2825 state.fill_color = match op.operands.len() {
2826 1 => op.operands.first().and_then(as_number).map(|v| {
2827 let b = clamp_unit_to_u8(v);
2828 [b, b, b, 255]
2829 }),
2830 3 => {
2831 let r = op.operands.first().and_then(as_number);
2832 let g = op.operands.get(1).and_then(as_number);
2833 let b = op.operands.get(2).and_then(as_number);
2834 match (r, g, b) {
2835 (Some(r), Some(g), Some(b)) => Some([
2836 clamp_unit_to_u8(r),
2837 clamp_unit_to_u8(g),
2838 clamp_unit_to_u8(b),
2839 255,
2840 ]),
2841 _ => None,
2842 }
2843 }
2844 4 => {
2845 let c = op.operands.first().and_then(as_number);
2846 let m = op.operands.get(1).and_then(as_number);
2847 let y = op.operands.get(2).and_then(as_number);
2848 let kk = op.operands.get(3).and_then(as_number);
2849 match (c, m, y, kk) {
2850 (Some(c), Some(m), Some(y), Some(kk)) => {
2851 Some(cmyk_to_rgba(c, m, y, kk))
2852 }
2853 _ => None,
2854 }
2855 }
2856 _ => None,
2857 };
2858 }
2859 "cs" | "CS" => {
2860 state.fill_color = None;
2861 }
2862 "BT" => {
2863 state.tm = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
2864 state.tlm = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
2865 }
2866 "Tf" if op.operands.len() >= 2 => {
2867 if let Object::Name(ref name) = op.operands[0] {
2868 state.font_name = String::from_utf8_lossy(name).to_string();
2869 }
2870 if let Some(size) = as_number(&op.operands[1]) {
2871 state.font_size = size;
2872 }
2873 }
2874 "Tc" => {
2875 if let Some(v) = op.operands.first().and_then(as_number) {
2876 state.tc = v;
2877 }
2878 }
2879 "Tw" => {
2880 if let Some(v) = op.operands.first().and_then(as_number) {
2881 state.tw = v;
2882 }
2883 }
2884 "Tz" => {
2885 if let Some(v) = op.operands.first().and_then(as_number) {
2886 state.th = v;
2887 }
2888 }
2889 "TL" => {
2890 if let Some(v) = op.operands.first().and_then(as_number) {
2891 state.tl = v;
2892 }
2893 }
2894 "Ts" => {
2895 if let Some(v) = op.operands.first().and_then(as_number) {
2896 state.ts = v;
2897 }
2898 }
2899 "Td" if op.operands.len() >= 2 => {
2900 let tx = as_number(&op.operands[0]).unwrap_or(0.0);
2901 let ty = as_number(&op.operands[1]).unwrap_or(0.0);
2902 let new_tlm = offset_text_line(&state.tlm, tx, ty);
2903 state.tlm = new_tlm;
2904 state.tm = new_tlm;
2905 }
2906 "TD" if op.operands.len() >= 2 => {
2907 let tx = as_number(&op.operands[0]).unwrap_or(0.0);
2908 let ty = as_number(&op.operands[1]).unwrap_or(0.0);
2909 state.tl = -ty;
2910 let new_tlm = offset_text_line(&state.tlm, tx, ty);
2911 state.tlm = new_tlm;
2912 state.tm = new_tlm;
2913 }
2914 "Tm" => {
2915 if let Some(m) = extract_matrix(&op.operands) {
2916 state.tm = m;
2917 state.tlm = m;
2918 }
2919 }
2920 "T*" => {
2921 let new_tlm = offset_text_line(&state.tlm, 0.0, -state.tl);
2922 state.tlm = new_tlm;
2923 state.tm = new_tlm;
2924 }
2925 "Tj" => {
2926 let fi = font_map.get(&state.font_name);
2927 if let Some(text) = extract_decoded_string_operand_with_font(&op.operands, fi) {
2928 let char_w = state.font_size * APPROX_CHAR_WIDTH * (state.th / 100.0);
2929 for (ch, is_ct_origin) in text.iter() {
2930 push_glyph_positioned(
2931 &mut chars,
2932 &mut state,
2933 page,
2934 ch,
2935 is_ct_origin,
2936 char_w,
2937 );
2938 }
2939 }
2940 }
2941 "TJ" => {
2942 if let Some(Object::Array(ref arr)) = op.operands.first() {
2943 let char_w = state.font_size * APPROX_CHAR_WIDTH * (state.th / 100.0);
2944 let fi = font_map.get(&state.font_name);
2945 for item in arr {
2946 match item {
2947 Object::String(bytes, _) => {
2948 let text = decode_pdf_string_with_font_marked(bytes, fi);
2949 for (ch, is_ct_origin) in text.iter() {
2950 push_glyph_positioned(
2951 &mut chars,
2952 &mut state,
2953 page,
2954 ch,
2955 is_ct_origin,
2956 char_w,
2957 );
2958 }
2959 }
2960 _ => {
2961 if let Some(adj) = as_number(item) {
2962 let d = -(adj / 1000.0 * state.font_size);
2963 advance_text_origin(&mut state, d);
2964 }
2965 }
2966 }
2967 }
2968 }
2969 }
2970 "'" => {
2971 let new_tlm = offset_text_line(&state.tlm, 0.0, -state.tl);
2972 state.tlm = new_tlm;
2973 state.tm = new_tlm;
2974
2975 let fi = font_map.get(&state.font_name);
2976 if let Some(text) = extract_decoded_string_operand_with_font(&op.operands, fi) {
2977 let char_w = state.font_size * APPROX_CHAR_WIDTH * (state.th / 100.0);
2978 for (ch, is_ct_origin) in text.iter() {
2979 push_glyph_positioned(
2980 &mut chars,
2981 &mut state,
2982 page,
2983 ch,
2984 is_ct_origin,
2985 char_w,
2986 );
2987 }
2988 }
2989 }
2990 "\"" if op.operands.len() >= 3 => {
2991 if let Some(tw) = as_number(&op.operands[0]) {
2992 state.tw = tw;
2993 }
2994 if let Some(tc) = as_number(&op.operands[1]) {
2995 state.tc = tc;
2996 }
2997
2998 let new_tlm = offset_text_line(&state.tlm, 0.0, -state.tl);
2999 state.tlm = new_tlm;
3000 state.tm = new_tlm;
3001
3002 let fi = font_map.get(&state.font_name);
3003 if let Some(text) = extract_decoded_string_operand_with_font(&op.operands[2..], fi)
3004 {
3005 let char_w = state.font_size * APPROX_CHAR_WIDTH * (state.th / 100.0);
3006 for (ch, is_ct_origin) in text.iter() {
3007 push_glyph_positioned(
3008 &mut chars,
3009 &mut state,
3010 page,
3011 ch,
3012 is_ct_origin,
3013 char_w,
3014 );
3015 }
3016 }
3017 }
3018 _ => {}
3019 }
3020 }
3021
3022 chars
3023}
3024
3025fn extract_decoded_string_operand_with_font(
3027 operands: &[Object],
3028 font_info: Option<&FontInfo>,
3029) -> Option<DecodedPdfString> {
3030 for op in operands {
3031 if let Object::String(bytes, _) = op {
3032 return Some(decode_pdf_string_with_font_marked(bytes, font_info));
3033 }
3034 }
3035 None
3036}
3037
3038fn as_number(obj: &Object) -> Option<f64> {
3040 match obj {
3041 Object::Integer(i) => Some(*i as f64),
3042 Object::Real(f) => Some(*f as f64),
3043 _ => None,
3044 }
3045}
3046
3047fn extract_matrix(operands: &[Object]) -> Option<[f64; 6]> {
3049 if operands.len() < 6 {
3050 return None;
3051 }
3052 let a = as_number(&operands[0])?;
3053 let b = as_number(&operands[1])?;
3054 let c = as_number(&operands[2])?;
3055 let d = as_number(&operands[3])?;
3056 let e = as_number(&operands[4])?;
3057 let f = as_number(&operands[5])?;
3058 Some([a, b, c, d, e, f])
3059}
3060
3061fn concat_cm(m: &[f64; 6], ctm: &[f64; 6]) -> [f64; 6] {
3065 multiply_matrix(m, ctm)
3066}
3067
3068fn offset_text_line(tlm: &[f64; 6], tx: f64, ty: f64) -> [f64; 6] {
3071 multiply_matrix(&[1.0, 0.0, 0.0, 1.0, tx, ty], tlm)
3072}
3073
3074fn multiply_matrix(m1: &[f64; 6], m2: &[f64; 6]) -> [f64; 6] {
3075 [
3076 m1[0] * m2[0] + m1[1] * m2[2],
3077 m1[0] * m2[1] + m1[1] * m2[3],
3078 m1[2] * m2[0] + m1[3] * m2[2],
3079 m1[2] * m2[1] + m1[3] * m2[3],
3080 m1[4] * m2[0] + m1[5] * m2[2] + m2[4],
3081 m1[4] * m2[1] + m1[5] * m2[3] + m2[5],
3082 ]
3083}
3084
3085#[cfg(test)]
3086mod tests {
3087 use super::*;
3088 use lopdf::{dictionary, Document, Object, Stream};
3089
3090 fn make_doc_with_text(content: &[u8]) -> Document {
3092 let mut doc = Document::with_version("1.7");
3093
3094 let content_stream = Stream::new(dictionary! {}, content.to_vec());
3095 let content_id = doc.add_object(Object::Stream(content_stream));
3096
3097 let page_dict = dictionary! {
3098 "Type" => "Page",
3099 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
3100 "Contents" => Object::Reference(content_id),
3101 };
3102 let page_id = doc.add_object(Object::Dictionary(page_dict));
3103
3104 let pages_dict = dictionary! {
3105 "Type" => "Pages",
3106 "Kids" => vec![Object::Reference(page_id)],
3107 "Count" => 1_i64,
3108 };
3109 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
3110
3111 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
3112 d.set("Parent", Object::Reference(pages_id));
3113 }
3114
3115 let catalog = dictionary! {
3116 "Type" => "Catalog",
3117 "Pages" => Object::Reference(pages_id),
3118 };
3119 let catalog_id = doc.add_object(Object::Dictionary(catalog));
3120 doc.trailer.set("Root", Object::Reference(catalog_id));
3121
3122 doc
3123 }
3124
3125 #[test]
3126 fn extract_simple_text() {
3127 let doc = make_doc_with_text(b"BT /F1 12 Tf (Hello World) Tj ET");
3128 let blocks = extract_text(&doc);
3129 assert_eq!(blocks.len(), 1);
3130 assert_eq!(blocks[0].text, "Hello World");
3131 assert_eq!(blocks[0].page, 1);
3132 assert_eq!(blocks[0].font_size, 12.0);
3133 }
3134
3135 #[test]
3136 fn extract_page_text_single() {
3137 let doc = make_doc_with_text(b"BT /F1 12 Tf (Hello) Tj ET");
3138 let text = extract_page_text(&doc, 1).unwrap();
3139 assert_eq!(text, "Hello");
3140 }
3141
3142 #[test]
3143 fn extract_page_text_out_of_range() {
3144 let doc = make_doc_with_text(b"BT /F1 12 Tf (Hello) Tj ET");
3145 let result = extract_page_text(&doc, 5);
3146 assert!(result.is_err());
3147 }
3148
3149 #[test]
3150 fn extract_positioned_chars_basic() {
3151 let doc = make_doc_with_text(b"BT /F1 12 Tf (AB) Tj ET");
3152 let chars = extract_positioned_chars(&doc, 1).unwrap();
3153 assert_eq!(chars.len(), 2);
3154 assert_eq!(chars[0].ch, 'A');
3155 assert_eq!(chars[1].ch, 'B');
3156 assert_eq!(chars[0].page, 1);
3157 assert!(chars[1].bbox[0] > chars[0].bbox[0]);
3159 }
3160
3161 #[test]
3162 fn extract_tj_array() {
3163 let doc = make_doc_with_text(b"BT /F1 12 Tf [(He) -100 (llo)] TJ ET");
3164 let blocks = extract_text(&doc);
3165 assert_eq!(blocks.len(), 1);
3166 assert_eq!(blocks[0].text, "Hello");
3167 }
3168
3169 #[test]
3170 fn empty_page_extracts_no_text() {
3171 let doc = make_doc_with_text(b"q Q");
3172 let blocks = extract_text(&doc);
3173 assert!(blocks.is_empty());
3174 }
3175
3176 #[test]
3177 fn multiline_text_extraction() {
3178 let doc = make_doc_with_text(b"BT /F1 12 Tf 12 TL (Line1) Tj T* (Line2) Tj ET");
3179 let blocks = extract_text(&doc);
3180 assert_eq!(blocks.len(), 2);
3181 assert_eq!(blocks[0].text, "Line1");
3182 assert_eq!(blocks[1].text, "Line2");
3183 }
3184
3185 #[test]
3189 fn actual_text_basic_bdc_override() {
3190 let doc = make_doc_with_text(b"BT /F1 12 Tf /Span <</ActualText (fi)>> BDC (X) Tj EMC ET");
3191 let blocks = extract_text(&doc);
3192 assert_eq!(blocks.len(), 1);
3193 assert_eq!(blocks[0].text, "X");
3194 assert_eq!(blocks[0].actual_text.as_deref(), Some("fi"));
3195 }
3196
3197 #[test]
3201 fn actual_text_nested_bdc_inner_overrides_outer() {
3202 let doc = make_doc_with_text(
3203 b"BT /F1 12 Tf \
3204 /Span <</ActualText (outer)>> BDC \
3205 (A) Tj \
3206 /Span <</ActualText (inner)>> BDC \
3207 (B) Tj \
3208 EMC \
3209 (C) Tj \
3210 EMC ET",
3211 );
3212 let blocks = extract_text(&doc);
3213 assert_eq!(blocks.len(), 3);
3214 assert_eq!(blocks[0].text, "A");
3215 assert_eq!(blocks[0].actual_text.as_deref(), Some("outer"));
3216 assert_eq!(blocks[1].text, "B");
3217 assert_eq!(blocks[1].actual_text.as_deref(), Some("inner"));
3218 assert_eq!(blocks[2].text, "C");
3219 assert_eq!(blocks[2].actual_text.as_deref(), Some("outer"));
3220 }
3221
3222 #[test]
3225 fn actual_text_bmc_does_not_leak_emc() {
3226 let doc = make_doc_with_text(
3227 b"BT /F1 12 Tf \
3228 /Span <</ActualText (X)>> BDC \
3229 /Artifact BMC (in) Tj EMC \
3230 (out) Tj \
3231 EMC \
3232 (after) Tj ET",
3233 );
3234 let blocks = extract_text(&doc);
3235 assert_eq!(blocks.len(), 3);
3236 assert_eq!(blocks[0].actual_text.as_deref(), Some("X"));
3237 assert_eq!(blocks[1].actual_text.as_deref(), Some("X"));
3238 assert_eq!(blocks[2].actual_text, None);
3239 }
3240
3241 fn make_doc_with_xobj_inside_bdc(
3247 page_pre: &[u8],
3248 xobj_content: &[u8],
3249 page_post: &[u8],
3250 actual_text: &str,
3251 ) -> Document {
3252 let mut doc = Document::with_version("1.7");
3253
3254 let xobj_dict = dictionary! {
3255 "Type" => "XObject",
3256 "Subtype" => "Form",
3257 "BBox" => vec![0.into(), 0.into(), 100.into(), 100.into()],
3258 };
3259 let xobj_stream = Stream::new(xobj_dict, xobj_content.to_vec());
3260 let xobj_id = doc.add_object(Object::Stream(xobj_stream));
3261
3262 let mut page_content = Vec::<u8>::new();
3264 page_content.extend_from_slice(b"BT /F1 12 Tf /Span <</ActualText (");
3265 page_content.extend_from_slice(actual_text.as_bytes());
3266 page_content.extend_from_slice(b")>> BDC ");
3267 page_content.extend_from_slice(page_pre);
3268 page_content.extend_from_slice(b" /Fm0 Do ");
3269 page_content.extend_from_slice(page_post);
3270 page_content.extend_from_slice(b" EMC ET");
3271
3272 let content_stream = Stream::new(dictionary! {}, page_content);
3273 let content_id = doc.add_object(Object::Stream(content_stream));
3274
3275 let resources = dictionary! {
3276 "XObject" => dictionary! { "Fm0" => Object::Reference(xobj_id) },
3277 };
3278 let resources_id = doc.add_object(Object::Dictionary(resources));
3279
3280 let page_dict = dictionary! {
3281 "Type" => "Page",
3282 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
3283 "Contents" => Object::Reference(content_id),
3284 "Resources" => Object::Reference(resources_id),
3285 };
3286 let page_id = doc.add_object(Object::Dictionary(page_dict));
3287
3288 let pages_dict = dictionary! {
3289 "Type" => "Pages",
3290 "Kids" => vec![Object::Reference(page_id)],
3291 "Count" => 1_i64,
3292 };
3293 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
3294
3295 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
3296 d.set("Parent", Object::Reference(pages_id));
3297 }
3298
3299 let catalog = dictionary! {
3300 "Type" => "Catalog",
3301 "Pages" => Object::Reference(pages_id),
3302 };
3303 let catalog_id = doc.add_object(Object::Dictionary(catalog));
3304 doc.trailer.set("Root", Object::Reference(catalog_id));
3305
3306 doc
3307 }
3308
3309 #[test]
3316 fn actual_text_propagates_into_form_xobject_recursion() {
3317 let doc = make_doc_with_xobj_inside_bdc(
3318 b"(pre) Tj",
3319 b"BT /F1 12 Tf (inside) Tj ET",
3320 b"(post) Tj",
3321 "wrapped",
3322 );
3323 let blocks = extract_text(&doc);
3324 assert_eq!(blocks.len(), 3);
3325 assert_eq!(blocks[0].text, "pre");
3326 assert_eq!(blocks[0].actual_text.as_deref(), Some("wrapped"));
3327 assert_eq!(blocks[1].text, "inside");
3330 assert_eq!(
3331 blocks[1].actual_text.as_deref(),
3332 Some("wrapped"),
3333 "Form XObject text lost surrounding /ActualText (issue #1358)"
3334 );
3335 assert_eq!(blocks[2].text, "post");
3336 assert_eq!(blocks[2].actual_text.as_deref(), Some("wrapped"));
3337 }
3338
3339 #[test]
3346 fn actual_text_inner_xobj_bdc_overrides_inherited() {
3347 let doc = make_doc_with_xobj_inside_bdc(
3348 b"",
3349 b"BT /F1 12 Tf \
3350 /Span <</ActualText (inner)>> BDC (B) Tj EMC \
3351 (after) Tj ET",
3352 b"",
3353 "outer",
3354 );
3355 let blocks = extract_text(&doc);
3356 assert_eq!(blocks.len(), 2);
3357 assert_eq!(blocks[0].text, "B");
3358 assert_eq!(blocks[0].actual_text.as_deref(), Some("inner"));
3359 assert_eq!(blocks[1].text, "after");
3360 assert_eq!(blocks[1].actual_text.as_deref(), Some("outer"));
3361 }
3362
3363 #[test]
3366 fn actual_text_utf16be_bom_decodes() {
3367 let doc = make_doc_with_text(
3369 b"BT /F1 12 Tf /Span <</ActualText <FEFF00660069> >> BDC (X) Tj EMC ET",
3370 );
3371 let blocks = extract_text(&doc);
3372 assert_eq!(blocks.len(), 1);
3373 assert_eq!(blocks[0].actual_text.as_deref(), Some("fi"));
3374 }
3375
3376 #[test]
3379 fn ligature_ff_decomposes() {
3380 assert_eq!(decompose_ligature_char('\u{FB00}'), Some("ff"));
3381 assert_eq!(decompose_ligatures("\u{FB00}"), "ff");
3382 assert_eq!(decompose_ligatures("o\u{FB00}ice"), "office");
3383 }
3384
3385 #[test]
3386 fn ligature_fi_decomposes() {
3387 assert_eq!(decompose_ligature_char('\u{FB01}'), Some("fi"));
3388 assert_eq!(decompose_ligatures("\u{FB01}"), "fi");
3389 assert_eq!(decompose_ligatures("of\u{FB01}ce"), "office");
3390 }
3391
3392 #[test]
3393 fn ligature_fl_decomposes() {
3394 assert_eq!(decompose_ligature_char('\u{FB02}'), Some("fl"));
3395 assert_eq!(decompose_ligatures("\u{FB02}ame"), "flame");
3396 }
3397
3398 #[test]
3399 fn ligature_ffi_decomposes() {
3400 assert_eq!(decompose_ligature_char('\u{FB03}'), Some("ffi"));
3401 assert_eq!(decompose_ligatures("o\u{FB03}ce"), "office");
3402 }
3403
3404 #[test]
3405 fn ligature_ffl_decomposes() {
3406 assert_eq!(decompose_ligature_char('\u{FB04}'), Some("ffl"));
3407 assert_eq!(decompose_ligatures("ba\u{FB04}e"), "baffle");
3408 }
3409
3410 #[test]
3415 fn ligature_st_decomposes() {
3416 assert_eq!(decompose_ligature_char('\u{FB06}'), Some("st"));
3417 assert_eq!(decompose_ligature_char('\u{FB05}'), Some("st"));
3418 assert_eq!(decompose_ligatures("fa\u{FB06}"), "fast");
3419 assert_eq!(glyph_name_to_unicode("st"), Some('\u{FB06}'));
3421 }
3422
3423 #[test]
3430 fn ligature_ct_via_glyph_name_emits_string() {
3431 assert_eq!(glyph_name_to_unicode("ct"), None);
3432 let doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <6101> Tj ET", "ct");
3434 let blocks = extract_text(&doc);
3435 assert_eq!(blocks.len(), 1);
3436 assert_eq!(blocks[0].text, "act");
3437 }
3438
3439 #[test]
3444 fn tounicode_pua_e007_is_preserved() {
3445 let doc = make_doc_with_tounicode_cmap(b"BT /F1 12 Tf <01> Tj ET", 0x01, 0xE007);
3446 let blocks = extract_text(&doc);
3447 assert_eq!(blocks.len(), 1);
3448 assert_eq!(blocks[0].text, "\u{E007}");
3449 assert_ne!(blocks[0].text, "ct");
3450 }
3451
3452 #[test]
3456 fn ligature_ct_positioned_chars_advance_as_single_glyph() {
3457 let doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <610162> Tj ET", "ct");
3459 let chars = extract_positioned_chars(&doc, 1).unwrap();
3460 let extracted: String = chars.iter().map(|c| c.ch).collect();
3461 assert_eq!(extracted, "actb");
3462 assert_eq!(chars.len(), 4);
3463
3464 let char_w = 12.0 * APPROX_CHAR_WIDTH;
3465 assert!((chars[1].bbox[0] - char_w).abs() < 1e-6);
3466 assert!((chars[3].bbox[0] - (2.0 * char_w)).abs() < 1e-6);
3467 }
3468
3469 fn make_doc_with_ligature_font(content: &[u8], glyph_name: &str) -> Document {
3474 let mut doc = Document::with_version("1.7");
3475
3476 let content_stream = Stream::new(dictionary! {}, content.to_vec());
3477 let content_id = doc.add_object(Object::Stream(content_stream));
3478
3479 let encoding = dictionary! {
3480 "Type" => "Encoding",
3481 "BaseEncoding" => "WinAnsiEncoding",
3482 "Differences" => vec![
3483 1_i64.into(),
3484 Object::Name(glyph_name.as_bytes().to_vec()),
3485 ],
3486 };
3487 let encoding_id = doc.add_object(Object::Dictionary(encoding));
3488
3489 let font = dictionary! {
3490 "Type" => "Font",
3491 "Subtype" => "Type1",
3492 "BaseFont" => "Helvetica",
3493 "Encoding" => Object::Reference(encoding_id),
3494 };
3495 let font_id = doc.add_object(Object::Dictionary(font));
3496
3497 let resources = dictionary! {
3498 "Font" => dictionary! { "F1" => Object::Reference(font_id) },
3499 };
3500 let resources_id = doc.add_object(Object::Dictionary(resources));
3501
3502 let page_dict = dictionary! {
3503 "Type" => "Page",
3504 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
3505 "Contents" => Object::Reference(content_id),
3506 "Resources" => Object::Reference(resources_id),
3507 };
3508 let page_id = doc.add_object(Object::Dictionary(page_dict));
3509
3510 let pages_dict = dictionary! {
3511 "Type" => "Pages",
3512 "Kids" => vec![Object::Reference(page_id)],
3513 "Count" => 1_i64,
3514 };
3515 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
3516
3517 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
3518 d.set("Parent", Object::Reference(pages_id));
3519 }
3520
3521 let catalog = dictionary! {
3522 "Type" => "Catalog",
3523 "Pages" => Object::Reference(pages_id),
3524 };
3525 let catalog_id = doc.add_object(Object::Dictionary(catalog));
3526 doc.trailer.set("Root", Object::Reference(catalog_id));
3527
3528 doc
3529 }
3530
3531 #[test]
3537 fn ligature_office_golden_ffi() {
3538 let doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <6F016365> Tj ET", "ffi");
3540 let blocks = extract_text(&doc);
3541 assert_eq!(blocks.len(), 1);
3542 assert_eq!(blocks[0].text, "office");
3543 }
3544
3545 fn make_doc_with_tounicode_cmap(content: &[u8], src: u8, dst: u32) -> Document {
3550 let mut doc = Document::with_version("1.7");
3551
3552 let content_stream = Stream::new(dictionary! {}, content.to_vec());
3553 let content_id = doc.add_object(Object::Stream(content_stream));
3554
3555 let cmap_text = format!(
3557 "/CIDInit /ProcSet findresource begin\n\
3558 12 dict begin\n\
3559 begincmap\n\
3560 /CMapType 2 def\n\
3561 1 beginbfchar\n\
3562 <{:02X}> <{:04X}>\n\
3563 endbfchar\n\
3564 endcmap CMapName currentdict /CMap defineresource pop end end",
3565 src, dst
3566 );
3567 let cmap_stream = Stream::new(dictionary! {}, cmap_text.into_bytes());
3568 let cmap_id = doc.add_object(Object::Stream(cmap_stream));
3569
3570 let font = dictionary! {
3571 "Type" => "Font",
3572 "Subtype" => "Type1",
3573 "BaseFont" => "Helvetica",
3574 "Encoding" => "WinAnsiEncoding",
3575 "ToUnicode" => Object::Reference(cmap_id),
3576 };
3577 let font_id = doc.add_object(Object::Dictionary(font));
3578
3579 let resources = dictionary! {
3580 "Font" => dictionary! { "F1" => Object::Reference(font_id) },
3581 };
3582 let resources_id = doc.add_object(Object::Dictionary(resources));
3583
3584 let page_dict = dictionary! {
3585 "Type" => "Page",
3586 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
3587 "Contents" => Object::Reference(content_id),
3588 "Resources" => Object::Reference(resources_id),
3589 };
3590 let page_id = doc.add_object(Object::Dictionary(page_dict));
3591
3592 let pages_dict = dictionary! {
3593 "Type" => "Pages",
3594 "Kids" => vec![Object::Reference(page_id)],
3595 "Count" => 1_i64,
3596 };
3597 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
3598
3599 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
3600 d.set("Parent", Object::Reference(pages_id));
3601 }
3602
3603 let catalog = dictionary! {
3604 "Type" => "Catalog",
3605 "Pages" => Object::Reference(pages_id),
3606 };
3607 let catalog_id = doc.add_object(Object::Dictionary(catalog));
3608 doc.trailer.set("Root", Object::Reference(catalog_id));
3609
3610 doc
3611 }
3612
3613 #[test]
3618 fn ligature_positioned_chars_split_bbox() {
3619 let doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <01> Tj ET", "ffi");
3620 let chars = extract_positioned_chars(&doc, 1).unwrap();
3621 assert_eq!(chars.len(), 3, "ffi → 3 chars");
3622 assert_eq!(chars[0].ch, 'f');
3623 assert_eq!(chars[1].ch, 'f');
3624 assert_eq!(chars[2].ch, 'i');
3625 assert!(chars[0].bbox[2] <= chars[1].bbox[0] + 1e-9);
3627 assert!(chars[1].bbox[2] <= chars[2].bbox[0] + 1e-9);
3628 let total = chars[2].bbox[2] - chars[0].bbox[0];
3630 let expected = 12.0 * APPROX_CHAR_WIDTH;
3631 assert!((total - expected).abs() < 1e-6);
3632 }
3633
3634 #[test]
3644 fn positioned_chars_match_extracted_text_for_armenian_ligature() {
3645 let doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <01> Tj ET", "uniFB13");
3648 let blocks = extract_text(&doc);
3649 let chars = extract_positioned_chars(&doc, 1).unwrap();
3650 assert_eq!(blocks.len(), 1);
3651 assert_eq!(
3653 blocks[0].text.chars().count(),
3654 chars.len(),
3655 "PositionedChar count drifted from extract_text() char count \
3656 (issue #1359 problem 1): blocks[0].text = {:?}, chars = {:?}",
3657 blocks[0].text,
3658 chars.iter().map(|c| c.ch).collect::<String>(),
3659 );
3660 let chars_str: String = chars.iter().map(|c| c.ch).collect();
3662 assert_eq!(blocks[0].text, chars_str);
3663 let total = chars.last().unwrap().bbox[2] - chars.first().unwrap().bbox[0];
3665 let expected = 12.0 * APPROX_CHAR_WIDTH;
3666 assert!((total - expected).abs() < 1e-6);
3667 }
3668
3669 #[test]
3673 fn hebrew_presentation_form_extracts_single_codepoint_nfkd_mapping() {
3674 let doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <01> Tj ET", "uniFB21");
3676 let blocks = extract_text(&doc);
3677 assert_eq!(blocks.len(), 1);
3678 assert_eq!(blocks[0].text, "\u{05D0}");
3679 assert_eq!(
3680 decompose_glyph_to_string('\u{FB21}').as_deref(),
3681 Some("\u{05D0}")
3682 );
3683 }
3684
3685 #[test]
3689 fn positioned_chars_match_extracted_text_for_hebrew_presentation_form() {
3690 let doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <01> Tj ET", "uniFB21");
3691 let blocks = extract_text(&doc);
3692 let chars = extract_positioned_chars(&doc, 1).unwrap();
3693 let chars_str: String = chars.iter().map(|c| c.ch).collect();
3694
3695 assert_eq!(blocks.len(), 1);
3696 assert_eq!(blocks[0].text, "\u{05D0}");
3697 assert_eq!(chars_str, blocks[0].text);
3698 assert_eq!(blocks[0].text.chars().count(), chars.len());
3699 }
3700
3701 #[test]
3708 fn tj_block_width_matches_rendered_glyph_count() {
3709 let doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <01> Tj ET", "ffi");
3710 let blocks = extract_text(&doc);
3711 assert_eq!(blocks.len(), 1);
3712 assert_eq!(blocks[0].text, "ffi");
3713 let width = blocks[0].bbox[2] - blocks[0].bbox[0];
3714 let expected = 12.0 * APPROX_CHAR_WIDTH;
3715 assert!(
3716 (width - expected).abs() < 1e-6,
3717 "Tj bbox width {} != one glyph's char_w {} (issue #1359 problem 2)",
3718 width,
3719 expected,
3720 );
3721 }
3722
3723 #[test]
3729 fn tj_and_tj_array_bbox_widths_agree_for_ligature() {
3730 let tj_doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <01> Tj ET", "ffi");
3731 let tj_blocks = extract_text(&tj_doc);
3732 let tj_doc_arr = make_doc_with_ligature_font(b"BT /F1 12 Tf [<01>] TJ ET", "ffi");
3733 let tj_arr_blocks = extract_text(&tj_doc_arr);
3734 assert_eq!(tj_blocks.len(), 1);
3735 assert_eq!(tj_arr_blocks.len(), 1);
3736 let tj_w = tj_blocks[0].bbox[2] - tj_blocks[0].bbox[0];
3737 let tj_arr_w = tj_arr_blocks[0].bbox[2] - tj_arr_blocks[0].bbox[0];
3738 assert!(
3739 (tj_w - tj_arr_w).abs() < 1e-6,
3740 "Tj bbox width {} disagrees with TJ bbox width {} for the same \
3741 single-glyph ligature input (issue #1359 problem 2)",
3742 tj_w,
3743 tj_arr_w,
3744 );
3745 }
3746
3747 fn make_doc_with_font(content: &[u8], base_font: &str, desc_flags: Option<u32>) -> Document {
3752 let mut doc = Document::with_version("1.7");
3753
3754 let mut font_dict = dictionary! {
3755 "Type" => "Font",
3756 "Subtype" => "Type1",
3757 "BaseFont" => Object::Name(base_font.as_bytes().to_vec()),
3758 };
3759 if let Some(flags) = desc_flags {
3760 let descriptor = dictionary! {
3761 "Type" => "FontDescriptor",
3762 "FontName" => Object::Name(base_font.as_bytes().to_vec()),
3763 "Flags" => Object::Integer(flags as i64),
3764 };
3765 let desc_id = doc.add_object(Object::Dictionary(descriptor));
3766 font_dict.set("FontDescriptor", Object::Reference(desc_id));
3767 }
3768 let font_id = doc.add_object(Object::Dictionary(font_dict));
3769
3770 let resources = dictionary! {
3771 "Font" => dictionary! { "F1" => Object::Reference(font_id) },
3772 };
3773 let resources_id = doc.add_object(Object::Dictionary(resources));
3774
3775 let content_stream = Stream::new(dictionary! {}, content.to_vec());
3776 let content_id = doc.add_object(Object::Stream(content_stream));
3777
3778 let page_dict = dictionary! {
3779 "Type" => "Page",
3780 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
3781 "Contents" => Object::Reference(content_id),
3782 "Resources" => Object::Reference(resources_id),
3783 };
3784 let page_id = doc.add_object(Object::Dictionary(page_dict));
3785
3786 let pages_dict = dictionary! {
3787 "Type" => "Pages",
3788 "Kids" => vec![Object::Reference(page_id)],
3789 "Count" => 1_i64,
3790 };
3791 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
3792
3793 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
3794 d.set("Parent", Object::Reference(pages_id));
3795 }
3796
3797 let catalog = dictionary! {
3798 "Type" => "Catalog",
3799 "Pages" => Object::Reference(pages_id),
3800 };
3801 let catalog_id = doc.add_object(Object::Dictionary(catalog));
3802 doc.trailer.set("Root", Object::Reference(catalog_id));
3803
3804 doc
3805 }
3806
3807 #[test]
3808 fn g1_tj_carries_basefont_metadata() {
3809 let doc = make_doc_with_font(b"BT /F1 12 Tf (Hello) Tj ET", "Helvetica", None);
3810 let blocks = extract_text(&doc);
3811 assert_eq!(blocks.len(), 1);
3812 assert_eq!(blocks[0].text, "Hello");
3813 assert_eq!(blocks[0].base_font.as_deref(), Some("Helvetica"));
3814 assert!(!blocks[0].is_bold);
3815 assert!(!blocks[0].is_italic);
3816 }
3817
3818 #[test]
3819 fn g1_tj_array_carries_basefont_metadata() {
3820 let doc = make_doc_with_font(b"BT /F1 12 Tf [(He) -100 (llo)] TJ ET", "Helvetica", None);
3821 let blocks = extract_text(&doc);
3822 assert_eq!(blocks.len(), 1);
3823 assert_eq!(blocks[0].text, "Hello");
3824 assert_eq!(blocks[0].base_font.as_deref(), Some("Helvetica"));
3825 }
3826
3827 #[test]
3828 fn g1_bold_inferred_from_basefont_name() {
3829 let doc = make_doc_with_font(b"BT /F1 12 Tf (Bold) Tj ET", "Helvetica-Bold", None);
3830 let blocks = extract_text(&doc);
3831 assert_eq!(blocks.len(), 1);
3832 assert!(blocks[0].is_bold);
3833 assert!(!blocks[0].is_italic);
3834 assert_eq!(blocks[0].base_font.as_deref(), Some("Helvetica-Bold"));
3835 }
3836
3837 #[test]
3838 fn g1_italic_inferred_from_basefont_name() {
3839 let doc = make_doc_with_font(b"BT /F1 12 Tf (Slanted) Tj ET", "Times-Italic", None);
3840 let blocks = extract_text(&doc);
3841 assert_eq!(blocks.len(), 1);
3842 assert!(blocks[0].is_italic);
3843 assert!(!blocks[0].is_bold);
3844 }
3845
3846 #[test]
3847 fn g1_subset_prefix_stripped_from_basefont() {
3848 let doc = make_doc_with_font(
3849 b"BT /F1 12 Tf (Subset) Tj ET",
3850 "ABCDEF+Helvetica-Bold",
3851 None,
3852 );
3853 let blocks = extract_text(&doc);
3854 assert_eq!(blocks.len(), 1);
3855 assert_eq!(blocks[0].base_font.as_deref(), Some("Helvetica-Bold"));
3856 assert!(blocks[0].is_bold);
3857 }
3858
3859 #[test]
3860 fn g1_italic_inferred_from_descriptor_flag() {
3861 let doc = make_doc_with_font(b"BT /F1 12 Tf (X) Tj ET", "Custom-Roman", Some(0x40));
3863 let blocks = extract_text(&doc);
3864 assert_eq!(blocks.len(), 1);
3865 assert!(blocks[0].is_italic);
3866 assert_eq!(blocks[0].base_font.as_deref(), Some("Custom-Roman"));
3868 }
3869
3870 #[test]
3871 fn g1_rg_color_propagates_to_block() {
3872 let doc = make_doc_with_font(b"1 0 0 rg BT /F1 12 Tf (Red) Tj ET", "Helvetica", None);
3874 let blocks = extract_text(&doc);
3875 assert_eq!(blocks.len(), 1);
3876 assert_eq!(blocks[0].color, Some([255, 0, 0, 255]));
3877 }
3878
3879 #[test]
3880 fn g1_gray_color_via_g_operator() {
3881 let doc = make_doc_with_font(b"0.5 g BT /F1 12 Tf (Gray) Tj ET", "Helvetica", None);
3883 let blocks = extract_text(&doc);
3884 assert_eq!(blocks.len(), 1);
3885 let c = blocks[0].color.expect("expected gray color");
3886 assert_eq!(c[3], 255);
3888 assert_eq!(c[0], c[1]);
3889 assert_eq!(c[1], c[2]);
3890 assert!((c[0] as i32 - 128).abs() <= 1);
3891 }
3892
3893 #[test]
3894 fn g1_default_color_is_black_for_compliant_initial_state() {
3895 let doc = make_doc_with_font(b"BT /F1 12 Tf (Default) Tj ET", "Helvetica", None);
3897 let blocks = extract_text(&doc);
3898 assert_eq!(blocks.len(), 1);
3899 assert_eq!(blocks[0].color, Some([0, 0, 0, 255]));
3900 }
3901
3902 #[test]
3903 fn g1_cmyk_color_via_k_operator() {
3904 let doc = make_doc_with_font(b"1 0 0 0 k BT /F1 12 Tf (Cyan) Tj ET", "Helvetica", None);
3906 let blocks = extract_text(&doc);
3907 assert_eq!(blocks.len(), 1);
3908 let c = blocks[0].color.expect("k should set color");
3909 assert_eq!(c, [0, 255, 255, 255]);
3910 }
3911
3912 #[test]
3913 fn g1_tj_and_tj_array_emit_same_metadata() {
3914 let tj_doc =
3916 make_doc_with_font(b"1 0 0 rg BT /F1 12 Tf (Hi) Tj ET", "Helvetica-Bold", None);
3917 let tj_array_doc = make_doc_with_font(
3918 b"1 0 0 rg BT /F1 12 Tf [(Hi)] TJ ET",
3919 "Helvetica-Bold",
3920 None,
3921 );
3922 let a = extract_text(&tj_doc);
3923 let b = extract_text(&tj_array_doc);
3924 assert_eq!(a.len(), 1);
3925 assert_eq!(b.len(), 1);
3926 assert_eq!(a[0].base_font, b[0].base_font);
3927 assert_eq!(a[0].is_bold, b[0].is_bold);
3928 assert_eq!(a[0].is_italic, b[0].is_italic);
3929 assert_eq!(a[0].color, b[0].color);
3930 }
3931
3932 #[test]
3933 fn g1_color_restored_across_q_capital_q() {
3934 let doc = make_doc_with_font(
3936 b"q 1 0 0 rg Q BT /F1 12 Tf (Outside) Tj ET",
3937 "Helvetica",
3938 None,
3939 );
3940 let blocks = extract_text(&doc);
3941 assert_eq!(blocks.len(), 1);
3942 assert_eq!(blocks[0].color, Some([0, 0, 0, 255]));
3944 }
3945
3946 #[test]
3947 fn g1_no_font_resource_means_no_base_font() {
3948 let doc = make_doc_with_text(b"BT /F1 12 Tf (NoFont) Tj ET");
3951 let blocks = extract_text(&doc);
3952 assert_eq!(blocks.len(), 1);
3953 assert_eq!(blocks[0].base_font, None);
3954 assert!(!blocks[0].is_bold);
3955 assert!(!blocks[0].is_italic);
3956 assert_eq!(blocks[0].color, Some([0, 0, 0, 255]));
3958 assert_eq!(blocks[0].font_name, "F1");
3960 }
3961
3962 #[test]
3963 fn g1_unknown_color_space_marks_color_none() {
3964 let doc = make_doc_with_font(
3966 b"/Cs1 cs 0.5 0.5 0.5 0.5 0.5 scn BT /F1 12 Tf (X) Tj ET",
3967 "Helvetica",
3968 None,
3969 );
3970 let blocks = extract_text(&doc);
3971 assert_eq!(blocks.len(), 1);
3972 assert_eq!(blocks[0].color, None);
3973 }
3974
3975 fn make_doc_with_widths(content: &[u8], first_char: i64, widths: Vec<i64>) -> Document {
3979 let mut doc = Document::with_version("1.7");
3980
3981 let widths_obj = Object::Array(widths.iter().map(|w| Object::Integer(*w)).collect());
3982
3983 let font_dict = dictionary! {
3984 "Type" => "Font",
3985 "Subtype" => "Type1",
3986 "BaseFont" => Object::Name(b"TestFont".to_vec()),
3987 "FirstChar" => Object::Integer(first_char),
3988 "LastChar" => Object::Integer(first_char + widths.len() as i64 - 1),
3989 "Widths" => widths_obj,
3990 };
3991 let font_id = doc.add_object(Object::Dictionary(font_dict));
3992
3993 let resources = dictionary! {
3994 "Font" => dictionary! { "F1" => Object::Reference(font_id) },
3995 };
3996 let resources_id = doc.add_object(Object::Dictionary(resources));
3997
3998 let content_stream = Stream::new(dictionary! {}, content.to_vec());
3999 let content_id = doc.add_object(Object::Stream(content_stream));
4000
4001 let page_dict = dictionary! {
4002 "Type" => "Page",
4003 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
4004 "Contents" => Object::Reference(content_id),
4005 "Resources" => Object::Reference(resources_id),
4006 };
4007 let page_id = doc.add_object(Object::Dictionary(page_dict));
4008
4009 let pages_dict = dictionary! {
4010 "Type" => "Pages",
4011 "Kids" => vec![Object::Reference(page_id)],
4012 "Count" => 1_i64,
4013 };
4014 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
4015
4016 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
4017 d.set("Parent", Object::Reference(pages_id));
4018 }
4019
4020 let catalog = dictionary! {
4021 "Type" => "Catalog",
4022 "Pages" => Object::Reference(pages_id),
4023 };
4024 let catalog_id = doc.add_object(Object::Dictionary(catalog));
4025 doc.trailer.set("Root", Object::Reference(catalog_id));
4026
4027 doc
4028 }
4029
4030 #[test]
4031 fn g2_no_widths_gives_estimate_source() {
4032 let doc = make_doc_with_font(b"BT /F1 10 Tf (A) Tj ET", "Helvetica", None);
4034 let blocks = extract_text(&doc);
4035 assert_eq!(blocks.len(), 1);
4036 assert_eq!(blocks[0].width_source, WidthSource::Estimate);
4037 }
4038
4039 #[test]
4040 fn g2_widths_array_gives_metric_source() {
4041 let doc = make_doc_with_widths(b"BT /F1 10 Tf (A) Tj ET", 65, vec![722]);
4044 let blocks = extract_text(&doc);
4045 assert_eq!(blocks.len(), 1);
4046 assert_eq!(blocks[0].width_source, WidthSource::Metric);
4047 assert_eq!(blocks[0].char_bounds.len(), 1);
4049 let [x0, _y0, x1, _y1] = blocks[0].char_bounds[0];
4050 let advance = x1 - x0;
4052 assert!((advance - 7.22).abs() < 0.01, "advance={advance:.4}");
4053 }
4054
4055 #[test]
4056 fn g2_monospace_uniform_char_bounds() {
4057 let widths: Vec<i64> = vec![600, 600, 600, 600, 600];
4060 let doc = make_doc_with_widths(b"BT /F1 12 Tf (ABCDE) Tj ET", 65, widths);
4061 let blocks = extract_text(&doc);
4062 assert_eq!(blocks.len(), 1);
4063 assert_eq!(blocks[0].width_source, WidthSource::Metric);
4064 assert_eq!(blocks[0].char_bounds.len(), 5);
4065 let expected_adv = 600.0 / 1000.0 * 12.0; for i in 0..5 {
4067 let [x0, _, x1, _] = blocks[0].char_bounds[i];
4068 let adv = x1 - x0;
4069 assert!(
4070 (adv - expected_adv).abs() < 0.01,
4071 "char {i}: advance={adv:.4} expected={expected_adv:.4}"
4072 );
4073 }
4074 }
4075
4076 #[test]
4077 fn g2_proportional_char_bounds_are_contiguous() {
4078 let widths: Vec<i64> = vec![722, 667, 667];
4080 let doc = make_doc_with_widths(b"BT /F1 10 Tf (ABC) Tj ET", 65, widths);
4081 let blocks = extract_text(&doc);
4082 assert_eq!(blocks.len(), 1);
4083 assert_eq!(blocks[0].char_bounds.len(), 3);
4084 for i in 0..2 {
4087 let x1_prev = blocks[0].char_bounds[i][2];
4088 let x0_next = blocks[0].char_bounds[i + 1][0];
4089 assert!(
4090 (x1_prev - x0_next).abs() < 0.001,
4091 "gap between char {i} and {}: {:.4}",
4092 i + 1,
4093 x0_next - x1_prev
4094 );
4095 }
4096 }
4097
4098 #[test]
4099 fn g2_tj_kerning_shifts_subsequent_bounds() {
4100 let widths: Vec<i64> = vec![722, 667];
4107 let doc = make_doc_with_widths(b"BT /F1 10 Tf [(A) -200 (B)] TJ ET", 65, widths);
4108 let blocks = extract_text(&doc);
4109 assert_eq!(blocks.len(), 1);
4110 assert_eq!(blocks[0].char_bounds.len(), 2);
4111 let [x0_a, _, x1_a, _] = blocks[0].char_bounds[0];
4112 let [x0_b, _, _, _] = blocks[0].char_bounds[1];
4113 assert!(
4115 (x1_a - x0_a - 7.22).abs() < 0.01,
4116 "A advance={:.4}",
4117 x1_a - x0_a
4118 );
4119 assert!(x0_b > x1_a, "B should start after A ends");
4121 let gap = x0_b - x1_a;
4122 assert!(
4123 (gap - 2.0).abs() < 0.01,
4124 "kerning gap={gap:.4} expected=2.0"
4125 );
4126 }
4127
4128 #[test]
4129 fn g2_subset_font_prefix_does_not_affect_widths() {
4130 let widths: Vec<i64> = vec![722];
4132 let doc = make_doc_with_widths(b"BT /F1 10 Tf (A) Tj ET", 65, widths);
4133 let blocks = extract_text(&doc);
4134 assert_eq!(blocks.len(), 1);
4135 assert_eq!(blocks[0].width_source, WidthSource::Metric);
4137 assert_eq!(blocks[0].char_bounds.len(), 1);
4138 }
4139
4140 #[test]
4141 fn g2_char_bounds_count_equals_glyph_count_not_unicode_len() {
4142 let doc = make_doc_with_font(b"BT /F1 12 Tf (A) Tj ET", "TestFont", None);
4146 let blocks = extract_text(&doc);
4147 assert_eq!(blocks.len(), 1);
4148 assert_eq!(blocks[0].char_bounds.len(), 1);
4150 }
4151}