1use crate::content_editor::{as_number, concat_cm, offset_text_line, ContentEditor};
13use crate::encoding_utils::build_font_encoding;
14use crate::error::{ManipError, Result};
15use lopdf::content::Operation;
16use lopdf::{Document, Object, ObjectId};
17use pdf_font::cmap::{BfString, CMap};
18use std::collections::HashMap;
19use std::ops::Range;
20
21const IDENTITY: [f64; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
22
23#[derive(Debug, Clone)]
25pub struct TextRun {
26 pub text: String,
28 pub ops_range: Range<usize>,
30 pub font_name: String,
32 pub font_size: f64,
34 pub x: f64,
36 pub y: f64,
38 pub width: f64,
40}
41
42#[derive(Debug, Clone)]
47pub struct FontMap {
48 pub(crate) fonts: HashMap<String, FontInfo>,
49}
50
51#[derive(Debug, Clone)]
52pub(crate) struct FontInfo {
53 pub(crate) to_unicode: Option<CMap>,
54 pub(crate) encoding: FontEncoding,
55 pub(crate) widths: GlyphWidths,
56 pub(crate) differences_encoding: HashMap<u8, char>,
59 pub(crate) is_subset: bool,
63 pub(crate) is_symbolic: bool,
69 pub(crate) forward_encoding: HashMap<u8, char>,
75 pub(crate) cmap_inverse: HashMap<u32, char>,
80}
81
82#[derive(Debug, Clone, PartialEq)]
84pub(crate) enum FontEncoding {
85 Builtin,
87 IdentityH,
89 CustomCMap,
91}
92
93#[derive(Debug, Clone)]
95pub(crate) struct GlyphWidths {
96 first_char: u32,
97 widths: Vec<f64>,
98 default_width: f64,
99}
100
101impl GlyphWidths {
102 fn empty() -> Self {
103 Self {
104 first_char: 0,
105 widths: Vec::new(),
106 default_width: 1000.0,
107 }
108 }
109
110 fn width_for_code(&self, code: u32) -> f64 {
112 let idx = code.checked_sub(self.first_char).and_then(|i| {
113 let i = i as usize;
114 if i < self.widths.len() {
115 Some(i)
116 } else {
117 None
118 }
119 });
120 match idx {
121 Some(i) => self.widths[i],
122 None => self.default_width,
123 }
124 }
125}
126
127impl FontMap {
128 pub fn from_page(doc: &Document, page_num: u32) -> Result<Self> {
130 let pages = doc.get_pages();
131 let total = pages.len() as u32;
132 let &page_id = pages.get(&page_num).ok_or(ManipError::PageOutOfRange(
133 page_num as usize,
134 total as usize,
135 ))?;
136
137 let mut fonts = HashMap::new();
138 let font_dict = get_page_font_dict(doc, page_id);
139
140 for (name, font_ref) in &font_dict {
141 let info = build_font_info(doc, font_ref);
142 fonts.insert(name.clone(), info);
143 }
144
145 Ok(Self { fonts })
146 }
147
148 pub fn empty() -> Self {
150 Self {
151 fonts: HashMap::new(),
152 }
153 }
154
155 pub fn from_xobject_stream(doc: &Document, stream_id: ObjectId, page_fonts: &FontMap) -> Self {
162 let mut fonts = page_fonts.fonts.clone();
164
165 let stream_dict = match doc.get_object(stream_id) {
166 Ok(Object::Stream(ref s)) => s.dict.clone(),
167 _ => return Self { fonts },
168 };
169
170 let resources = match stream_dict.get(b"Resources") {
171 Ok(Object::Dictionary(ref d)) => d.clone(),
172 Ok(Object::Reference(id)) => match doc.get_object(*id) {
173 Ok(Object::Dictionary(ref d)) => d.clone(),
174 _ => return Self { fonts },
175 },
176 _ => return Self { fonts },
177 };
178
179 let font_dict = match resources.get(b"Font") {
180 Ok(Object::Dictionary(ref d)) => d.clone(),
181 Ok(Object::Reference(id)) => match doc.get_object(*id) {
182 Ok(Object::Dictionary(ref d)) => d.clone(),
183 _ => return Self { fonts },
184 },
185 _ => return Self { fonts },
186 };
187
188 for (key, value) in font_dict.iter() {
189 let name = String::from_utf8_lossy(key).to_string();
190 if let Object::Reference(id) = value {
191 let info = build_font_info(doc, id);
194 fonts.insert(name, info);
195 }
196 }
197
198 Self { fonts }
199 }
200
201 pub fn decode_string(&self, font_name: &str, bytes: &[u8]) -> String {
203 let info = match self.fonts.get(font_name) {
204 Some(info) => info,
205 None => return decode_pdf_string_fallback(bytes),
206 };
207
208 if let Some(ref cmap) = info.to_unicode {
209 return decode_with_cmap(bytes, cmap, &info.encoding);
210 }
211
212 if !info.forward_encoding.is_empty() {
218 return bytes
219 .iter()
220 .map(|&b| info.forward_encoding.get(&b).copied().unwrap_or(b as char))
221 .collect();
222 }
223
224 if !info.cmap_inverse.is_empty() {
228 return bytes
229 .iter()
230 .map(|&b| {
231 info.cmap_inverse
232 .get(&(b as u32))
233 .copied()
234 .unwrap_or(b as char)
235 })
236 .collect();
237 }
238
239 decode_pdf_string_fallback(bytes)
240 }
241
242 fn char_width(&self, font_name: &str, code: u32) -> f64 {
244 self.fonts
245 .get(font_name)
246 .map(|info| info.widths.width_for_code(code))
247 .unwrap_or(1000.0)
248 }
249
250 pub(crate) fn text_width_em(&self, font: &str, text: &str) -> f64 {
257 let Some(info) = self.fonts.get(font) else {
258 return text.chars().count() as f64 * 0.5;
259 };
260 let reverse = self.build_reverse_map(font);
261 text.chars()
262 .map(|ch| {
263 reverse
264 .get(&ch)
265 .map(|&code| info.widths.width_for_code(code) / 1000.0)
266 .unwrap_or(0.5)
267 })
268 .sum()
269 }
270
271 pub(crate) fn is_cid_font(&self, font_name: &str) -> bool {
273 self.fonts
274 .get(font_name)
275 .map(|info| {
276 matches!(
277 info.encoding,
278 FontEncoding::IdentityH | FontEncoding::CustomCMap
279 )
280 })
281 .unwrap_or(false)
282 }
283
284 pub(crate) fn is_subset_font(&self, font_name: &str) -> bool {
290 self.fonts
291 .get(font_name)
292 .map(|info| info.is_subset)
293 .unwrap_or(false)
294 }
295
296 pub(crate) fn is_symbolic_font(&self, font_name: &str) -> bool {
303 self.fonts
304 .get(font_name)
305 .map(|info| info.is_symbolic)
306 .unwrap_or(false)
307 }
308
309 pub(crate) fn cid_byte_offset_for_chars(
320 &self,
321 font_name: &str,
322 orig_bytes: &[u8],
323 char_count: usize,
324 ) -> usize {
325 if char_count == 0 {
326 return 0;
327 }
328 let info = match self.fonts.get(font_name) {
329 Some(info) => info,
330 None => return char_count * 2,
331 };
332 let cmap = match info.to_unicode {
333 Some(ref c) => c,
334 None => return char_count * 2,
335 };
336 let mut chars_seen = 0;
337 let mut i = 0;
338 while i + 1 < orig_bytes.len() {
339 if chars_seen >= char_count {
340 break;
341 }
342 let code = u32::from(orig_bytes[i]) << 8 | u32::from(orig_bytes[i + 1]);
343 let n = match cmap.lookup_bf_string(code) {
344 Some(BfString::Char(_)) => 1,
345 Some(BfString::String(ref s)) => s.chars().count().max(1),
346 None => 1,
347 };
348 chars_seen += n;
349 i += 2;
350 }
351 i
352 }
353
354 pub fn build_reverse_map(&self, font_name: &str) -> std::collections::HashMap<char, u32> {
363 let mut reverse = std::collections::HashMap::new();
364 let info = match self.fonts.get(font_name) {
365 Some(info) => info,
366 None => return reverse,
367 };
368
369 if let Some(ref cmap) = info.to_unicode {
370 let max_code: u32 = if matches!(info.encoding, FontEncoding::Builtin) {
371 0xFF
372 } else {
373 0xFFFF
374 };
375 for code in 0..=max_code {
376 if let Some(BfString::Char(ch)) = cmap.lookup_bf_string(code) {
377 reverse.entry(ch).or_insert(code);
378 }
379 }
380 return reverse;
381 }
382
383 if !info.differences_encoding.is_empty() && matches!(info.encoding, FontEncoding::Builtin) {
386 for (&code, &ch) in &info.differences_encoding {
387 reverse.entry(ch).or_insert(u32::from(code));
388 }
389 }
390
391 reverse
392 }
393}
394
395pub fn extract_text_runs(editor: &ContentEditor, fonts: &FontMap) -> Vec<TextRun> {
397 let ops = editor.operations();
398 let mut runs = Vec::new();
399 let mut state = TextState::default();
400
401 for (idx, op) in ops.iter().enumerate() {
402 match op.operator.as_str() {
403 "q" => {
404 state.gs_stack.push(SavedGS { ctm: state.ctm });
405 }
406 "Q" => {
407 if let Some(saved) = state.gs_stack.pop() {
408 state.ctm = saved.ctm;
409 }
410 }
411 "cm" => {
412 if let Some(m) = extract_matrix(&op.operands) {
413 state.ctm = concat_cm(&m, &state.ctm);
414 }
415 }
416 "BT" => {
417 state.in_text = true;
418 state.tm = IDENTITY;
419 state.tlm = IDENTITY;
420 }
421 "ET" => {
422 state.in_text = false;
423 }
424 "Tf"
425 if op.operands.len() >= 2 => {
426 if let Object::Name(ref name) = op.operands[0] {
427 state.font_name = String::from_utf8_lossy(name).to_string();
428 }
429 if let Some(size) = as_number(&op.operands[1]) {
430 state.font_size = size;
431 }
432 }
433 "Tc" => {
434 if let Some(v) = op.operands.first().and_then(as_number) {
435 state.tc = v;
436 }
437 }
438 "Tw" => {
439 if let Some(v) = op.operands.first().and_then(as_number) {
440 state.tw = v;
441 }
442 }
443 "Tz" => {
444 if let Some(v) = op.operands.first().and_then(as_number) {
445 state.th = v;
446 }
447 }
448 "TL" => {
449 if let Some(v) = op.operands.first().and_then(as_number) {
450 state.tl = v;
451 }
452 }
453 "Ts" => {
454 if let Some(v) = op.operands.first().and_then(as_number) {
455 state.ts = v;
456 }
457 }
458 "Td"
459 if op.operands.len() >= 2 => {
460 let tx = as_number(&op.operands[0]).unwrap_or(0.0);
461 let ty = as_number(&op.operands[1]).unwrap_or(0.0);
462 let new_tlm = offset_text_line(&state.tlm, tx, ty);
463 state.tlm = new_tlm;
464 state.tm = new_tlm;
465 }
466 "TD"
467 if op.operands.len() >= 2 => {
468 let tx = as_number(&op.operands[0]).unwrap_or(0.0);
469 let ty = as_number(&op.operands[1]).unwrap_or(0.0);
470 state.tl = -ty;
471 let new_tlm = offset_text_line(&state.tlm, tx, ty);
472 state.tlm = new_tlm;
473 state.tm = new_tlm;
474 }
475 "Tm" => {
476 if let Some(m) = extract_matrix(&op.operands) {
477 state.tm = m;
478 state.tlm = m;
479 }
480 }
481 "T*" => {
482 let new_tlm = offset_text_line(&state.tlm, 0.0, -state.tl);
483 state.tlm = new_tlm;
484 state.tm = new_tlm;
485 }
486 "Tj" => {
487 if let Some(run) = process_tj(idx, op, &mut state, fonts) {
488 runs.push(run);
489 }
490 }
491 "TJ" => {
492 if let Some(run) = process_tj_array(idx, op, &mut state, fonts) {
493 runs.push(run);
494 }
495 }
496 "'" => {
497 let new_tlm = offset_text_line(&state.tlm, 0.0, -state.tl);
499 state.tlm = new_tlm;
500 state.tm = new_tlm;
501
502 if let Some(run) = process_tj(idx, op, &mut state, fonts) {
503 runs.push(run);
504 }
505 }
506 "\""
507 if op.operands.len() >= 3 => {
509 if let Some(tw) = as_number(&op.operands[0]) {
510 state.tw = tw;
511 }
512 if let Some(tc) = as_number(&op.operands[1]) {
513 state.tc = tc;
514 }
515 let new_tlm =
516 offset_text_line(&state.tlm, 0.0, -state.tl);
517 state.tlm = new_tlm;
518 state.tm = new_tlm;
519
520 if let Some(Object::String(ref bytes, _)) = op.operands.get(2) {
522 let text = fonts.decode_string(&state.font_name, bytes);
523 if !text.is_empty() {
524 let x = compute_x(&state);
525 let y = compute_y(&state);
526 let width = compute_text_width(bytes, &state, fonts);
527 advance_text_position(bytes, &mut state, fonts);
528 runs.push(TextRun {
529 text,
530 ops_range: idx..idx + 1,
531 font_name: state.font_name.clone(),
532 font_size: state.font_size,
533 x,
534 y,
535 width,
536 });
537 }
538 }
539 }
540 _ => {}
541 }
542 }
543
544 runs
545}
546
547pub fn extract_page_text_runs(doc: &Document, page_num: u32) -> Result<Vec<TextRun>> {
549 let editor = crate::content_editor::editor_for_page(doc, page_num)?;
550 let fonts = FontMap::from_page(doc, page_num)?;
551 Ok(extract_text_runs(&editor, &fonts))
552}
553
554#[derive(Debug, Clone)]
559struct TextState {
560 ctm: [f64; 6],
561 tm: [f64; 6],
562 tlm: [f64; 6],
563 font_name: String,
564 font_size: f64,
565 tc: f64,
566 tw: f64,
567 th: f64,
568 tl: f64,
569 ts: f64,
570 in_text: bool,
571 gs_stack: Vec<SavedGS>,
572}
573
574#[derive(Debug, Clone)]
575struct SavedGS {
576 ctm: [f64; 6],
577}
578
579impl Default for TextState {
580 fn default() -> Self {
581 Self {
582 ctm: IDENTITY,
583 tm: IDENTITY,
584 tlm: IDENTITY,
585 font_name: String::new(),
586 font_size: 12.0,
587 tc: 0.0,
588 tw: 0.0,
589 th: 100.0,
590 tl: 0.0,
591 ts: 0.0,
592 in_text: false,
593 gs_stack: Vec::new(),
594 }
595 }
596}
597
598fn process_tj(
603 idx: usize,
604 op: &Operation,
605 state: &mut TextState,
606 fonts: &FontMap,
607) -> Option<TextRun> {
608 let bytes = match op.operands.first() {
609 Some(Object::String(ref b, _)) => b,
610 _ => return None,
611 };
612
613 let text = fonts.decode_string(&state.font_name, bytes);
614 if text.is_empty() {
615 return None;
616 }
617
618 let x = compute_x(state);
619 let y = compute_y(state);
620 let width = compute_text_width(bytes, state, fonts);
621 advance_text_position(bytes, state, fonts);
622
623 Some(TextRun {
624 text,
625 ops_range: idx..idx + 1,
626 font_name: state.font_name.clone(),
627 font_size: state.font_size,
628 x,
629 y,
630 width,
631 })
632}
633
634fn process_tj_array(
635 idx: usize,
636 op: &Operation,
637 state: &mut TextState,
638 fonts: &FontMap,
639) -> Option<TextRun> {
640 let arr = match op.operands.first() {
641 Some(Object::Array(ref a)) => a,
642 _ => return None,
643 };
644
645 let x_start = compute_x(state);
646 let y = compute_y(state);
647 let mut combined_text = String::new();
648
649 for item in arr {
650 match item {
651 Object::String(ref bytes, _) => {
652 let text = fonts.decode_string(&state.font_name, bytes);
653 combined_text.push_str(&text);
654 advance_text_position(bytes, state, fonts);
655 }
656 _ => {
657 if let Some(adj) = as_number(item) {
658 advance_tm(state, -adj / 1000.0 * state.font_size * (state.th / 100.0));
660 }
661 }
662 }
663 }
664
665 if combined_text.is_empty() {
666 return None;
667 }
668
669 let x_end = compute_x(state);
670 let width = (x_end - x_start).abs();
671
672 Some(TextRun {
673 text: combined_text,
674 ops_range: idx..idx + 1,
675 font_name: state.font_name.clone(),
676 font_size: state.font_size,
677 x: x_start,
678 y,
679 width,
680 })
681}
682
683fn compute_x(state: &TextState) -> f64 {
688 state.ctm[0] * state.tm[4] + state.ctm[2] * state.tm[5] + state.ctm[4]
690}
691
692fn compute_y(state: &TextState) -> f64 {
693 state.ctm[1] * state.tm[4] + state.ctm[3] * state.tm[5] + state.ctm[5]
694}
695
696fn compute_text_width(bytes: &[u8], state: &TextState, fonts: &FontMap) -> f64 {
697 let scale = state.font_size * (state.th / 100.0);
698 if fonts.is_cid_font(&state.font_name) {
699 let mut width = 0.0;
701 let mut i = 0;
702 while i + 1 < bytes.len() {
703 let code = u32::from(bytes[i]) << 8 | u32::from(bytes[i + 1]);
704 let w = fonts.char_width(&state.font_name, code);
705 width += w / 1000.0 * scale + state.tc;
706 i += 2;
707 }
708 width
709 } else {
710 let mut width = 0.0;
712 for &b in bytes {
713 let w = fonts.char_width(&state.font_name, u32::from(b));
714 width += w / 1000.0 * scale + state.tc;
715 if b == b' ' {
716 width += state.tw;
717 }
718 }
719 width
720 }
721}
722
723fn advance_tm(state: &mut TextState, tx: f64) {
727 state.tm = offset_text_line(&state.tm, tx, 0.0);
728}
729
730fn advance_text_position(bytes: &[u8], state: &mut TextState, fonts: &FontMap) {
731 let scale = state.font_size * (state.th / 100.0);
732 let mut tx = 0.0;
733 if fonts.is_cid_font(&state.font_name) {
734 let mut i = 0;
735 while i + 1 < bytes.len() {
736 let code = u32::from(bytes[i]) << 8 | u32::from(bytes[i + 1]);
737 let w = fonts.char_width(&state.font_name, code);
738 tx += w / 1000.0 * scale + state.tc;
739 i += 2;
740 }
741 } else {
742 for &b in bytes {
743 let w = fonts.char_width(&state.font_name, u32::from(b));
744 tx += w / 1000.0 * scale + state.tc;
745 if b == b' ' {
746 tx += state.tw;
747 }
748 }
749 }
750 advance_tm(state, tx);
751}
752
753fn get_page_font_dict(doc: &Document, page_id: ObjectId) -> HashMap<String, ObjectId> {
759 let mut result = HashMap::new();
760
761 let page_dict = match doc.get_object(page_id) {
762 Ok(Object::Dictionary(ref d)) => d.clone(),
763 _ => return result,
764 };
765
766 let resources = match page_dict.get(b"Resources") {
768 Ok(Object::Dictionary(ref d)) => d.clone(),
769 Ok(Object::Reference(id)) => match doc.get_object(*id) {
770 Ok(Object::Dictionary(ref d)) => d.clone(),
771 _ => return result,
772 },
773 _ => return result,
774 };
775
776 let font_dict = match resources.get(b"Font") {
777 Ok(Object::Dictionary(ref d)) => d.clone(),
778 Ok(Object::Reference(id)) => match doc.get_object(*id) {
779 Ok(Object::Dictionary(ref d)) => d.clone(),
780 _ => return result,
781 },
782 _ => return result,
783 };
784
785 for (key, value) in font_dict.iter() {
786 let name = String::from_utf8_lossy(key).to_string();
787 if let Object::Reference(id) = value {
788 result.insert(name, *id);
789 }
790 }
791
792 result
793}
794
795fn get_font_descriptor_flags(doc: &Document, font_dict: &lopdf::Dictionary) -> u32 {
797 let desc_obj = match font_dict.get(b"FontDescriptor").ok() {
798 Some(o) => o.clone(),
799 None => return 0,
800 };
801 let desc_dict = match desc_obj {
802 Object::Reference(id) => match doc.get_object(id) {
803 Ok(Object::Dictionary(ref d)) => d.clone(),
804 _ => return 0,
805 },
806 Object::Dictionary(ref d) => d.clone(),
807 _ => return 0,
808 };
809 match desc_dict.get(b"Flags").ok() {
810 Some(Object::Integer(i)) => *i as u32,
811 _ => 0,
812 }
813}
814
815fn build_font_info(doc: &Document, font_id: &ObjectId) -> FontInfo {
817 let font_dict = match doc.get_object(*font_id) {
818 Ok(Object::Dictionary(ref d)) => d.clone(),
819 _ => {
820 return FontInfo {
821 to_unicode: None,
822 encoding: FontEncoding::Builtin,
823 widths: GlyphWidths::empty(),
824 differences_encoding: HashMap::new(),
825 is_subset: false,
826 is_symbolic: false,
827 forward_encoding: HashMap::new(),
828 cmap_inverse: HashMap::new(),
829 }
830 }
831 };
832
833 let to_unicode = extract_to_unicode(doc, &font_dict);
835
836 let encoding = determine_encoding(doc, &font_dict);
838
839 let widths = extract_widths(doc, &font_dict);
841
842 let is_subset = font_dict
847 .get(b"BaseFont")
848 .ok()
849 .and_then(|o| match o {
850 Object::Name(ref n) => Some(n.clone()),
851 _ => None,
852 })
853 .map(|name| {
854 name.len() > 7 && name[6] == b'+' && name[..6].iter().all(|b| b.is_ascii_uppercase())
855 })
856 .unwrap_or(false);
857
858 let flags = get_font_descriptor_flags(doc, &font_dict);
870 let is_symbolic = flags & 4 != 0; let differences_encoding =
877 if to_unicode.is_none() && matches!(encoding, FontEncoding::Builtin) && !is_symbolic {
878 build_font_encoding(doc, &font_dict)
879 } else {
880 HashMap::new()
881 };
882
883 let cmap_inverse = if to_unicode.is_none() && matches!(encoding, FontEncoding::Builtin) {
888 build_truetype_cmap_inverse(doc, &font_dict)
889 } else {
890 HashMap::new()
891 };
892
893 let forward_encoding = if to_unicode.is_none() && matches!(encoding, FontEncoding::Builtin) {
900 build_font_encoding(doc, &font_dict)
901 } else {
902 HashMap::new()
903 };
904
905 FontInfo {
906 to_unicode,
907 encoding,
908 widths,
909 differences_encoding,
910 is_subset,
911 is_symbolic,
912 forward_encoding,
913 cmap_inverse,
914 }
915}
916
917fn extract_to_unicode(doc: &Document, font_dict: &lopdf::Dictionary) -> Option<CMap> {
919 let to_unicode_obj = font_dict.get(b"ToUnicode").ok()?;
920
921 let stream_data = match to_unicode_obj {
922 Object::Reference(id) => {
923 let stream = match doc.get_object(*id) {
924 Ok(Object::Stream(ref s)) => s.clone(),
925 _ => return None,
926 };
927 let mut s = stream;
928 let _ = s.decompress();
929 s.content
930 }
931 Object::Stream(ref s) => {
932 let mut s = s.clone();
933 let _ = s.decompress();
934 s.content
935 }
936 _ => return None,
937 };
938
939 CMap::parse(&stream_data, pdf_font::cmap::load_embedded)
940}
941
942fn build_truetype_cmap_inverse(
949 doc: &Document,
950 font_dict: &lopdf::Dictionary,
951) -> HashMap<u32, char> {
952 let font_data = match get_truetype_font_data(doc, font_dict) {
953 Some(d) => d,
954 None => return HashMap::new(),
955 };
956 let face = match ttf_parser::Face::parse(&font_data, 0) {
957 Ok(f) => f,
958 Err(_) => return HashMap::new(),
959 };
960 let cmap_table = match face.tables().cmap {
961 Some(c) => c,
962 None => return HashMap::new(),
963 };
964
965 let mut inverse: HashMap<u32, char> = HashMap::new();
966
967 let preferred = cmap_table
970 .subtables
971 .into_iter()
972 .find(|st| st.platform_id == ttf_parser::PlatformId::Windows && st.encoding_id == 1)
973 .or_else(|| cmap_table.subtables.into_iter().find(|st| st.is_unicode()));
974
975 if let Some(subtable) = preferred {
976 subtable.codepoints(|cp| {
977 if let Some(c) = char::from_u32(cp) {
978 if let Some(glyph_id) = subtable.glyph_index(cp) {
979 if glyph_id.0 != 0 {
980 inverse.entry(glyph_id.0 as u32).or_insert(c);
981 }
982 }
983 }
984 });
985 }
986
987 inverse
988}
989
990fn get_truetype_font_data(doc: &Document, font_dict: &lopdf::Dictionary) -> Option<Vec<u8>> {
992 let desc_ref = font_dict.get(b"FontDescriptor").ok()?;
993 let desc_dict = match desc_ref {
994 Object::Reference(id) => match doc.get_object(*id) {
995 Ok(Object::Dictionary(ref d)) => d.clone(),
996 _ => return None,
997 },
998 Object::Dictionary(ref d) => d.clone(),
999 _ => return None,
1000 };
1001 let ff2_ref = desc_dict.get(b"FontFile2").ok()?;
1002 let stream_id = match ff2_ref {
1003 Object::Reference(id) => *id,
1004 _ => return None,
1005 };
1006 if let Ok(Object::Stream(ref s)) = doc.get_object(stream_id) {
1007 let mut s2 = s.clone();
1008 let _ = s2.decompress();
1009 Some(s2.content)
1010 } else {
1011 None
1012 }
1013}
1014
1015fn determine_encoding(doc: &Document, font_dict: &lopdf::Dictionary) -> FontEncoding {
1017 let subtype = font_dict.get(b"Subtype").ok().and_then(|o| match o {
1019 Object::Name(ref n) => Some(String::from_utf8_lossy(n).to_string()),
1020 _ => None,
1021 });
1022
1023 if subtype.as_deref() == Some("Type0") {
1024 let encoding_name = font_dict.get(b"Encoding").ok().and_then(|o| match o {
1026 Object::Name(ref n) => Some(String::from_utf8_lossy(n).to_string()),
1027 Object::Reference(id) => doc.get_object(*id).ok().and_then(|o| match o {
1028 Object::Name(ref n) => Some(String::from_utf8_lossy(n).to_string()),
1029 _ => None,
1030 }),
1031 _ => None,
1032 });
1033
1034 return match encoding_name.as_deref() {
1035 Some("Identity-H") | Some("Identity-V") => FontEncoding::IdentityH,
1036 _ => FontEncoding::CustomCMap,
1037 };
1038 }
1039
1040 FontEncoding::Builtin
1041}
1042
1043fn extract_widths(doc: &Document, font_dict: &lopdf::Dictionary) -> GlyphWidths {
1045 let first_char = font_dict
1046 .get(b"FirstChar")
1047 .ok()
1048 .and_then(|o| match o {
1049 Object::Integer(i) => Some(*i as u32),
1050 _ => None,
1051 })
1052 .unwrap_or(0);
1053
1054 let widths_array = font_dict.get(b"Widths").ok().and_then(|o| match o {
1055 Object::Array(ref a) => Some(a.clone()),
1056 Object::Reference(id) => match doc.get_object(*id).ok() {
1057 Some(Object::Array(ref a)) => Some(a.clone()),
1058 _ => None,
1059 },
1060 _ => None,
1061 });
1062
1063 let widths = match widths_array {
1064 Some(arr) => arr.iter().map(|o| as_number(o).unwrap_or(1000.0)).collect(),
1065 None => {
1066 let dw = get_cid_default_width(doc, font_dict);
1068 return GlyphWidths {
1069 first_char: 0,
1070 widths: Vec::new(),
1071 default_width: dw,
1072 };
1073 }
1074 };
1075
1076 GlyphWidths {
1077 first_char,
1078 widths,
1079 default_width: 1000.0,
1080 }
1081}
1082
1083fn get_cid_default_width(doc: &Document, font_dict: &lopdf::Dictionary) -> f64 {
1085 let descendants = match font_dict.get(b"DescendantFonts").ok() {
1086 Some(Object::Array(ref arr)) => arr.clone(),
1087 Some(Object::Reference(id)) => match doc.get_object(*id).ok() {
1088 Some(Object::Array(ref arr)) => arr.clone(),
1089 _ => return 1000.0,
1090 },
1091 _ => return 1000.0,
1092 };
1093
1094 let desc_id = match descendants.first() {
1095 Some(Object::Reference(id)) => *id,
1096 _ => return 1000.0,
1097 };
1098
1099 let desc_dict = match doc.get_object(desc_id) {
1100 Ok(Object::Dictionary(ref d)) => d,
1101 _ => return 1000.0,
1102 };
1103
1104 desc_dict
1105 .get(b"DW")
1106 .ok()
1107 .and_then(as_number)
1108 .unwrap_or(1000.0)
1109}
1110
1111fn decode_with_cmap(bytes: &[u8], cmap: &CMap, encoding: &FontEncoding) -> String {
1117 let mut result = String::new();
1118
1119 match encoding {
1120 FontEncoding::IdentityH | FontEncoding::CustomCMap => {
1121 let mut i = 0;
1123 while i + 1 < bytes.len() {
1124 let code = u32::from(bytes[i]) << 8 | u32::from(bytes[i + 1]);
1125 match cmap.lookup_bf_string(code) {
1126 Some(BfString::Char(c)) => result.push(c),
1127 Some(BfString::String(s)) => result.push_str(&s),
1128 None => result.push(char::REPLACEMENT_CHARACTER),
1129 }
1130 i += 2;
1131 }
1132 }
1133 FontEncoding::Builtin => {
1134 for &b in bytes {
1136 match cmap.lookup_bf_string(u32::from(b)) {
1137 Some(BfString::Char(c)) => result.push(c),
1138 Some(BfString::String(s)) => result.push_str(&s),
1139 None => result.push(b as char),
1140 }
1141 }
1142 }
1143 }
1144
1145 result
1146}
1147
1148fn decode_pdf_string_fallback(bytes: &[u8]) -> String {
1150 if bytes.len() >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF {
1152 let chars: Vec<u16> = bytes[2..]
1153 .chunks(2)
1154 .filter_map(|chunk| {
1155 if chunk.len() == 2 {
1156 Some(u16::from_be_bytes([chunk[0], chunk[1]]))
1157 } else {
1158 None
1159 }
1160 })
1161 .collect();
1162 String::from_utf16_lossy(&chars)
1163 } else {
1164 bytes.iter().map(|&b| b as char).collect()
1165 }
1166}
1167
1168fn extract_matrix(operands: &[Object]) -> Option<[f64; 6]> {
1170 if operands.len() < 6 {
1171 return None;
1172 }
1173 Some([
1174 as_number(&operands[0])?,
1175 as_number(&operands[1])?,
1176 as_number(&operands[2])?,
1177 as_number(&operands[3])?,
1178 as_number(&operands[4])?,
1179 as_number(&operands[5])?,
1180 ])
1181}
1182
1183#[cfg(test)]
1184mod tests {
1185 use super::*;
1186 use lopdf::{dictionary, Document, Object, Stream};
1187
1188 fn make_doc_with_font_and_content(content: &[u8], to_unicode_data: Option<&[u8]>) -> Document {
1189 let mut doc = Document::with_version("1.7");
1190
1191 let mut font = dictionary! {
1193 "Type" => "Font",
1194 "Subtype" => "Type1",
1195 "BaseFont" => "Helvetica",
1196 };
1197
1198 if let Some(tu_data) = to_unicode_data {
1199 let tu_stream = Stream::new(dictionary! {}, tu_data.to_vec());
1200 let tu_id = doc.add_object(Object::Stream(tu_stream));
1201 font.set("ToUnicode", Object::Reference(tu_id));
1202 }
1203
1204 let font_id = doc.add_object(Object::Dictionary(font));
1205
1206 let font_resources = dictionary! {
1207 "F1" => Object::Reference(font_id),
1208 };
1209 let resources = dictionary! {
1210 "Font" => Object::Dictionary(font_resources),
1211 };
1212
1213 let content_stream = Stream::new(dictionary! {}, content.to_vec());
1214 let content_id = doc.add_object(Object::Stream(content_stream));
1215
1216 let page_dict = dictionary! {
1217 "Type" => "Page",
1218 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
1219 "Contents" => Object::Reference(content_id),
1220 "Resources" => Object::Dictionary(resources),
1221 };
1222 let page_id = doc.add_object(Object::Dictionary(page_dict));
1223
1224 let pages_dict = dictionary! {
1225 "Type" => "Pages",
1226 "Kids" => vec![Object::Reference(page_id)],
1227 "Count" => 1_i64,
1228 };
1229 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
1230
1231 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
1232 d.set("Parent", Object::Reference(pages_id));
1233 }
1234
1235 let catalog = dictionary! {
1236 "Type" => "Catalog",
1237 "Pages" => Object::Reference(pages_id),
1238 };
1239 let catalog_id = doc.add_object(Object::Dictionary(catalog));
1240 doc.trailer.set("Root", Object::Reference(catalog_id));
1241
1242 doc
1243 }
1244
1245 #[test]
1246 fn extract_simple_text_run() {
1247 let doc =
1248 make_doc_with_font_and_content(b"BT /F1 12 Tf 100 700 Td (Hello World) Tj ET", None);
1249 let runs = extract_page_text_runs(&doc, 1).unwrap();
1250 assert_eq!(runs.len(), 1);
1251 assert_eq!(runs[0].text, "Hello World");
1252 assert_eq!(runs[0].font_name, "F1");
1253 assert_eq!(runs[0].font_size, 12.0);
1254 }
1255
1256 #[test]
1257 fn extract_multiple_text_runs() {
1258 let doc = make_doc_with_font_and_content(
1259 b"BT /F1 12 Tf 100 700 Td (Hello) Tj 0 -20 Td (World) Tj ET",
1260 None,
1261 );
1262 let runs = extract_page_text_runs(&doc, 1).unwrap();
1263 assert_eq!(runs.len(), 2);
1264 assert_eq!(runs[0].text, "Hello");
1265 assert_eq!(runs[1].text, "World");
1266 }
1267
1268 #[test]
1269 fn extract_tj_array_run() {
1270 let doc = make_doc_with_font_and_content(
1271 b"BT /F1 12 Tf 100 700 Td [(He) -100 (llo)] TJ ET",
1272 None,
1273 );
1274 let runs = extract_page_text_runs(&doc, 1).unwrap();
1275 assert_eq!(runs.len(), 1);
1276 assert_eq!(runs[0].text, "Hello");
1277 }
1278
1279 #[test]
1280 fn text_run_position_tracking() {
1281 let doc =
1282 make_doc_with_font_and_content(b"BT /F1 12 Tf 1 0 0 1 100 700 Tm (Hello) Tj ET", None);
1283 let runs = extract_page_text_runs(&doc, 1).unwrap();
1284 assert_eq!(runs.len(), 1);
1285 assert!((runs[0].x - 100.0).abs() < 1e-6);
1286 assert!((runs[0].y - 700.0).abs() < 1e-6);
1287 }
1288
1289 #[test]
1290 fn text_run_with_to_unicode_cmap() {
1291 let cmap = b"/CIDInit /ProcSet findresource begin
129312 dict begin
1294begincmap
1295/CIDSystemInfo << /Registry (Test) /Ordering (UCS) /Supplement 0 >> def
1296/CMapName /Test def
12971 begincodespacerange
1298<00> <FF>
1299endcodespacerange
13002 beginbfchar
1301<48> <0048>
1302<69> <0069>
1303endbfchar
1304endcmap
1305CMapName currentdict /CMap defineresource pop
1306end
1307end";
1308
1309 let doc = make_doc_with_font_and_content(b"BT /F1 12 Tf 100 700 Td (Hi) Tj ET", Some(cmap));
1310 let runs = extract_page_text_runs(&doc, 1).unwrap();
1311 assert_eq!(runs.len(), 1);
1312 assert_eq!(runs[0].text, "Hi");
1313 }
1314
1315 #[test]
1316 fn text_run_ops_range() {
1317 let doc = make_doc_with_font_and_content(b"BT /F1 12 Tf 100 700 Td (Hello) Tj ET", None);
1318 let runs = extract_page_text_runs(&doc, 1).unwrap();
1319 assert_eq!(runs.len(), 1);
1320 assert_eq!(runs[0].ops_range, 3..4);
1322 }
1323
1324 #[test]
1327 fn run_position_follows_pdf_matrix_order() {
1328 let doc = make_doc_with_font_and_content(
1330 b"q 1 0 0 1 100 100 cm 2 0 0 2 0 0 cm BT /F1 12 Tf 5 7 Td (Hi) Tj ET Q",
1331 None,
1332 );
1333 let runs = extract_page_text_runs(&doc, 1).unwrap();
1334 assert_eq!(runs.len(), 1);
1335 assert!((runs[0].x - 110.0).abs() < 1e-6, "x = {}", runs[0].x);
1336 assert!((runs[0].y - 114.0).abs() < 1e-6, "y = {}", runs[0].y);
1337
1338 let doc = make_doc_with_font_and_content(
1340 b"BT /F1 12 Tf 2 0 0 2 10 10 Tm 5 7 Td (Hi) Tj ET",
1341 None,
1342 );
1343 let runs = extract_page_text_runs(&doc, 1).unwrap();
1344 assert!((runs[0].x - 20.0).abs() < 1e-6, "x = {}", runs[0].x);
1345 assert!((runs[0].y - 24.0).abs() < 1e-6, "y = {}", runs[0].y);
1346 }
1347
1348 #[test]
1352 fn glyph_advance_is_scaled_by_the_text_matrix() {
1353 let second = |tm: &str| {
1354 let content = format!("BT /F1 10 Tf {tm} Tm (H) Tj (i) Tj ET");
1355 let doc = make_doc_with_font_and_content(content.as_bytes(), None);
1356 let runs = extract_page_text_runs(&doc, 1).unwrap();
1357 assert_eq!(runs.len(), 2);
1358 (runs[1].x, runs[1].y)
1359 };
1360 let unscaled = second("1 0 0 1 10 10").0 - 10.0;
1361 let scaled = second("2 0 0 2 10 10").0 - 10.0;
1362 let (rx, ry) = second("0 1 -1 0 10 10");
1365 assert!((rx - 10.0).abs() < 1e-6, "x moved to {rx}");
1366 assert!(
1367 (ry - 10.0 - unscaled).abs() < 1e-6,
1368 "y = {ry}, expected {}",
1369 10.0 + unscaled
1370 );
1371 assert!(unscaled > 0.0, "the first glyph must advance");
1372 assert!(
1373 (scaled - 2.0 * unscaled).abs() < 1e-6,
1374 "{scaled} != 2 * {unscaled}"
1375 );
1376 }
1377
1378 #[test]
1379 fn font_map_empty_fallback() {
1380 let fonts = FontMap::empty();
1381 let text = fonts.decode_string("F1", b"Hello");
1382 assert_eq!(text, "Hello");
1383 }
1384
1385 #[test]
1386 fn text_run_font_change() {
1387 let mut doc = Document::with_version("1.7");
1388
1389 let font1 = dictionary! {
1390 "Type" => "Font",
1391 "Subtype" => "Type1",
1392 "BaseFont" => "Helvetica",
1393 };
1394 let font1_id = doc.add_object(Object::Dictionary(font1));
1395
1396 let font2 = dictionary! {
1397 "Type" => "Font",
1398 "Subtype" => "Type1",
1399 "BaseFont" => "Courier",
1400 };
1401 let font2_id = doc.add_object(Object::Dictionary(font2));
1402
1403 let font_resources = dictionary! {
1404 "F1" => Object::Reference(font1_id),
1405 "F2" => Object::Reference(font2_id),
1406 };
1407 let resources = dictionary! {
1408 "Font" => Object::Dictionary(font_resources),
1409 };
1410
1411 let content = b"BT /F1 12 Tf (Hello) Tj /F2 14 Tf (World) Tj ET";
1412 let content_stream = Stream::new(dictionary! {}, content.to_vec());
1413 let content_id = doc.add_object(Object::Stream(content_stream));
1414
1415 let page_dict = dictionary! {
1416 "Type" => "Page",
1417 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
1418 "Contents" => Object::Reference(content_id),
1419 "Resources" => Object::Dictionary(resources),
1420 };
1421 let page_id = doc.add_object(Object::Dictionary(page_dict));
1422
1423 let pages_dict = dictionary! {
1424 "Type" => "Pages",
1425 "Kids" => vec![Object::Reference(page_id)],
1426 "Count" => 1_i64,
1427 };
1428 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
1429
1430 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
1431 d.set("Parent", Object::Reference(pages_id));
1432 }
1433
1434 let catalog = dictionary! {
1435 "Type" => "Catalog",
1436 "Pages" => Object::Reference(pages_id),
1437 };
1438 let catalog_id = doc.add_object(Object::Dictionary(catalog));
1439 doc.trailer.set("Root", Object::Reference(catalog_id));
1440
1441 let runs = extract_page_text_runs(&doc, 1).unwrap();
1442 assert_eq!(runs.len(), 2);
1443 assert_eq!(runs[0].font_name, "F1");
1444 assert_eq!(runs[0].font_size, 12.0);
1445 assert_eq!(runs[1].font_name, "F2");
1446 assert_eq!(runs[1].font_size, 14.0);
1447 }
1448
1449 #[test]
1450 fn empty_content_no_runs() {
1451 let doc = make_doc_with_font_and_content(b"q Q", None);
1452 let runs = extract_page_text_runs(&doc, 1).unwrap();
1453 assert!(runs.is_empty());
1454 }
1455}